Create pikepdf backend renderer
This commit is contained in:
@@ -0,0 +1,325 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
import warnings
|
||||
from io import BufferedReader, BufferedWriter, BytesIO
|
||||
from pathlib import Path
|
||||
|
||||
import pikepdf
|
||||
from bs4 import BeautifulSoup
|
||||
from pikepdf import (
|
||||
ContentStreamInstruction,
|
||||
Dictionary,
|
||||
Name,
|
||||
Operator,
|
||||
Pdf,
|
||||
unparse_content_stream,
|
||||
)
|
||||
|
||||
from ocrmypdf.hocrtransform.backends._base import Canvas as BaseCanvas
|
||||
from ocrmypdf.hocrtransform.backends._base import Text as BaseText
|
||||
|
||||
GLYPHLESS_FONT = importlib.resources.read_binary("ocrmypdf", "pdf.ttf")
|
||||
CHAR_ASPECT = 2
|
||||
|
||||
|
||||
def register_glyphlessfont(pdf: Pdf):
|
||||
"""Register the glyphless font.
|
||||
|
||||
Create several data structures in the Pdf to describe the font. While it create
|
||||
the data, a reference should be set in at least one page's /Resources dictionary
|
||||
to retain the font in the output PDF and ensure it is usable on that page.
|
||||
"""
|
||||
PLACEHOLDER = Name.Placeholder
|
||||
|
||||
basefont = pdf.make_indirect(
|
||||
Dictionary(
|
||||
BaseFont=Name.GlyphLessFont,
|
||||
DescendantFonts=[PLACEHOLDER],
|
||||
Encoding=Name("/Identity-H"),
|
||||
Subtype=Name.Type0,
|
||||
ToUnicode=PLACEHOLDER,
|
||||
Type=Name.Font,
|
||||
)
|
||||
)
|
||||
cid_font_type2 = pdf.make_indirect(
|
||||
Dictionary(
|
||||
BaseFont=Name.GlyphLessFont,
|
||||
CIDToGIDMap=PLACEHOLDER,
|
||||
CIDSystemInfo=Dictionary(
|
||||
Ordering="Identity",
|
||||
Registry="Adobe",
|
||||
Supplement=0,
|
||||
),
|
||||
FontDescriptor=PLACEHOLDER,
|
||||
Subtype=Name.CIDFontType2,
|
||||
Type=Name.Font,
|
||||
DW=1000 // CHAR_ASPECT,
|
||||
)
|
||||
)
|
||||
basefont.DescendantFonts = [cid_font_type2]
|
||||
cid_font_type2.CIDToGIDMap = pdf.make_stream(b"\x00\x01" * 65536)
|
||||
basefont.ToUnicode = pdf.make_stream(
|
||||
b"/CIDInit /ProcSet findresource begin\n"
|
||||
b"12 dict begin\n"
|
||||
b"begincmap\n"
|
||||
b"/CIDSystemInfo\n"
|
||||
b"<<\n"
|
||||
b" /Registry (Adobe)\n"
|
||||
b" /Ordering (UCS)\n"
|
||||
b" /Supplement 0\n"
|
||||
b">> def\n"
|
||||
b"/CMapName /Adobe-Identify-UCS def\n"
|
||||
b"/CMapType 2 def\n"
|
||||
b"1 begincodespacerange\n"
|
||||
b"<0000> <FFFF>\n"
|
||||
b"endcodespacerange\n"
|
||||
b"1 beginbfrange\n"
|
||||
b"<0000> <FFFF> <0000>\n"
|
||||
b"endbfrange\n"
|
||||
b"endcmap\n"
|
||||
b"CMapName currentdict /CMap defineresource pop\n"
|
||||
b"end\n"
|
||||
b"end\n"
|
||||
)
|
||||
font_descriptor = pdf.make_indirect(
|
||||
Dictionary(
|
||||
Ascent=1000,
|
||||
CapHeight=1000,
|
||||
Descent=-1,
|
||||
Flags=5, # Fixed pitch and symbolic
|
||||
FontBBox=[0, 0, 1000 // CHAR_ASPECT, 1000],
|
||||
FontFile2=PLACEHOLDER,
|
||||
FontName=Name.GlyphLessFont,
|
||||
ItalicAngle=0,
|
||||
StemV=80,
|
||||
Type=Name.FontDescriptor,
|
||||
)
|
||||
)
|
||||
font_descriptor.FontFile2 = pdf.make_stream(GLYPHLESS_FONT)
|
||||
cid_font_type2.FontDescriptor = font_descriptor
|
||||
return basefont
|
||||
|
||||
|
||||
class ContentStreamBuilder:
|
||||
def __init__(self, instructions=None):
|
||||
self._instructions: list[ContentStreamInstruction] = instructions or []
|
||||
|
||||
def push(self):
|
||||
"""Save the graphics state."""
|
||||
inst = ContentStreamInstruction([], Operator("q"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def pop(self):
|
||||
"""Restore the graphics state."""
|
||||
inst = ContentStreamInstruction([], Operator("Q"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def cm(self, a: float, b: float, c: float, d: float, e: float, f: float):
|
||||
"""Concatenate matrix."""
|
||||
inst = ContentStreamInstruction([a, b, c, d, e, f], Operator("cm"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def begin_text(self):
|
||||
"""Begin text object."""
|
||||
inst = ContentStreamInstruction([], Operator("BT"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def end_text(self):
|
||||
"""End text object."""
|
||||
inst = ContentStreamInstruction([], Operator("ET"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def begin_marked_content(self, mctype: Name, mcid: int):
|
||||
"""Begin marked content sequence."""
|
||||
inst = ContentStreamInstruction(
|
||||
[mctype, Dictionary(MCID=mcid)], Operator("BDC")
|
||||
)
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def end_marked_content(self):
|
||||
"""End marked content sequence."""
|
||||
inst = ContentStreamInstruction([], Operator("EMC"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_text_font(self, font: Name, size: int):
|
||||
"""Set text font and size."""
|
||||
inst = ContentStreamInstruction([font, size], Operator("Tf"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_text_matrix(
|
||||
self, a: float, b: float, c: float, d: float, e: float, f: float
|
||||
):
|
||||
"""Set text matrix."""
|
||||
inst = ContentStreamInstruction([a, b, c, d, e, f], Operator("Tm"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_text_rendering(self, mode: int):
|
||||
"""Set text rendering mode."""
|
||||
inst = ContentStreamInstruction([mode], Operator("Tr"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_text_horizontal_scaling(self, scale: float):
|
||||
"""Set text horizontal scaling."""
|
||||
inst = ContentStreamInstruction([scale], Operator("Tz"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def show_text(self, text: str):
|
||||
"""Show text."""
|
||||
inst = ContentStreamInstruction([[text.encode("utf-16be")]], Operator("TJ"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def move_cursor(self, dx, dy):
|
||||
"""Move cursor."""
|
||||
inst = ContentStreamInstruction([dx, dy], Operator("Td"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def stroke_and_close(self):
|
||||
"""Stroke and close path."""
|
||||
inst = ContentStreamInstruction([], Operator("s"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def append_rectangle(self, x: float, y: float, w: float, h: float):
|
||||
"""Append rectangle to path."""
|
||||
inst = ContentStreamInstruction([x, y, w, h], Operator("re"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_stroke_color(self, r: float, g: float, b: float):
|
||||
"""Set RGB stroke color."""
|
||||
inst = ContentStreamInstruction([r, g, b], Operator("RG"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_fill_color(self, r: float, g: float, b: float):
|
||||
"""Set RGB fill color."""
|
||||
inst = ContentStreamInstruction([r, g, b], Operator("rg"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def set_line_width(self, width):
|
||||
"""Set line width."""
|
||||
inst = ContentStreamInstruction([width], Operator("w"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def line(self, x1: float, y1: float, x2: float, y2: float):
|
||||
"""Draw line."""
|
||||
insts = [
|
||||
ContentStreamInstruction([x1, y1], Operator("m")),
|
||||
ContentStreamInstruction([x2, y2], Operator("l")),
|
||||
]
|
||||
self._instructions.extend(insts)
|
||||
return self
|
||||
|
||||
def set_dashes(self, array=None, phase=0):
|
||||
"""Set dashes."""
|
||||
if array is None:
|
||||
array = []
|
||||
if isinstance(array, (int, float)):
|
||||
array = (array, phase)
|
||||
phase = 0
|
||||
inst = ContentStreamInstruction([array, phase], Operator("d"))
|
||||
self._instructions.append(inst)
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
return self._instructions
|
||||
|
||||
|
||||
class PikepdfCanvas(BaseCanvas):
|
||||
def __init__(self, path, *, page_size):
|
||||
super().__init__(path, page_size=page_size)
|
||||
self._pdf = Pdf.new()
|
||||
self._page = self._pdf.add_blank_page(page_size=page_size)
|
||||
self._cs = ContentStreamBuilder()
|
||||
page_height = page_size[1]
|
||||
self._cs.push()
|
||||
self._cs.cm(1, 0, 0, -1, 0, page_height)
|
||||
self._font_name = Name("/f-0-0")
|
||||
|
||||
def set_stroke_color(self, color):
|
||||
r, g, b = color
|
||||
self._cs.set_stroke_color(r, g, b)
|
||||
|
||||
def set_fill_color(self, color):
|
||||
r, g, b = color
|
||||
self._cs.set_fill_color(r, g, b)
|
||||
|
||||
def set_line_width(self, width):
|
||||
self._cs.set_line_width(width)
|
||||
|
||||
def line(self, x1, y1, x2, y2):
|
||||
self._cs.line(x1, y1, x2, y2)
|
||||
|
||||
def begin_text(self, x=0, y=0, direction=None):
|
||||
return PikepdfText(self._cs, x, y, direction)
|
||||
|
||||
def draw_text(self, text):
|
||||
self._canvas.drawText(text._text)
|
||||
|
||||
def draw_image(self, image: Path, x, y, width, height):
|
||||
raise NotImplementedError()
|
||||
|
||||
def string_width(self, text, fontname, fontsize):
|
||||
# NFKC: split ligatures, combine diacritics
|
||||
return len(unicodedata.normalize("NFKC", text)) / fontsize * CHAR_ASPECT
|
||||
|
||||
def set_dashes(self, dashes):
|
||||
self._cs.set_dashes(*dashes)
|
||||
|
||||
def save(self):
|
||||
self._cs.pop()
|
||||
self._page.Contents = self._pdf.make_stream(
|
||||
unparse_content_stream(self._cs.build())
|
||||
)
|
||||
self._page.Resources = Dictionary(
|
||||
Font=Dictionary({self._font_name: register_glyphlessfont(self._pdf)})
|
||||
)
|
||||
self._pdf.save(self.path)
|
||||
|
||||
|
||||
class PikepdfText(BaseText):
|
||||
def __init__(self, cs: ContentStreamBuilder, x=0, y=0, direction=None):
|
||||
self._cs = cs
|
||||
self._cs.begin_text()
|
||||
self._p0 = (x, y)
|
||||
|
||||
def set_font(self, font, size):
|
||||
self._cs.set_text_font(font, size)
|
||||
|
||||
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)
|
||||
|
||||
def show(self, text):
|
||||
self._cs.show_text(text)
|
||||
|
||||
def set_horiz_scale(self, scale):
|
||||
self._cs.set_text_horizontal_scaling(scale)
|
||||
|
||||
def get_start_of_line(self):
|
||||
return self._p0
|
||||
|
||||
def move_cursor(self, x, y):
|
||||
# ???
|
||||
self._cs.move_cursor(x - self._p0[0], y - self._p0[1])
|
||||
Reference in New Issue
Block a user