Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3336d67e77 | ||
|
|
73e16e7821 | ||
|
|
6f1d37d78f | ||
|
|
2ed82de2e0 | ||
|
|
c43903fa14 | ||
|
|
1c89cacfef | ||
|
|
75714fe43e | ||
|
|
e371ce95ca | ||
|
|
716a2e22c3 |
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,3 +1,3 @@
|
||||
# SPDX-FileCopyrightText: 2022 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
__version__ = "17.4.1"
|
||||
__version__ = "17.4.2"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user