diff --git a/src/ocrmypdf/_graft.py b/src/ocrmypdf/_graft.py index 9c3d6574..0ebc63c1 100644 --- a/src/ocrmypdf/_graft.py +++ b/src/ocrmypdf/_graft.py @@ -117,6 +117,10 @@ def _build_text_layer_ctm( ): """Build transformation matrix to align text layer with page content. + Always computes the full CTM to handle non-zero page origins (e.g., + JSTOR PDFs with MediaBox like [0, 100, 595, 982]) and minor scale + differences due to DPI rounding. + Args: text_width: Width of text layer mediabox. text_height: Height of text layer mediabox. @@ -127,11 +131,8 @@ def _build_text_layer_ctm( text_rotation: Rotation in degrees (clockwise) to apply to text layer. Returns: - pikepdf.Matrix transformation matrix, or None if no rotation needed. + pikepdf.Matrix transformation matrix, or None if identity. """ - if text_rotation == 0: - return None - from pikepdf import Matrix wt, ht = text_width, text_height @@ -153,7 +154,14 @@ def _build_text_layer_ctm( scale_y = page_height / ht if ht else 1.0 scale = Matrix().scaled(scale_x, scale_y) - return translate @ rotate @ scale @ untranslate @ corner + ctm = translate @ rotate @ scale @ untranslate @ corner + + # Return None if the result is effectively identity + identity = Matrix() + if ctm == identity: + return None + + return ctm log = logging.getLogger(__name__) diff --git a/tests/test_graft.py b/tests/test_graft.py index 462e1b1c..5c0fbbe1 100644 --- a/tests/test_graft.py +++ b/tests/test_graft.py @@ -46,24 +46,19 @@ def test_redo_ocr_with_offset_mediabox(resources, outdir): """Test that --redo-ocr handles non-zero mediabox origins correctly. Regression test for issue #1630 where PDFs with mediabox origins like - [0, 100, width, height+100] (common in cropped PDFs) - would have OCR text shifted vertically because the Form XObject BBox - used the text layer's mediabox [0, 0, w, h] instead of the base page's - mediabox [0, 100, w, h+100]. - - Before the fix, the BBox would be [0, 0, w, h] but the transformation - matrix would expect [0, 100, w, h+100], causing a 100pt vertical shift. + [0, 100, width, height+100] (common in cropped/JSTOR-style PDFs) + would have OCR text shifted vertically because the text layer CTM + did not account for the page origin offset. """ # Create a PDF with a non-zero mediabox origin input_pdf = outdir / 'offset_mediabox_input.pdf' + y_offset = 100 with pikepdf.open(resources / 'graph_ocred.pdf') as pdf: page = pdf.pages[0] original_mb = list(page.MediaBox) # Shift mediabox Y origin to simulate cropped/JSTOR-style PDFs - # This is the scenario that triggers the bug - y_offset = 100 page.MediaBox = [ original_mb[0], original_mb[1] + y_offset, @@ -84,18 +79,15 @@ def test_redo_ocr_with_offset_mediabox(resources, outdir): # MediaBox origin should be preserved assert ( - float(mediabox[1]) == 100.0 - ), f"MediaBox Y origin should be preserved at 100, got {mediabox[1]}" + float(mediabox[1]) == y_offset + ), f"MediaBox Y origin should be preserved at {y_offset}, got {mediabox[1]}" - # MediaBox should have valid dimensions - width = float(mediabox[2]) - float(mediabox[0]) - height = float(mediabox[3]) - float(mediabox[1]) - assert width > 0 and height > 0, "MediaBox should have positive dimensions" - - # Page content should be present (may include XObject references - # for the OCR text layer and/or inline image operators) - text_content = page.Contents.read_bytes() - assert len(text_content) > 0, "Page should have content" + # The content stream should include a CTM with the Y origin translation. + # Without the fix, the CTM was omitted for rotation==0, causing a shift. + content = page.Contents.read_bytes() + assert b'cm' in content, ( + "Content stream should include a CTM to translate by the page origin" + ) def test_strip_invisble_text():