Source code for pdfje.fonts.common

from __future__ import annotations

import abc
from dataclasses import dataclass, field
from itertools import chain, count
from pathlib import Path
from typing import TYPE_CHECKING, Iterable, Tuple, Union, final

from .. import atoms
from ..atoms import ASCII
from ..common import (
    Char,
    Func,
    Pos,
    Pt,
    add_slots,
    fix_abstract_properties,
    setattr_frozen,
)
from ..compat import pairwise

FontID = bytes  # unique, internal identifier assigned to a font within a PDF
GlyphPt = float  # length unit in glyph space
TEXTSPACE_TO_GLYPHSPACE = 1000  # See PDF32000-1:2008 (9.7.3)


@fix_abstract_properties
class Font(abc.ABC):
    """A specific font within a typeface"""

    __slots__ = ()

    @property
    @abc.abstractmethod
    def id(self) -> FontID: ...

    # It's worth caching this value, as it is used often
    @property
    @abc.abstractmethod
    def spacewidth(self) -> GlyphPt: ...

    @property
    @abc.abstractmethod
    def encoding_width(self) -> int:
        """The number of bytes assigned to each character when encoding"""

    @abc.abstractmethod
    def encode(self, s: str, /) -> bytes: ...

    @abc.abstractmethod
    def width(self, s: str, /) -> Pt:
        """The total width of the given string (excluding kerning)"""

    @staticmethod
    @abc.abstractmethod
    def charwidth(c: Char, /) -> GlyphPt: ...

    @abc.abstractmethod
    def kern(self, s: str, /, prev: Char | None) -> Iterable[Kern]: ...

    @abc.abstractmethod
    def charkern(self, a: Char, b: Char, /) -> GlyphPt: ...


[docs] @final @add_slots @dataclass(frozen=True, init=False) class TrueType: """A TrueType font to be embedded in a PDF Parameters ---------- regular The regular (i.e. non-bold, non-italic) .ttf file bold The bold .ttf file italic The italic .ttf file bold_italic The bold italic .ttf file """ regular: Path bold: Path italic: Path bold_italic: Path def __init__( self, regular: Path | str, bold: Path | str, italic: Path | str, bold_italic: Path | str, ) -> None: setattr_frozen(self, "regular", Path(regular)) setattr_frozen(self, "bold", Path(bold)) setattr_frozen(self, "italic", Path(italic)) setattr_frozen(self, "bold_italic", Path(bold_italic)) # This method cannot be defined in the class body, as it would cause a # circular import. The implementation is patched into the class # in the `style` module. if TYPE_CHECKING: # pragma: no cover from ..common import HexColor from ..style import Style, StyleLike def __or__(self, _: StyleLike, /) -> Style: ... def __ror__(self, _: HexColor, /) -> Style: ... def font(self, bold: bool, italic: bool) -> Path: if bold: return self.bold_italic if italic else self.bold else: return self.italic if italic else self.regular
[docs] @final @add_slots @dataclass(frozen=True, repr=False) class BuiltinTypeface: """A typeface that is built into the PDF renderer.""" regular: BuiltinFont bold: BuiltinFont italic: BuiltinFont bold_italic: BuiltinFont # This method cannot be defined in the class body, as it would cause a # circular import. The implementation is patched into the class # in the `style` module. if TYPE_CHECKING: # pragma: no cover from ..common import HexColor from ..style import Style, StyleLike def __or__(self, _: StyleLike, /) -> Style: ... def __ror__(self, _: HexColor, /) -> Style: ... def __repr__(self) -> str: return f"{self.__class__.__name__}({self.regular.name.decode()})" def font(self, bold: bool, italic: bool) -> BuiltinFont: if bold: return self.bold_italic if italic else self.bold else: return self.italic if italic else self.regular
Typeface = Union[BuiltinTypeface, TrueType] @final @add_slots @dataclass(frozen=True, eq=False) class BuiltinFont(Font): name: ASCII id: FontID charwidth: Func[Char, GlyphPt] = field(repr=False) kerning: KerningTable | None = field(repr=False) spacewidth: Pt = field(init=False, repr=False) encoding_width = 1 def __post_init__(self) -> None: setattr_frozen(self, "spacewidth", self.charwidth(" ")) def width(self, s: str) -> Pt: return sum(map(self.charwidth, s)) / TEXTSPACE_TO_GLYPHSPACE @staticmethod def encode(s: str) -> bytes: # FUTURE: normalize unicode to allow better unicode representation return s.encode("cp1252", errors="replace") def kern(self, s: str, /, prev: Char | None) -> Iterable[Kern]: return kern(self.kerning, s, prev) if self.kerning else () def charkern(self, a: Char, b: Char) -> GlyphPt: return self.kerning((a, b)) if self.kerning else 0 def to_resource(self) -> atoms.Dictionary: return atoms.Dictionary( (b"Type", atoms.Name(b"Font")), (b"Subtype", atoms.Name(b"Type1")), (b"BaseFont", atoms.Name(self.name)), (b"Encoding", atoms.Name(b"WinAnsiEncoding")), ) KerningTable = Func[Tuple[Char, Char], GlyphPt] Kern = Tuple[Pos, GlyphPt] def kern( table: KerningTable, s: str, prev: Char | None, ) -> Iterable[Kern]: for i, pair in zip( count(not prev), pairwise(chain(prev, s) if prev else s), ): if space := table(pair): yield (i, space)