From 7fdeeb36354e3477cb84dc65d047088d1cd1c3e9 Mon Sep 17 00:00:00 2001 From: "James R. Barlow" Date: Sat, 4 Apr 2026 01:43:25 -0700 Subject: [PATCH] Refactor word_render_data tuple into WordRenderData dataclass --- src/ocrmypdf/fpdf_renderer/renderer.py | 70 +++++++++++++++----------- 1 file changed, 40 insertions(+), 30 deletions(-) diff --git a/src/ocrmypdf/fpdf_renderer/renderer.py b/src/ocrmypdf/fpdf_renderer/renderer.py index 471372f7..6f6ff715 100644 --- a/src/ocrmypdf/fpdf_renderer/renderer.py +++ b/src/ocrmypdf/fpdf_renderer/renderer.py @@ -82,6 +82,17 @@ def transform_box( ) +@dataclass +class WordRenderData: + """Rendering parameters for a single word on a line.""" + + text: str + x_baseline: float + font_family: str + word_tz: float + is_rtl: bool + + @dataclass class DebugRenderOptions: """Options for debug visualization during rendering. @@ -442,9 +453,7 @@ class Fpdf2PdfRenderer: ): return - # Collect word rendering data: - # (text, x_baseline, font_family, word_tz, is_rtl) - word_render_data: list[tuple[str, float, str, float, bool]] = [] + word_render_data: list[WordRenderData] = [] for word in words: if word is None or not word.text or word.bbox is None: continue @@ -498,9 +507,13 @@ class Fpdf2PdfRenderer: else: word_tz = 100.0 - word_render_data.append( - (word.text, box_llx, font_family, word_tz, word_is_rtl) - ) + word_render_data.append(WordRenderData( + text=word.text, + x_baseline=box_llx, + font_family=font_family, + word_tz=word_tz, + is_rtl=word_is_rtl, + )) if not word_render_data: return @@ -599,7 +612,7 @@ class Fpdf2PdfRenderer: def _emit_line_bt_block( self, pdf: FPDF, - word_render_data: list[tuple[str, float, str, float, bool]], + word_render_data: list[WordRenderData], baseline_matrix: Matrix, font_size: float, total_rotation_deg: float, @@ -615,8 +628,7 @@ class Fpdf2PdfRenderer: Args: pdf: FPDF instance - word_render_data: List of (text, x_baseline, font_family, word_tz, - is_rtl) tuples, one per word on this line + word_render_data: List of WordRenderData, one per word on this line baseline_matrix: Transform from baseline coords to page coords font_size: Font size in points total_rotation_deg: Total rotation angle (textangle + slope) @@ -661,7 +673,7 @@ class Fpdf2PdfRenderer: ops.append(f'{tr} Tr') # Initial text position - first_x_baseline = word_render_data[0][1] + first_x_baseline = word_render_data[0].x_baseline if has_rotation: # In the cm-transformed space, origin is at the baseline start ops.append(f'{first_x_baseline:.2f} 0 Td') @@ -676,14 +688,12 @@ class Fpdf2PdfRenderer: prev_font_family: str | None = None prev_x_baseline = first_x_baseline - for i, (text, x_baseline, font_family, word_tz, is_rtl) in enumerate( - word_render_data - ): + for i, word in enumerate(word_render_data): is_last = i == len(word_render_data) - 1 # Set font if changed - if font_family != prev_font_family: - pdf.set_font(font_family, size=font_size) + if word.font_family != prev_font_family: + pdf.set_font(word.font_family, size=font_size) # Register font resource on this page pdf._resource_catalog.add( PDFResourceType.FONT, pdf.current_font.i, pdf.page @@ -691,13 +701,13 @@ class Fpdf2PdfRenderer: ops.append( f'/F{pdf.current_font.i} {pdf.font_size_pt:.2f} Tf' ) - prev_font_family = font_family + prev_font_family = word.font_family # Relative positioning (for words after the first) if i > 0: if has_rotation: # In rotated space, advance is purely along x-axis - dx_baseline = x_baseline - prev_x_baseline + dx_baseline = word.x_baseline - prev_x_baseline ops.append(f'{dx_baseline:.2f} 0 Td') else: # Non-rotated: compute delta in PDF coordinates @@ -705,7 +715,7 @@ class Fpdf2PdfRenderer: baseline_matrix, prev_x_baseline, 0 ) px_curr, py_curr_f = transform_point( - baseline_matrix, x_baseline, 0 + baseline_matrix, word.x_baseline, 0 ) dx_pdf = px_curr - px_prev # Flip y delta for PDF coordinates (y-up) @@ -714,31 +724,31 @@ class Fpdf2PdfRenderer: # Determine text to render if not is_last: - next_text, next_x_baseline, _, _, _ = word_render_data[i + 1] - advance = next_x_baseline - x_baseline + next_word = word_render_data[i + 1] + advance = next_word.x_baseline - word.x_baseline # Add trailing space for text extraction unless both are CJK if ( advance > 0 and not ( - self._is_cjk_only(text) - and self._is_cjk_only(next_text) + self._is_cjk_only(word.text) + and self._is_cjk_only(next_word.text) ) ): - text_to_render = text + ' ' + text_to_render = word.text + ' ' else: - text_to_render = text + text_to_render = word.text else: - text_to_render = text + text_to_render = word.text # Use word_tz (fits word into its hOCR bbox) — Td handles # inter-word gaps, so Tz should not stretch to fill them. - render_tz = word_tz + ops.append(f'{word.word_tz:.2f} Tz') + ops.append( + self._encode_shaped_text(pdf, text_to_render, word.is_rtl) + ) - ops.append(f'{render_tz:.2f} Tz') - ops.append(self._encode_shaped_text(pdf, text_to_render, is_rtl)) - - prev_x_baseline = x_baseline + prev_x_baseline = word.x_baseline # End text object ops.append('ET')