Fix remaining mypy errors in fpdf_renderer and its tests
Resolves the last 16 mypy errors in the project (src/ocrmypdf and
tests are now fully clean).
fpdf_renderer/renderer.py (9 errors):
- add_page(format=...): fpdf2's own stub types this param as str, but
its docstring and get_page_format() helper confirm a (width, height)
tuple is accepted too - the stub annotation on add_page() itself is
the outlier. Used cast() to match the documented/actual behavior.
- pdf.current_font is typed CoreFont | TTFFont | None, but this
renderer only ever registers fonts via add_font() with a TTF file
(see _register_font/set_font call sites) - it never falls back to
fpdf2's built-in CoreFont. Added assertions (isinstance(font,
TTFFont) where shape_text()/escape_text() are needed, which
CoreFont lacks; plain not-None elsewhere) documenting that
invariant instead of narrowing defensively for a case that can't
happen here.
tests/test_pdf_renderer.py (7 errors): the ToUnicode/glyph-extraction
test helpers used `.get(key, {})` (a plain dict literal default) then
called `.values()`/`.items()` on the result. pikepdf.Object doesn't
declare `values()` in its stub (only `keys()`), so this silently
degraded to Object's catch-all `__getattr__` returning another Object,
which then failed as "not callable". Switched to `.get(key,
Dictionary()).as_dict()`, which returns pikepdf's properly-typed
_ObjectMapping helper.
This commit is contained in:
@@ -14,9 +14,11 @@ import unicodedata
|
||||
from dataclasses import dataclass
|
||||
from math import atan, cos, degrees, radians, sin, sqrt
|
||||
from pathlib import Path
|
||||
from typing import cast
|
||||
|
||||
from fpdf import FPDF
|
||||
from fpdf.enums import PDFResourceType, TextMode
|
||||
from fpdf.fonts import TTFFont
|
||||
from pikepdf import Matrix, Rectangle
|
||||
|
||||
from ocrmypdf.font import FontManager, MultiFontManager
|
||||
@@ -241,10 +243,16 @@ class Fpdf2PdfRenderer:
|
||||
pdf: FPDF instance to render into
|
||||
"""
|
||||
# Add page with correct dimensions
|
||||
# fpdf2's add_page() stub says format: str, but its docstring and
|
||||
# get_page_format() helper confirm a (width, height) tuple is
|
||||
# supported too - the annotation on add_page() itself is just wrong.
|
||||
pdf.add_page(
|
||||
format=(
|
||||
self.coord_transform.page_width_pt,
|
||||
self.coord_transform.page_height_pt,
|
||||
format=cast(
|
||||
'str',
|
||||
(
|
||||
self.coord_transform.page_width_pt,
|
||||
self.coord_transform.page_height_pt,
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -695,6 +703,10 @@ class Fpdf2PdfRenderer:
|
||||
# Set font if changed
|
||||
if word.font_family != prev_font_family:
|
||||
pdf.set_font(word.font_family, size=font_size)
|
||||
# We only ever register fonts via add_font() with a TTF file
|
||||
# (see _register_font), so set_font() always resolves to a
|
||||
# TTFFont, never a built-in CoreFont or leaves it unset.
|
||||
assert pdf.current_font is not None
|
||||
# Register font resource on this page
|
||||
pdf._resource_catalog.add(
|
||||
PDFResourceType.FONT, pdf.current_font.i, pdf.page
|
||||
@@ -773,6 +785,10 @@ class Fpdf2PdfRenderer:
|
||||
joining forms and ligature shaping is harmless.
|
||||
"""
|
||||
font = pdf.current_font
|
||||
# We only ever register fonts via add_font() with a TTF file (see
|
||||
# _register_font), so current_font is always a TTFFont - never the
|
||||
# built-in CoreFont (which lacks shape_text()/escape_text()) or None.
|
||||
assert isinstance(font, TTFFont)
|
||||
if is_rtl:
|
||||
# Reverse the text so that after bidi reversal by the text
|
||||
# extractor, the characters end up in correct logical order.
|
||||
|
||||
+30
-24
@@ -655,16 +655,19 @@ def _tounicode_map(pdf_path: Path) -> dict[int, str]:
|
||||
"""
|
||||
pdf = pikepdf.open(pdf_path)
|
||||
page = pdf.pages[0]
|
||||
resources = page.get('/Resources', {})
|
||||
resources = page.get('/Resources', pikepdf.Dictionary()).as_dict()
|
||||
|
||||
# Collect fonts from the page and from any Form XObjects (OCR overlay)
|
||||
fonts: dict[str, pikepdf.Object] = {}
|
||||
if '/Font' in resources:
|
||||
for name, obj in resources['/Font'].items():
|
||||
for name, obj in resources['/Font'].as_dict().items():
|
||||
fonts[str(name)] = obj
|
||||
for xobj in resources.get('/XObject', {}).values():
|
||||
for xobj in resources.get('/XObject', pikepdf.Dictionary()).as_dict().values():
|
||||
if xobj.get('/Subtype') == '/Form':
|
||||
for name, obj in xobj.get('/Resources', {}).get('/Font', {}).items():
|
||||
xobj_resources = xobj.get('/Resources', pikepdf.Dictionary()).as_dict()
|
||||
for name, obj in (
|
||||
xobj_resources.get('/Font', pikepdf.Dictionary()).as_dict().items()
|
||||
):
|
||||
fonts[str(name)] = obj
|
||||
|
||||
result: dict[int, str] = {}
|
||||
@@ -701,30 +704,33 @@ def _decode_tounicode_stream(
|
||||
"""
|
||||
pdf = pikepdf.open(pdf_path)
|
||||
page = pdf.pages[0]
|
||||
resources = page.get('/Resources', {})
|
||||
resources = page.get('/Resources', pikepdf.Dictionary()).as_dict()
|
||||
|
||||
# Collect fonts from page and from Form XObjects
|
||||
cmap: dict[int, str] = {}
|
||||
for font_dict in [resources.get('/Font', {})]:
|
||||
for fobj in font_dict.values():
|
||||
tounicode = fobj.get('/ToUnicode')
|
||||
if tounicode is None:
|
||||
continue
|
||||
raw = bytes(tounicode.read_bytes()).decode('latin-1', errors='replace')
|
||||
for m in re.finditer(r'<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>', raw):
|
||||
src = int(m.group(1), 16)
|
||||
dst_hex = m.group(2)
|
||||
chars = ''.join(
|
||||
chr(int(dst_hex[i : i + 4], 16))
|
||||
for i in range(0, len(dst_hex), 4)
|
||||
if int(dst_hex[i : i + 4], 16) > 0
|
||||
)
|
||||
if src > 0 and chars:
|
||||
cmap[src] = chars
|
||||
for xobj in resources.get('/XObject', {}).values():
|
||||
font_dict = resources.get('/Font', pikepdf.Dictionary()).as_dict()
|
||||
for fobj in font_dict.values():
|
||||
tounicode = fobj.get('/ToUnicode')
|
||||
if tounicode is None:
|
||||
continue
|
||||
raw = bytes(tounicode.read_bytes()).decode('latin-1', errors='replace')
|
||||
for m in re.finditer(r'<([0-9A-Fa-f]+)>\s*<([0-9A-Fa-f]+)>', raw):
|
||||
src = int(m.group(1), 16)
|
||||
dst_hex = m.group(2)
|
||||
chars = ''.join(
|
||||
chr(int(dst_hex[i : i + 4], 16))
|
||||
for i in range(0, len(dst_hex), 4)
|
||||
if int(dst_hex[i : i + 4], 16) > 0
|
||||
)
|
||||
if src > 0 and chars:
|
||||
cmap[src] = chars
|
||||
for xobj in resources.get('/XObject', pikepdf.Dictionary()).as_dict().values():
|
||||
if xobj.get('/Subtype') != '/Form':
|
||||
continue
|
||||
for fobj in xobj.get('/Resources', {}).get('/Font', {}).values():
|
||||
xobj_resources = xobj.get('/Resources', pikepdf.Dictionary()).as_dict()
|
||||
for fobj in (
|
||||
xobj_resources.get('/Font', pikepdf.Dictionary()).as_dict().values()
|
||||
):
|
||||
tounicode = fobj.get('/ToUnicode')
|
||||
if tounicode is None:
|
||||
continue
|
||||
@@ -746,7 +752,7 @@ def _decode_tounicode_stream(
|
||||
contents = page.get('/Contents')
|
||||
if contents:
|
||||
streams.append(bytes(contents.read_bytes()))
|
||||
for xobj in resources.get('/XObject', {}).values():
|
||||
for xobj in resources.get('/XObject', pikepdf.Dictionary()).as_dict().values():
|
||||
if xobj.get('/Subtype') == '/Form':
|
||||
streams.append(bytes(xobj.read_bytes()))
|
||||
for data in streams:
|
||||
|
||||
Reference in New Issue
Block a user