Replace Rect with pikepdf.Rectangle, migrate line matrix to page

This commit is contained in:
James R. Barlow
2023-11-19 23:51:27 -08:00
parent d7680cae27
commit 0ca314e066
2 changed files with 65 additions and 95 deletions
+55 -84
View File
@@ -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),
)
+10 -11
View File
@@ -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)