From f2714586d80e22ae984925752c197c8f4577ebd6 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 23 Dec 2023 23:44:37 -0800 Subject: [PATCH 01/10] Fix upside down glyphless font --- src/ocrmypdf/hocrtransform/_hocr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ocrmypdf/hocrtransform/_hocr.py b/src/ocrmypdf/hocrtransform/_hocr.py index 77088b36..aa17252d 100644 --- a/src/ocrmypdf/hocrtransform/_hocr.py +++ b/src/ocrmypdf/hocrtransform/_hocr.py @@ -363,7 +363,7 @@ class HocrTransform: # If this word is 0 units wide, our best bet seems to be to suppress this text if font_width > 0: - text.text_transform(Matrix(1, 0, 0, 1, box.llx, 0)) + text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0)) text.horiz_scale(100 * box.width / font_width) text.show(elemtxt.encode('utf-16be')) From 9adcad84da99bc5295991fa90b6409c02d3a620b Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 23 Dec 2023 23:47:35 -0800 Subject: [PATCH 02/10] Overhaul debug rendering to support possible use of Helvetica for testing --- src/ocrmypdf/_pipeline.py | 17 +++++++++--- src/ocrmypdf/hocrtransform/_font.py | 33 ++++++++++++++++++++-- src/ocrmypdf/hocrtransform/_hocr.py | 43 ++++++++++++++++------------- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index ee121060..a91c5ad8 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -34,7 +34,7 @@ from ocrmypdf.exceptions import ( UnsupportedImageFormatError, ) from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink -from ocrmypdf.hocrtransform import HocrTransform +from ocrmypdf.hocrtransform import DebugRenderOptions, HocrTransform from ocrmypdf.pdfa import generate_pdfa_ps from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo from ocrmypdf.pluginspec import OrientationConfidence @@ -741,12 +741,21 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path: return output_file dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context)) - debug_mode = options.pdf_renderer == 'hocrdebug' - + debug = options.pdf_renderer == 'hocrdebug' + debug_render_options = None + if debug: + debug_render_options = DebugRenderOptions( + render_baseline=True, + render_triangle=True, + render_line_bbox=False, + render_word_bbox=True, + render_paragraph_bbox=False, + render_space_bbox=False, + ) HocrTransform( hocr_filename=hocr, dpi=dpi.to_scalar(), # square - debug=debug_mode, + debug_render_options=debug_render_options, ).to_pdf( out_filename=output_file, image_filename=None, diff --git a/src/ocrmypdf/hocrtransform/_font.py b/src/ocrmypdf/hocrtransform/_font.py index 7935cdb1..784ef469 100644 --- a/src/ocrmypdf/hocrtransform/_font.py +++ b/src/ocrmypdf/hocrtransform/_font.py @@ -18,7 +18,12 @@ from pikepdf.canvas import Font log = logging.getLogger(__name__) -class GlyphlessFont(Font): +class EncodableFont(Font): + def text_encode(self, text: str) -> bytes: + raise NotImplementedError() + + +class GlyphlessFont(EncodableFont): CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536) GLYPHLESS_FONT_NAME = 'pdf.ttf' GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes() @@ -27,11 +32,14 @@ class GlyphlessFont(Font): def __init__(self): pass - def text_width(self, text: str, fontsize: float) -> int: + def text_width(self, text: str, fontsize: float) -> float: """Estimate the width of a text string when rendered with the given font.""" # NFKC: split ligatures, combine diacritics return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT) + def text_encode(self, text: str) -> bytes: + return text.encode('utf-16be') + def register(self, pdf: Pdf): """Register the glyphless font. @@ -110,3 +118,24 @@ class GlyphlessFont(Font): font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT) cid_font_type2.FontDescriptor = font_descriptor return basefont + + +class Helvetica(EncodableFont): + """Helvetica font.""" + + def text_width(self, text: str, fontsize: float) -> float: + """Estimate the width of a text string when rendered with the given font.""" + return len(text) * fontsize / 2.0 + + def text_encode(self, text: str) -> bytes: + return text.encode('pdfdoc', errors='ignore') + + def register(self, pdf: Pdf) -> Dictionary: + """Register the font.""" + return pdf.make_indirect( + Dictionary( + BaseFont=Name.Helvetica, + Type=Name.Font, + Subtype=Name.Type1, + ) + ) diff --git a/src/ocrmypdf/hocrtransform/_hocr.py b/src/ocrmypdf/hocrtransform/_hocr.py index aa17252d..939ac3c4 100644 --- a/src/ocrmypdf/hocrtransform/_hocr.py +++ b/src/ocrmypdf/hocrtransform/_hocr.py @@ -27,11 +27,11 @@ from pikepdf.canvas import ( MAGENTA, RED, Canvas, - Font, Text, TextDirection, ) +from ocrmypdf.hocrtransform._font import EncodableFont as Font from ocrmypdf.hocrtransform._font import GlyphlessFont log = logging.getLogger(__name__) @@ -45,12 +45,12 @@ Element = ElementTree.Element class DebugRenderOptions: """A class for managing rendering options.""" - render_paragraph_bbox: bool - render_baseline: bool - render_triangle: bool - render_line_bbox: bool - render_word_bbox: bool - render_space_bbox: bool + render_paragraph_bbox: bool = False + render_baseline: bool = False + render_triangle: bool = False + render_line_bbox: bool = False + render_word_bbox: bool = False + render_space_bbox: bool = False class HocrTransformError(Exception): @@ -81,8 +81,22 @@ class HocrTransform: debug: bool = False, fontname: Name = Name("/f-0-0"), font: Font = GlyphlessFont(), + debug_render_options: DebugRenderOptions | None = None, ): """Initialize the HocrTransform object.""" + + if debug: + log.warning("Use debug_render_options instead", DeprecationWarning) + self.render_options = DebugRenderOptions( + render_baseline=debug, + render_triangle=debug, + render_line_bbox=False, + render_word_bbox=debug, + render_paragraph_bbox=False, + render_space_bbox=False, + ) + else: + self.render_options = debug_render_options or DebugRenderOptions() self.dpi = dpi self.hocr = ElementTree.parse(os.fspath(hocr_filename)) self._fontname = fontname @@ -103,14 +117,6 @@ class HocrTransform: self.height = (coords.ury - coords.lly) / (self.dpi / INCH) # Stop after first div that has page coordinates break - self.render_options = DebugRenderOptions( - render_baseline=debug, - render_triangle=debug, - render_line_bbox=False, - render_word_bbox=debug, - render_paragraph_bbox=False, - render_space_bbox=False, - ) def _get_element_text(self, element: Element): """Return the textual content of the element and its children.""" @@ -311,8 +317,7 @@ class HocrTransform: line_box_height = abs(line_box.height) / cos(angle) fontsize = line_box_height + intercept text.font(self._fontname, fontsize) - if invisible_text or True: - text.render_mode(3) # Invisible (indicates OCR text) + text.render_mode(3 if invisible_text else 0) self._debug_draw_baseline( canvas, line_matrix.inverse().transform(line_box), 0 @@ -365,7 +370,7 @@ class HocrTransform: if font_width > 0: text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0)) text.horiz_scale(100 * box.width / font_width) - text.show(elemtxt.encode('utf-16be')) + text.show(self._font.text_encode(elemtxt)) # Get coordinates of the next word (if there is one) hocr_next_box = ( @@ -389,7 +394,7 @@ class HocrTransform: space_width = self._font.text_width(' ', fontsize) if space_width > 0: text.horiz_scale(100 * space_box.width / space_width) - text.show(' '.encode('utf-16be')) + text.show(self._font.text_encode(' ')) def _debug_draw_paragraph_boxes(self, canvas: Canvas, color=CYAN): """Draw boxes around paragraphs in the document.""" From 18353e1e94023890089c39f1bed732ca3eedc794 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 24 Dec 2023 00:09:47 -0800 Subject: [PATCH 03/10] Also fix space rendering --- src/ocrmypdf/hocrtransform/_hocr.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ocrmypdf/hocrtransform/_hocr.py b/src/ocrmypdf/hocrtransform/_hocr.py index 939ac3c4..efa39598 100644 --- a/src/ocrmypdf/hocrtransform/_hocr.py +++ b/src/ocrmypdf/hocrtransform/_hocr.py @@ -390,7 +390,7 @@ class HocrTransform: elif text_direction == TextDirection.RTL: space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury) self._debug_draw_space_bbox(canvas, space_box) - text.text_transform(Matrix(1, 0, 0, 1, space_box.llx, 0)) + text.text_transform(Matrix(1, 0, 0, -1, space_box.llx, 0)) space_width = self._font.text_width(' ', fontsize) if space_width > 0: text.horiz_scale(100 * space_box.width / space_width) From a0b98abb945aa344958e1268401db750499f44a7 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 24 Dec 2023 00:10:09 -0800 Subject: [PATCH 04/10] Improve and externalize debug rendering settings --- src/ocrmypdf/_pipeline.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index a91c5ad8..3285f6c4 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -35,6 +35,7 @@ from ocrmypdf.exceptions import ( ) from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink from ocrmypdf.hocrtransform import DebugRenderOptions, HocrTransform +from ocrmypdf.hocrtransform._font import Helvetica from ocrmypdf.pdfa import generate_pdfa_ps from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo from ocrmypdf.pluginspec import OrientationConfidence @@ -741,24 +742,25 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path: return output_file dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context)) - debug = options.pdf_renderer == 'hocrdebug' - debug_render_options = None - if debug: - debug_render_options = DebugRenderOptions( - render_baseline=True, - render_triangle=True, - render_line_bbox=False, - render_word_bbox=True, - render_paragraph_bbox=False, - render_space_bbox=False, + debug_kwargs = {} + if options.pdf_renderer == 'hocrdebug': + debug_kwargs = dict( + debug_render_options=DebugRenderOptions( + render_baseline=True, + render_triangle=True, + render_line_bbox=False, + render_word_bbox=True, + render_paragraph_bbox=False, + render_space_bbox=False, + ), + font=Helvetica(), ) HocrTransform( - hocr_filename=hocr, - dpi=dpi.to_scalar(), # square - debug_render_options=debug_render_options, + hocr_filename=hocr, dpi=dpi.to_scalar(), **debug_kwargs # square ).to_pdf( out_filename=output_file, image_filename=None, + invisible_text=True if not debug_kwargs else False, ) return output_file From fd593eb5e9bbd8d8e129a4ca7d95b8753ddd2cf0 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 24 Dec 2023 01:24:44 -0800 Subject: [PATCH 05/10] Reversing character order for RTL helps output --- src/ocrmypdf/hocrtransform/_hocr.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/ocrmypdf/hocrtransform/_hocr.py b/src/ocrmypdf/hocrtransform/_hocr.py index efa39598..75c699e5 100644 --- a/src/ocrmypdf/hocrtransform/_hocr.py +++ b/src/ocrmypdf/hocrtransform/_hocr.py @@ -367,10 +367,15 @@ class HocrTransform: self._debug_draw_word_bbox(canvas, box) # If this word is 0 units wide, our best bet seems to be to suppress this text + if text_direction == TextDirection.RTL: + log.info("RTL: %s", elemtxt) if font_width > 0: text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0)) text.horiz_scale(100 * box.width / font_width) - text.show(self._font.text_encode(elemtxt)) + if text_direction == TextDirection.LTR: + text.show(self._font.text_encode(elemtxt)) + elif text_direction == TextDirection.RTL: + text.show(self._font.text_encode(elemtxt[::-1])) # Get coordinates of the next word (if there is one) hocr_next_box = ( From b71d16dd9627a4b3c0adc317acc594b9041dbd22 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sun, 24 Dec 2023 01:34:03 -0800 Subject: [PATCH 06/10] Draw RTL text with reversed matrix instead of reversing characters --- src/ocrmypdf/hocrtransform/_hocr.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/ocrmypdf/hocrtransform/_hocr.py b/src/ocrmypdf/hocrtransform/_hocr.py index 75c699e5..913a3b39 100644 --- a/src/ocrmypdf/hocrtransform/_hocr.py +++ b/src/ocrmypdf/hocrtransform/_hocr.py @@ -370,12 +370,12 @@ class HocrTransform: if text_direction == TextDirection.RTL: log.info("RTL: %s", elemtxt) if font_width > 0: - text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0)) - text.horiz_scale(100 * box.width / font_width) if text_direction == TextDirection.LTR: - text.show(self._font.text_encode(elemtxt)) + text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0)) elif text_direction == TextDirection.RTL: - text.show(self._font.text_encode(elemtxt[::-1])) + text.text_transform(Matrix(-1, 0, 0, -1, box.llx + box.width, 0)) + text.horiz_scale(100 * box.width / font_width) + text.show(self._font.text_encode(elemtxt)) # Get coordinates of the next word (if there is one) hocr_next_box = ( @@ -395,9 +395,14 @@ class HocrTransform: elif text_direction == TextDirection.RTL: space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury) self._debug_draw_space_bbox(canvas, space_box) - text.text_transform(Matrix(1, 0, 0, -1, space_box.llx, 0)) space_width = self._font.text_width(' ', fontsize) if space_width > 0: + if text_direction == TextDirection.LTR: + text.text_transform(Matrix(1, 0, 0, -1, space_box.llx, 0)) + elif text_direction == TextDirection.RTL: + text.text_transform( + Matrix(-1, 0, 0, -1, space_box.llx + space_box.width, 0) + ) text.horiz_scale(100 * space_box.width / space_width) text.show(self._font.text_encode(' ')) From 7e608fd1dfa19ae5c71df74ddbe7c908022b8583 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 6 Jan 2024 14:24:25 -0800 Subject: [PATCH 07/10] Use Courier as debug font --- src/ocrmypdf/_pipeline.py | 4 ++-- src/ocrmypdf/hocrtransform/_font.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ocrmypdf/_pipeline.py b/src/ocrmypdf/_pipeline.py index 3285f6c4..cd67eafa 100644 --- a/src/ocrmypdf/_pipeline.py +++ b/src/ocrmypdf/_pipeline.py @@ -35,7 +35,7 @@ from ocrmypdf.exceptions import ( ) from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink from ocrmypdf.hocrtransform import DebugRenderOptions, HocrTransform -from ocrmypdf.hocrtransform._font import Helvetica +from ocrmypdf.hocrtransform._font import Courier from ocrmypdf.pdfa import generate_pdfa_ps from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo from ocrmypdf.pluginspec import OrientationConfidence @@ -753,7 +753,7 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path: render_paragraph_bbox=False, render_space_bbox=False, ), - font=Helvetica(), + font=Courier(), ) HocrTransform( hocr_filename=hocr, dpi=dpi.to_scalar(), **debug_kwargs # square diff --git a/src/ocrmypdf/hocrtransform/_font.py b/src/ocrmypdf/hocrtransform/_font.py index 784ef469..9324b71e 100644 --- a/src/ocrmypdf/hocrtransform/_font.py +++ b/src/ocrmypdf/hocrtransform/_font.py @@ -120,12 +120,12 @@ class GlyphlessFont(EncodableFont): return basefont -class Helvetica(EncodableFont): - """Helvetica font.""" +class Courier(EncodableFont): + """Courier font.""" def text_width(self, text: str, fontsize: float) -> float: """Estimate the width of a text string when rendered with the given font.""" - return len(text) * fontsize / 2.0 + return len(text) * fontsize def text_encode(self, text: str) -> bytes: return text.encode('pdfdoc', errors='ignore') @@ -134,7 +134,7 @@ class Helvetica(EncodableFont): """Register the font.""" return pdf.make_indirect( Dictionary( - BaseFont=Name.Helvetica, + BaseFont=Name.Courier, Type=Name.Font, Subtype=Name.Type1, ) From 51301d69c9b7a3b7fa2974cb5342e075c5814a37 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 6 Jan 2024 16:20:30 -0800 Subject: [PATCH 08/10] Change default graft render mode back to underneath It was mistakenly changed to on top at some point --- src/ocrmypdf/_graft.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ocrmypdf/_graft.py b/src/ocrmypdf/_graft.py index 256db368..10c6108a 100644 --- a/src/ocrmypdf/_graft.py +++ b/src/ocrmypdf/_graft.py @@ -313,7 +313,7 @@ class OcrGrafter: strip_invisible_text(self.pdf_base, base_page) base_page.contents_add( - new_text_layer, prepend=self.render_mode == RenderMode.ON_TOP + new_text_layer, prepend=self.render_mode == RenderMode.UNDERNEATH ) _update_resources(obj=base_page.obj, font=font, font_key=font_key) From 2c59aca5a184ae503e3ff3cad64b48b59411ffe2 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 6 Jan 2024 16:20:55 -0800 Subject: [PATCH 09/10] Fix render_mode ON_TOP when previous content stream does not restore CTM --- src/ocrmypdf/_graft.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/ocrmypdf/_graft.py b/src/ocrmypdf/_graft.py index 10c6108a..481f029c 100644 --- a/src/ocrmypdf/_graft.py +++ b/src/ocrmypdf/_graft.py @@ -292,6 +292,7 @@ class OcrGrafter: # finally move the lower left corner to match the mediabox. All transforms # must be premultiplied so they are applied in reverse order here. ctm = corner @ untranslate @ scale @ rotate @ translate + log.debug("Grafting with ctm %r", ctm) base_resources = _ensure_dictionary(base_page.obj, Name.Resources) base_xobjs = _ensure_dictionary(base_resources, Name.XObject) @@ -311,9 +312,16 @@ class OcrGrafter: if strip_old_text: strip_invisible_text(self.pdf_base, base_page) - + base_page.contents_coalesce() + if self.render_mode == RenderMode.ON_TOP: + # Add q/Q to ensure content we append is drawn correctly + # Strictly speaking this needs to trace the whole q/Q stack in case + # stack is not balanced. + original = base_page.Contents.read_bytes() + base_page.Contents.write(b'q\n' + original + b'\nQ\n') base_page.contents_add( new_text_layer, prepend=self.render_mode == RenderMode.UNDERNEATH ) + base_page.contents_coalesce() _update_resources(obj=base_page.obj, font=font, font_key=font_key) From 9498f01f597566b17fc068dd6b1a516ad480b515 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 6 Jan 2024 16:21:21 -0800 Subject: [PATCH 10/10] hocr: Pass text direction for standalone words --- src/ocrmypdf/hocrtransform/_hocr.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ocrmypdf/hocrtransform/_hocr.py b/src/ocrmypdf/hocrtransform/_hocr.py index 913a3b39..592c1748 100644 --- a/src/ocrmypdf/hocrtransform/_hocr.py +++ b/src/ocrmypdf/hocrtransform/_hocr.py @@ -226,7 +226,7 @@ class HocrTransform: root, "ocrx_word", invisible_text, - TextDirection.LTR, + direction, True, ) # put the image on the page, scaled to fill the page @@ -376,6 +376,8 @@ class HocrTransform: text.text_transform(Matrix(-1, 0, 0, -1, box.llx + box.width, 0)) text.horiz_scale(100 * box.width / font_width) text.show(self._font.text_encode(elemtxt)) + # elif text_direction == TextDirection.RTL: + # text.show(self._font.text_encode(elemtxt[::-1])) # Get coordinates of the next word (if there is one) hocr_next_box = (