v17.11.0
Added 3
- Add `--max-ocr-image-mpixels` option to downsample images sent to OCR when a page exceeds the given size, bounding memory consumption while preserving visible page appearance
- Add `--output-structure` option to `watcher.py` with FLAT, YEAR_MONTH, and HIERARCHY layout choices for output files
- Add `--on-conflict` option to `watcher.py` to control behavior when destination file already exists, with SUFFIX, SKIP, and OVERWRITE choices
Changed 10
- Change default conflict policy in `watcher.py` from silently overwriting to using SUFFIX mode, so existing output files are no longer overwritten by default
- Apply output structure and conflict policy settings equally to both `OCR_OUTPUT_DIRECTORY` and archive directory used by `OCR_ON_SUCCESS_ARCHIVE`
- Sanitize output and archive filenames and directory components for filesystems more restrictive than the input side, replacing illegal Windows/SMB characters and reserved DOS device names
- Avoid decoding and re-encoding OCR images when nothing needs to change them, linking the rasterized page directly instead on pages with no pre-existing text to mask and no filtering plugin
- Defer mask building until a text area actually needs blanking, eliminating forced image decode steps
- Reduce peak memory consumption on files with large images by approximately one third at default settings through rasterization improvements and earlier heap memory return to operating system
- Allow multiple OCR jobs to run concurrently in a single Python process using a readers-writer lock for plugin infrastructure instead of a process-wide lock
- Install a plugin set once per interpreter and reuse it across calls instead of reinstalling on every `ocrmypdf.ocr()` call
- Substantially speed up image optimization on documents with large images by repackaging `/FlateDecode` images with PNG predictor directly as PNG instead of decoding and re-encoding
- Raise minimum Pillow version to 12 to support image decoding detection improvements
Removed 1
- Remove full-page buffer allocation during rasterization that previously corrected PDFium rounding errors by a pixel or two
Deprecated 1
- Deprecate `OCR_OUTPUT_DIRECTORY_YEAR_MONTH` in favor of `OCR_OUTPUT_STRUCTURE=YEAR_MONTH`, which takes precedence if both are set
From OCRmyPDF
Enhancements
- New
--max-ocr-image-mpixelsdownsamples the image sent to OCR when a page exceeds the given size, which bounds the largest consumer of memory. The visible page is never downsampled, so output appearance is unaffected in every mode; what it trades is OCR accuracy on very high resolution scans. A 34 megapixel page that peaks at 492 MB peaks at 325 MB under--max-ocr-image-mpixels 8, and recognizes the same text. See the "Memory" section of the performance documentation for how to size a memory limit. watcher.py(thewatcherextra) gained a configurable output layout and conflict policy. These are watcher-only changes; they do not affect theocrmypdflibrary API.- New
OCR_OUTPUT_STRUCTUREsetting (--output-structure):FLAT(default, all outputs directly in the destination directory),YEAR_MONTH({destination}/{year}/{month}/{filename}, same layout as the oldOCR_OUTPUT_DIRECTORY_YEAR_MONTH=1), orHIERARCHY, which mirrors the input directory tree under the destination, e.g.input/a/b/c.pdf→output/a/b/c.pdf. - New
OCR_ON_CONFLICTsetting (--on-conflict) controls what happens when the intended destination file already exists:SUFFIX(default) writesname (1).pdf,name (2).pdf, ... in the OS style;SKIPlogs and leaves the file unprocessed;OVERWRITEis the old behavior. Behavior change: the default is nowSUFFIX, so existing output files are no longer silently overwritten. - Both settings now apply equally to
OCR_OUTPUT_DIRECTORYand to the archive directory used byOCR_ON_SUCCESS_ARCHIVE; previously the archive directory was always flat and silently overwrote on a name collision. - Output and archive filenames and directory components are sanitized
for filesystems more restrictive than the input side (e.g. an SMB
share): characters illegal on Windows/SMB (
<>:"/\|?*and control characters) are replaced with_, trailing dots/spaces are stripped, and reserved DOS device names (CON,PRN,AUX,NUL,COM1-9,LPT1-9) are prefixed with_. OCR_OUTPUT_DIRECTORY_YEAR_MONTHis now deprecated in favor ofOCR_OUTPUT_STRUCTURE=YEAR_MONTH. It is still honored and logs a deprecation warning; if both are set,OCR_OUTPUT_STRUCTUREwins.- See the "Watched folders with watcher.py" section of the batch processing documentation for the full description, including a note for SMB users about client-side directory/file-info caching delays.
- New
Performance
- The image sent to OCR is no longer decoded and re-encoded when nothing needs
to change it. On a page with no pre-existing text to mask and no filtering
plugin -- the ordinary case for a scanned document -- the rasterized page
already is the OCR image, so it is linked rather than rewritten. Producing
it was costing 3.3 seconds and a full-size buffer on a 34 megapixel page, out
of about 15 seconds for the whole file. Building the mask is now deferred
until a text area actually needs blanking, since that step is what forced the
decode. The minimum Pillow version is raised to 12, because deciding whether
anything decoded the image reads an attribute whose shape settled in Pillow 11
(
pi-heifalready required Pillow 11.1, so the effective floor barely moves). - Reduced peak memory on files with large images by about a third at default settings. On a 34 megapixel page the process tree peaked at 752 MB and now peaks at 492 MB. Two changes account for it: rasterizing a page no longer allocates a third full-page buffer to correct PDFium's rounding of the rendered size by a pixel or two, and freed heap memory is now returned to the operating system before the OCR engine runs, instead of counting against our resident set for as long as the engine is working. Rasterization is also faster, since correcting the size no longer resamples the whole page.
- Several OCR jobs may now run concurrently in a single Python process. The API
previously held a lock for the whole duration of
ocrmypdf.ocr(), so a second call in another thread had to wait for the first to finish. Plugin state is now guarded by a readers-writer lock: installing a plugin set takes it exclusively, and a job holds it shared for its run. An in-flight job therefore cannot have the plugin infrastructure it depends on replaced underneath it, while jobs that are past installation proceed concurrently. - A plugin set is now installed once per interpreter and reused, rather than
being reinstalled on every call. Previously each
ocrmypdf.ocr()call re-executed plugin modules given as file paths and rebound them insys.modules. Plugins must not rely on being re-executed for each job, and must not store per-job state on the plugin manager, which concurrent jobs requesting the same plugin set now share. - Jobs requesting different plugin sets serialize against each other, since installing the second set must wait for the first set's jobs to finish. Use the same plugin set across concurrent jobs, or separate processes.
- Image optimization is substantially faster on documents with large images.
An image stored as
/FlateDecodewith a PNG predictor already holds exactly what a PNGIDATchunk holds, so it is now repackaged as a PNG directly instead of being decoded to a bitmap and re-encoded. On a 6-page document containing one 9000x9000 image, the optimization step went from 3.4s to 0.6s, and to 0.04s together with the JPEG change below; total runtime went from 10.0s to 7.0s. Output is unchanged: the compressed data is reused verbatim. Images that are not in a directly repackageable form still take the previous path. - During image optimization, we decoded all JPEGs, even if the code
path was an optimization setting with the decoded JPEG would be never be
re-encoded (below
--optimize 2). We now decode only on code paths that use the decoded JPEG. Output is unchanged. - An uncompressed image (one with no
/Filter) no longer produces a spurious "could not be processed by the optimizer" warning. Such an image raisedIndexErrorinternally, which the optimizer's best-effort handler caught and reported as a warning; it is now recognized and skipped quietly. - Removed an unreachable branch in the image optimizer that claimed to handle 1 bit per component images in an ICC-based colorspace. An earlier check sends every 1 bpc image to the JBIG2 pass, which handles ICC-based images by neutralizing the profile before extracting, so the branch could never run.
- Removed the process-wide lock that serialized worker pools across all
Executorinstances.Executor.pool_lockis retained but no longer acquired, and is deprecated; it will be removed in a future major release. The invariant it protected - that only one progress bar renders on the shared console - is now enforced by the progress bar, which disables itself if another bar already owns the console. - Note that N concurrent jobs each configured with
jobs=Mmay now spawn up to N*M workers, where previously they were serialized to M. Sizejobsaccordingly. - Known limitation of concurrent in-process jobs: they must use the same
max_image_mpixels. Pillow's decompression-bomb limit is interpreter-global and the last job to set it wins. Use separate processes to run jobs with differing configurations.
Fixes
- Fixed a latent use-after-free in the pypdfium2 rasterizer.
to_pil()lets Pillow alias PDFium's bitmap buffer for some formats -- grayscale renders among them, which is every mono and grayscale page -- and the buffer was freed immediately afterwards, leaving Pillow reading memory PDFium had released. - Windows: OCRmyPDF no longer prints
[WinError 2] The system cannot find the file specifiedwarnings while it searches for Ghostscript and Tesseract (#1671). These messages came from probing registry keys that simply don't exist when the programs were installed by a package manager such as Scoop, or not installed at all. Since the search then continues elsewhere and usually succeeds, these failures are normal, and are now logged at debug level, naming the location that was searched. If a program genuinely cannot be found, OCRmyPDF still reports that as an error. - Windows: fixed a crash when the
PROGRAMFILESenvironment variable pointed to a folder that does not exist. --mode stripfailed to remove OCR text layers that OCRmyPDF itself produced ({issue}1730). OCRmyPDF grafts its text layer as a Form XObject and stripping only examined the page content stream, so the invisible text was never found. The same flaw made--mode redostack a second text layer on top of the old one instead of replacing it. Stripping now descends into Form XObjects. Thanks @Anai-Guo ({issue}1732).- Stripping now also resolves page
/Resourcesinherited from an ancestor/Pagesnode, rather than only looking at the page's own resources. - A text layer that becomes empty after stripping is now removed from the page instead of being left behind as a vestigial Form XObject husk.
- Setting
clean_finalon an existing options object in the Python API no longer leavescleanunset.--clean-finalimplies--clean, but the rule lived in a field validator that only ran while the options object was being constructed, so assigning to the attribute afterwards silently skipped it. - Validation errors for out-of-range or misspelled options now name the option
they are about, and their wording comes from Pydantic rather than being
hand-written, so it has changed slightly. For example,
--jobs 999now reports--jobs: Input should be less than or equal to 256. The set of accepted values is unchanged.