diff --git a/src/ocrmypdf/hocrtransform/__init__.py b/src/ocrmypdf/hocrtransform/__init__.py index 1315d4aa..38642a8c 100755 --- a/src/ocrmypdf/hocrtransform/__init__.py +++ b/src/ocrmypdf/hocrtransform/__init__.py @@ -36,28 +36,6 @@ INCH = 72.0 Element = ElementTree.Element -class Rect(NamedTuple): - """A rectangle for managing PDF coordinates.""" - - x1: Any - y1: Any - x2: Any - y2: Any - - def transform(self, matrix, inverse=True): - """Transform the rectangle by the given matrix.""" - if inverse: - matrix = matrix.inverse() - return Rect._make( - [ - matrix.a * self.x1 + matrix.c * self.y1 + matrix.e, - matrix.b * self.x1 + matrix.d * self.y1 + matrix.f, - matrix.a * self.x2 + matrix.c * self.y2 + matrix.e, - matrix.b * self.x2 + matrix.d * self.y2 + matrix.f, - ] - ) - - @dataclass class DebugRenderOptions: """A class for managing rendering options.""" @@ -81,7 +59,7 @@ class HocrTransform: http://kba.cloud/hocr-spec/. """ - box_pattern = re.compile(r'bbox((\s+\d+){4})') + box_pattern = re.compile(r'bbox (\d+) (\d+) (\d+) (\d+)') baseline_pattern = re.compile( r''' baseline \s+ @@ -105,13 +83,11 @@ class HocrTransform: if matches: self.xmlns = matches.group(1) - # get dimension in pt (not pixel!!!!) of the OCRed image self.width, self.height = None, None for div in self.hocr.findall(self._child_xpath('div', 'ocr_page')): coords = self.element_coordinates(div) - pt_coords = self.pt_from_pixel(coords) - self.width = pt_coords.x2 - pt_coords.x1 - self.height = pt_coords.y2 - pt_coords.y1 + self.width = (coords.urx - coords.llx) / (self.dpi / INCH) + self.height = (coords.ury - coords.lly) / (self.dpi / INCH) # there shouldn't be more than one, and if there is, we don't want # it break @@ -148,15 +124,18 @@ class HocrTransform: return text @classmethod - def element_coordinates(cls, element: Element) -> Rect: + def element_coordinates(cls, element: Element) -> Rectangle | None: """Get coordinates of the bounding box around an element.""" - out = Rect._make(0 for _ in range(4)) if 'title' in element.attrib: matches = cls.box_pattern.search(element.attrib['title']) if matches: - coords = matches.group(1).split() - out = Rect._make(int(coords[n]) for n in range(4)) - return out + return Rectangle( + float(matches.group(1)), # llx = left + float(matches.group(2)), # lly = top + float(matches.group(3)), # urx = right + float(matches.group(4)), # ury = bottom + ) + return None @classmethod def baseline(cls, element: Element) -> tuple[float, float]: @@ -167,22 +146,6 @@ class HocrTransform: return float(matches.group(1)), int(matches.group(2)) return (0.0, 0.0) - def pt_from_pixel(self, pxl: Rect, bottomup=False) -> Rect: - """Returns the quantity in PDF units (pt) given quantity in pixels.""" - if bottomup: - return Rect._make( - [ - (pxl.x1 / self.dpi * INCH), - self.height - (pxl.y2 / self.dpi * INCH), # swap y1/y2 - (pxl.x2 / self.dpi * INCH), - self.height - (pxl.y1 / self.dpi * INCH), - ] - ) - else: - return Rect._make( - (c / self.dpi * INCH) for c in (pxl.x1, pxl.y1, pxl.x2, pxl.y2) - ) - def _child_xpath(self, html_tag: str, html_class: str | None = None) -> str: xpath = f".//{self.xmlns}{html_tag}" if html_class: @@ -237,20 +200,30 @@ class HocrTransform: out_filename, page_size=(self.width, self.height), ) + canvas.push() + page_matrix = ( + Matrix() + .translated(0, self.height) + .scaled(1, -1) + .scaled(INCH / self.dpi, INCH / self.dpi) + ) + canvas.cm(page_matrix) + print(page_matrix) for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')): elemtxt = self._get_element_text(elem).rstrip() if len(elemtxt) == 0: continue - pxl_coords = self.element_coordinates(elem) - pt = self.pt_from_pixel(pxl_coords, bottomup=True) + ocr_par = self.element_coordinates(elem) # draw cyan box around paragraph if self.render_options.render_paragraph_bbox: # pragma: no cover canvas.set_stroke_color(CYAN) canvas.set_line_width(0.1) # no line for bounding box - canvas.rect(pt.x1, pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=0) + canvas.rect( + ocr_par.llx, ocr_par.lly, ocr_par.width, ocr_par.height, fill=0 + ) found_lines = False for line in ( @@ -280,6 +253,7 @@ class HocrTransform: invisible_text, interword_spaces, ) + canvas.pop() # put the image on the page, scaled to fill the page if image_filename is not None: canvas.draw_image( @@ -306,7 +280,7 @@ class HocrTransform: if line is None: return line_box = self.element_coordinates(line) - assert line_box.y2 > line_box.y1 + assert line_box.ury > line_box.lly # lly is top, ury is bottom # Baseline is a polynomial (usually straight line) in the coordinate system # of the line @@ -320,23 +294,20 @@ class HocrTransform: canvas.push() line_matrix = ( Matrix() - .translated(0, self.height) - .scaled(1, -1) - .scaled(INCH / self.dpi, INCH / self.dpi) - .translated(-line_box.x1, -line_box.y1) - .translated(0, -intercept) - .rotated(angle / pi * 180) - .translated(0, intercept) - .translated(line_box.x1, line_box.y1) + # .translated(-line_box.llx, -line_box.lly) + # .translated(0, -intercept) + # .rotated(angle / pi * 180) + # .translated(0, intercept) + # .translated(line_box.llx, line_box.lly) ) - canvas.cm(*line_matrix.shorthand) + canvas.cm(line_matrix) print(line_matrix) text = canvas.begin_text() # Don't allow the font to break out of the bounding box. Division by # cos_a accounts for extra clearance between the glyph's vertical axis # on a sloped baseline and the edge of the bounding box. - line_box_height = abs(line_box.y2 - line_box.y1) + line_box_height = abs(line_box.lly - line_box.ury) fontsize = line_box_height + intercept text.set_font(fontname, fontsize) if invisible_text or True: @@ -344,7 +315,7 @@ class HocrTransform: self._do_debug_line_bbox(canvas, line_box) self._do_debug_baseline( - canvas, line_box.y2 + intercept, line_box, line_box.y2 + intercept + canvas, line_box.ury + intercept, line_box, line_box.ury + intercept ) canvas.set_fill_color(BLACK) # text in black @@ -354,7 +325,7 @@ class HocrTransform: canvas, fontname, interword_spaces, - line_box.y2 - line_box.y1, + line_box.ury - line_box.lly, line_box, text, fontsize, @@ -382,7 +353,7 @@ class HocrTransform: return box = self.element_coordinates(elem) - box_width = box.x2 - box.x1 + box_width = box.urx - box.llx font_width = canvas.string_width(elemtxt, fontname, fontsize) # Debug sketches @@ -391,17 +362,17 @@ class HocrTransform: # If this word is 0 units wide, our best bet seems to be to suppress this text if font_width > 0: - text.set_text_transform(1, 0, 0, 1, box.x1, line_box.y2) + text.set_text_transform(Matrix(1, 0, 0, 1, box.llx, line_box.ury)) text.set_horiz_scale(100 * box_width / font_width) text.show(elemtxt) if interword_spaces and next_elem is not None: next_box = self.element_coordinates(next_elem) - space_box = Rect(box.x2, line_box.y1, next_box.x1, line_box.y2) + space_box = Rectangle(box.urx, line_box.lly, next_box.llx, line_box.ury) self._do_debug_space_bbox(canvas, space_box) - text.set_text_transform(1, 0, 0, 1, space_box.x1, line_box.y2) + text.set_text_transform(Matrix(1, 0, 0, 1, space_box.llx, line_box.ury)) space_width = canvas.string_width(' ', fontname, fontsize) - box_width = space_box.x2 - space_box.x1 + box_width = space_box.urx - space_box.llx text.set_horiz_scale(100 * box_width / space_width) text.show(' ') @@ -413,10 +384,10 @@ class HocrTransform: canvas.set_stroke_color(BLUE) canvas.set_line_width(0.15) canvas.rect( - line_box.x1, - line_box.y1, - line_box.x2 - line_box.x1, - line_box.y2 - line_box.y1, + line_box.llx, + line_box.lly, + line_box.urx - line_box.llx, + line_box.ury - line_box.lly, fill=0, ) canvas.pop() @@ -433,9 +404,9 @@ class HocrTransform: canvas.set_stroke_color(RED) canvas.set_line_width(0.1) # Draw a triangle that conveys word height and drawing direction - canvas.line(box.x1, box.y1, box.x2, box.y1) # across bottom - canvas.line(box.x2, box.y1, box.x1, box.y2) # diagonal - canvas.line(box.x1, box.y1, box.x1, box.y2) # rise + canvas.line(box.llx, box.lly, box.urx, box.lly) # across bottom + canvas.line(box.urx, box.lly, box.llx, box.ury) # diagonal + canvas.line(box.llx, box.lly, box.llx, box.ury) # rise canvas.pop() def _do_debug_word_bbox(self, canvas, line_height, line_box, box, box_width): @@ -445,7 +416,7 @@ class HocrTransform: canvas.set_dashes() canvas.set_stroke_color(GREEN) canvas.set_line_width(0.1) - canvas.rect(box.x1, line_box.y1, box_width, line_height, fill=0) + canvas.rect(box.llx, line_box.lly, box_width, line_height, fill=0) canvas.pop() def _do_debug_space_bbox(self, canvas, box): @@ -455,10 +426,10 @@ class HocrTransform: canvas.set_dashes() canvas.set_fill_color(GREEN) canvas.set_line_width(0.1) - canvas.rect(box.x1, box.y1, box.x2 - box.x1, box.y2 - box.y1, fill=1) + canvas.rect(box.llx, box.lly, box.urx - box.llx, box.ury - box.lly, fill=1) canvas.pop() - def _do_debug_baseline(self, canvas, slope, line_box, baseline_y1): + def _do_debug_baseline(self, canvas, slope, line_box, baseline_lly): if not self.render_options.render_baseline: return # draw the baseline in magenta, dashed @@ -468,11 +439,11 @@ class HocrTransform: # negate slope because it is defined as a rise/run in pixel # coordinates and page coordinates have the y axis flipped canvas.line( - line_box.x1, - baseline_y1, - line_box.x2, - baseline_y1, - # self.polyval((-slope, baseline_y1), line_box.x2 - line_box.x1), + line_box.llx, + baseline_lly, + line_box.urx, + baseline_lly, + # self.polyval((-slope, baseline_lly), line_box.urx - line_box.llx), ) diff --git a/src/ocrmypdf/hocrtransform/_canvas.py b/src/ocrmypdf/hocrtransform/_canvas.py index ebc00c11..6813b69c 100644 --- a/src/ocrmypdf/hocrtransform/_canvas.py +++ b/src/ocrmypdf/hocrtransform/_canvas.py @@ -7,6 +7,7 @@ from pathlib import Path from pikepdf import ( ContentStreamInstruction, Dictionary, + Matrix, Name, Operator, Pdf, @@ -113,9 +114,9 @@ class ContentStreamBuilder: self._instructions.append(inst) return self - def cm(self, a: float, b: float, c: float, d: float, e: float, f: float): + def cm(self, matrix: Matrix): """Concatenate matrix.""" - inst = ContentStreamInstruction([a, b, c, d, e, f], Operator("cm")) + inst = ContentStreamInstruction(matrix.shorthand, Operator("cm")) self._instructions.append(inst) return self @@ -151,11 +152,9 @@ class ContentStreamBuilder: self._instructions.append(inst) return self - def set_text_matrix( - self, a: float, b: float, c: float, d: float, e: float, f: float - ): + def set_text_matrix(self, matrix: Matrix): """Set text matrix.""" - inst = ContentStreamInstruction([a, b, c, d, e, f], Operator("Tm")) + inst = ContentStreamInstruction(matrix.shorthand, Operator("Tm")) self._instructions.append(inst) return self @@ -302,8 +301,8 @@ class PikepdfCanvas: def pop(self): self._cs.pop() - def cm(self, a, b, c, d, e, f): - self._cs.cm(a, b, c, d, e, f) + def cm(self, matrix): + self._cs.cm(matrix) def save(self): self._cs.pop() @@ -327,9 +326,9 @@ class PikepdfText: def set_render_mode(self, mode): self._cs.set_text_rendering(mode) - def set_text_transform(self, a, b, c, d, e, f): - self._cs.set_text_matrix(a, b, c, d, e, f) - self._p0 = (e, f) + def set_text_transform(self, matrix: Matrix): + self._cs.set_text_matrix(matrix) + self._p0 = (matrix.e, matrix.f) def show(self, text): self._cs.show_text(text)