4419 Commits

Author SHA1 Message Date
James R. Barlow
39bf09f1eb docs: format 2026-07-28 11:30:52 -07:00
James R. Barlow
aaffc46f73 Update uv lock 2026-07-28 01:10:04 -07:00
James R. Barlow
0817542883 Merge remote-tracking branches 'origin/dependabot/uv/gitpython-3.1.52', 'origin/dependabot/uv/pillow-12.3.0' and 'origin/dependabot/github_actions/actions/setup-python-7' 2026-07-27 23:21:25 -07:00
James R. Barlow
6f4744dd20 Fix --jpeg-quality/--jpg-quality being dropped by the CLI (closes #1723)
namespace_to_options() only copied argparse namespace keys that were
literal members of OcrOptions.model_fields. The CLI dest for both
--jpeg-quality and --jpg-quality was jpeg_quality, but the pydantic field
was named jpg_quality (jpeg_quality existed only as a compatibility
property, absent from model_fields). The value was silently dropped into
extra_attrs, and the optimizer always fell back to its own hardcoded
default regardless of the flag.

The same alias mismatch also affected the Python API: create_options()
uses the same model_fields-matching logic as namespace_to_options(), so
ocrmypdf.ocr(jpeg_quality=...) was silently dropped too - only the
canonical jpg_quality= kwarg worked.

Rather than patch around the mismatch, consolidate on a single canonical
name: OcrOptions.jpeg_quality (matching the primary --jpeg-quality CLI
flag and the already-consistent naming in OptimizeOptions). jpg_quality
becomes a deprecated compatibility property, and ocrmypdf.ocr(jpg_quality=)
is a deprecated alias that warns and forwards to jpeg_quality via a new
create_options() remap step. --jpg-quality remains a working (already
hidden) CLI alias with no code-path divergence, since it now shares an
argparse dest that matches the field name directly.
2026-07-27 23:14:27 -07:00
James R. Barlow
5d49f75c56 Use any installed Noto font when named families lack glyphs (closes #1722)
SystemFontProvider.NOTO_FONT_PATTERNS enumerates about two dozen Noto
families by name, and MultiFontManager only ever asked for those. Any
script outside that list fell through to the glyphless Occulta fallback
even when the correct font was installed, which is the common case on
macOS: it ships around a hundred script-specific Noto faces in
/System/Library/Fonts/Supplemental, almost none of which we knew how to
ask for. The reporter had the fonts and still got told to install them.

Add an optional GlyphSearchingFontProvider protocol (find_font_with_glyphs)
implemented by SystemFontProvider, BuiltinFontProvider and
ChainedFontProvider, and a new selection phase that uses it once the named
families fail. The system scan enumerates every Noto face, reusing the
existing variable-font/-Regular/-VF filename classification, and keeps
only the font it selects so a full scan does not retain every font file on
the system. Fonts found this way are remembered and retried by name for
later words. Capability detection is isinstance-based so third-party
providers keep working unchanged.

The warning itself was also unactionable: it named neither the characters
nor the script, which is why the reporter had to ask which package to
install. It now identifies what it could not render, e.g.
'Ꮳ' U+13E3 CHEROKEE LETTER TSA. A word mixing scripts that no single font
covers now gets a distinct message saying that installing fonts will not
help, rather than sending the user after fonts they already have.

Finally, the documented macOS install command was wrong: `brew install
font-noto` does not exist, since Homebrew has no single Noto package, only
a cask per family. The Fedora package name was also corrected, as
google-noto-fonts-common ships no actual fonts.
2026-07-26 23:25:51 -07:00
dependabot[bot]
5a824ddd8c build(deps): bump gitpython from 3.1.50 to 3.1.52
Bumps [gitpython](https://github.com/gitpython-developers/GitPython) from 3.1.50 to 3.1.52.
- [Release notes](https://github.com/gitpython-developers/GitPython/releases)
- [Changelog](https://github.com/gitpython-developers/GitPython/blob/main/CHANGES)
- [Commits](https://github.com/gitpython-developers/GitPython/compare/3.1.50...3.1.52)

---
updated-dependencies:
- dependency-name: gitpython
  dependency-version: 3.1.52
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-22 14:07:17 +00:00
dependabot[bot]
54bf03a454 build(deps): bump pillow from 12.2.0 to 12.3.0
Bumps [pillow](https://github.com/python-pillow/Pillow) from 12.2.0 to 12.3.0.
- [Release notes](https://github.com/python-pillow/Pillow/releases)
- [Changelog](https://github.com/python-pillow/Pillow/blob/main/CHANGES.rst)
- [Commits](https://github.com/python-pillow/Pillow/compare/12.2.0...12.3.0)

---
updated-dependencies:
- dependency-name: pillow
  dependency-version: 12.3.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-21 10:59:50 +00:00
dependabot[bot]
009754d137 build(deps): bump actions/setup-python from 6 to 7
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-20 10:42:33 +00:00
James R. Barlow
f0a3a74374 Don't re-draft an already-published release on every main push
stage_release runs on every push to main and unconditionally deletes
and recreates the draft release for the version in _version.py. Since
_version.py isn't bumped until sometime after a release is tagged and
published by release.yml, every intervening push to main was clobbering
the just-published release back to draft status. Confirmed this hit
v17.3.0 through v17.8.0 (and briefly v17.8.1, manually fixed).

Skip the delete/recreate if a non-draft release already exists for
this tag.
2026-07-17 09:52:35 -07:00
James R. Barlow
178d339c8e Merge branch 'fix-image-xobject-nondict' 2026-07-17 00:59:30 -07:00
jbarlow
d3f8d01227 Merge pull request #1714 from mvanhorn/fix-nonembedded-cid-fonts-nondict-resource
fix: guard find_nonembedded_cid_fonts against non-dictionary resources
2026-07-17 00:39:35 -07:00
James R. Barlow
b60df59c62 Tolerate non-dictionary /Resources and /XObject in image scanner
A malformed PDF may store a non-dictionary object (an array, name, or
other type) at /Resources or /Resources /XObject. The pdfinfo image
scanner iterated these with .items()/.as_dict() and probed them with the
`in` operator, which raise TypeError/ValueError on non-dictionary pikepdf
objects and crashed PdfInfo on otherwise-processable files. OCRmyPDF's
domain is messy, machine-generated PDFs, so scanning must tolerate this.

Guard _image_xobjects and _find_form_xobject_images with
isinstance(x, Dictionary) before iterating, treating a non-dictionary
/Resources or /XObject as "no image XObjects". This is the same
robustness class as the pdfa.py find_nonembedded_cid_fonts fix, applied
to the pdfinfo image scanner.
2026-07-17 00:05:50 -07:00
James R. Barlow
640b3062b2 Refine non-dict resource guard: prefer isinstance, cover FontDescriptor
Follow-on to the non-dictionary /Font and /XObject guard. Replace the
_dict_entries() helper with an isinstance(..., pikepdf.Dictionary) check
at each resource lookup, which pikepdf's metaclass supports directly and
which reads as exactly the invariant being enforced. Iterate via
as_dict().values() so the values are typed and mypy stays clean once the
Any from the untyped resources argument is narrowed away.

Also guard _cid_font_is_embedded against a non-dictionary /FontDescriptor:
`key in descriptor` raises ValueError on a non-dict, which the caller's
except (AttributeError, TypeError, KeyError) does not catch. Such a font
now counts as non-embedded and is reported, so PDF/A conversion is refused
rather than risking Ghostscript corrupting a pre-existing CID text layer.

Adds a regression test for the FontDescriptor case (issue #1713).
2026-07-16 23:42:50 -07:00
James R. Barlow
ef903db360 Merge remote-tracking branch 'origin/main' into fix-nonembedded-cid-fonts-nondict-resource 2026-07-16 23:42:13 -07:00
James R. Barlow
9cda02317b v17.8.1 release ntoes update v17.8.1 2026-07-16 11:54:25 -07:00
Matt Van Horn
92a2fe880a Guard find_nonembedded_cid_fonts against non-dictionary resources
A malformed PDF can store a non-dictionary object under a page's /Font or
/XObject resource. find_nonembedded_cid_fonts() iterated .values() on that
object outside the per-entry try/except, so scanning such a page raised
(TypeError/AttributeError depending on the pikepdf version) instead of
producing output. This surfaced as a PDF/A conversion crash.

Route both resource lookups through a small helper that returns an empty
list when the resource is missing or not a dictionary, so a garbage entry
is simply treated as having no fonts. Add a regression test covering a
non-dictionary /Font and /XObject.
2026-07-16 00:56:07 -07:00
James R. Barlow
089f46690a Enable ruff PTH (flake8-use-pathlib) and fix all findings
Replaces os.path/open()/os.stat()/os.chmod() calls with their Path
method equivalents across src, tests, misc, and bin, wrapping str
variables in Path(...) where they must stay str for other uses (e.g.
subprocess argv, CLI-arg formatting). helpers.safe_symlink() now
decodes StrOrBytesPath to a str Path via os.fsdecode() upfront, same
pattern already used elsewhere for the str|bytes union.
2026-07-16 00:52:57 -07:00
James R. Barlow
e45c40b063 Fix mypy/CI mismatch: drop obsolete types-Pillow stubs
Pillow >=9.2 ships its own inline types (py.typed), which the separate
types-Pillow stub package shadows when both are installed. Since
types-Pillow lived in the `test` dependency group (not `dev`), a local
`uv sync --group dev --group test` env had it installed and mypy
silently preferred its stubs, while CI's lint job only syncs the
default `dev` group and used Pillow's own (correct) stubs - surfacing
5 real "Incompatible types in assignment" errors that never appeared
locally.

Fixed the underlying type errors in _pipeline.py and ghostscript.py:
`im` was inferred as ImageFile.ImageFile from `Image.open(...) as im`,
but later reassigned the result of `.resize()`/`.transpose()`, which
return the broader Image.Image - now declared explicitly as
`im: Image.Image` before the `with` block.
2026-07-15 23:47:31 -07:00
James R. Barlow
bbac5307f2 v17.8.1 release notes 2026-07-15 23:18:34 -07:00
James R. Barlow
6167783696 Drop unreferenced validation coordinator registry 2026-07-09 15:04:56 -07:00
James R. Barlow
3d291e72c0 Enable check_untyped_defs and make mypy hook blocking
Fix the 13 errors that surfaced under mypy --check-untyped-defs so the
flag can be turned on permanently in pyproject.toml, and drop the
advisory exit-0 wrapper on the mypy pre-commit hook now that the tree is
clean.

- _plugin_manager: rename colliding loop vars (module/name were reused
  with conflicting types) and guard spec/spec.loader from
  spec_from_file_location; call __init__ via the class in __setstate__.
- __main__: pass Verbosity(options.verbose), not a bare int.
- subprocess/_check: widen package to str | Mapping[str, str] to match
  _error_trailer's existing per-platform handling.
- optimize.main: annotate the standalone PdfContext(..., None, None) that
  only ever reads context.options.
2026-07-07 15:02:32 -07:00
James R. Barlow
efebe9ca2e Improve type checks under check-untyped-defs 2026-07-07 14:51:56 -07:00
James R. Barlow
12ec97f732 Fix remaining mypy errors in fpdf_renderer and its tests
Resolves the last 16 mypy errors in the project (src/ocrmypdf and
tests are now fully clean).

fpdf_renderer/renderer.py (9 errors):
- add_page(format=...): fpdf2's own stub types this param as str, but
  its docstring and get_page_format() helper confirm a (width, height)
  tuple is accepted too - the stub annotation on add_page() itself is
  the outlier. Used cast() to match the documented/actual behavior.
- pdf.current_font is typed CoreFont | TTFFont | None, but this
  renderer only ever registers fonts via add_font() with a TTF file
  (see _register_font/set_font call sites) - it never falls back to
  fpdf2's built-in CoreFont. Added assertions (isinstance(font,
  TTFFont) where shape_text()/escape_text() are needed, which
  CoreFont lacks; plain not-None elsewhere) documenting that
  invariant instead of narrowing defensively for a case that can't
  happen here.

tests/test_pdf_renderer.py (7 errors): the ToUnicode/glyph-extraction
test helpers used `.get(key, {})` (a plain dict literal default) then
called `.values()`/`.items()` on the result. pikepdf.Object doesn't
declare `values()` in its stub (only `keys()`), so this silently
degraded to Object's catch-all `__getattr__` returning another Object,
which then failed as "not callable". Switched to `.get(key,
Dictionary()).as_dict()`, which returns pikepdf's properly-typed
_ObjectMapping helper.
2026-07-07 00:57:45 -07:00
James R. Barlow
3f1aceade2 Fix mypy errors in concurrency abstractions
Resolves all 11 remaining errors in _concurrent.py and
builtin_plugins/concurrency.py.

Root cause for 9 of the 11: setup_executor() was annotated to return
ocrmypdf's own Executor ABC as its second tuple element, but it
actually returns a concurrent.futures pool class (ThreadPoolExecutor
or ProcessPoolExecutor) - an already-existing FuturesExecutorClass
alias was defined for exactly this but never wired into the
signature. That wrong annotation made mypy check
`executor_class(initializer=..., initargs=...)` in _execute() against
Executor.__call__'s signature (which has entirely different
parameters and returns None), cascading into 8 further errors
(unexpected keyword args, "function does not return a value", "None
has no attribute __enter__/__exit__"). Fixing the one annotation
resolved all of them.

Also:
- Added a proper Queue[LogRecord | None] generic parameter (was bare
  Queue, which needs an explicit type argument for mypy to infer
  loq_queue's type across the use_threads branches).
- _concurrent.py: Executor.__call__'s task parameter defaults to
  _task_noop (return type None) when the caller omits a task, but the
  parameter is typed Callable[..., T] for an unbound per-call T. Used
  cast() since task_finished's own no-op default already accepts Any,
  so the mismatch is never actually exercised unsafely.
2026-07-07 00:49:07 -07:00
James R. Barlow
212b28e602 Fix mypy pikepdf Object typing errors in pdfinfo/optimize/graft
Resolves the remaining 22 pikepdf-related mypy errors in
pdfinfo/_image.py, pdfinfo/info.py, optimize.py, and _graft.py
(89 -> 27 errors remaining, all in the concurrency/fpdf_renderer
clusters).

Adds pikepdf_get_int()/pikepdf_get_bool() to helpers.py: safe
accessors for dict.get(key, default) results, whose static type is
the ambiguous `Object | int`/`Object | bool` and doesn't support
arithmetic/comparison against a plain int/bool.

Important correctness fix caught by the test suite: pikepdf only
returns pikepdf.Object wrappers under explicit_conversion() mode,
which this codebase never enables. By default (implicit mode), PDF
Integers/Booleans are already unboxed to native int/bool by the time
callers see them, so calling the `.as_int()`/`.as_bool()`/
`.as_decimal()` safe accessors unconditionally crashes with
AttributeError on the native-type case (test_oversized_page caught
this for UserUnit). Fixed by using int()/float() builtins, which work
polymorphically on both native numbers and pikepdf.Object (bool()
does not, so pikepdf_get_bool checks isinstance first).

Also:
- pdfinfo/info.py: pass page.obj (an Object) to
  _process_content_streams() instead of page (a Page wrapper, not an
  Object subtype).
- pdfinfo/_image.py: cast() around Matrix(array_object) - pikepdf's
  stub omits the Object/Array constructor overload that the C++
  implementation actually supports.
- optimize.py: cast() around Object.write()'s filter/decode_parms
  args for the same reason.
- _graft.py: iterate parse_content_stream() via .operands/.operator
  instead of tuple-unpacking, since ContentStreamInstruction supports
  the legacy __getitem__-based iteration protocol but not __iter__,
  which mypy doesn't statically recognize.
- pdfinfo/_worker.py: _pdf_pageinfo_concurrent's return type was
  Sequence[PageInfo | None] but always returns a real list; narrowed
  to match, fixing PdfInfo.pages' declared list[...] return type.
2026-07-07 00:44:56 -07:00
James R. Barlow
dfdb32995e Fix mypy errors: drop deprecation dep, fix PathOrIO union bugs
Reduces mypy errors in src/ocrmypdf and tests from 89 to 51.

- Replace the `deprecation` package with stdlib `warnings.deprecated`
  (falling back to typing_extensions on <3.13); drop the dependency.
- Add pypdfium2/uharfbuzz/pi_heif to mypy's ignore_missing_imports
  overrides (no upstream stubs); drop pluggy, which now ships py.typed.
- Add a tests.* mypy override so test functions aren't required to
  annotate -> None.

Real bugs found and fixed along the way, not just annotations:
- OcrmypdfPluginManager had a `pluggy` property shadowing the `pluggy`
  module import within its own class body, breaking every
  `pluggy.PluginManager` annotation below it; renamed to
  `pluggy_manager`.
- `_option_registry` was bolted onto OcrmypdfPluginManager from outside
  and read via `getattr(..., None)` instead of being a declared
  attribute; declared it properly.
- ValidationCoordinator.__init__ was typed to accept a raw
  pluggy.PluginManager, but every caller passes the OcrmypdfPluginManager
  wrapper.
- check_options_sidecar() did `options.output_file + '.txt'`, assuming
  output_file is always a str; would raise a raw TypeError if ever hit
  with a stream/bytes output. Added an explicit guard.
- is_file_writable() called Path(test_file), which raises TypeError on
  a bytes path; fixed via os.fsdecode().
- copy_final() had a dead, unused `original_file` parameter; removed it.
- run_hocr_pipeline() constructed PdfContext with the raw, untriaged
  input_file instead of the locally-copied origin_pdf, inconsistent
  with the other two pipelines.
- _options.py had jbig2_threshold declared twice in the same model.
2026-07-07 00:25:33 -07:00
James R. Barlow
273826377e Migrate pre-commit to prek
prek runs local hooks as plain execs against tools uv already provisions,
so ruff/mypy can never drift from the versions/config uv.lock pins
elsewhere and CI needs no separate hook-cache download.

- Add ruff and prek to the uv dev dependency group (ruff wasn't a
  uv-managed dependency before; pre-commit silently vendored its own).
- Replace .pre-commit-config.yaml with prek.toml: keep the
  pre-commit-hooks repo for generic file checks, convert ruff-format/
  ruff-check to local `uv run ruff ...` hooks, and add a local mypy
  hook that reports but never fails (87 pre-existing errors need a
  separate cleanup before it can be made blocking).
- Add a `lint` job to CI that runs `prek run --all-files` and gate the
  OS/Python test matrix on it so lint issues fail fast.
- Fix the ruff debt (format + lint) uncovered by actually running it,
  since it was small and mechanical, so the new CI gate starts green.
2026-07-06 23:35:52 -07:00
sokai
5569d4db07 Minor Update to tesseract_ocr.py (#1711)
Changed tesseract (v5.5.2) help command for `--tesseract-pagesegmode` parameter to faster help access: `tesseract --help` → `tesseract --help-extra`
2026-07-03 16:21:22 -07:00
James R. Barlow
8de7b05fb9 Fix flaky tagged-PDF skip-text test under Ghostscript 10.x
The test asserted that --mode skip preserves the structure tree, but with
the default --output-type auto the output runs through Ghostscript PDF/A
conversion, which discards /StructTreeRoot on Ghostscript 10.x (9.x kept
it). This failed on macOS CI and locally while passing on the Ubuntu
runners' Ghostscript 9.55. Pin the test to --output-type pdf so it exercises
OCRmyPDF's own structure-tree handling without the version-dependent GS step,
and document the caveat in advanced.md.
v17.8.0
2026-07-01 00:10:57 -07:00
James R. Barlow
72ce05768e Bump version: v17.8.0 2026-06-30 01:45:30 -07:00
James R. Barlow
3dc68778fc Fix stale auto-mode comment and harden GS-raises test 2026-06-30 01:19:00 -07:00
James R. Barlow
1aec92b919 docs: auto output type falls back to Ghostscript for PDF/A (#1561) 2026-06-30 01:12:08 -07:00
James R. Barlow
43d3448709 Make --output-type auto fall back to Ghostscript for PDF/A 2026-06-30 01:08:04 -07:00
James R. Barlow
7512b1042a Detect veraPDF when version line is preceded by JVM warnings 2026-06-30 01:03:02 -07:00
James R. Barlow
efe83e8c54 Protect non-embedded CID text layers from PDF/A corruption (closes #1561)
Ghostscript's PDF/A conversion re-embeds non-embedded CID (CJK) fonts by
substituting a system font, which corrupts the character-to-Unicode
mapping and silently destroys an existing text layer -- commonly the OCR
layer Adobe Acrobat adds to scanned CJK documents.

Detect non-embedded CID/Type0 fonts before conversion: with
--output-type auto (the default) downgrade to a regular PDF and preserve
the text layer; with an explicit --output-type pdfa* stop with an error
rather than emit corrupted output. Simple non-embedded fonts (e.g. Latin)
are left alone -- Ghostscript substitutes them without corrupting the
text, and they are far too common to treat as conversion blockers.

Use --output-type pdf to keep the existing text layer, or --force-ocr to
rebuild it with embedded fonts.
2026-06-30 00:01:42 -07:00
James R. Barlow
a13d27bfb5 Protect stdout from corruption when writing PDF to standard output
Writing the output PDF to stdout (ocrmypdf in.pdf -) previously relied on
an honor system: no in-process code -- third-party libraries, plugins, or
stray print() calls -- was supposed to write to stdout, enforced only
indirectly. A single accidental write to fd 1 would silently corrupt the
output PDF.

Enforce this at the OS level. At CLI startup, before plugins load or any
worker process/thread starts, save the real stdout via os.dup() and point
fd 1 at stderr, so stray writes are diverted to stderr while only the final
"produce the PDF" step writes to the preserved descriptor. Exposed as the
opt-in public API function configure_stdout_protection(), mirroring
configure_logging(); it is not enabled inside ocr() so in-process library
users keep their own stdout.

Also fix check_requested_output_file() to test the preserved real stdout
for tty-ness, since after the redirect sys.stdout reports stderr's status.

Fold unreleased v17.7.2 notes into v17.8.0.
2026-06-28 12:02:34 -07:00
James R. Barlow
ea7ad7d683 Handle non-UTF-8 DocumentInfo keys in repair_docinfo_nuls (closes #1540)
Some PDFs use a /Name dictionary key in /DocumentInfo whose bytes are not
valid UTF-8/PDFDocEncoding, e.g. a Latin-1 /Saks#e5r. Older pikepdf raised
UnicodeDecodeError while iterating such a block, crashing the pipeline
during PDF/A conversion. repair_docinfo_nuls is documented to log and
continue on a malformed DocumentInfo block, so catch UnicodeDecodeError
alongside TypeError.

Add a mock-based unit test that drives the decode-error branch (current
pikepdf surrogate-escapes instead of raising) and an end-to-end test over
the reporter's file, committed as docinfo_latin1_key.pdf.
2026-06-27 01:50:58 -07:00
James R. Barlow
8b20bb3c5b docs: add "Edit on GitHub" link to ReadTheDocs pages (closes #1490)
ReadTheDocs stopped injecting the GitHub edit context when it migrated to
Addons, so the sphinx_rtd_theme "Edit on GitHub" breadcrumb link vanished,
leaving only the static "View page source" (_sources/*.txt) copy. Set the
html_context explicitly so each page links back to its source on GitHub.
2026-06-27 01:25:17 -07:00
James R. Barlow
320876a6d1 docs: document Tesseract configs/ requirement (closes #1567)
When TESSDATA_PREFIX points at a hand-assembled tessdata folder lacking
the configs/ subdirectory (e.g. files pulled from tessdata_best),
Tesseract prints "read_params_file: Can't open hocr/txt" and produces no
output. The runtime now surfaces a clear error (v17.5.0); add the
matching documentation: a new errors.md entry and a note on the
TESSDATA_PREFIX docs.
2026-06-27 00:32:22 -07:00
James R. Barlow
dfbb4c9275 Add regression test for blank page with non-zero MediaBox origin (#1709)
A page whose MediaBox has a non-zero origin (e.g. from PDF Arranger crops)
was rendered blank by --force-ocr in v16.12.0, because fix_pagepdf_boxes
offset the CropBox with the wrong sign, pushing it entirely outside the
image-page MediaBox. The behavior was fixed in v17.0.0 but had no
end-to-end test: the existing box tests only assert that ocrmypdf runs,
which the blank-page bug passed (it exited 0 with a valid, empty PDF/A).

Add a test that renders the output and asserts the visible content
survives, parametrized over both the ghostscript and pypdfium rasterizers
since the bug reproduced regardless of rasterizer.
2026-06-22 14:57:52 -07:00
James R. Barlow
d4f5c2d160 Merge remote-tracking branch 'origin/dependabot/github_actions/actions/checkout-7' 2026-06-22 14:11:53 -07:00
dependabot[bot]
263d6034be build(deps): bump actions/checkout from 6 to 7
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-22 10:42:29 +00:00
James R. Barlow
de403f6d5e Bump version: v17.7.1 v17.7.1 2026-06-19 16:44:31 -07:00
James R. Barlow
86b6f2c907 v17.7.1 release notes 2026-06-19 16:44:10 -07:00
jbarlow
e6fab76918 Fix Windows redo-ocr performance regression; drop pdfminer BUFSIZ workaround (#1706)
Since v16.4.3, OCRmyPDF forced pdfminer's read buffer to 256 MiB to work
around a pdfminer bug that mishandled tokens split across the buffer
boundary (gh #1361). On Windows this caused a severe performance
regression (gh #1662): CPython's BufferedReader.read(n) eagerly allocates
an n-byte buffer on every read, so pdfminer's thousands of seek+read
cycles each paid a ~30 ms 256 MiB allocation (this allocation is lazy and
effectively free on Linux). For a typical PDF the "Scanning contents"
phase went from ~5s on Linux to ~60s on Windows.

The underlying pdfminer bug was fixed upstream in pdfminer.six 20250327
(pdfminer/pdfminer.six#1030), with a follow-up for tokens split across
streams in 20260107 (pdfminer/pdfminer.six#1158). Remove the monkeypatch
entirely and raise the minimum pdfminer.six to 20260107 so we rely on the
upstream fix instead.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:34:20 -07:00
jbarlow
334918d0f7 Discover variable and per-language Noto fonts for the OCR text layer (#1652) (#1707)
* Bump uv lock, fix bump_version

* Discover variable and per-language Noto fonts for the OCR text layer (#1652)

SystemFontProvider only matched static "-Regular.ttf/.otf" filenames, so
the variable fonts shipped by Homebrew casks and current Google Fonts
(e.g. NotoSansArabic[wdth,wght].ttf) were never found. Users who had
installed the font still got the glyphless Occulta fallback and a cryptic
"No font found" warning.

- Match variable fonts (Base[...]), -VF, and bare-family filenames via a
  boundary-aware flexible search, escaping the glob-special brackets.
- Make CJK language-aware: the modern per-language Noto fonts (NotoSansSC
  /TC/HK/JP/KR) are region subsets, so map each CJK language to its own
  family and keep the full-coverage pan-CJK super font as a shared
  fallback. Glyph coverage, not shape, is what matters for the invisible
  text layer.
- Reword the missing-font warning to explain the consequence (searchable
  but blank when highlighted) and name the language-specific font.
2026-06-18 10:34:08 -07:00
James R. Barlow
d6329489ce Bump version: v17.7.0 v17.7.0 2026-06-17 15:37:30 -07:00
James R. Barlow
e6d240ee93 Update release notes 2026-06-17 15:37:19 -07:00
James R. Barlow
ff45e54c07 Run Docker images as non-root user and default to /data workdir
Harden the Docker images by dropping root privileges, and make the
bind-mount workflow less fiddly.

- Create a non-root `app` user (uid/gid 1000) in both images and add
  `USER app` before the entrypoint, so ocrmypdf (and the
  webservice/watcher) no longer run as root. This also fixes the
  previously dangling `--chown=app:app`, which referenced a user that
  was never created. The Ubuntu base ships a default `ubuntu`/1000 user,
  so remove it first so `app` can take uid 1000 (parity with Alpine).

- Add `WORKDIR /data` (created and app-owned) so bind-mounted input and
  output can be passed as relative paths without `--workdir`. The
  webservice/watcher are now invoked by absolute path (`/app/*.py`)
  since the working directory is no longer `/app`.

- Drop the redundant `ppa:alex-p/tesseract-ocr5` from the Ubuntu image:
  Tesseract 5 ships in the Ubuntu archive as of 24.04, and the PPA had
  no build for the 26.04 base, which broke the build outright.

- Rewrite docs/docker.md rootless-first: stdin/stdout piping as the
  recommended permission-free path, then per-runtime volume guidance
  (rootless Docker `--user 0:0`, Podman `--userns keep-id`, rootful
  Docker as the special case). Update batch.md and the compose example
  to match (absolute script paths, per-runtime `user:` guidance).
2026-06-17 15:13:19 -07:00
James R. Barlow
e0ee0882ef Fix typo in error message 2026-06-17 14:49:31 -07:00