refactor: excel parse
This commit is contained in:
@@ -0,0 +1,745 @@
|
||||
# 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()
|
||||
Reference in New Issue
Block a user