initial
This commit is contained in:
206
.venv/lib/python3.12/site-packages/ezdxf/fonts/ttfonts.py
Normal file
206
.venv/lib/python3.12/site-packages/ezdxf/fonts/ttfonts.py
Normal file
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2023, Manfred Moitzi
|
||||
# License: MIT License
|
||||
from __future__ import annotations
|
||||
from typing import Any, no_type_check
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
from ezdxf.math import Matrix44, UVec, BoundingBox2d
|
||||
from ezdxf.path import Path
|
||||
from .font_manager import FontManager, UnsupportedFont
|
||||
from .font_measurements import FontMeasurements
|
||||
from .glyphs import GlyphPath, Glyphs
|
||||
|
||||
UNICODE_WHITE_SQUARE = 9633 # U+25A1
|
||||
UNICODE_REPLACEMENT_CHAR = 65533 # U+FFFD
|
||||
|
||||
font_manager = FontManager()
|
||||
|
||||
|
||||
class PathPen(BasePen):
|
||||
def __init__(self, glyph_set) -> None:
|
||||
super().__init__(glyph_set)
|
||||
self._path = Path()
|
||||
|
||||
@property
|
||||
def path(self) -> GlyphPath:
|
||||
return GlyphPath(self._path)
|
||||
|
||||
def _moveTo(self, pt: UVec) -> None:
|
||||
self._path.move_to(pt)
|
||||
|
||||
def _lineTo(self, pt: UVec) -> None:
|
||||
self._path.line_to(pt)
|
||||
|
||||
def _curveToOne(self, pt1: UVec, pt2: UVec, pt3: UVec) -> None:
|
||||
self._path.curve4_to(pt3, pt1, pt2)
|
||||
|
||||
def _qCurveToOne(self, pt1: UVec, pt2: UVec) -> None:
|
||||
self._path.curve3_to(pt2, pt1)
|
||||
|
||||
def _closePath(self) -> None:
|
||||
self._path.close_sub_path()
|
||||
|
||||
|
||||
class NoKerning:
|
||||
def get(self, c0: str, c1: str) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
class KerningTable(NoKerning):
|
||||
__slots__ = ("_cmap", "_kern_table")
|
||||
|
||||
def __init__(self, font: TTFont, cmap, fmt: int = 0):
|
||||
self._cmap = cmap
|
||||
self._kern_table = font["kern"].getkern(fmt)
|
||||
|
||||
def get(self, c0: str, c1: str) -> float:
|
||||
try:
|
||||
return self._kern_table[(self._cmap[ord(c0)], self._cmap[ord(c1)])]
|
||||
except (KeyError, TypeError):
|
||||
return 0.0
|
||||
|
||||
|
||||
def get_fontname(font: TTFont) -> str:
|
||||
names = font["name"].names
|
||||
for record in names:
|
||||
if record.nameID == 1:
|
||||
return record.string.decode(record.getEncoding())
|
||||
return "unknown"
|
||||
|
||||
|
||||
class TTFontRenderer(Glyphs):
|
||||
def __init__(self, font: TTFont, kerning=False):
|
||||
self._glyph_path_cache: dict[str, GlyphPath] = dict()
|
||||
self._generic_glyph_cache: dict[str, Any] = dict()
|
||||
self._glyph_width_cache: dict[str, float] = dict()
|
||||
self.font = font
|
||||
self.cmap = self.font.getBestCmap()
|
||||
if self.cmap is None:
|
||||
raise UnsupportedFont(f"font '{self.font_name}' has no character map.")
|
||||
self.glyph_set = self.font.getGlyphSet()
|
||||
self.kerning = NoKerning()
|
||||
if kerning:
|
||||
try:
|
||||
self.kerning = KerningTable(self.font, self.cmap)
|
||||
except KeyError: # kerning table does not exist
|
||||
pass
|
||||
self.undefined_generic_glyph = self.glyph_set[".notdef"]
|
||||
self.font_measurements = self._get_font_measurements()
|
||||
self.space_width = self.detect_space_width()
|
||||
|
||||
@property
|
||||
def font_name(self) -> str:
|
||||
return get_fontname(self.font)
|
||||
|
||||
@no_type_check
|
||||
def _get_font_measurements(self) -> FontMeasurements:
|
||||
bbox = BoundingBox2d(self.get_glyph_path("x").control_vertices())
|
||||
baseline = bbox.extmin.y
|
||||
x_height = bbox.extmax.y - baseline
|
||||
bbox = BoundingBox2d(self.get_glyph_path("A").control_vertices())
|
||||
cap_height = bbox.extmax.y - baseline
|
||||
bbox = BoundingBox2d(self.get_glyph_path("p").control_vertices())
|
||||
descender_height = baseline - bbox.extmin.y
|
||||
return FontMeasurements(
|
||||
baseline=baseline,
|
||||
cap_height=cap_height,
|
||||
x_height=x_height,
|
||||
descender_height=descender_height,
|
||||
)
|
||||
|
||||
def get_scaling_factor(self, cap_height: float) -> float:
|
||||
return 1.0 / self.font_measurements.cap_height * cap_height
|
||||
|
||||
def get_generic_glyph(self, char: str):
|
||||
try:
|
||||
return self._generic_glyph_cache[char]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
generic_glyph = self.glyph_set[self.cmap[ord(char)]]
|
||||
except KeyError:
|
||||
generic_glyph = self.undefined_generic_glyph
|
||||
self._generic_glyph_cache[char] = generic_glyph
|
||||
return generic_glyph
|
||||
|
||||
def get_glyph_path(self, char: str) -> GlyphPath:
|
||||
"""Returns the raw glyph path, without any scaling applied."""
|
||||
try:
|
||||
return self._glyph_path_cache[char].clone()
|
||||
except KeyError:
|
||||
pass
|
||||
pen = PathPen(self.glyph_set)
|
||||
self.get_generic_glyph(char).draw(pen)
|
||||
glyph_path = pen.path
|
||||
self._glyph_path_cache[char] = glyph_path
|
||||
return glyph_path.clone()
|
||||
|
||||
def get_glyph_width(self, char: str) -> float:
|
||||
"""Returns the raw glyph width, without any scaling applied."""
|
||||
try:
|
||||
return self._glyph_width_cache[char]
|
||||
except KeyError:
|
||||
pass
|
||||
width = 0.0
|
||||
try:
|
||||
width = self.get_generic_glyph(char).width
|
||||
except KeyError:
|
||||
pass
|
||||
self._glyph_width_cache[char] = width
|
||||
return width
|
||||
|
||||
def get_text_glyph_paths(
|
||||
self, s: str, cap_height: float = 1.0, width_factor: float = 1.0
|
||||
) -> list[GlyphPath]:
|
||||
"""Returns the glyph paths of string `s` as a list, scaled to cap height."""
|
||||
glyph_paths: list[GlyphPath] = []
|
||||
x_offset: float = 0
|
||||
requires_kerning = isinstance(self.kerning, KerningTable)
|
||||
resize_factor = self.get_scaling_factor(cap_height)
|
||||
y_factor = resize_factor
|
||||
x_factor = resize_factor * width_factor
|
||||
# set scaling factor:
|
||||
m = Matrix44.scale(x_factor, y_factor, 1.0)
|
||||
# set vertical offset:
|
||||
m[3, 1] = -self.font_measurements.baseline * y_factor
|
||||
prev_char = ""
|
||||
|
||||
for char in s:
|
||||
if requires_kerning:
|
||||
x_offset += self.kerning.get(prev_char, char) * x_factor
|
||||
# set horizontal offset:
|
||||
m[3, 0] = x_offset
|
||||
glyph_path = self.get_glyph_path(char)
|
||||
glyph_path.transform_inplace(m)
|
||||
if len(glyph_path):
|
||||
glyph_paths.append(glyph_path)
|
||||
x_offset += self.get_glyph_width(char) * x_factor
|
||||
prev_char = char
|
||||
return glyph_paths
|
||||
|
||||
def detect_space_width(self) -> float:
|
||||
"""Returns the space width for the raw (unscaled) font."""
|
||||
return self.get_glyph_width(" ")
|
||||
|
||||
def _get_text_length_with_kerning(self, s: str, cap_height: float = 1.0) -> float:
|
||||
length = 0.0
|
||||
c0 = ""
|
||||
kern = self.kerning.get
|
||||
width = self.get_glyph_width
|
||||
for c1 in s:
|
||||
length += kern(c0, c1) + width(c1)
|
||||
c0 = c1
|
||||
return length * self.get_scaling_factor(cap_height)
|
||||
|
||||
def get_text_length(
|
||||
self, s: str, cap_height: float = 1.0, width_factor: float = 1.0
|
||||
) -> float:
|
||||
if isinstance(self.kerning, KerningTable):
|
||||
return self._get_text_length_with_kerning(s, cap_height) * width_factor
|
||||
width = self.get_glyph_width
|
||||
return (
|
||||
sum(width(c) for c in s)
|
||||
* self.get_scaling_factor(cap_height)
|
||||
* width_factor
|
||||
)
|
||||
Reference in New Issue
Block a user