Refactor reportlab into generic backend

This commit is contained in:
James R. Barlow
2023-11-19 23:51:26 -08:00
parent f3b89e66eb
commit 6d30b497dc
4 changed files with 190 additions and 31 deletions
+30 -31
View File
@@ -16,8 +16,11 @@ from pathlib import Path
from typing import Any, NamedTuple
from xml.etree import ElementTree
from ocrmypdf.hocrtransform.backends.reportlab import (
from ocrmypdf.hocrtransform.backends import (
Canvas,
)
from ocrmypdf.hocrtransform.backends.reportlab import (
ReportlabCanvas,
black,
cyan,
inch,
@@ -231,18 +234,17 @@ class HocrTransform:
"""
# create the PDF file
# page size in points (1/72 in.)
pdf = Canvas(
os.fspath(out_filename),
pagesize=(self.width, self.height),
pageCompression=1,
pdf = ReportlabCanvas(
out_filename,
page_size=(self.width, self.height),
)
# draw bounding box for each paragraph
# light blue for bounding box of paragraph
pdf.setStrokeColor(cyan)
pdf.set_stroke_color(cyan)
# light blue for bounding box of paragraph
pdf.setFillColor(cyan)
pdf.setLineWidth(0) # no line for bounding box
pdf.set_fill_color(cyan)
pdf.set_line_width(0) # no line for bounding box
for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
elemtxt = self._get_element_text(elem).rstrip()
if len(elemtxt) == 0:
@@ -289,12 +291,9 @@ class HocrTransform:
)
# put the image on the page, scaled to fill the page
if image_filename is not None:
pdf.drawImage(
os.fspath(image_filename), 0, 0, width=self.width, height=self.height
)
pdf.draw_image(image_filename, 0, 0, width=self.width, height=self.height)
# finish up the page and save it
pdf.showPage()
pdf.save()
@classmethod
@@ -324,16 +323,16 @@ class HocrTransform:
angle = atan(slope)
cos_a, sin_a = cos(angle), sin(angle)
text = pdf.beginText()
text = pdf.begin_text()
intercept = pxl_intercept / self.dpi * inch
# 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.
fontsize = (line_height - abs(intercept)) / cos_a
text.setFont(fontname, fontsize)
text.set_font(fontname, fontsize)
if invisible_text:
text.setTextRenderMode(3) # Invisible (indicates OCR text)
text.set_render_mode(3) # Invisible (indicates OCR text)
# Intercept is normally negative, so this places it above the bottom
# of the line box
@@ -341,9 +340,9 @@ class HocrTransform:
if show_bounding_boxes: # pragma: no cover
# draw the baseline in magenta, dashed
pdf.setDash()
pdf.setStrokeColor(magenta)
pdf.setLineWidth(0.5)
pdf.set_dash()
pdf.set_stroke_color(magenta)
pdf.set_line_width(0.5)
# negate slope because it is defined as a rise/run in pixel
# coordinates and page coordinates have the y axis flipped
pdf.line(
@@ -353,11 +352,11 @@ class HocrTransform:
self.polyval((-slope, baseline_y2), line_box.x2 - line_box.x1),
)
# light green for bounding box of word/line
pdf.setDash(6, 3)
pdf.setStrokeColor(red)
pdf.set_dash(6, 3)
pdf.set_stroke_color(red)
text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2)
pdf.setFillColor(black) # text in black
text.set_text_transform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2)
pdf.set_fill_color(black) # text in black
elements = line.findall(self._child_xpath('span', elemclass))
for elem in elements:
@@ -380,12 +379,12 @@ class HocrTransform:
(
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
box.x2 + pdf.string_width(' ', fontname, line_height),
line_box.y2,
)
)
box_width = box.x2 - box.x1
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
font_width = pdf.string_width(elemtxt, fontname, fontsize)
# draw the bbox border
if show_bounding_boxes: # pragma: no cover
@@ -398,24 +397,24 @@ class HocrTransform:
# text.setTextOrigin(pt.x1, self.height - line_box.y2)
# but the former generates a full text reposition matrix (Tm) in the
# content stream while this issues a "offset" (Td) command.
# .moveCursor() is relative to start of the text line, where the
# .move_cursor() is relative to start of the text line, where the
# "text line" means whatever reportlab defines it as. Do not use
# use .getCursor(), since moveCursor() rather unintuitively plans
# use .getCursor(), since move_cursor() rather unintuitively plans
# its moves relative to .getStartOfLine().
# For skewed lines, in the text transform we set up a rotated
# coordinate system, so we don't have to account for the
# incremental offset. Surprisingly most PDF viewers can handle this.
cursor = text.getStartOfLine()
cursor = text.get_start_of_line()
dx = box.x1 - cursor[0]
dy = baseline_y2 - cursor[1]
text.moveCursor(dx, dy)
text.move_cursor(dx, dy)
# If reportlab tells us this word is 0 units wide, our best seems
# to be to suppress this text
if font_width > 0:
text.setHorizScale(100 * box_width / font_width)
text.textOut(elemtxt)
pdf.drawText(text)
text.set_horiz_scale(100 * box_width / font_width)
text.show(elemtxt)
pdf.draw_text(text)
if __name__ == "__main__":
@@ -0,0 +1,3 @@
from __future__ import annotations
from ._base import Canvas, Text
@@ -0,0 +1,88 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from pathlib import Path
class Canvas(ABC):
def __init__(self, path: Path, *, page_size):
self.path = path
self.page_size = page_size
@abstractmethod
def set_stroke_color(color):
pass
@abstractmethod
def set_fill_color(color):
pass
@abstractmethod
def set_line_width(width):
pass
@abstractmethod
def line(x1, y1, x2, y2):
pass
@abstractmethod
def begin_text(self) -> Text:
pass
@abstractmethod
def draw_text(self, text: Text):
pass
@abstractmethod
def draw_image(self, image: Path, x, y, width, height):
pass
@abstractmethod
def string_width(self, text, fontname, fontsize):
pass
@abstractmethod
def set_dashes(self, dashes):
pass
@abstractmethod
def save(self):
pass
class Text(ABC):
# @abstractmethod
# def begin_marked_content(self, mctype, mcid):
# pass
# @abstractmethod
# def end_marked_content(self):
# pass
@abstractmethod
def set_render_mode(self, mode):
pass
@abstractmethod
def set_font(self, font, size):
pass
@abstractmethod
def set_text_transform(self, a, b, c, d, e, f):
pass
@abstractmethod
def show(self, text):
pass
@abstractmethod
def set_horiz_scale(self, scale):
pass
@abstractmethod
def get_start_of_line(self):
pass
@abstractmethod
def move_cursor(self, x, y):
pass
@@ -1,6 +1,8 @@
from __future__ import annotations
import os
import warnings
from pathlib import Path
with warnings.catch_warnings():
# reportlab uses deprecated load_module
@@ -11,3 +13,70 @@ with warnings.catch_warnings():
from reportlab.lib.colors import black, cyan, magenta, red
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
from reportlab.pdfgen.textobject import PDFTextObject
from ocrmypdf.hocrtransform.backends._base import Canvas as BaseCanvas
from ocrmypdf.hocrtransform.backends._base import Text as BaseText
class ReportlabCanvas(BaseCanvas):
def __init__(self, path, *, page_size):
super().__init__(path, page_size=page_size)
self._canvas = Canvas(os.fspath(path), pagesize=page_size, pageCompression=1)
def set_stroke_color(self, color):
self._canvas.setStrokeColor(color)
def set_fill_color(self, color):
self._canvas.setFillColor(color)
def set_line_width(self, width):
self._canvas.setLineWidth(width)
def line(self, x1, y1, x2, y2):
self._canvas.line(x1, y1, x2, y2)
def begin_text(self, x=0, y=0, direction=None):
return ReportlabText(self._canvas.beginText(x, y, direction))
def draw_text(self, text):
self._canvas.drawText(text._text)
def draw_image(self, image: Path, x, y, width, height):
self._canvas.drawImage(os.fspath(image), x, y, width, height)
def string_width(self, text, fontname, fontsize):
return self._canvas.stringWidth(text, fontname, fontsize)
def set_dashes(self, dashes):
self._canvas.setDash(*dashes)
def save(self):
self._canvas.showPage()
self._canvas.save()
class ReportlabText(BaseText):
def __init__(self, text: PDFTextObject):
self._text = text
def set_font(self, font, size):
self._text.setFont(font, size)
def set_render_mode(self, mode):
self._text.setTextRenderMode(mode)
def set_text_transform(self, a, b, c, d, e, f):
return self._text.setTextTransform(a, b, c, d, e, f)
def show(self, text):
return self._text.textOut(text)
def set_horiz_scale(self, scale):
return self._text.setHorizScale(scale)
def get_start_of_line(self):
return self._text.getStartOfLine()
def move_cursor(self, x, y):
return self._text.moveCursor(x, y)