Move canvas API to pikepdf and import it
This commit is contained in:
+1
-1
@@ -17,7 +17,7 @@ dependencies = [
|
||||
"img2pdf>=0.4.4",
|
||||
"packaging>=20",
|
||||
"pdfminer.six>=20220319",
|
||||
"pikepdf>=8.7.1",
|
||||
"pikepdf>=8.8.0",
|
||||
"pluggy>=0.13.0",
|
||||
"rich>=13",
|
||||
]
|
||||
|
||||
@@ -1,502 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import unicodedata
|
||||
import zlib
|
||||
from contextlib import contextmanager
|
||||
from dataclasses import dataclass
|
||||
from enum import Enum
|
||||
from importlib.resources import files as package_files
|
||||
from pathlib import Path
|
||||
|
||||
from pikepdf import (
|
||||
ContentStreamInstruction,
|
||||
Dictionary,
|
||||
Matrix,
|
||||
Name,
|
||||
Operator,
|
||||
Pdf,
|
||||
unparse_content_stream,
|
||||
)
|
||||
from PIL import Image
|
||||
|
||||
from .color import Color
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class TextDirection(Enum):
|
||||
LTR = 1 # Left to right: the default
|
||||
RTL = 2 # Right to left: Arabic, Hebrew, Persian
|
||||
|
||||
|
||||
class Font:
|
||||
def text_width(self, text: str, fontsize: float) -> int:
|
||||
"""Estimate the width of a text string when rendered with the given font."""
|
||||
raise NotImplementedError
|
||||
|
||||
def register(self, pdf: Pdf):
|
||||
"""Register the 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.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class GlyphlessFont(Font):
|
||||
CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536)
|
||||
GLYPHLESS_FONT_NAME = 'pdf.ttf'
|
||||
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
|
||||
CHAR_ASPECT = 2
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def text_width(self, text: str, fontsize: float) -> int:
|
||||
"""Estimate the width of a text string when rendered with the given font."""
|
||||
# NFKC: split ligatures, combine diacritics
|
||||
return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT)
|
||||
|
||||
def register(self, 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 // self.CHAR_ASPECT,
|
||||
)
|
||||
)
|
||||
basefont.DescendantFonts = [cid_font_type2]
|
||||
cid_font_type2.CIDToGIDMap = pdf.make_stream(
|
||||
self.CID_TO_GID_DATA, Filter=Name.FlateDecode
|
||||
)
|
||||
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 // self.CHAR_ASPECT, 1000],
|
||||
FontFile2=PLACEHOLDER,
|
||||
FontName=Name.GlyphLessFont,
|
||||
ItalicAngle=0,
|
||||
StemV=80,
|
||||
Type=Name.FontDescriptor,
|
||||
)
|
||||
)
|
||||
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
|
||||
cid_font_type2.FontDescriptor = font_descriptor
|
||||
return basefont
|
||||
|
||||
|
||||
class ContentStreamBuilder:
|
||||
def __init__(self):
|
||||
self._stream = b""
|
||||
|
||||
def _append(self, inst: ContentStreamInstruction):
|
||||
self._stream += unparse_content_stream([inst]) + b"\n"
|
||||
|
||||
def extend(self, other: ContentStreamBuilder):
|
||||
self._stream += other._stream
|
||||
|
||||
def push(self):
|
||||
"""Save the graphics state."""
|
||||
inst = ContentStreamInstruction([], Operator("q"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def pop(self):
|
||||
"""Restore the graphics state."""
|
||||
inst = ContentStreamInstruction([], Operator("Q"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def cm(self, matrix: Matrix):
|
||||
"""Concatenate matrix."""
|
||||
inst = ContentStreamInstruction(matrix.shorthand, Operator("cm"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def begin_text(self):
|
||||
"""Begin text object."""
|
||||
inst = ContentStreamInstruction([], Operator("BT"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def end_text(self):
|
||||
"""End text object."""
|
||||
inst = ContentStreamInstruction([], Operator("ET"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def begin_marked_content_proplist(self, mctype: Name, mcid: int):
|
||||
"""Begin marked content sequence."""
|
||||
inst = ContentStreamInstruction(
|
||||
[mctype, Dictionary(MCID=mcid)], Operator("BDC")
|
||||
)
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def begin_marked_content(self, mctype: Name):
|
||||
"""Begin marked content sequence."""
|
||||
inst = ContentStreamInstruction([mctype], Operator("BMC"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def end_marked_content(self):
|
||||
"""End marked content sequence."""
|
||||
inst = ContentStreamInstruction([], Operator("EMC"))
|
||||
self._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._append(inst)
|
||||
return self
|
||||
|
||||
def set_text_matrix(self, matrix: Matrix):
|
||||
"""Set text matrix."""
|
||||
inst = ContentStreamInstruction(matrix.shorthand, Operator("Tm"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def set_text_rendering(self, mode: int):
|
||||
"""Set text rendering mode."""
|
||||
inst = ContentStreamInstruction([mode], Operator("Tr"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def set_text_horizontal_scaling(self, scale: float):
|
||||
"""Set text horizontal scaling."""
|
||||
inst = ContentStreamInstruction([scale], Operator("Tz"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def show_text(self, text: str):
|
||||
"""Show text."""
|
||||
encoded = text.encode("utf-16be")
|
||||
inst = ContentStreamInstruction([[encoded]], Operator("TJ"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def move_cursor(self, dx, dy):
|
||||
"""Move cursor."""
|
||||
inst = ContentStreamInstruction([dx, dy], Operator("Td"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def stroke_and_close(self):
|
||||
"""Stroke and close path."""
|
||||
inst = ContentStreamInstruction([], Operator("s"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def fill(self):
|
||||
"""Stroke and close path."""
|
||||
inst = ContentStreamInstruction([], Operator("f"))
|
||||
self._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._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._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._append(inst)
|
||||
return self
|
||||
|
||||
def set_line_width(self, width):
|
||||
"""Set line width."""
|
||||
inst = ContentStreamInstruction([width], Operator("w"))
|
||||
self._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._append(insts[0])
|
||||
self._append(insts[1])
|
||||
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._append(inst)
|
||||
return self
|
||||
|
||||
def draw_form_xobject(self, name: Name):
|
||||
inst = ContentStreamInstruction([name], Operator("Do"))
|
||||
self._append(inst)
|
||||
return self
|
||||
|
||||
def build(self):
|
||||
return self._stream
|
||||
|
||||
|
||||
@dataclass
|
||||
class LoadedImage:
|
||||
name: Name
|
||||
image: Image.Image
|
||||
|
||||
|
||||
class _PikepdfCanvasAccessor:
|
||||
"""Support class for drawing on a pikepdf canvas."""
|
||||
|
||||
def __init__(self, cs: ContentStreamBuilder, images=None):
|
||||
self._cs = cs
|
||||
self._images = images if images is not None else []
|
||||
self._stack_depth = 0
|
||||
|
||||
def stroke_color(self, color: Color):
|
||||
"""Set stroke color."""
|
||||
r, g, b = color.red, color.green, color.blue
|
||||
self._cs.set_stroke_color(r, g, b)
|
||||
return self
|
||||
|
||||
def fill_color(self, color: Color):
|
||||
"""Set fill color."""
|
||||
r, g, b = color.red, color.green, color.blue
|
||||
self._cs.set_fill_color(r, g, b)
|
||||
return self
|
||||
|
||||
def line_width(self, width):
|
||||
"""Set line width."""
|
||||
self._cs.set_line_width(width)
|
||||
return self
|
||||
|
||||
def line(self, x1, y1, x2, y2):
|
||||
"""Draw line from (x1,y1) to (x2,y2)."""
|
||||
self._cs.line(x1, y1, x2, y2)
|
||||
self._cs.stroke_and_close()
|
||||
return self
|
||||
|
||||
def rect(self, x, y, w, h, fill):
|
||||
"""Draw optionally filled rectangle at (x,y) with width w and height h."""
|
||||
self._cs.append_rectangle(x, y, w, h)
|
||||
if fill:
|
||||
self._cs.fill()
|
||||
else:
|
||||
self._cs.stroke_and_close()
|
||||
return self
|
||||
|
||||
def draw_image(self, image: Path | str | Image.Image, x, y, width, height):
|
||||
"""Draw image at (x,y) with width w and height h."""
|
||||
with self.save_state(cm=Matrix(width, 0, 0, height, x, y)):
|
||||
if isinstance(image, (Path, str)):
|
||||
image = Image.open(image)
|
||||
image.load()
|
||||
if image.mode == "P":
|
||||
image = image.convert("RGB")
|
||||
if image.mode not in ("1", "L", "RGB"):
|
||||
raise ValueError(f"Unsupported image mode: {image.mode}")
|
||||
name = Name.random(prefix="Im")
|
||||
li = LoadedImage(name, image)
|
||||
self._images.append(li)
|
||||
self._cs.draw_form_xobject(name)
|
||||
return self
|
||||
|
||||
def draw_text(self, text: PikepdfText):
|
||||
"""Draw text object."""
|
||||
self._cs.extend(text._cs)
|
||||
self._cs.end_text()
|
||||
return self
|
||||
|
||||
def dashes(self, *args):
|
||||
"""Set dashes."""
|
||||
self._cs.set_dashes(*args)
|
||||
return self
|
||||
|
||||
def push(self):
|
||||
"""Save the graphics state."""
|
||||
self._cs.push()
|
||||
self._stack_depth += 1
|
||||
return self
|
||||
|
||||
def pop(self):
|
||||
"""Restore the graphics state."""
|
||||
self._cs.pop()
|
||||
self._stack_depth -= 1
|
||||
return self
|
||||
|
||||
@contextmanager
|
||||
def save_state(self, *, cm: Matrix | None = None):
|
||||
"""Save the graphics state and restore it on exit.
|
||||
|
||||
Optionally, concatenate a transformation matrix. Implements
|
||||
the commonly used pattern of:
|
||||
q cm ... Q
|
||||
"""
|
||||
self.push()
|
||||
if cm is not None:
|
||||
self.cm(cm)
|
||||
yield self
|
||||
self.pop()
|
||||
|
||||
def cm(self, matrix: Matrix):
|
||||
"""Concatenate a new transformation matrix to the current matrix."""
|
||||
self._cs.cm(matrix)
|
||||
return self
|
||||
|
||||
|
||||
class PikepdfCanvas:
|
||||
"""Canvas for rendering PDFs with pikepdf.
|
||||
|
||||
All drawing is done on a pikepdf canvas using the .do property.
|
||||
This interface manages the graphics state of the canvas and saves it.
|
||||
"""
|
||||
|
||||
def __init__(self, *, page_size: tuple[int | float, int | float]):
|
||||
self.page_size = page_size
|
||||
self._pdf = Pdf.new()
|
||||
self._page = self._pdf.add_blank_page(page_size=page_size)
|
||||
self._page.Resources = Dictionary(Font=Dictionary(), XObject=Dictionary())
|
||||
self._cs = ContentStreamBuilder()
|
||||
self._images: list[LoadedImage] = []
|
||||
self._accessor = _PikepdfCanvasAccessor(self._cs, self._images)
|
||||
self._stack_depth = 0
|
||||
self.do.push()
|
||||
|
||||
def add_font(self, resource_name: Name, font: Font):
|
||||
"""Add a font to the page."""
|
||||
self._page.Resources.Font[resource_name] = font.register(self._pdf)
|
||||
|
||||
@property
|
||||
def do(self) -> _PikepdfCanvasAccessor:
|
||||
"""Do operations on the current graphics state."""
|
||||
return self._accessor
|
||||
|
||||
def _save_image(self, li: LoadedImage):
|
||||
return self._pdf.make_stream(
|
||||
li.image.tobytes(),
|
||||
Width=li.image.width,
|
||||
Height=li.image.height,
|
||||
ColorSpace=Name.DeviceGray
|
||||
if li.image.mode in ("1", "L")
|
||||
else Name.DeviceRGB,
|
||||
Type=Name.XObject,
|
||||
Subtype=Name.Image,
|
||||
BitsPerComponent=1 if li.image.mode == '1' else 8,
|
||||
)
|
||||
|
||||
def save(self, output_file: Path):
|
||||
"""Save the page to the output file."""
|
||||
self.do.pop()
|
||||
if self._stack_depth != 0:
|
||||
log.warning(
|
||||
"Graphics state stack is not empty when page saved - "
|
||||
"rendering may be incorrect"
|
||||
)
|
||||
self._page.Contents = self._pdf.make_stream(self._cs.build())
|
||||
for li in self._images:
|
||||
self._page.Resources.XObject[li.name] = self._save_image(li)
|
||||
self._pdf.save(output_file)
|
||||
|
||||
|
||||
class PikepdfText:
|
||||
"""Text object for rendering text on a pikepdf canvas."""
|
||||
|
||||
def __init__(self, direction=TextDirection.LTR):
|
||||
self._cs = ContentStreamBuilder()
|
||||
self._cs.begin_text()
|
||||
self._direction = direction
|
||||
|
||||
def font(self, font: Name, size: float):
|
||||
self._cs.set_text_font(font, size)
|
||||
return self
|
||||
|
||||
def render_mode(self, mode):
|
||||
self._cs.set_text_rendering(mode)
|
||||
return self
|
||||
|
||||
def text_transform(self, matrix: Matrix):
|
||||
self._cs.set_text_matrix(matrix)
|
||||
return self
|
||||
|
||||
def show(self, text: str):
|
||||
if self._direction == TextDirection.LTR:
|
||||
self._cs.show_text(text)
|
||||
else:
|
||||
self._cs.begin_marked_content(Name.ReversedChars)
|
||||
self._cs.show_text(text)
|
||||
self._cs.end_marked_content()
|
||||
return self
|
||||
|
||||
def horiz_scale(self, scale):
|
||||
self._cs.set_text_horizontal_scaling(scale)
|
||||
return self
|
||||
|
||||
def move_cursor(self, x, y):
|
||||
self._cs.move_cursor(x, y)
|
||||
return self
|
||||
@@ -0,0 +1,112 @@
|
||||
# SPDX-FileCopyrightText: 2023 James R. Barlow
|
||||
# SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import unicodedata
|
||||
import zlib
|
||||
from importlib.resources import files as package_files
|
||||
|
||||
from pikepdf import (
|
||||
Dictionary,
|
||||
Name,
|
||||
Pdf,
|
||||
)
|
||||
from pikepdf.canvas import Font
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class GlyphlessFont(Font):
|
||||
CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536)
|
||||
GLYPHLESS_FONT_NAME = 'pdf.ttf'
|
||||
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
|
||||
CHAR_ASPECT = 2
|
||||
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def text_width(self, text: str, fontsize: float) -> int:
|
||||
"""Estimate the width of a text string when rendered with the given font."""
|
||||
# NFKC: split ligatures, combine diacritics
|
||||
return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT)
|
||||
|
||||
def register(self, 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 // self.CHAR_ASPECT,
|
||||
)
|
||||
)
|
||||
basefont.DescendantFonts = [cid_font_type2]
|
||||
cid_font_type2.CIDToGIDMap = pdf.make_stream(
|
||||
self.CID_TO_GID_DATA, Filter=Name.FlateDecode
|
||||
)
|
||||
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 // self.CHAR_ASPECT, 1000],
|
||||
FontFile2=PLACEHOLDER,
|
||||
FontName=Name.GlyphLessFont,
|
||||
ItalicAngle=0,
|
||||
StemV=80,
|
||||
Type=Name.FontDescriptor,
|
||||
)
|
||||
)
|
||||
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
|
||||
cid_font_type2.FontDescriptor = font_descriptor
|
||||
return basefont
|
||||
@@ -18,15 +18,7 @@ from pathlib import Path
|
||||
from xml.etree import ElementTree
|
||||
|
||||
from pikepdf import Matrix, Name, Rectangle
|
||||
|
||||
from ocrmypdf.hocrtransform._canvas import (
|
||||
Font,
|
||||
GlyphlessFont,
|
||||
PikepdfText,
|
||||
TextDirection,
|
||||
)
|
||||
from ocrmypdf.hocrtransform._canvas import PikepdfCanvas as Canvas
|
||||
from ocrmypdf.hocrtransform.color import (
|
||||
from pikepdf.canvas import (
|
||||
BLACK,
|
||||
BLUE,
|
||||
CYAN,
|
||||
@@ -34,8 +26,14 @@ from ocrmypdf.hocrtransform.color import (
|
||||
GREEN,
|
||||
MAGENTA,
|
||||
RED,
|
||||
Canvas,
|
||||
Font,
|
||||
Text,
|
||||
TextDirection,
|
||||
)
|
||||
|
||||
from ocrmypdf.hocrtransform._font import GlyphlessFont
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
INCH = 72.0
|
||||
@@ -232,7 +230,7 @@ class HocrTransform:
|
||||
)
|
||||
|
||||
# finish up the page and save it
|
||||
canvas.save(out_filename)
|
||||
canvas.to_pdf().save(out_filename)
|
||||
|
||||
def _get_text_direction(self, par):
|
||||
"""Get the text direction of the paragraph.
|
||||
@@ -305,7 +303,7 @@ class HocrTransform:
|
||||
)
|
||||
log.debug(line_matrix)
|
||||
with canvas.do.save_state(cm=line_matrix):
|
||||
text = PikepdfText(direction=text_direction)
|
||||
text = Text(direction=text_direction)
|
||||
|
||||
# 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
|
||||
@@ -339,7 +337,7 @@ class HocrTransform:
|
||||
self,
|
||||
canvas: Canvas,
|
||||
line_matrix: Matrix,
|
||||
text: PikepdfText,
|
||||
text: Text,
|
||||
fontsize: float,
|
||||
elem: Element,
|
||||
next_elem: Element | None,
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
Color = namedtuple('Color', ['red', 'green', 'blue', 'alpha'])
|
||||
|
||||
BLACK = Color(0, 0, 0, 1)
|
||||
WHITE = Color(1, 1, 1, 1)
|
||||
BLUE = Color(0, 0, 1, 1)
|
||||
CYAN = Color(0, 1, 1, 1)
|
||||
GREEN = Color(0, 1, 0, 1)
|
||||
DARKGREEN = Color(0, 0.5, 0, 1)
|
||||
MAGENTA = Color(1, 0, 1, 1)
|
||||
RED = Color(1, 0, 0, 1)
|
||||
Reference in New Issue
Block a user