test: add comprehensive tests for --rasterizer option
Add test_rasterizer.py with tests covering:
- Basic rasterizer option validation ('auto', 'ghostscript', 'pypdfium')
- Rasterizer + --rotate-pages interaction
- PDFs with nonstandard MediaBox/TrimBox/CropBox
- Direct hook tests verifying plugins respect the option
Also fix pluggy parameter passing: make 'options' a required parameter
(no default) in the hookspec so pluggy forwards it to implementations.
Update test plugins and test_rotation.py to pass the new parameter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
ed813cec67
commit
b9f488d65c
@@ -128,7 +128,7 @@ def rasterize_pdf_page(
|
||||
rotation,
|
||||
filter_vector,
|
||||
stop_on_soft_error,
|
||||
options=None,
|
||||
options,
|
||||
):
|
||||
"""Rasterize a single page of a PDF file using Ghostscript."""
|
||||
# Check if user explicitly requested a different rasterizer
|
||||
|
||||
@@ -130,7 +130,7 @@ def rasterize_pdf_page(
|
||||
rotation: int | None,
|
||||
filter_vector: bool,
|
||||
stop_on_soft_error: bool,
|
||||
options=None,
|
||||
options,
|
||||
) -> Path | None:
|
||||
"""Rasterize a single page of a PDF file using pypdfium2.
|
||||
|
||||
|
||||
@@ -213,7 +213,7 @@ def rasterize_pdf_page(
|
||||
rotation: int | None,
|
||||
filter_vector: bool,
|
||||
stop_on_soft_error: bool,
|
||||
options: OCROptions | None = None,
|
||||
options: OCROptions | None,
|
||||
) -> Path: # type: ignore[return-value]
|
||||
"""Rasterize one page of a PDF at resolution raster_dpi in canvas units.
|
||||
|
||||
|
||||
@@ -24,9 +24,11 @@ def rasterize_pdf_page(
|
||||
raster_device,
|
||||
raster_dpi,
|
||||
pageno,
|
||||
page_dpi=None,
|
||||
rotation=None,
|
||||
filter_vector=False,
|
||||
page_dpi,
|
||||
rotation,
|
||||
filter_vector,
|
||||
stop_on_soft_error,
|
||||
options,
|
||||
) -> Path:
|
||||
with patch('ocrmypdf._exec.ghostscript.run') as mock:
|
||||
mock.side_effect = raise_gs_fail
|
||||
@@ -39,7 +41,8 @@ def rasterize_pdf_page(
|
||||
page_dpi=page_dpi,
|
||||
rotation=rotation,
|
||||
filter_vector=filter_vector,
|
||||
stop_on_soft_error=True,
|
||||
stop_on_soft_error=stop_on_soft_error,
|
||||
options=options,
|
||||
)
|
||||
mock.assert_called()
|
||||
return output_file
|
||||
|
||||
@@ -29,6 +29,7 @@ def rasterize_pdf_page(
|
||||
rotation,
|
||||
filter_vector,
|
||||
stop_on_soft_error,
|
||||
options,
|
||||
) -> Path:
|
||||
with patch('ocrmypdf._exec.ghostscript.run') as mock:
|
||||
mock.side_effect = fail_if_stoponerror
|
||||
@@ -42,6 +43,7 @@ def rasterize_pdf_page(
|
||||
rotation=rotation,
|
||||
filter_vector=filter_vector,
|
||||
stop_on_soft_error=stop_on_soft_error,
|
||||
options=options,
|
||||
)
|
||||
mock.assert_called()
|
||||
return output_file
|
||||
|
||||
@@ -0,0 +1,603 @@
|
||||
# SPDX-FileCopyrightText: 2025 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
"""Tests for the --rasterizer CLI option."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from io import BytesIO
|
||||
|
||||
import img2pdf
|
||||
import pikepdf
|
||||
import pytest
|
||||
from PIL import Image
|
||||
|
||||
from ocrmypdf._options import OCROptions
|
||||
from ocrmypdf._plugin_manager import get_plugin_manager
|
||||
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution
|
||||
|
||||
from .conftest import check_ocrmypdf
|
||||
|
||||
# Check if pypdfium2 is available
|
||||
try:
|
||||
import pypdfium2 # noqa: F401
|
||||
|
||||
PYPDFIUM_AVAILABLE = True
|
||||
except ImportError:
|
||||
PYPDFIUM_AVAILABLE = False
|
||||
|
||||
|
||||
class TestRasterizerOption:
|
||||
"""Test the --rasterizer CLI option."""
|
||||
|
||||
def test_rasterizer_auto_default(self, resources, outpdf):
|
||||
"""Test that --rasterizer auto (default) works."""
|
||||
check_ocrmypdf(
|
||||
resources / 'graph.pdf',
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'auto',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
def test_rasterizer_ghostscript(self, resources, outpdf):
|
||||
"""Test that --rasterizer ghostscript works."""
|
||||
check_ocrmypdf(
|
||||
resources / 'graph.pdf',
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'ghostscript',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_rasterizer_pypdfium(self, resources, outpdf):
|
||||
"""Test that --rasterizer pypdfium works when pypdfium2 is installed."""
|
||||
check_ocrmypdf(
|
||||
resources / 'graph.pdf',
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'pypdfium',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
def test_rasterizer_invalid(self):
|
||||
"""Test that an invalid rasterizer value is rejected."""
|
||||
with pytest.raises(ValueError, match="rasterizer must be one of"):
|
||||
OCROptions(
|
||||
input_file='test.pdf', output_file='out.pdf', rasterizer='invalid'
|
||||
)
|
||||
|
||||
|
||||
class TestRasterizerWithRotation:
|
||||
"""Test --rasterizer interaction with --rotate-pages."""
|
||||
|
||||
def test_ghostscript_with_rotation(self, resources, outpdf):
|
||||
"""Test Ghostscript rasterizer with page rotation."""
|
||||
check_ocrmypdf(
|
||||
resources / 'cardinal.pdf',
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'ghostscript',
|
||||
'--rotate-pages',
|
||||
'--rotate-pages-threshold',
|
||||
'0.1',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_cache.py',
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_pypdfium_with_rotation(self, resources, outpdf):
|
||||
"""Test pypdfium rasterizer with page rotation."""
|
||||
check_ocrmypdf(
|
||||
resources / 'cardinal.pdf',
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'pypdfium',
|
||||
'--rotate-pages',
|
||||
'--rotate-pages-threshold',
|
||||
'0.1',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_cache.py',
|
||||
)
|
||||
|
||||
def test_auto_with_rotation(self, resources, outpdf):
|
||||
"""Test auto rasterizer with page rotation."""
|
||||
check_ocrmypdf(
|
||||
resources / 'cardinal.pdf',
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'auto',
|
||||
'--rotate-pages',
|
||||
'--rotate-pages-threshold',
|
||||
'0.1',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_cache.py',
|
||||
)
|
||||
|
||||
|
||||
class TestRasterizerHookDirect:
|
||||
"""Test rasterize_pdf_page hook directly with different rasterizer options."""
|
||||
|
||||
def test_ghostscript_hook_respects_option(self, resources, tmp_path):
|
||||
"""Test that Ghostscript hook returns None when pypdfium is requested."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
# Create options requesting pypdfium
|
||||
options = OCROptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='pypdfium',
|
||||
)
|
||||
|
||||
img = tmp_path / 'ghostscript_test.png'
|
||||
result = pm.hook.rasterize_pdf_page(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=img,
|
||||
raster_device='pngmono',
|
||||
raster_dpi=Resolution(50, 50),
|
||||
page_dpi=Resolution(50, 50),
|
||||
pageno=1,
|
||||
rotation=0,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
# When pypdfium is requested:
|
||||
# - If pypdfium IS available, pypdfium handles it and returns the path
|
||||
# - If pypdfium is NOT available, both plugins return None
|
||||
# (ghostscript returns None because pypdfium was requested,
|
||||
# pypdfium returns None because it's not installed)
|
||||
if PYPDFIUM_AVAILABLE:
|
||||
assert result == img
|
||||
else:
|
||||
assert result is None
|
||||
|
||||
def test_pypdfium_hook_respects_option(self, resources, tmp_path):
|
||||
"""Test that pypdfium hook returns None when ghostscript is requested."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
# Create options requesting ghostscript
|
||||
options = OCROptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript',
|
||||
)
|
||||
|
||||
img = tmp_path / 'pypdfium_test.png'
|
||||
result = pm.hook.rasterize_pdf_page(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=img,
|
||||
raster_device='pngmono',
|
||||
raster_dpi=Resolution(50, 50),
|
||||
page_dpi=Resolution(50, 50),
|
||||
pageno=1,
|
||||
rotation=0,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
# Ghostscript should handle it
|
||||
assert result == img
|
||||
assert img.exists()
|
||||
|
||||
def test_auto_uses_pypdfium_when_available(self, resources, tmp_path):
|
||||
"""Test that auto mode uses pypdfium when available."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='auto',
|
||||
)
|
||||
|
||||
img = tmp_path / 'auto_test.png'
|
||||
result = pm.hook.rasterize_pdf_page(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=img,
|
||||
raster_device='pngmono',
|
||||
raster_dpi=Resolution(50, 50),
|
||||
page_dpi=Resolution(50, 50),
|
||||
pageno=1,
|
||||
rotation=0,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
assert result == img
|
||||
assert img.exists()
|
||||
|
||||
|
||||
def _create_gradient_image(width: int, height: int) -> Image.Image:
|
||||
"""Create an image with multiple gradients to detect rasterization errors.
|
||||
|
||||
The image contains:
|
||||
- Horizontal gradient from red to blue
|
||||
- Vertical gradient overlay from green to transparent
|
||||
- Diagonal bands for edge detection
|
||||
"""
|
||||
img = Image.new('RGB', (width, height))
|
||||
pixels = img.load()
|
||||
|
||||
for y in range(height):
|
||||
for x in range(width):
|
||||
# Horizontal gradient: red to blue
|
||||
r = int(255 * (1 - x / width))
|
||||
b = int(255 * (x / width))
|
||||
|
||||
# Vertical gradient: add green component
|
||||
g = int(255 * (y / height))
|
||||
|
||||
# Add diagonal bands for edge detection
|
||||
band = ((x + y) // 20) % 2
|
||||
if band:
|
||||
r = min(255, r + 40)
|
||||
g = min(255, g + 40)
|
||||
b = min(255, b + 40)
|
||||
|
||||
pixels[x, y] = (r, g, b)
|
||||
|
||||
return img
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pdf_with_nonstandard_boxes(tmp_path):
|
||||
"""Create a PDF with nonstandard MediaBox, TrimBox and CropBox."""
|
||||
# Create an image with gradients to detect rasterization errors
|
||||
img = _create_gradient_image(200, 300)
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format='PNG')
|
||||
img_bytes.seek(0)
|
||||
|
||||
# Convert to PDF
|
||||
pdf_bytes = BytesIO()
|
||||
img2pdf.convert(
|
||||
img_bytes.read(),
|
||||
layout_fun=img2pdf.get_fixed_dpi_layout_fun((72, 72)),
|
||||
outputstream=pdf_bytes,
|
||||
**IMG2PDF_KWARGS,
|
||||
)
|
||||
pdf_bytes.seek(0)
|
||||
|
||||
# Modify the PDF to have nonstandard boxes
|
||||
pdf_path = tmp_path / 'nonstandard_boxes.pdf'
|
||||
with pikepdf.open(pdf_bytes) as pdf:
|
||||
page = pdf.pages[0]
|
||||
# Set MediaBox larger than content
|
||||
page.MediaBox = pikepdf.Array([0, 0, 400, 500])
|
||||
# Set CropBox smaller - this is what viewers typically show
|
||||
page.CropBox = pikepdf.Array([50, 50, 350, 450])
|
||||
# Set TrimBox even smaller - indicates intended trim area
|
||||
page.TrimBox = pikepdf.Array([75, 75, 325, 425])
|
||||
pdf.save(pdf_path)
|
||||
|
||||
return pdf_path
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def pdf_with_negative_mediabox(tmp_path):
|
||||
"""Create a PDF with MediaBox that has negative origin coordinates."""
|
||||
# Create an image with gradients to detect rasterization errors
|
||||
img = _create_gradient_image(200, 300)
|
||||
img_bytes = BytesIO()
|
||||
img.save(img_bytes, format='PNG')
|
||||
img_bytes.seek(0)
|
||||
|
||||
pdf_bytes = BytesIO()
|
||||
img2pdf.convert(
|
||||
img_bytes.read(),
|
||||
layout_fun=img2pdf.get_fixed_dpi_layout_fun((72, 72)),
|
||||
outputstream=pdf_bytes,
|
||||
**IMG2PDF_KWARGS,
|
||||
)
|
||||
pdf_bytes.seek(0)
|
||||
|
||||
pdf_path = tmp_path / 'negative_mediabox.pdf'
|
||||
with pikepdf.open(pdf_bytes) as pdf:
|
||||
page = pdf.pages[0]
|
||||
# MediaBox with negative origin (valid PDF but unusual)
|
||||
page.MediaBox = pikepdf.Array([-100, -100, 300, 400])
|
||||
pdf.save(pdf_path)
|
||||
|
||||
return pdf_path
|
||||
|
||||
|
||||
class TestRasterizerWithNonStandardBoxes:
|
||||
"""Test rasterizers with PDFs having nonstandard MediaBox/TrimBox/CropBox."""
|
||||
|
||||
def test_ghostscript_nonstandard_boxes(self, pdf_with_nonstandard_boxes, outpdf):
|
||||
"""Test Ghostscript handles nonstandard page boxes correctly."""
|
||||
check_ocrmypdf(
|
||||
pdf_with_nonstandard_boxes,
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'ghostscript',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_pypdfium_nonstandard_boxes(self, pdf_with_nonstandard_boxes, outpdf):
|
||||
"""Test pypdfium handles nonstandard page boxes correctly."""
|
||||
check_ocrmypdf(
|
||||
pdf_with_nonstandard_boxes,
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'pypdfium',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
def test_ghostscript_negative_mediabox(self, pdf_with_negative_mediabox, outpdf):
|
||||
"""Test Ghostscript handles negative MediaBox origin."""
|
||||
check_ocrmypdf(
|
||||
pdf_with_negative_mediabox,
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'ghostscript',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_pypdfium_negative_mediabox(self, pdf_with_negative_mediabox, outpdf):
|
||||
"""Test pypdfium handles negative MediaBox origin."""
|
||||
check_ocrmypdf(
|
||||
pdf_with_negative_mediabox,
|
||||
outpdf,
|
||||
'--rasterizer',
|
||||
'pypdfium',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_noop.py',
|
||||
)
|
||||
|
||||
def test_compare_rasterizers_nonstandard_boxes(
|
||||
self, pdf_with_nonstandard_boxes, tmp_path
|
||||
):
|
||||
"""Compare output dimensions between rasterizers for nonstandard boxes."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options_gs = OCROptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out_gs.pdf',
|
||||
rasterizer='ghostscript',
|
||||
)
|
||||
|
||||
img_gs = tmp_path / 'gs.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=img_gs,
|
||||
raster_device='png16m',
|
||||
raster_dpi=Resolution(72, 72),
|
||||
page_dpi=Resolution(72, 72),
|
||||
pageno=1,
|
||||
rotation=0,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options_gs,
|
||||
)
|
||||
|
||||
with Image.open(img_gs) as im_gs:
|
||||
gs_size = im_gs.size
|
||||
|
||||
if PYPDFIUM_AVAILABLE:
|
||||
options_pdfium = OCROptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out_pdfium.pdf',
|
||||
rasterizer='pypdfium',
|
||||
)
|
||||
|
||||
img_pdfium = tmp_path / 'pdfium.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=img_pdfium,
|
||||
raster_device='png16m',
|
||||
raster_dpi=Resolution(72, 72),
|
||||
page_dpi=Resolution(72, 72),
|
||||
pageno=1,
|
||||
rotation=0,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options_pdfium,
|
||||
)
|
||||
|
||||
with Image.open(img_pdfium) as im_pdfium:
|
||||
pdfium_size = im_pdfium.size
|
||||
|
||||
# Note: Ghostscript and pypdfium use different page boxes:
|
||||
# - Ghostscript uses MediaBox (400x500)
|
||||
# - pypdfium uses CropBox (300x400)
|
||||
# This is expected behavior - verify each produces valid output
|
||||
assert gs_size == (400, 500), f"Ghostscript size: {gs_size}"
|
||||
assert pdfium_size == (300, 400), f"pypdfium size: {pdfium_size}"
|
||||
|
||||
|
||||
class TestRasterizerWithRotationAndBoxes:
|
||||
"""Test rasterizer + rotation + nonstandard boxes combinations."""
|
||||
|
||||
# The pdf_with_nonstandard_boxes fixture creates a PDF with:
|
||||
# - MediaBox: [0, 0, 400, 500] → 400x500 points
|
||||
# - CropBox: [50, 50, 350, 450] → 300x400 points
|
||||
# - TrimBox: [75, 75, 325, 425] → 250x350 points
|
||||
#
|
||||
# The rasterizers use different boxes:
|
||||
# - Ghostscript uses MediaBox → 400x500 pixels at 72 DPI
|
||||
# - pypdfium uses CropBox → 300x400 pixels at 72 DPI
|
||||
GS_WIDTH = 400 # MediaBox width
|
||||
GS_HEIGHT = 500 # MediaBox height
|
||||
PDFIUM_WIDTH = 300 # CropBox width
|
||||
PDFIUM_HEIGHT = 400 # CropBox height
|
||||
|
||||
def _get_expected_size(
|
||||
self, rotation: int, rasterizer: str = 'ghostscript'
|
||||
) -> tuple[int, int]:
|
||||
"""Get expected image dimensions after rotation."""
|
||||
if rasterizer == 'ghostscript':
|
||||
width, height = self.GS_WIDTH, self.GS_HEIGHT
|
||||
else:
|
||||
width, height = self.PDFIUM_WIDTH, self.PDFIUM_HEIGHT
|
||||
|
||||
if rotation in (0, 180):
|
||||
return (width, height)
|
||||
else: # 90, 270
|
||||
return (height, width)
|
||||
|
||||
def test_ghostscript_rotation_dimensions(
|
||||
self, pdf_with_nonstandard_boxes, tmp_path
|
||||
):
|
||||
"""Test Ghostscript produces correct dimensions with rotation."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript',
|
||||
)
|
||||
|
||||
for rotation in [0, 90, 180, 270]:
|
||||
img_path = tmp_path / f'gs_rot{rotation}.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=img_path,
|
||||
raster_device='png16m',
|
||||
raster_dpi=Resolution(72, 72),
|
||||
page_dpi=Resolution(72, 72),
|
||||
pageno=1,
|
||||
rotation=rotation,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
assert img_path.exists(), f"Failed to rasterize with rotation {rotation}"
|
||||
|
||||
with Image.open(img_path) as img:
|
||||
expected = self._get_expected_size(rotation, 'ghostscript')
|
||||
# Allow small tolerance for rounding
|
||||
assert abs(img.size[0] - expected[0]) <= 2, (
|
||||
f"Width mismatch at {rotation}°: got {img.size[0]}, "
|
||||
f"expected {expected[0]}"
|
||||
)
|
||||
assert abs(img.size[1] - expected[1]) <= 2, (
|
||||
f"Height mismatch at {rotation}°: got {img.size[1]}, "
|
||||
f"expected {expected[1]}"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_pypdfium_rotation_dimensions(
|
||||
self, pdf_with_nonstandard_boxes, tmp_path
|
||||
):
|
||||
"""Test pypdfium produces correct dimensions with rotation."""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='pypdfium',
|
||||
)
|
||||
|
||||
for rotation in [0, 90, 180, 270]:
|
||||
img_path = tmp_path / f'pdfium_rot{rotation}.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=img_path,
|
||||
raster_device='png16m',
|
||||
raster_dpi=Resolution(72, 72),
|
||||
page_dpi=Resolution(72, 72),
|
||||
pageno=1,
|
||||
rotation=rotation,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
assert img_path.exists(), f"Failed to rasterize with rotation {rotation}"
|
||||
|
||||
with Image.open(img_path) as img:
|
||||
expected = self._get_expected_size(rotation, 'pypdfium')
|
||||
# Allow small tolerance for rounding
|
||||
assert abs(img.size[0] - expected[0]) <= 2, (
|
||||
f"Width mismatch at {rotation}°: got {img.size[0]}, "
|
||||
f"expected {expected[0]}"
|
||||
)
|
||||
assert abs(img.size[1] - expected[1]) <= 2, (
|
||||
f"Height mismatch at {rotation}°: got {img.size[1]}, "
|
||||
f"expected {expected[1]}"
|
||||
)
|
||||
|
||||
@pytest.mark.skipif(not PYPDFIUM_AVAILABLE, reason="pypdfium2 not installed")
|
||||
def test_rasterizers_dimensions_differ_as_expected(
|
||||
self, pdf_with_nonstandard_boxes, tmp_path
|
||||
):
|
||||
"""Verify ghostscript and pypdfium produce expected different dimensions.
|
||||
|
||||
Ghostscript uses MediaBox while pypdfium uses CropBox, so their output
|
||||
dimensions differ for PDFs with different MediaBox/CropBox sizes.
|
||||
"""
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
for rotation in [0, 90, 180, 270]:
|
||||
# Rasterize with Ghostscript
|
||||
gs_options = OCROptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript',
|
||||
)
|
||||
gs_img_path = tmp_path / f'gs_cmp_rot{rotation}.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=gs_img_path,
|
||||
raster_device='png16m',
|
||||
raster_dpi=Resolution(72, 72),
|
||||
page_dpi=Resolution(72, 72),
|
||||
pageno=1,
|
||||
rotation=rotation,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=gs_options,
|
||||
)
|
||||
|
||||
# Rasterize with pypdfium
|
||||
pdfium_options = OCROptions(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='pypdfium',
|
||||
)
|
||||
pdfium_img_path = tmp_path / f'pdfium_cmp_rot{rotation}.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=pdf_with_nonstandard_boxes,
|
||||
output_file=pdfium_img_path,
|
||||
raster_device='png16m',
|
||||
raster_dpi=Resolution(72, 72),
|
||||
page_dpi=Resolution(72, 72),
|
||||
pageno=1,
|
||||
rotation=rotation,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=pdfium_options,
|
||||
)
|
||||
|
||||
# Verify each produces its expected dimensions
|
||||
with Image.open(gs_img_path) as gs_img, Image.open(
|
||||
pdfium_img_path
|
||||
) as pdfium_img:
|
||||
gs_expected = self._get_expected_size(rotation, 'ghostscript')
|
||||
pdfium_expected = self._get_expected_size(rotation, 'pypdfium')
|
||||
|
||||
assert abs(gs_img.size[0] - gs_expected[0]) <= 2, (
|
||||
f"GS width at {rotation}°: {gs_img.size[0]}, "
|
||||
f"expected {gs_expected[0]}"
|
||||
)
|
||||
assert abs(gs_img.size[1] - gs_expected[1]) <= 2, (
|
||||
f"GS height at {rotation}°: {gs_img.size[1]}, "
|
||||
f"expected {gs_expected[1]}"
|
||||
)
|
||||
assert abs(pdfium_img.size[0] - pdfium_expected[0]) <= 2, (
|
||||
f"pdfium width at {rotation}°: {pdfium_img.size[0]}, "
|
||||
f"expected {pdfium_expected[0]}"
|
||||
)
|
||||
assert abs(pdfium_img.size[1] - pdfium_expected[1]) <= 2, (
|
||||
f"pdfium height at {rotation}°: {pdfium_img.size[1]}, "
|
||||
f"expected {pdfium_expected[1]}"
|
||||
)
|
||||
@@ -171,6 +171,8 @@ def test_rotated_skew_timeout(resources, outpdf):
|
||||
'--deskew',
|
||||
'--tesseract-timeout',
|
||||
'0',
|
||||
'--rasterizer',
|
||||
'ghostscript', # Use Ghostscript for consistent dimensions
|
||||
)
|
||||
|
||||
out_pageinfo = PdfInfo(out)[0]
|
||||
@@ -197,6 +199,8 @@ def test_rotate_deskew_ocr_timeout(resources, outdir):
|
||||
'0',
|
||||
'--pdf-renderer',
|
||||
'hocr',
|
||||
'--rasterizer',
|
||||
'ghostscript', # Use Ghostscript for consistent dimensions
|
||||
)
|
||||
|
||||
cmp = compare_images_monochrome(
|
||||
@@ -285,8 +289,16 @@ def test_page_rotate_tag(page_rotate_angle, resources, outdir, caplog):
|
||||
|
||||
|
||||
def test_rasterize_rotates(resources, tmp_path):
|
||||
from ocrmypdf._options import OCROptions
|
||||
|
||||
pm = get_plugin_manager([])
|
||||
|
||||
options = OCROptions(
|
||||
input_file=resources / 'graph.pdf',
|
||||
output_file=tmp_path / 'out.pdf',
|
||||
rasterizer='ghostscript', # Use Ghostscript for consistent dimensions
|
||||
)
|
||||
|
||||
img = tmp_path / 'img90.png'
|
||||
pm.hook.rasterize_pdf_page(
|
||||
input_file=resources / 'graph.pdf',
|
||||
@@ -298,6 +310,7 @@ def test_rasterize_rotates(resources, tmp_path):
|
||||
rotation=90,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
with Image.open(img) as im:
|
||||
assert im.size == (83, 200), "Image not rotated"
|
||||
@@ -313,6 +326,7 @@ def test_rasterize_rotates(resources, tmp_path):
|
||||
rotation=180,
|
||||
filter_vector=False,
|
||||
stop_on_soft_error=True,
|
||||
options=options,
|
||||
)
|
||||
assert Image.open(img).size == (200, 83), "Image not rotated"
|
||||
|
||||
@@ -346,6 +360,8 @@ def test_simulated_scan(outdir):
|
||||
'--rotate-pages',
|
||||
'--plugin',
|
||||
'tests/plugins/tesseract_debug_rotate.py',
|
||||
'--rasterizer',
|
||||
'ghostscript', # Use Ghostscript to avoid pypdfium2 thread safety issues
|
||||
)
|
||||
|
||||
with pikepdf.open(outdir / 'out.pdf') as pdf:
|
||||
|
||||
Reference in New Issue
Block a user