746 lines
25 KiB
Python
746 lines
25 KiB
Python
# Copyright (c) 2021-2023, Manfred Moitzi
|
|
# License: MIT License
|
|
from __future__ import annotations
|
|
from typing import Optional, TYPE_CHECKING, cast
|
|
import abc
|
|
import enum
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
|
|
from ezdxf import options
|
|
from .font_face import FontFace
|
|
from .font_manager import (
|
|
FontManager,
|
|
SUPPORTED_TTF_TYPES,
|
|
FontNotFoundError,
|
|
UnsupportedFont,
|
|
)
|
|
from .font_synonyms import FONT_SYNONYMS
|
|
from .font_measurements import FontMeasurements
|
|
from .glyphs import GlyphPath, Glyphs
|
|
|
|
if TYPE_CHECKING:
|
|
from ezdxf.document import Drawing
|
|
from ezdxf.entities import DXFEntity, Textstyle
|
|
|
|
logger = logging.getLogger("ezdxf")
|
|
FONT_MANAGER_CACHE_FILE = "font_manager_cache.json"
|
|
# SUT = System Under Test, see build_sut_font_manager_cache() function
|
|
SUT_FONT_MANAGER_CACHE = False
|
|
CACHE_DIRECTORY = ".cache"
|
|
font_manager = FontManager()
|
|
|
|
SHX_FONTS = {
|
|
# See examples in: CADKitSamples/Shapefont.dxf
|
|
# Shape file structure is not documented, therefore, replace these fonts by
|
|
# true type fonts.
|
|
# `None` is for: use the default font.
|
|
#
|
|
# All these replacement TTF fonts have a copyright remark:
|
|
# "(c) Copyright 1996 by Autodesk Inc., All rights reserved"
|
|
# and therefore can not be included in ezdxf or the associated repository!
|
|
# You got them if you install any Autodesk product, like the free available
|
|
# DWG/DXF viewer "TrueView" : https://www.autodesk.com/viewers
|
|
"AMGDT": "amgdt___.ttf", # Tolerance symbols
|
|
"AMGDT.SHX": "amgdt___.ttf",
|
|
"COMPLEX": "complex_.ttf",
|
|
"COMPLEX.SHX": "complex_.ttf",
|
|
"ISOCP": "isocp.ttf",
|
|
"ISOCP.SHX": "isocp.ttf",
|
|
"ITALIC": "italicc_.ttf",
|
|
"ITALIC.SHX": "italicc_.ttf",
|
|
"GOTHICG": "gothicg_.ttf",
|
|
"GOTHICG.SHX": "gothicg_.ttf",
|
|
"GREEKC": "greekc.ttf",
|
|
"GREEKC.SHX": "greekc.ttf",
|
|
"ROMANS": "romans__.ttf",
|
|
"ROMANS.SHX": "romans__.ttf",
|
|
"SCRIPTS": "scripts_.ttf",
|
|
"SCRIPTS.SHX": "scripts_.ttf",
|
|
"SCRIPTC": "scriptc_.ttf",
|
|
"SCRIPTC.SHX": "scriptc_.ttf",
|
|
"SIMPLEX": "simplex_.ttf",
|
|
"SIMPLEX.SHX": "simplex_.ttf",
|
|
"SYMATH": "symath__.ttf",
|
|
"SYMATH.SHX": "symath__.ttf",
|
|
"SYMAP": "symap___.ttf",
|
|
"SYMAP.SHX": "symap___.ttf",
|
|
"SYMETEO": "symeteo_.ttf",
|
|
"SYMETEO.SHX": "symeteo_.ttf",
|
|
"TXT": "txt_____.ttf", # Default AutoCAD font
|
|
"TXT.SHX": "txt_____.ttf",
|
|
}
|
|
LFF_FONTS = {
|
|
"TXT": "standard.lff",
|
|
"TXT.SHX": "standard.lff",
|
|
}
|
|
TTF_TO_SHX = {v: k for k, v in SHX_FONTS.items() if k.endswith("SHX")}
|
|
DESCENDER_FACTOR = 0.333 # from TXT SHX font - just guessing
|
|
X_HEIGHT_FACTOR = 0.666 # from TXT SHX font - just guessing
|
|
MONOSPACE = "*monospace" # last resort fallback font only for measurement
|
|
|
|
|
|
def map_shx_to_ttf(font_name: str) -> str:
|
|
"""Map .shx font names to .ttf file names. e.g. "TXT" -> "txt_____.ttf" """
|
|
# Map SHX fonts to True Type Fonts:
|
|
font_upper = font_name.upper()
|
|
if font_upper in SHX_FONTS:
|
|
font_name = SHX_FONTS[font_upper]
|
|
return font_name
|
|
|
|
|
|
def map_shx_to_lff(font_name: str) -> str:
|
|
"""Map .shx font names to .lff file names. e.g. "TXT" -> "standard.lff" """
|
|
font_upper = font_name.upper()
|
|
name = LFF_FONTS.get(font_upper, "")
|
|
if font_manager.has_font(name):
|
|
return name
|
|
if not font_upper.endswith(".SHX"):
|
|
lff_name = font_name + ".lff"
|
|
else:
|
|
lff_name = font_name[:-4] + ".lff"
|
|
if font_manager.has_font(lff_name):
|
|
return lff_name
|
|
return font_name
|
|
|
|
|
|
def is_shx_font_name(font_name: str) -> bool:
|
|
name = font_name.lower()
|
|
if name.endswith(".shx"):
|
|
return True
|
|
if "." not in name:
|
|
return True
|
|
return False
|
|
|
|
|
|
def map_ttf_to_shx(ttf: str) -> Optional[str]:
|
|
"""Maps .ttf filenames to .shx font names. e.g. "txt_____.ttf" -> "TXT" """
|
|
return TTF_TO_SHX.get(ttf.lower())
|
|
|
|
|
|
def build_system_font_cache() -> None:
|
|
"""Builds or rebuilds the font manager cache. The font manager cache has a fixed
|
|
location in the cache directory of the users home directory "~/.cache/ezdxf" or the
|
|
directory specified by the environment variable "XDG_CACHE_HOME".
|
|
"""
|
|
build_font_manager_cache(_get_font_manager_path())
|
|
|
|
|
|
def find_font_face(font_name: str) -> FontFace:
|
|
"""Returns the :class:`FontFace` definition for the given font filename
|
|
e.g. "LiberationSans-Regular.ttf".
|
|
|
|
"""
|
|
return font_manager.get_font_face(font_name)
|
|
|
|
|
|
def get_font_face(font_name: str, map_shx=True) -> FontFace:
|
|
"""Returns the :class:`FontFace` definition for the given font filename
|
|
e.g. "LiberationSans-Regular.ttf".
|
|
|
|
This function translates a DXF font definition by the TTF font file name into a
|
|
:class:`FontFace` object. Returns the :class:`FontFace` of the default font when a
|
|
font is not available on the current system.
|
|
|
|
Args:
|
|
font_name: raw font file name as stored in the
|
|
:class:`~ezdxf.entities.Textstyle` entity
|
|
map_shx: maps SHX font names to TTF replacement fonts,
|
|
e.g. "TXT" -> "txt_____.ttf"
|
|
|
|
"""
|
|
if not isinstance(font_name, str):
|
|
raise TypeError("font_name has invalid type")
|
|
if map_shx:
|
|
font_name = map_shx_to_ttf(font_name)
|
|
return find_font_face(font_name)
|
|
|
|
|
|
def resolve_shx_font_name(font_name: str, order: str) -> str:
|
|
"""Resolves a .shx font name, the argument `order` defines the resolve order:
|
|
|
|
- "t" = map .shx fonts to TrueType fonts (.ttf, .ttc, .otf)
|
|
- "s" = use shapefile fonts (.shx, .shp)
|
|
- "l" = map .shx fonts to LibreCAD fonts (.lff)
|
|
|
|
"""
|
|
if len(order) == 0:
|
|
return font_name
|
|
order = order.lower()
|
|
for type_str in order:
|
|
if type_str == "t":
|
|
name = map_shx_to_ttf(font_name)
|
|
if font_manager.has_font(name):
|
|
return name
|
|
elif type_str == "s":
|
|
if not font_name.lower().endswith(".shx"):
|
|
font_name += ".shx"
|
|
if font_manager.has_font(font_name):
|
|
return font_name
|
|
elif type_str == "l":
|
|
name = map_shx_to_lff(font_name)
|
|
if font_manager.has_font(name):
|
|
return name
|
|
return font_name
|
|
|
|
|
|
def resolve_font_face(font_name: str, order="tsl") -> FontFace:
|
|
"""Returns the :class:`FontFace` definition for the given font filename
|
|
e.g. "LiberationSans-Regular.ttf".
|
|
|
|
This function translates a DXF font definition by the TTF font file name into a
|
|
:class:`FontFace` object. Returns the :class:`FontFace` of the default font when a
|
|
font is not available on the current system.
|
|
|
|
The order argument defines the resolve order for .shx fonts:
|
|
|
|
- "t" = map .shx fonts to TrueType fonts (.ttf, .ttc, .otf)
|
|
- "s" = use shapefile fonts (.shx, .shp)
|
|
- "l" = map .shx fonts to LibreCAD fonts (.lff)
|
|
|
|
Args:
|
|
font_name: raw font file name as stored in the
|
|
:class:`~ezdxf.entities.Textstyle` entity
|
|
order: resolving order
|
|
|
|
"""
|
|
if not isinstance(font_name, str):
|
|
raise TypeError("font_name has invalid type")
|
|
if is_shx_font_name(font_name):
|
|
font_name = resolve_shx_font_name(font_name, order)
|
|
return find_font_face(font_name)
|
|
|
|
|
|
def get_font_measurements(font_name: str, map_shx=True) -> FontMeasurements:
|
|
"""Get :class:`FontMeasurements` for the given font filename
|
|
e.g. "LiberationSans-Regular.ttf".
|
|
|
|
Args:
|
|
font_name: raw font file name as stored in the
|
|
:class:`~ezdxf.entities.Textstyle` entity
|
|
map_shx: maps SHX font names to TTF replacement fonts,
|
|
e.g. "TXT" -> "txt_____.ttf"
|
|
|
|
"""
|
|
if map_shx:
|
|
font_name = map_shx_to_ttf(font_name)
|
|
elif is_shx_font_name(font_name):
|
|
return FontMeasurements(
|
|
baseline=0,
|
|
cap_height=1,
|
|
x_height=X_HEIGHT_FACTOR,
|
|
descender_height=DESCENDER_FACTOR,
|
|
)
|
|
font = TrueTypeFont(font_name, cap_height=1)
|
|
return font.measurements
|
|
|
|
|
|
def find_best_match(
|
|
*,
|
|
family: str = "sans-serif",
|
|
style: str = "Regular",
|
|
weight: int = 400,
|
|
width: int = 5,
|
|
italic: Optional[bool] = False,
|
|
) -> Optional[FontFace]:
|
|
"""Returns a :class:`FontFace` that matches the given properties best. The search
|
|
is based the descriptive properties and not on comparing glyph shapes. Returns
|
|
``None`` if no font was found.
|
|
|
|
Args:
|
|
family: font family name e.g. "sans-serif", "Liberation Sans"
|
|
style: font style e.g. "Regular", "Italic", "Bold"
|
|
weight: weight in the range from 1-1000 (usWeightClass)
|
|
width: width in the range from 1-9 (usWidthClass)
|
|
italic: ``True``, ``False`` or ``None`` to ignore this flag
|
|
|
|
"""
|
|
return font_manager.find_best_match(family, style, weight, width, italic)
|
|
|
|
|
|
def find_font_file_name(font_face: FontFace) -> str:
|
|
"""Returns the true type font file name without parent directories e.g. "Arial.ttf"."""
|
|
return font_manager.find_font_name(font_face)
|
|
|
|
|
|
def load():
|
|
"""Reload all cache files. The cache files are loaded automatically at the import
|
|
of `ezdxf`.
|
|
"""
|
|
_load_font_manager()
|
|
# Add font name synonyms, see discussion #1002
|
|
# Find macOS fonts on Windows/Linux and vice versa.
|
|
font_manager.add_synonyms(FONT_SYNONYMS, reverse=True)
|
|
|
|
|
|
def _get_font_manager_path():
|
|
cache_path = options.xdg_path("XDG_CACHE_HOME", CACHE_DIRECTORY)
|
|
return cache_path / FONT_MANAGER_CACHE_FILE
|
|
|
|
|
|
def _load_font_manager() -> None:
|
|
fm_path = _get_font_manager_path()
|
|
if fm_path.exists():
|
|
try:
|
|
font_manager.loads(fm_path.read_text())
|
|
return
|
|
except IOError as e:
|
|
logger.info(f"Error loading cache file: {str(e)}")
|
|
build_font_manager_cache(fm_path)
|
|
|
|
|
|
def build_sut_font_manager_cache(repo_font_path: pathlib.Path) -> None:
|
|
"""Load font manger cache for system under test (sut).
|
|
|
|
Load the fonts included in the repository folder "./fonts" to guarantee the tests
|
|
have the same fonts available on all systems.
|
|
|
|
This function should be called from "conftest.py".
|
|
|
|
"""
|
|
global SUT_FONT_MANAGER_CACHE
|
|
SUT_FONT_MANAGER_CACHE = True
|
|
font_manager.clear()
|
|
cache_file = repo_font_path / "font_manager_cache.json"
|
|
if cache_file.exists():
|
|
try:
|
|
font_manager.loads(cache_file.read_text())
|
|
return
|
|
except IOError as e:
|
|
print(f"Error loading cache file: {str(e)}")
|
|
font_manager.build([str(repo_font_path)], support_dirs=False)
|
|
s = font_manager.dumps()
|
|
try:
|
|
cache_file.write_text(s)
|
|
except IOError as e:
|
|
print(f"Error writing cache file: {str(e)}")
|
|
|
|
|
|
def make_cache_directory(path: pathlib.Path) -> None:
|
|
if not path.exists():
|
|
try:
|
|
path.mkdir(parents=True)
|
|
except IOError:
|
|
pass
|
|
|
|
|
|
def build_font_manager_cache(path: pathlib.Path) -> None:
|
|
font_manager.clear()
|
|
font_manager.build()
|
|
s = font_manager.dumps()
|
|
cache_dir = path.parent
|
|
make_cache_directory(cache_dir)
|
|
if not cache_dir.exists():
|
|
logger.warning(
|
|
f"Cannot create cache home directory: '{str(cache_dir)}', cache files will "
|
|
f"not be saved.\nSee also issue https://github.com/mozman/ezdxf/issues/923."
|
|
)
|
|
return
|
|
try:
|
|
path.write_text(s)
|
|
except IOError as e:
|
|
logger.warning(f"Error writing cache file: '{str(e)}'")
|
|
|
|
|
|
class FontRenderType(enum.Enum):
|
|
# render glyphs as filled paths: TTF, OTF
|
|
OUTLINE = enum.auto()
|
|
|
|
# render glyphs as line strokes: SHX, SHP
|
|
STROKE = enum.auto
|
|
|
|
|
|
class AbstractFont:
|
|
"""The `ezdxf` font abstraction for text measurement and text path rendering."""
|
|
|
|
font_render_type = FontRenderType.STROKE
|
|
name: str = "undefined"
|
|
|
|
def __init__(self, measurements: FontMeasurements):
|
|
self.measurements = measurements
|
|
|
|
@abc.abstractmethod
|
|
def text_width(self, text: str) -> float:
|
|
"""Returns the text width in drawing units for the given `text` string."""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def text_width_ex(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> float:
|
|
"""Returns the text width in drawing units, bypasses the stored `cap_height` and
|
|
`width_factor`.
|
|
"""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def space_width(self) -> float:
|
|
"""Returns the width of a "space" character a.k.a. word spacing."""
|
|
pass
|
|
|
|
@abc.abstractmethod
|
|
def text_path(self, text: str) -> GlyphPath:
|
|
"""Returns the 2D text path for the given text."""
|
|
...
|
|
|
|
@abc.abstractmethod
|
|
def text_path_ex(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> GlyphPath:
|
|
"""Returns the 2D text path for the given text, bypasses the stored `cap_height`
|
|
and `width_factor`."""
|
|
...
|
|
|
|
@abc.abstractmethod
|
|
def text_glyph_paths(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> list[GlyphPath]:
|
|
"""Returns a list of 2D glyph paths for the given text, bypasses the stored
|
|
`cap_height` and `width_factor`."""
|
|
...
|
|
|
|
|
|
class MonospaceFont(AbstractFont):
|
|
"""Represents a monospaced font where each letter has the same cap- and descender
|
|
height and the same width. The given cap height and width factor are the default
|
|
values for measurements and rendering. The extended methods can override these
|
|
default values.
|
|
|
|
This font exists only for generic text measurement in tests and does not render any
|
|
glyphs!
|
|
|
|
"""
|
|
|
|
font_render_type = FontRenderType.STROKE
|
|
name = MONOSPACE
|
|
|
|
def __init__(
|
|
self,
|
|
cap_height: float,
|
|
width_factor: float = 1.0,
|
|
baseline: float = 0,
|
|
descender_factor: float = DESCENDER_FACTOR,
|
|
x_height_factor: float = X_HEIGHT_FACTOR,
|
|
):
|
|
super().__init__(
|
|
FontMeasurements(
|
|
baseline=baseline,
|
|
cap_height=cap_height,
|
|
x_height=cap_height * x_height_factor,
|
|
descender_height=cap_height * descender_factor,
|
|
)
|
|
)
|
|
self._width_factor: float = abs(width_factor)
|
|
self._space_width = self.measurements.cap_height * self._width_factor
|
|
|
|
def text_width(self, text: str) -> float:
|
|
"""Returns the text width in drawing units for the given `text`."""
|
|
return self.text_width_ex(
|
|
text, self.measurements.cap_height, self._width_factor
|
|
)
|
|
|
|
def text_width_ex(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> float:
|
|
"""Returns the text width in drawing units, bypasses the stored `cap_height` and
|
|
`width_factor`.
|
|
"""
|
|
return len(text) * cap_height * width_factor
|
|
|
|
def text_path(self, text: str) -> GlyphPath:
|
|
"""Returns the rectangle text width x cap height as :class:`NumpyPath2d` instance."""
|
|
return self.text_path_ex(text, self.measurements.cap_height, self._width_factor)
|
|
|
|
def text_path_ex(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> GlyphPath:
|
|
"""Returns the rectangle text width x cap height as :class:`NumpyPath2d`
|
|
instance, bypasses the stored `cap_height` and `width_factor`.
|
|
"""
|
|
from ezdxf.path import Path
|
|
|
|
text_width = self.text_width_ex(text, cap_height, width_factor)
|
|
p = Path((0, 0))
|
|
p.line_to((text_width, 0))
|
|
p.line_to((text_width, cap_height))
|
|
p.line_to((0, cap_height))
|
|
p.close()
|
|
return GlyphPath(p)
|
|
|
|
def text_glyph_paths(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> list[GlyphPath]:
|
|
"""Returns the same rectangle as the method :meth:`text_path_ex` in a list."""
|
|
return [self.text_path_ex(text, cap_height, width_factor)]
|
|
|
|
def space_width(self) -> float:
|
|
"""Returns the width of a "space" char."""
|
|
return self._space_width
|
|
|
|
|
|
class _CachedFont(AbstractFont, abc.ABC):
|
|
"""Abstract font with caching support."""
|
|
|
|
_glyph_caches: dict[str, Glyphs] = dict()
|
|
|
|
def __init__(self, font_name: str, cap_height: float, width_factor: float = 1.0):
|
|
self.name = font_name
|
|
cache = self.create_cache(font_name)
|
|
self.glyph_cache = cache
|
|
self.cap_height = float(cap_height)
|
|
self.width_factor = float(width_factor)
|
|
scale_factor: float = cache.get_scaling_factor(self.cap_height)
|
|
super().__init__(cache.font_measurements.scale(scale_factor))
|
|
self._space_width: float = (
|
|
self.glyph_cache.space_width * scale_factor * width_factor
|
|
)
|
|
|
|
@abc.abstractmethod
|
|
def create_cache(self, font_name: str) -> Glyphs:
|
|
...
|
|
|
|
def text_width(self, text: str) -> float:
|
|
"""Returns the text width in drawing units for the given `text` string."""
|
|
return self.text_width_ex(text, self.cap_height, self.width_factor)
|
|
|
|
def text_width_ex(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> float:
|
|
"""Returns the text width in drawing units, bypasses the stored `cap_height` and
|
|
`width_factor`.
|
|
"""
|
|
if not text.strip():
|
|
return 0
|
|
return self.glyph_cache.get_text_length(text, cap_height, width_factor)
|
|
|
|
def text_path(self, text: str) -> GlyphPath:
|
|
"""Returns the 2D text path for the given text."""
|
|
|
|
return self.text_path_ex(text, self.cap_height, self.width_factor)
|
|
|
|
def text_path_ex(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> GlyphPath:
|
|
"""Returns the 2D text path for the given text, bypasses the stored `cap_height`
|
|
and `width_factor`."""
|
|
return self.glyph_cache.get_text_path(text, cap_height, width_factor)
|
|
|
|
def text_glyph_paths(
|
|
self, text: str, cap_height: float, width_factor: float = 1.0
|
|
) -> list[GlyphPath]:
|
|
"""Returns a list of 2D glyph paths for the given text, bypasses the stored
|
|
`cap_height` and `width_factor`."""
|
|
return self.glyph_cache.get_text_glyph_paths(text, cap_height, width_factor)
|
|
|
|
def space_width(self) -> float:
|
|
"""Returns the width of a "space" char."""
|
|
return self._space_width
|
|
|
|
|
|
class TrueTypeFont(_CachedFont):
|
|
"""Represents a TrueType font. Font measurement and glyph rendering is done by the
|
|
`fontTools` package. The given cap height and width factor are the default values
|
|
for measurements and glyph rendering. The extended methods can override these
|
|
default values.
|
|
"""
|
|
|
|
font_render_type = FontRenderType.OUTLINE
|
|
|
|
def create_cache(self, ttf: str) -> Glyphs:
|
|
from .ttfonts import TTFontRenderer
|
|
|
|
key = pathlib.Path(ttf).name.lower()
|
|
try:
|
|
return self._glyph_caches[key]
|
|
except KeyError:
|
|
pass
|
|
try:
|
|
tt_font = font_manager.get_ttf_font(ttf)
|
|
try: # see issue #990
|
|
cache = TTFontRenderer(tt_font)
|
|
except Exception:
|
|
raise UnsupportedFont
|
|
except UnsupportedFont:
|
|
fallback_font_name = font_manager.fallback_font_name()
|
|
logger.info(f"replacing unsupported font '{ttf}' by '{fallback_font_name}'")
|
|
cache = TTFontRenderer(font_manager.get_ttf_font(fallback_font_name))
|
|
self._glyph_caches[key] = cache
|
|
return cache
|
|
|
|
|
|
class _UnmanagedTrueTypeFont(_CachedFont):
|
|
font_render_type = FontRenderType.OUTLINE
|
|
|
|
def create_cache(self, ttf: str) -> Glyphs:
|
|
from .ttfonts import TTFontRenderer
|
|
from fontTools.ttLib import TTFont
|
|
|
|
key = ttf.lower()
|
|
try:
|
|
return self._glyph_caches[key]
|
|
except KeyError:
|
|
pass
|
|
cache = TTFontRenderer(TTFont(ttf, fontNumber=0))
|
|
self._glyph_caches[key] = cache
|
|
return cache
|
|
|
|
|
|
def sideload_ttf(font_path: str | os.PathLike, cap_height) -> AbstractFont:
|
|
"""This function bypasses the FontManager and loads the TrueType font straight from
|
|
the file system, requires the absolute font file path e.g. "C:/Windows/Fonts/Arial.ttf".
|
|
|
|
.. warning::
|
|
|
|
Expert feature, use with care: no fallback font and no error handling.
|
|
|
|
"""
|
|
|
|
return _UnmanagedTrueTypeFont(str(font_path), cap_height)
|
|
|
|
|
|
class ShapeFileFont(_CachedFont):
|
|
"""Represents a shapefile font (.shx, .shp). Font measurement and glyph rendering is
|
|
done by the ezdxf.fonts.shapefile module. The given cap height and width factor are
|
|
the default values for measurements and glyph rendering. The extended methods can
|
|
override these default values.
|
|
|
|
"""
|
|
|
|
font_render_type = FontRenderType.STROKE
|
|
|
|
def create_cache(self, font_name: str) -> Glyphs:
|
|
key = font_name.lower()
|
|
try:
|
|
return self._glyph_caches[key]
|
|
except KeyError:
|
|
pass
|
|
glyph_cache = font_manager.get_shapefile_glyph_cache(font_name)
|
|
self._glyph_caches[key] = glyph_cache
|
|
return glyph_cache
|
|
|
|
|
|
class LibreCadFont(_CachedFont):
|
|
"""Represents a LibreCAD font (.shx, .shp). Font measurement and glyph rendering is
|
|
done by the ezdxf.fonts.lff module. The given cap height and width factor are the
|
|
default values for measurements and glyph rendering. The extended methods can
|
|
override these default values.
|
|
|
|
"""
|
|
|
|
font_render_type = FontRenderType.STROKE
|
|
|
|
def create_cache(self, font_name: str) -> Glyphs:
|
|
key = font_name.lower()
|
|
try:
|
|
return self._glyph_caches[key]
|
|
except KeyError:
|
|
pass
|
|
glyph_cache = font_manager.get_lff_glyph_cache(font_name)
|
|
self._glyph_caches[key] = glyph_cache
|
|
return glyph_cache
|
|
|
|
|
|
def make_font(
|
|
font_name: str, cap_height: float, width_factor: float = 1.0
|
|
) -> AbstractFont:
|
|
r"""Returns a font abstraction based on class :class:`AbstractFont`.
|
|
|
|
Supported font types:
|
|
|
|
- .ttf, .ttc and .otf - TrueType fonts
|
|
- .shx, .shp - Autodesk® shapefile fonts
|
|
- .lff - LibreCAD font format
|
|
|
|
The special name "\*monospace" returns the fallback font :class:`MonospaceFont` for
|
|
testing and basic measurements.
|
|
|
|
.. note:: The font definition files are not included in `ezdxf`.
|
|
|
|
Args:
|
|
font_name: font file name as stored in the :class:`~ezdxf.entities.Textstyle`
|
|
entity e.g. "OpenSans-Regular.ttf"
|
|
cap_height: desired cap height in drawing units.
|
|
width_factor: horizontal text stretch factor
|
|
|
|
"""
|
|
if font_name == MONOSPACE:
|
|
return MonospaceFont(cap_height, width_factor)
|
|
ext = pathlib.Path(font_name).suffix.lower()
|
|
last_resort = MonospaceFont(cap_height, width_factor)
|
|
if ext in SUPPORTED_TTF_TYPES:
|
|
try:
|
|
return TrueTypeFont(font_name, cap_height, width_factor)
|
|
except FontNotFoundError as e:
|
|
logger.warning(f"no default font found: {str(e)}")
|
|
return last_resort
|
|
elif ext == ".shx" or ext == ".shp":
|
|
try:
|
|
return ShapeFileFont(font_name, cap_height, width_factor)
|
|
except FontNotFoundError:
|
|
pass
|
|
except UnsupportedFont:
|
|
# change name - the font exists but is not supported
|
|
font_name = font_manager.fallback_font_name()
|
|
elif ext == ".lff":
|
|
try:
|
|
return LibreCadFont(font_name, cap_height, width_factor)
|
|
except FontNotFoundError:
|
|
pass
|
|
elif ext == "": # e.g. "TXT"
|
|
font_face = font_manager.find_best_match(
|
|
family=font_name, style=".shx", italic=None
|
|
)
|
|
if font_face is not None:
|
|
return make_font(font_face.filename, cap_height, width_factor)
|
|
else:
|
|
logger.warning(f"unsupported font-name suffix: {font_name}")
|
|
font_name = font_manager.fallback_font_name()
|
|
|
|
# return default TrueType font
|
|
try:
|
|
return TrueTypeFont(font_name, cap_height, width_factor)
|
|
except FontNotFoundError as e:
|
|
logger.warning(f"no default font found: {str(e)}")
|
|
return last_resort
|
|
|
|
|
|
def get_entity_font_face(entity: DXFEntity, doc: Optional[Drawing] = None) -> FontFace:
|
|
"""Returns the :class:`FontFace` defined by the associated text style.
|
|
Returns the default font face if the `entity` does not have or support
|
|
the DXF attribute "style". Supports the extended font information stored in
|
|
:class:`~ezdxf.entities.Textstyle` table entries.
|
|
|
|
Pass a DXF document as argument `doc` to resolve text styles for virtual
|
|
entities which are not assigned to a DXF document. The argument `doc`
|
|
always overrides the DXF document to which the `entity` is assigned to.
|
|
|
|
"""
|
|
if entity.doc and doc is None:
|
|
doc = entity.doc
|
|
if doc is None:
|
|
return FontFace()
|
|
|
|
style_name = ""
|
|
# This works also for entities which do not support "style",
|
|
# where style_name = entity.dxf.get("style") would fail.
|
|
if entity.dxf.is_supported("style"):
|
|
style_name = entity.dxf.style
|
|
|
|
font_face = FontFace()
|
|
if style_name:
|
|
style = cast("Textstyle", doc.styles.get(style_name))
|
|
family, italic, bold = style.get_extended_font_data()
|
|
if family:
|
|
text_style = "Italic" if italic else "Regular"
|
|
text_weight = 700 if bold else 400
|
|
font_face = FontFace(family=family, style=text_style, weight=text_weight)
|
|
else:
|
|
ttf = style.dxf.font
|
|
if ttf:
|
|
font_face = get_font_face(ttf)
|
|
return font_face
|
|
|
|
|
|
load()
|