Compare commits

...
9 Commits
Author SHA1 Message Date
James R. Barlow 3336d67e77 Fix CJK test broken by fpdf2 2.8.7 CFF font encoding change
fpdf2 >= 2.8.7 emits a custom begincidchar Encoding CMap for CFF-based
CID fonts (e.g. NotoSansCJK). pdfminer.six returns <CMap: None> for such
CMaps, so text extraction yields empty output. Switch to pdftotext (poppler)
which handles the new encoding correctly.
2026-04-19 23:26:30 -07:00
James R. Barlow 73e16e7821 Merge remote-tracking branch 'origin/dependabot/github_actions/codecov/codecov-action-6' 2026-04-19 13:59:42 -07:00
James R. Barlow 6f1d37d78f Merge remote-tracking branch 'origin/dependabot/github_actions/sigstore/gh-action-sigstore-python-3.3.0' 2026-04-19 13:59:30 -07:00
James R. Barlow 2ed82de2e0 Update uv.lock again - pygithub 2026-04-19 13:58:46 -07:00
James R. Barlow c43903fa14 Bump version: v17.4.2 2026-04-19 13:45:34 -07:00
James R. Barlow 1c89cacfef Respect host-set PIL.Image.MAX_IMAGE_PIXELS in Python API
The API previously clobbered PIL.Image.MAX_IMAGE_PIXELS unconditionally
on every call, so host applications (e.g. Paperless-NGX) that configured
the PIL limit before invoking ocrmypdf.ocr() saw their setting silently
overwritten with the 250 MP default. Make max_image_mpixels default to
None and only apply the override when the caller explicitly sets it.
The CLI default of 250 MP is unchanged.

Fixes #1665
2026-04-19 13:44:57 -07:00
James R. Barlow 75714fe43e Update uv.lock
For Pillow vuln. Fixes #1669
2026-04-19 13:06:22 -07:00
dependabot[bot]andGitHub e371ce95ca Bump sigstore/gh-action-sigstore-python from 3.2.0 to 3.3.0
Bumps [sigstore/gh-action-sigstore-python](https://github.com/sigstore/gh-action-sigstore-python) from 3.2.0 to 3.3.0.
- [Release notes](https://github.com/sigstore/gh-action-sigstore-python/releases)
- [Changelog](https://github.com/sigstore/gh-action-sigstore-python/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sigstore/gh-action-sigstore-python/compare/v3.2.0...v3.3.0)

---
updated-dependencies:
- dependency-name: sigstore/gh-action-sigstore-python
  dependency-version: 3.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:50:26 +00:00
dependabot[bot]andGitHub 716a2e22c3 Bump codecov/codecov-action from 5 to 6
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 5 to 6.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-06 10:50:22 +00:00
10 changed files with 1368 additions and 1108 deletions
+3 -3
View File
@@ -87,7 +87,7 @@ jobs:
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -149,7 +149,7 @@ jobs:
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
@@ -196,7 +196,7 @@ jobs:
uv run --no-dev pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v5
uses: codecov/codecov-action@v6
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
+1 -1
View File
@@ -34,7 +34,7 @@ jobs:
# PyPI doesn't support sigstore publishing, so generate after publishing to PyPI
- name: Sign the dists with Sigstore
uses: sigstore/gh-action-sigstore-python@v3.2.0
uses: sigstore/gh-action-sigstore-python@v3.3.0
with:
inputs: |
./dist/*.tar.gz
+9
View File
@@ -3,6 +3,15 @@
# v17
## v17.4.2
- Fixed Python API unconditionally overriding ``PIL.Image.MAX_IMAGE_PIXELS``
when the caller did not explicitly set ``max_image_mpixels``. Host
applications (e.g. Paperless-NGX) that configure the PIL limit before
invoking ``ocrmypdf.ocr()`` now have their setting respected. The CLI
default of 250 megapixels is unchanged. {issue}`1665`
- Updated uv.lock to avoid pinning a vulnerable version of Pillow. {issue}`1666`
## v17.4.1
- Fixed RTL text extraction order in the fpdf2 renderer. Arabic lam-alef
+2 -1
View File
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "ocrmypdf"
version = "17.4.1"
version = "17.4.2"
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
readme = "README.md"
license = "MPL-2.0"
@@ -161,6 +161,7 @@ dev = [
"ipykernel>=6.29.5",
"reportlab>=4.4.4",
"cyclopts>=4.5.1",
"pygithub>=2.9.1",
]
test = [
# Core testing framework
+2 -2
View File
@@ -192,7 +192,7 @@ class OcrOptions(BaseModel):
no_overwrite: bool = False
# Advanced options
max_image_mpixels: float = 250.0
max_image_mpixels: float | None = None
pdf_renderer: str = 'auto'
ocr_engine: str = 'auto'
rasterizer: str = 'auto'
@@ -301,7 +301,7 @@ class OcrOptions(BaseModel):
@classmethod
def validate_max_image_mpixels(cls, v):
"""Validate max image megapixels."""
if v < 0:
if v is not None and v < 0:
raise ValueError("max_image_mpixels must be non-negative")
return v
+7 -4
View File
@@ -329,10 +329,13 @@ def setup_pipeline(
# Note: OcrOptions is immutable, so we can't modify options.jobs directly
# The jobs field should already be set correctly during OcrOptions creation
# Apply PIL max image pixels side effect
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000)
if PIL.Image.MAX_IMAGE_PIXELS == 0:
PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore
# Apply PIL max image pixels side effect only when explicitly requested.
# When None, leave PIL.Image.MAX_IMAGE_PIXELS as the host application
# configured it. The CLI passes its own default (250.0) via argparse.
if options.max_image_mpixels is not None:
PIL.Image.MAX_IMAGE_PIXELS = int(options.max_image_mpixels * 1_000_000)
if PIL.Image.MAX_IMAGE_PIXELS == 0:
PIL.Image.MAX_IMAGE_PIXELS = None # type: ignore
pikepdf_enable_mmap()
executor = setup_executor(plugin_manager)
+1 -1
View File
@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
__version__ = "17.4.1"
__version__ = "17.4.2"
+30 -6
View File
@@ -5,6 +5,8 @@
from __future__ import annotations
import shutil
import subprocess
from pathlib import Path
import pytest
@@ -37,6 +39,26 @@ def resources():
return Path(__file__).parent / "resources"
@pytest.fixture
def pdftotext():
"""Return a function to extract text from PDF using pdftotext.
Skips the test if pdftotext is not available.
"""
pdftotext_path = shutil.which('pdftotext')
if pdftotext_path is None:
pytest.skip("pdftotext not available")
def extract_text(pdf_path: Path) -> str:
return subprocess.check_output(
['pdftotext', '-enc', 'UTF-8', str(pdf_path), '-'],
text=True,
encoding='utf-8',
)
return extract_text
class TestFpdf2RendererImports:
"""Test that all fpdf2 renderer modules can be imported."""
@@ -443,14 +465,16 @@ class TestWordSegmentation:
assert "World" in words_found
assert "Test" in words_found
def test_cjk_no_spurious_spaces(self, multi_font_manager, tmp_path):
def test_cjk_no_spurious_spaces(self, multi_font_manager, tmp_path, pdftotext):
"""Test that CJK text does not get spurious spaces inserted.
CJK scripts don't use spaces between characters/words, so we should
not insert spaces between adjacent CJK words.
"""
from pdfminer.high_level import extract_text
Uses pdftotext (poppler) instead of pdfminer.six because the latter
cannot decode the custom Encoding CMap that fpdf2 >= 2.8.7 emits for
subsetted CFF-based CID fonts (e.g. NotoSansCJK).
"""
from ocrmypdf.models.ocr_element import BoundingBox, OcrElement
# Create a page with CJK words (Chinese characters)
@@ -487,15 +511,15 @@ class TestWordSegmentation:
output_path = tmp_path / "test_cjk_segmentation.pdf"
renderer.render(output_path)
# Extract text using pdfminer.six
extracted_text = extract_text(str(output_path))
extracted_text = pdftotext(output_path)
# CJK text should be present
assert "你好" in extracted_text
assert "世界" in extracted_text
# There should NOT be spaces between CJK characters
# (but pdfminer may add some whitespace, so we check the raw chars)
# (a space between the two words is acceptable, since they are
# separated horizontally on the rendered page)
extracted_chars = extracted_text.replace(" ", "").replace("\n", "")
assert "你好世界" in extracted_chars or (
"你好" in extracted_chars and "世界" in extracted_chars
+39
View File
@@ -102,6 +102,45 @@ def test_pillow_options():
with pytest.raises(ValueError, match="max_image_mpixels must be non-negative"):
make_ocr_opts(max_image_mpixels=-1)
# Default is None, meaning "do not override host-set PIL.Image.MAX_IMAGE_PIXELS"
opts = make_ocr_opts()
assert opts.max_image_mpixels is None
def test_pillow_max_image_pixels_not_overridden_when_unset():
"""Issue #1665: respect host-set PIL.Image.MAX_IMAGE_PIXELS.
API callers (e.g. Paperless-NGX) that set PIL.Image.MAX_IMAGE_PIXELS
before invoking ocrmypdf should not have their setting clobbered when
max_image_mpixels is not explicitly passed.
"""
import PIL.Image
from ocrmypdf._pipelines._common import setup_pipeline
parser = get_parser()
pm = setup_plugin_infrastructure(plugins=[])
pm.add_options(parser=parser)
saved = PIL.Image.MAX_IMAGE_PIXELS
try:
PIL.Image.MAX_IMAGE_PIXELS = None # host disables the limit
opts = make_ocr_opts()
assert opts.max_image_mpixels is None
setup_pipeline(opts, pm)
assert PIL.Image.MAX_IMAGE_PIXELS is None
PIL.Image.MAX_IMAGE_PIXELS = 1_000_000_000 # host sets a high limit
setup_pipeline(opts, pm)
assert PIL.Image.MAX_IMAGE_PIXELS == 1_000_000_000
# When explicitly passed, it still takes effect.
opts = make_ocr_opts(max_image_mpixels=100)
setup_pipeline(opts, pm)
assert PIL.Image.MAX_IMAGE_PIXELS == 100_000_000
finally:
PIL.Image.MAX_IMAGE_PIXELS = saved
def test_output_tty():
with patch('sys.stdout.isatty', return_value=True), pytest.raises(BadArgsError):
Generated
+1274 -1090
View File
File diff suppressed because it is too large Load Diff