summaryrefslogtreecommitdiffstats
path: root/contrib/python/fonttools
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2026-03-24 22:03:23 +0300
committerrobot-piglet <[email protected]>2026-03-24 22:34:09 +0300
commit6092233e61d1dc129fe1eb007399cc192c5ceb59 (patch)
tree90522e5b7449e5cdb06bd24eafb333b9e9d3e9f1 /contrib/python/fonttools
parentc8c3fda4b2e47ceaad9790b7a5fb192110162f15 (diff)
Intermediate changes
commit_hash:5e2a2254279501ad2bde571fbd53c1a27a00e898
Diffstat (limited to 'contrib/python/fonttools')
-rw-r--r--contrib/python/fonttools/.dist-info/METADATA53
-rw-r--r--contrib/python/fonttools/README.rst2
-rw-r--r--contrib/python/fonttools/fontTools/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/cffLib/__init__.py2
-rw-r--r--contrib/python/fonttools/fontTools/colorLib/builder.py20
-rw-r--r--contrib/python/fonttools/fontTools/cu2qu/cu2qu.py20
-rw-r--r--contrib/python/fonttools/fontTools/cu2qu/ufo.py36
-rw-r--r--contrib/python/fonttools/fontTools/designspaceLib/__init__.py15
-rw-r--r--contrib/python/fonttools/fontTools/diff/__init__.py441
-rw-r--r--contrib/python/fonttools/fontTools/diff/__main__.py6
-rw-r--r--contrib/python/fonttools/fontTools/diff/color.py44
-rw-r--r--contrib/python/fonttools/fontTools/diff/diff.py294
-rw-r--r--contrib/python/fonttools/fontTools/diff/utils.py28
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/builder.py52
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/parser.py3
-rw-r--r--contrib/python/fonttools/fontTools/feaLib/variableScalar.py303
-rw-r--r--contrib/python/fonttools/fontTools/pens/pointPen.py12
-rw-r--r--contrib/python/fonttools/fontTools/qu2cu/cli.py6
-rw-r--r--contrib/python/fonttools/fontTools/qu2cu/qu2cu.py30
-rw-r--r--contrib/python/fonttools/fontTools/svgLib/path/shapes.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/D_S_I_G_.py38
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py36
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py148
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/G_P_K_G_.py133
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py352
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py99
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py12
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/__init__.py4
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py10
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py9
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/_t_r_a_k.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py2
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/tables/ttProgram.py16
-rw-r--r--contrib/python/fonttools/fontTools/ttLib/ttFont.py26
-rw-r--r--contrib/python/fonttools/fontTools/ufoLib/glifLib.py13
-rw-r--r--contrib/python/fonttools/fontTools/ufoLib/validators.py13
-rw-r--r--contrib/python/fonttools/fontTools/varLib/__init__.py26
-rw-r--r--contrib/python/fonttools/fontTools/varLib/avar/build.py4
-rw-r--r--contrib/python/fonttools/fontTools/varLib/avar/map.py36
-rw-r--r--contrib/python/fonttools/fontTools/varLib/avar/unbuild.py7
-rw-r--r--contrib/python/fonttools/fontTools/varLib/instancer/__init__.py5
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatable.py20
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py45
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py19
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py25
-rw-r--r--contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py6
-rw-r--r--contrib/python/fonttools/fontTools/varLib/models.py16
-rw-r--r--contrib/python/fonttools/ya.make11
49 files changed, 1426 insertions, 1080 deletions
diff --git a/contrib/python/fonttools/.dist-info/METADATA b/contrib/python/fonttools/.dist-info/METADATA
index fcaae0d0409..c3a12aacdd4 100644
--- a/contrib/python/fonttools/.dist-info/METADATA
+++ b/contrib/python/fonttools/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.4
Name: fonttools
-Version: 4.61.1
+Version: 4.62.0
Summary: Tools to manipulate font files
Home-page: http://github.com/fonttools/fonttools
Author: Just van Rossum
@@ -258,7 +258,7 @@ are required to unlock the extra features named "ufo", etc.
Simplify TrueType glyphs by merging overlapping contours and components.
- * `skia-pathops <https://pypi.python.org/pypy/skia-pathops>`__: Python
+ * `skia-pathops <https://pypi.python.org/pypi/skia-pathops>`__: Python
bindings for the Skia library's PathOps module, performing boolean
operations on paths (union, intersection, etc.).
@@ -392,6 +392,55 @@ Have fun!
Changelog
~~~~~~~~~
+4.62.0 (released 2026-03-09)
+----------------------------
+
+- [diff] Add new ``fonttools diff`` command for comparing font files, imported from the
+ ``fdiff`` project and heavily reworked (#1190, #4007, #4009, #4011, #4013, #4019).
+- [feaLib] Fix ``VariableScalar`` interpolation bug with non-linear avar mappings. Also
+ decouple ``VariableScalar`` from compiled fonts, allowing it to work with designspace data
+ before compilation (#3938, #4054).
+- [feaLib] Fix ``VariableScalar`` axis ordering and iterative delta rounding to match fontc
+ behavior (#4053).
+- [feaLib] Merge chained multi subst rules with same context into a single subtable instead of
+ emitting one subtable per glyph (#4016, #4058).
+- [feaLib] Pass location to ``ConditionsetStatement`` to fix glyphsLib round-tripping
+ (fontra/fontra-glyphs#130, #4057).
+- [feaLib] Write ``0xFFFF`` instead of ``0`` for missing nameIDs in ``cv`` feature params
+ (#4010, #4012).
+- [cmap] Fix ``CmapSubtable.__lt__()`` ``TypeError`` on Python 3 when subtables share the
+ same encoding record, and add compile-time validation for unique encoding records (#4035,
+ #4055).
+- [svgLib] Skip non-element XML nodes (comments, processing instructions) when drawing SVG
+ paths (#4042, #4043).
+- [glifLib] Fix regression reading glyph outlines when ``glyphObject=None`` (#4030, #4031).
+- [pointPen] Fix ``SegmentToPointPen`` edge case: only remove a duplicate final point on
+ ``closePath()`` if it is an on-curve point (#4014, #4015).
+- [cffLib] **SECURITY** Replace ``eval()`` with ``safeEval()`` in ``parseBlendList()`` to
+ prevent arbitrary code execution from crafted TTX files (#4039, #4040).
+- [ttLib] Remove defunct Adobe SING Glyphlet tables (``META``, ``SING``, ``GMAP``, ``GPKG``)
+ (#4044).
+- [varLib.interpolatable] Various bugfixes: fix swapped nodeTypes assignment, duplicate
+ kink-detector condition, typos, CFF2 vsindex parsing, glyph existence check, and plot
+ helpers (#4046).
+- [varLib.models] Fix ``getSubModel`` not forwarding ``extrapolate``/``axisRanges``; check
+ location uniqueness after stripping zeros (#4047).
+- [varLib] Fix ``--variable-fonts`` filter in ``build_many``; remove dead code and fix
+ comments (#4048).
+- [avar] Preserve existing name table in build; keep ``unbuild`` return types consistent;
+ validate ``map`` CLI coordinates (#4051).
+- [cu2qu/qu2cu] Add input validation: reject non-positive tolerances, validate curve inputs
+ and list lengths (#4052).
+- [colorLib] Raise a clear ``ColorLibError`` when base glyphs are missing from glyphMap,
+ instead of a confusing ``KeyError`` (#4041).
+- [glyf] Remove unnecessary ``fvar`` table dependency (#4017).
+- [fvar/trak] Remove unnecessary ``name`` table dependency (#4018).
+- [ufoLib] Relax guideline validation to follow the updated spec (#3537, #3553).
+- [ttFont] Fix ``saveXML`` regression with empty table lists, clarify docstring (#4025, #4026,
+ #4056).
+- [setup.py] Link ``libm`` for Cython extensions using math functions (#4028, #4029).
+- Add typing annotations for ``DSIG``, ``DefaultTable``, ``ttProgram`` (#4033).
+
4.61.1 (released 2025-12-12)
----------------------------
diff --git a/contrib/python/fonttools/README.rst b/contrib/python/fonttools/README.rst
index 2c0af437467..b1391d392f1 100644
--- a/contrib/python/fonttools/README.rst
+++ b/contrib/python/fonttools/README.rst
@@ -171,7 +171,7 @@ are required to unlock the extra features named "ufo", etc.
Simplify TrueType glyphs by merging overlapping contours and components.
- * `skia-pathops <https://pypi.python.org/pypy/skia-pathops>`__: Python
+ * `skia-pathops <https://pypi.python.org/pypi/skia-pathops>`__: Python
bindings for the Skia library's PathOps module, performing boolean
operations on paths (union, intersection, etc.).
diff --git a/contrib/python/fonttools/fontTools/__init__.py b/contrib/python/fonttools/fontTools/__init__.py
index 953cda045ee..c5eb5eaca3f 100644
--- a/contrib/python/fonttools/fontTools/__init__.py
+++ b/contrib/python/fonttools/fontTools/__init__.py
@@ -3,6 +3,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
-version = __version__ = "4.61.1"
+version = __version__ = "4.62.0"
__all__ = ["version", "log", "configLogger"]
diff --git a/contrib/python/fonttools/fontTools/cffLib/__init__.py b/contrib/python/fonttools/fontTools/cffLib/__init__.py
index 4ad724a27a8..8ced9c79fc2 100644
--- a/contrib/python/fonttools/fontTools/cffLib/__init__.py
+++ b/contrib/python/fonttools/fontTools/cffLib/__init__.py
@@ -1264,7 +1264,7 @@ def parseBlendList(s):
continue
name, attrs, content = element
blendList = attrs["value"].split()
- blendList = [eval(val) for val in blendList]
+ blendList = [safeEval(val) for val in blendList]
valueList.append(blendList)
if len(valueList) == 1:
valueList = valueList[0]
diff --git a/contrib/python/fonttools/fontTools/colorLib/builder.py b/contrib/python/fonttools/fontTools/colorLib/builder.py
index 6e45e7a8850..8ec90170771 100644
--- a/contrib/python/fonttools/fontTools/colorLib/builder.py
+++ b/contrib/python/fonttools/fontTools/colorLib/builder.py
@@ -154,6 +154,7 @@ def populateCOLRv0(
``TTFont.getReverseGlyphMap()``, to optionally sort base records by GID.
"""
if glyphMap is not None:
+ _check_base_glyphs_exist(colorGlyphsV0, glyphMap, "populateCOLRv0")
colorGlyphItems = sorted(
colorGlyphsV0.items(), key=lambda item: glyphMap[item[0]]
)
@@ -621,6 +622,24 @@ def buildBaseGlyphPaintRecord(
return self
+def _check_base_glyphs_exist(colorGlyphs, glyphMap, where):
+ """Checks that every base glyph name in colorGlyphs exists in glyphMap."""
+
+ missing = []
+ for baseGlyph in colorGlyphs.keys():
+ if baseGlyph not in glyphMap:
+ missing.append(baseGlyph)
+
+ if missing:
+ preview = ", ".join(missing[:10])
+ extra = ""
+ if len(missing) > 10:
+ extra = f" (and {len(missing) - 10} more)"
+ raise ColorLibError(
+ f"{where}: base glyph(s) not found in glyphMap: {preview}{extra}"
+ )
+
+
def _format_glyph_errors(errors: Mapping[str, Exception]) -> str:
lines = []
for baseGlyph, error in sorted(errors.items()):
@@ -635,6 +654,7 @@ def buildColrV1(
allowLayerReuse: bool = True,
) -> Tuple[Optional[ot.LayerList], ot.BaseGlyphList]:
if glyphMap is not None:
+ _check_base_glyphs_exist(colorGlyphs, glyphMap, "buildColrV1")
colorGlyphItems = sorted(
colorGlyphs.items(), key=lambda item: glyphMap[item[0]]
)
diff --git a/contrib/python/fonttools/fontTools/cu2qu/cu2qu.py b/contrib/python/fonttools/fontTools/cu2qu/cu2qu.py
index 150c03fb4a0..575bbc7b451 100644
--- a/contrib/python/fonttools/fontTools/cu2qu/cu2qu.py
+++ b/contrib/python/fonttools/fontTools/cu2qu/cu2qu.py
@@ -480,13 +480,15 @@ def curve_to_quadratic(curve, max_err, all_quadratic=True):
curve or a single cubic curve.
Returns:
- If all_quadratic is True: A list of 2D tuples, representing
- control points of the quadratic spline if it fits within the
- given tolerance, or ``None`` if no suitable spline could be
- calculated.
+ If all_quadratic is True: A list of 2D tuples representing
+ control points of the quadratic spline.
If all_quadratic is False: Either a quadratic curve (if length
of output is 3), or a cubic curve (if length of output is 4).
+
+ Raises:
+ fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable
+ approximation can be found with the given parameters.
"""
curve = [complex(*p) for p in curve]
@@ -529,18 +531,22 @@ def curves_to_quadratic(curves, max_errors, all_quadratic=True):
Returns:
If all_quadratic is True, a list of splines, each spline being a list
- of 2D tuples.
+ of 2D tuples. If ``curves`` is empty, returns an empty list.
If all_quadratic is False, a list of curves, each curve being a quadratic
(length 3), or cubic (length 4).
Raises:
- fontTools.cu2qu.Errors.ApproxNotFoundError: if no suitable approximation
+ ValueError: if ``max_errors`` does not match the number of curves.
+ fontTools.cu2qu.errors.ApproxNotFoundError: if no suitable approximation
can be found for all curves with the given parameters.
"""
curves = [[complex(*p) for p in curve] for curve in curves]
- assert len(max_errors) == len(curves)
+ if len(max_errors) != len(curves):
+ raise ValueError("max_errors must match the number of curves")
+ if not curves:
+ return []
l = len(curves)
splines = [None] * l
diff --git a/contrib/python/fonttools/fontTools/cu2qu/ufo.py b/contrib/python/fonttools/fontTools/cu2qu/ufo.py
index db9a1b0384b..6b114d73d7f 100644
--- a/contrib/python/fonttools/fontTools/cu2qu/ufo.py
+++ b/contrib/python/fonttools/fontTools/cu2qu/ufo.py
@@ -62,6 +62,21 @@ def zip(*args):
return list(_zip(*args))
+def _validate_positive_tolerance(value, name):
+ if value <= 0:
+ raise ValueError(f"{name} must be greater than zero")
+
+
+def _validate_positive_tolerances(values, name):
+ for value in values:
+ _validate_positive_tolerance(value, name)
+
+
+def _validate_length(values, expected, name):
+ if len(values) != expected:
+ raise ValueError(f"{name} must match the number of inputs")
+
+
class GetSegmentsPen(AbstractPen):
"""Pen to collect segments into lists of points for conversion.
@@ -231,7 +246,7 @@ def glyphs_to_quadratic(
if stats is None:
stats = {}
- if not max_err:
+ if max_err is None:
# assume 1000 is the default UPEM
max_err = DEFAULT_MAX_ERR * 1000
@@ -239,7 +254,8 @@ def glyphs_to_quadratic(
max_errors = max_err
else:
max_errors = [max_err] * len(glyphs)
- assert len(max_errors) == len(glyphs)
+ _validate_length(max_errors, len(glyphs), "max_err")
+ _validate_positive_tolerances(max_errors, "max_err")
return _glyphs_to_quadratic(
glyphs, max_errors, reverse_direction, stats, all_quadratic
@@ -293,21 +309,25 @@ def fonts_to_quadratic(
if stats is None:
stats = {}
- if max_err_em and max_err:
+ if max_err_em is not None and max_err is not None:
raise TypeError("Only one of max_err and max_err_em can be specified.")
- if not (max_err_em or max_err):
+ if max_err_em is None and max_err is None:
max_err_em = DEFAULT_MAX_ERR
if isinstance(max_err, (list, tuple)):
- assert len(max_err) == len(fonts)
+ _validate_length(max_err, len(fonts), "max_err")
max_errors = max_err
- elif max_err:
+ _validate_positive_tolerances(max_errors, "max_err")
+ elif max_err is not None:
+ _validate_positive_tolerance(max_err, "max_err")
max_errors = [max_err] * len(fonts)
if isinstance(max_err_em, (list, tuple)):
- assert len(fonts) == len(max_err_em)
+ _validate_length(max_err_em, len(fonts), "max_err_em")
+ _validate_positive_tolerances(max_err_em, "max_err_em")
max_errors = [f.info.unitsPerEm * e for f, e in zip(fonts, max_err_em)]
- elif max_err_em:
+ elif max_err_em is not None:
+ _validate_positive_tolerance(max_err_em, "max_err_em")
max_errors = [f.info.unitsPerEm * max_err_em for f in fonts]
modified = set()
diff --git a/contrib/python/fonttools/fontTools/designspaceLib/__init__.py b/contrib/python/fonttools/fontTools/designspaceLib/__init__.py
index 4782041b43a..6f4bfdb9591 100644
--- a/contrib/python/fonttools/fontTools/designspaceLib/__init__.py
+++ b/contrib/python/fonttools/fontTools/designspaceLib/__init__.py
@@ -878,16 +878,19 @@ def tagForAxisName(name):
class AbstractAxisDescriptor(SimpleDescriptor):
+ name: str | None
+ map: list[tuple[float, float]]
+
flavor = "axis"
def __init__(
self,
*,
tag=None,
- name=None,
+ name: str | None = None,
labelNames=None,
hidden=False,
- map=None,
+ map: list[tuple[float, float]] | None = None,
axisOrdering=None,
axisLabels=None,
):
@@ -977,13 +980,13 @@ class AxisDescriptor(AbstractAxisDescriptor):
self,
*,
tag=None,
- name=None,
+ name: str | None = None,
labelNames=None,
minimum=None,
default=None,
maximum=None,
hidden=False,
- map=None,
+ map: list[tuple[float, float]] | None = None,
axisOrdering=None,
axisLabels=None,
):
@@ -1084,12 +1087,12 @@ class DiscreteAxisDescriptor(AbstractAxisDescriptor):
self,
*,
tag=None,
- name=None,
+ name: str | None = None,
labelNames=None,
values=None,
default=None,
hidden=False,
- map=None,
+ map: list[tuple[float, float]] | None = None,
axisOrdering=None,
axisLabels=None,
):
diff --git a/contrib/python/fonttools/fontTools/diff/__init__.py b/contrib/python/fonttools/fontTools/diff/__init__.py
new file mode 100644
index 00000000000..b876520b2a6
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/__init__.py
@@ -0,0 +1,441 @@
+import argparse
+import os
+import sys
+import shutil
+import subprocess
+from typing import Iterable, Iterator, List, Optional, Text, Tuple
+
+from .color import color_unified_diff_line
+from .diff import run_external_diff, u_diff
+from .utils import file_exists, get_tables_argument_list
+
+
+def pipe_output(output: str) -> None:
+ """Pipes output to a pager if stdout is a TTY and a pager is available."""
+
+ if not output:
+ return
+
+ if not sys.stdout.isatty():
+ sys.stdout.write(output)
+ return
+
+ pager = os.getenv("PAGER") or shutil.which("less")
+
+ if not pager:
+ sys.stdout.write(output)
+ return
+
+ pager_cmd = [pager]
+ if "less" in os.path.basename(pager):
+ pager_cmd.append("-R")
+
+ proc = subprocess.Popen(pager_cmd, stdin=subprocess.PIPE, text=True)
+ try:
+ proc.stdin.write(output)
+ proc.stdin.close()
+ proc.wait()
+ except (BrokenPipeError, KeyboardInterrupt):
+ # Pager process was terminated before all output was written.
+ # This is not an error. The main exception handler will deal with it.
+ if proc.stdin:
+ proc.stdin.close()
+ # The process might still be running, but we have closed our side of the
+ # pipe. The Popen destructor will send a SIGKILL to the child.
+ except Exception:
+ if proc.stdin:
+ proc.stdin.close()
+ raise
+
+
+def _is_gnu_diff(diff_tool: str) -> bool:
+ """Returns True if the provided diff executable is GNU diff."""
+ try:
+ proc = subprocess.run(
+ [diff_tool, "--version"],
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ text=True,
+ )
+ except OSError:
+ return False
+
+ version_output = (proc.stdout or "") + (proc.stderr or "")
+ return "GNU diffutils" in version_output
+
+
+def _iter_filtered_table_tags(
+ tags: Iterable[str],
+ include_tables: Optional[List[str]] = None,
+ exclude_tables: Optional[List[str]] = None,
+) -> Iterator[str]:
+ for tag in tags:
+ if exclude_tables and tag in exclude_tables:
+ continue
+ if include_tables and tag not in include_tables:
+ continue
+ yield tag
+
+
+def summarize(
+ file1: str,
+ file2: str,
+ include_tables: Optional[List[str]] = None,
+ exclude_tables: Optional[List[str]] = None,
+ font_number_1: int = -1,
+ font_number_2: int = -1,
+) -> Tuple[bool, str]:
+ from fontTools.ttLib import TTFont
+
+ with (
+ TTFont(file1, lazy=True, fontNumber=font_number_1) as font1,
+ TTFont(file2, lazy=True, fontNumber=font_number_2) as font2,
+ ):
+ tags1 = {str(tag) for tag in font1.reader.keys()}
+ tags2 = {str(tag) for tag in font2.reader.keys()}
+
+ all_tags = sorted(
+ set(
+ _iter_filtered_table_tags(
+ tags1 | tags2,
+ include_tables=include_tables,
+ exclude_tables=exclude_tables,
+ )
+ )
+ )
+
+ only1 = [tag for tag in all_tags if tag in tags1 and tag not in tags2]
+ only2 = [tag for tag in all_tags if tag in tags2 and tag not in tags1]
+ both = [tag for tag in all_tags if tag in tags1 and tag in tags2]
+
+ identical = True
+ lines: List[str] = []
+
+ lines.append(f"Binary table summary:\n")
+ lines.append(f" file1: {file1}\n")
+ lines.append(f" file2: {file2}\n")
+
+ if only1:
+ identical = False
+ lines.append(f"\nTables only in file1 ({len(only1)}):\n")
+ for tag in only1:
+ lines.append(f"- {tag} ({len(font1.reader[tag])} bytes)\n")
+ if only2:
+ identical = False
+ lines.append(f"\nTables only in file2 ({len(only2)}):\n")
+ for tag in only2:
+ lines.append(f"+ {tag} ({len(font2.reader[tag])} bytes)\n")
+
+ lines.append(f"\nTables in both ({len(both)}):\n")
+ for tag in both:
+ data1 = font1.reader[tag]
+ data2 = font2.reader[tag]
+ if data1 == data2:
+ lines.append(f" {tag}: SAME ({len(data1)} bytes)\n")
+ else:
+ identical = False
+ lines.append(f"* {tag}: DIFF ({len(data1)} vs {len(data2)} bytes)\n")
+
+ if identical:
+ lines.append("\nResult: SAME\n")
+ else:
+ lines.append("\nResult: DIFFERENT\n")
+
+ return identical, "".join(lines)
+
+
+def get_binary_exclude_tables(
+ file1: str,
+ file2: str,
+ include_tables: Optional[List[str]] = None,
+ exclude_tables: Optional[List[str]] = None,
+ font_number_1: int = -1,
+ font_number_2: int = -1,
+) -> Tuple[bool, str]:
+ from fontTools.ttLib import TTFont
+
+ with (
+ TTFont(file1, lazy=True, fontNumber=font_number_1) as font1,
+ TTFont(file2, lazy=True, fontNumber=font_number_2) as font2,
+ ):
+ tags1 = {str(tag) for tag in font1.reader.keys()}
+ tags2 = {str(tag) for tag in font2.reader.keys()}
+
+ all_tags = sorted(
+ set(
+ _iter_filtered_table_tags(
+ tags1 | tags2,
+ include_tables=include_tables,
+ exclude_tables=exclude_tables,
+ )
+ )
+ )
+
+ both = [tag for tag in all_tags if tag in tags1 and tag in tags2]
+ out = set()
+
+ for tag in both:
+ data1 = font1.reader[tag]
+ data2 = font2.reader[tag]
+ if data1 == data2:
+ out.add(tag)
+
+ return out
+
+
+def main():
+ """Compare two fonts for differences"""
+ # try/except block rationale:
+ # handles "premature" socket closure exception that is
+ # raised by Python when stdout is piped to tools like
+ # the `head` executable and socket is closed early
+ # see: https://docs.python.org/3/library/signal.html#note-on-sigpipe
+ ret = 0
+ try:
+ ret = run(sys.argv[1:])
+ except KeyboardInterrupt:
+ pass
+ except BrokenPipeError:
+ # Python flushes standard streams on exit; redirect remaining output
+ # to devnull to avoid another BrokenPipeError at shutdown
+ devnull = os.open(os.devnull, os.O_WRONLY)
+ os.dup2(devnull, sys.stdout.fileno())
+ return ret
+
+
+def run(argv: List[Text]):
+ # ------------------------------------------
+ # argparse command line argument definitions
+ # ------------------------------------------
+ parser = argparse.ArgumentParser(
+ description="An OpenType table diff tool for fonts."
+ )
+ parser.add_argument(
+ "-l",
+ "--summary",
+ action="store_true",
+ help="Report table presence and binary equality only",
+ )
+ parser.add_argument(
+ "-U",
+ "--lines",
+ type=int,
+ default=3,
+ help="Number of context lines for unified diff (default: 3)",
+ )
+ parser.add_argument(
+ "-t",
+ "--include",
+ type=str,
+ nargs="+",
+ default=None,
+ help="Font tables to include. Multiple options are allowed.",
+ )
+ parser.add_argument(
+ "-x",
+ "--exclude",
+ type=str,
+ nargs="+",
+ default=None,
+ help="Font tables to exclude. Multiple options are allowed.",
+ )
+ parser.add_argument(
+ "--diff", type=str, help="Run external diff tool command (default: diff)"
+ )
+ parser.add_argument(
+ "--diff-arg",
+ type=str,
+ default=None,
+ help="External diff tool arguments (default: -u)",
+ )
+ parser.add_argument(
+ "--color",
+ choices=["auto", "never", "always"],
+ default="auto",
+ help="Whether to colorize output (default: auto)",
+ )
+ parser.add_argument(
+ "--y1",
+ type=int,
+ default=-1,
+ metavar="NUMBER",
+ help="Select font number for TrueType Collection (.ttc/.otc) FILE1, starting from 0",
+ )
+ parser.add_argument(
+ "--y2",
+ type=int,
+ default=-1,
+ metavar="NUMBER",
+ help="Select font number for TrueType Collection (.ttc/.otc) FILE2, starting from 0",
+ )
+ parser.add_argument(
+ "-a",
+ "--always",
+ action="store_true",
+ help="Compare tables even if binary identical",
+ )
+ parser.add_argument(
+ "-b",
+ "--binary",
+ action="store_true",
+ help="Compare tables only if binaries differ (default)",
+ )
+ parser.add_argument(
+ "-q", "--quiet", action="store_true", help="Suppress all output"
+ )
+ parser.add_argument("FILE1", help="Font file path 1")
+ parser.add_argument("FILE2", help="Font file path 2")
+
+ args: argparse.Namespace = parser.parse_args(argv)
+
+ # /////////////////////////////////////////////////////////
+ #
+ # Validations
+ #
+ # /////////////////////////////////////////////////////////
+
+ # ----------------------------------
+ # Incompatible argument validations
+ # ----------------------------------
+
+ if args.always and args.binary:
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] Error: --always and --binary are mutually exclusive options. "
+ f"Please use ONLY one of these options in your command.{os.linesep}"
+ )
+ return 2
+ if not args.always:
+ args.binary = True
+
+ # -------------------------------
+ # File path argument validations
+ # -------------------------------
+
+ if not file_exists(args.FILE1):
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] ERROR: The file path '{args.FILE1}' can not be found.{os.linesep}"
+ )
+ return 2
+ if not file_exists(args.FILE2):
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] ERROR: The file path '{args.FILE2}' can not be found.{os.linesep}"
+ )
+ return 2
+
+ # /////////////////////////////////////////////////////////
+ #
+ # Command line logic
+ #
+ # /////////////////////////////////////////////////////////
+
+ # parse explicitly included or excluded tables in
+ # the command line arguments
+ # set as a Python list if it was defined on the command line
+ # or as None if it was not set on the command line
+ include_list: Optional[List[Text]] = get_tables_argument_list(args.include)
+ exclude_list: Optional[List[Text]] = get_tables_argument_list(args.exclude)
+
+ if args.summary:
+ try:
+ identical, output = summarize(
+ args.FILE1,
+ args.FILE2,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_1=args.y1,
+ font_number_2=args.y2,
+ )
+ if not args.quiet:
+ sys.stdout.write(output)
+ return 0 if identical else 1
+ except Exception as e:
+ if not args.quiet:
+ sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
+ return 2
+
+ if args.binary:
+ excluded_binary_tables = get_binary_exclude_tables(
+ args.FILE1,
+ args.FILE2,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_1=args.y1,
+ font_number_2=args.y2,
+ )
+ if include_list is not None:
+ include_list = [
+ tag for tag in include_list if tag not in excluded_binary_tables
+ ]
+ else:
+ if exclude_list is None:
+ exclude_list = []
+ exclude_list.extend(sorted(excluded_binary_tables))
+
+ diff_tool = args.diff
+ color_output = args.color == "always" or (
+ args.color == "auto" and sys.stdout.isatty
+ )
+
+ if diff_tool is None:
+ diff_tool = shutil.which("diff")
+ elif diff_tool:
+ diff_tool = shutil.which(diff_tool)
+ if diff_tool is None:
+ if not args.quiet:
+ sys.stderr.write(
+ f"[*] ERROR: The external diff tool executable "
+ f"'{args.diff}' was not found.{os.linesep}"
+ )
+ return 2
+
+ try:
+ if diff_tool:
+ diff_arg = args.diff_arg
+ if diff_arg is None:
+ if args.lines == 3:
+ diff_arg = ["-u"]
+ else:
+ diff_arg = ["-u{}".format(args.lines)]
+ if _is_gnu_diff(diff_tool):
+ diff_arg.append(r"-F^\s\s<")
+ else:
+ diff_arg = diff_arg.split()
+
+ output = run_external_diff(
+ diff_tool,
+ diff_arg,
+ args.FILE1,
+ args.FILE2,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_a=args.y1,
+ font_number_b=args.y2,
+ use_multiprocess=True,
+ )
+ else:
+ output = u_diff(
+ args.FILE1,
+ args.FILE2,
+ context_lines=args.lines,
+ include_tables=include_list,
+ exclude_tables=exclude_list,
+ font_number_a=args.y1,
+ font_number_b=args.y2,
+ use_multiprocess=True,
+ )
+
+ if color_output:
+ output = [color_unified_diff_line(line) for line in output]
+
+ output = "".join(output)
+ if not args.quiet:
+ pipe_output(output)
+ return 1 if output else 0
+
+ except Exception as e:
+ if not args.quiet:
+ sys.stderr.write(f"[*] ERROR: {e}{os.linesep}")
+ return 2
diff --git a/contrib/python/fonttools/fontTools/diff/__main__.py b/contrib/python/fonttools/fontTools/diff/__main__.py
new file mode 100644
index 00000000000..8dbef1dfc5d
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/__main__.py
@@ -0,0 +1,6 @@
+import sys
+from fontTools.diff import main
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/contrib/python/fonttools/fontTools/diff/color.py b/contrib/python/fonttools/fontTools/diff/color.py
new file mode 100644
index 00000000000..e6ef489d67e
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/color.py
@@ -0,0 +1,44 @@
+from typing import Dict, Text
+
+ansicolors: Dict[Text, Text] = {
+ "BLACK": "\033[30m",
+ "RED": "\033[31m",
+ "GREEN": "\033[32m",
+ "YELLOW": "\033[33m",
+ "BLUE": "\033[34m",
+ "MAGENTA": "\033[35m",
+ "CYAN": "\033[36m",
+ "WHITE": "\033[37m",
+ "BOLD": "\033[1m",
+ "RESET": "\033[0m",
+}
+
+green_start: Text = ansicolors["GREEN"]
+red_start: Text = ansicolors["RED"]
+cyan_start: Text = ansicolors["CYAN"]
+reset: Text = ansicolors["RESET"]
+
+
+def color_unified_diff_line(line: Text) -> Text:
+ """Returns an ANSI escape code colored string with color based
+ on the unified diff line type."""
+ if line[0:2] == "+ ":
+ return f"{green_start}{line}{reset}"
+ elif line == "+\n":
+ # some lines are formatted as hyphen only with no other characters
+ # this indicates an added empty line
+ return f"{green_start}{line}{reset}"
+ elif line[0:2] == "- ":
+ return f"{red_start}{line}{reset}"
+ elif line == "-\n":
+ # some lines are formatted as hyphen only with no other characters
+ # this indicates a deleted empty line
+ return f"{red_start}{line}{reset}"
+ elif line[0:3] == "@@ ":
+ return f"{cyan_start}{line}{reset}"
+ elif line[0:4] == "--- ":
+ return f"{red_start}{line}{reset}"
+ elif line[0:4] == "+++ ":
+ return f"{green_start}{line}{reset}"
+ else:
+ return line
diff --git a/contrib/python/fonttools/fontTools/diff/diff.py b/contrib/python/fonttools/fontTools/diff/diff.py
new file mode 100644
index 00000000000..302f2e677ae
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/diff.py
@@ -0,0 +1,294 @@
+import os
+import subprocess
+import tempfile
+from contextlib import contextmanager
+from difflib import unified_diff
+from multiprocessing import Pool, cpu_count
+from typing import Any, Callable, Iterable, Iterator, List, Optional, Text, Tuple
+
+from fontTools.ttLib import TTFont # type: ignore
+
+from .utils import get_file_modtime
+
+#
+#
+# Private functions
+#
+#
+
+
+def _get_fonts_and_save_xml(
+ filepath_a: Text,
+ filepath_b: Text,
+ tmpdirpath: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+ font_number_a: int,
+ font_number_b: int,
+ use_multiprocess: bool,
+) -> Tuple[Text, Text, Text, Text, Text, Text]:
+ post_pathname, postpath, pre_pathname, prepath = _get_pre_post_paths(
+ filepath_a, filepath_b
+ )
+ # instantiate left and right fontTools.ttLib.TTFont objects
+ tt_left = TTFont(prepath, fontNumber=font_number_a)
+ tt_right = TTFont(postpath, fontNumber=font_number_b)
+ left_ttxpath = os.path.join(tmpdirpath, "left.ttx")
+ right_ttxpath = os.path.join(tmpdirpath, "right.ttx")
+ _mp_save_ttx_xml(
+ tt_left,
+ tt_right,
+ left_ttxpath,
+ right_ttxpath,
+ exclude_tables,
+ include_tables,
+ use_multiprocess,
+ )
+ return left_ttxpath, right_ttxpath, pre_pathname, prepath, post_pathname, postpath
+
+
+def _get_pre_post_paths(
+ filepath_a: Text,
+ filepath_b: Text,
+) -> Tuple[Text, Text, Text, Text]:
+ prepath = filepath_a
+ postpath = filepath_b
+ pre_pathname = filepath_a
+ post_pathname = filepath_b
+ return post_pathname, postpath, pre_pathname, prepath
+
+
+def _mp_save_ttx_xml(
+ tt_left: Any,
+ tt_right: Any,
+ left_ttxpath: Text,
+ right_ttxpath: Text,
+ exclude_tables: Optional[List[Text]],
+ include_tables: Optional[List[Text]],
+ use_multiprocess: bool,
+) -> None:
+ if use_multiprocess and cpu_count() > 1:
+ # Use parallel fontTools.ttLib.TTFont.saveXML dump
+ # by default on multi CPU systems. This is a performance
+ # optimization. Profiling demonstrates that this can reduce
+ # execution time by up to 30% for some fonts
+ mp_args_list = [
+ (tt_left, left_ttxpath, include_tables, exclude_tables),
+ (tt_right, right_ttxpath, include_tables, exclude_tables),
+ ]
+ with Pool(processes=2) as pool:
+ pool.starmap(_ttfont_save_xml, mp_args_list)
+ else:
+ # use sequential fontTools.ttLib.TTFont.saveXML dumps
+ # when use_multiprocess is False or single CPU system
+ # detected
+ _ttfont_save_xml(tt_left, left_ttxpath, include_tables, exclude_tables)
+ _ttfont_save_xml(tt_right, right_ttxpath, include_tables, exclude_tables)
+
+
+def _ttfont_save_xml(
+ ttf: Any,
+ filepath: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+) -> bool:
+ """Writes TTX specification formatted XML to disk on filepath."""
+ ttf.saveXML(filepath, tables=include_tables, skipTables=exclude_tables)
+ return True
+
+
+@contextmanager
+def _saved_ttx_files(
+ filepath_a: Text,
+ filepath_b: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+ font_number_a: int,
+ font_number_b: int,
+ use_multiprocess: bool,
+) -> Iterator[Tuple[Text, Text, Text, Text, Text, Text]]:
+ with tempfile.TemporaryDirectory() as tmpdirpath:
+ yield _get_fonts_and_save_xml(
+ filepath_a,
+ filepath_b,
+ tmpdirpath,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ )
+
+
+def _diff_with_saved_ttx_files(
+ filepath_a: Text,
+ filepath_b: Text,
+ include_tables: Optional[List[Text]],
+ exclude_tables: Optional[List[Text]],
+ font_number_a: int,
+ font_number_b: int,
+ use_multiprocess: bool,
+ create_differ: Callable[[Text, Text, Text, Text, Text, Text], Iterable[Text]],
+) -> Iterator[Text]:
+ with _saved_ttx_files(
+ filepath_a,
+ filepath_b,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ ) as (
+ left_ttxpath,
+ right_ttxpath,
+ pre_pathname,
+ prepath,
+ post_pathname,
+ postpath,
+ ):
+ yield from create_differ(
+ left_ttxpath,
+ right_ttxpath,
+ pre_pathname,
+ prepath,
+ post_pathname,
+ postpath,
+ )
+
+
+#
+#
+# Public functions
+#
+#
+
+
+def u_diff(
+ filepath_a: Text,
+ filepath_b: Text,
+ context_lines: int = 3,
+ include_tables: Optional[List[Text]] = None,
+ exclude_tables: Optional[List[Text]] = None,
+ font_number_a: int = -1,
+ font_number_b: int = -1,
+ use_multiprocess: bool = True,
+) -> Iterator[Text]:
+ """Performs a unified diff on a TTX serialized data format dump of font binary data using
+ a modified version of the Python standard libary difflib module.
+
+ filepath_a: (string) pre-file local file path
+ filepath_b: (string) post-file local file path
+ context_lines: (int) number of context lines to include in the diff (default=3)
+ include_tables: (list of str) Python list of OpenType tables to include in the diff
+ exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
+ use_multiprocess: (bool) use multi-processor optimizations (default=True)
+
+ include_tables and exclude_tables are mutually exclusive arguments. Only one should
+ be defined
+
+ :returns: Generator of ordered diff line strings that include newline line endings
+ :raises: KeyError if include_tables or exclude_tables includes a mis-specified table
+ that is not included in filepath_a OR filepath_b
+ """
+
+ def _create_unified_diff(
+ left_ttxpath: Text,
+ right_ttxpath: Text,
+ pre_pathname: Text,
+ prepath: Text,
+ post_pathname: Text,
+ postpath: Text,
+ ) -> Iterable[Text]:
+ with open(left_ttxpath) as ff:
+ fromlines = ff.readlines()
+ with open(right_ttxpath) as tf:
+ tolines = tf.readlines()
+
+ fromdate = get_file_modtime(prepath)
+ todate = get_file_modtime(postpath)
+
+ yield from unified_diff(
+ fromlines,
+ tolines,
+ pre_pathname,
+ post_pathname,
+ fromdate,
+ todate,
+ n=context_lines,
+ )
+
+ yield from _diff_with_saved_ttx_files(
+ filepath_a,
+ filepath_b,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ _create_unified_diff,
+ )
+
+
+def run_external_diff(
+ diff_tool: Text,
+ diff_args: List[Text],
+ filepath_a: Text,
+ filepath_b: Text,
+ include_tables: Optional[List[Text]] = None,
+ exclude_tables: Optional[List[Text]] = None,
+ font_number_a: int = -1,
+ font_number_b: int = -1,
+ use_multiprocess: bool = True,
+) -> Iterator[Text]:
+ """Performs a unified diff on a TTX serialized data format dump of font binary data using
+ an external diff executable that is requested by the caller via `command`
+
+ diff_tool: (string) command line executable string
+ diff_args: (list of strings) arguments for the diff tool
+ filepath_a: (string) pre-file local file path
+ filepath_b: (string) post-file local file path
+ include_tables: (list of str) Python list of OpenType tables to include in the diff
+ exclude_tables: (list of str) Python list of OpentType tables to exclude from the diff
+ use_multiprocess: (bool) use multi-processor optimizations (default=True)
+
+ include_tables and exclude_tables are mutually exclusive arguments. Only one should
+ be defined
+
+ :returns: Generator of ordered diff line strings that include newline line endings
+ :raises: KeyError if include_tables or exclude_tables includes a mis-specified table
+ that is not included in filepath_a OR filepath_b
+ :raises: IOError if exception raised during execution of `command` on TTX files
+ """
+
+ def _create_external_diff(
+ left_ttxpath: Text,
+ right_ttxpath: Text,
+ _pre_pathname: Text,
+ _prepath: Text,
+ _post_pathname: Text,
+ _postpath: Text,
+ ) -> Iterable[Text]:
+ command = [diff_tool] + diff_args + [left_ttxpath, right_ttxpath]
+ process = subprocess.Popen(
+ command,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ encoding="utf8",
+ )
+
+ for line in process.stdout:
+ yield line
+ err = process.stderr.read()
+ if err:
+ raise IOError(err)
+
+ yield from _diff_with_saved_ttx_files(
+ filepath_a,
+ filepath_b,
+ include_tables,
+ exclude_tables,
+ font_number_a,
+ font_number_b,
+ use_multiprocess,
+ _create_external_diff,
+ )
diff --git a/contrib/python/fonttools/fontTools/diff/utils.py b/contrib/python/fonttools/fontTools/diff/utils.py
new file mode 100644
index 00000000000..aed97063636
--- /dev/null
+++ b/contrib/python/fonttools/fontTools/diff/utils.py
@@ -0,0 +1,28 @@
+import os
+from datetime import datetime, timezone
+from typing import List, Optional, Text, Union
+
+
+def file_exists(path: Union[bytes, str, "os.PathLike[Text]"]) -> bool:
+ """Validates file path as existing local file"""
+ return os.path.isfile(path)
+
+
+def get_file_modtime(path: Union[bytes, str, "os.PathLike[Text]"]) -> Text:
+ """Returns ISO formatted file modification time in local system timezone"""
+ return (
+ datetime.fromtimestamp(os.stat(path).st_mtime, timezone.utc)
+ .astimezone()
+ .isoformat()
+ )
+
+
+def get_tables_argument_list(table_list: Optional[List[Text]]) -> Optional[List[Text]]:
+ """Converts a list of OpenType table string into a Python list or
+ return None if the table_list was not defined (i.e., it was not included
+ in an option on the command line). Tables that are composed of three
+ characters must be right padded with a space."""
+ if table_list is None:
+ return None
+ else:
+ return [table.ljust(4) for table in table_list]
diff --git a/contrib/python/fonttools/fontTools/feaLib/builder.py b/contrib/python/fonttools/fontTools/feaLib/builder.py
index 21b7f5bdf26..971b87ca610 100644
--- a/contrib/python/fonttools/fontTools/feaLib/builder.py
+++ b/contrib/python/fonttools/fontTools/feaLib/builder.py
@@ -1,3 +1,5 @@
+from __future__ import annotations
+
from fontTools.misc import sstruct
from fontTools.misc.textTools import Tag, tostr, binary2num, safeEval
from fontTools.feaLib.error import FeatureLibError
@@ -8,7 +10,7 @@ from fontTools.feaLib.lookupDebugInfo import (
)
from fontTools.feaLib.parser import Parser
from fontTools.feaLib.ast import FeatureFile
-from fontTools.feaLib.variableScalar import VariableScalar
+from fontTools.feaLib.variableScalar import VariableScalar, VariableScalarBuilder
from fontTools.otlLib import builder as otl
from fontTools.otlLib.maxContextCalc import maxCtxFont
from fontTools.ttLib import newTable, getTableModule
@@ -124,6 +126,7 @@ class Builder(object):
self.varstorebuilder = OnlineVarStoreBuilder(
[ax.axisTag for ax in self.axes]
)
+ self.scalar_builder = VariableScalarBuilder.from_ttf(font)
self.default_language_systems_ = set()
self.script_ = None
self.lookupflag_ = 0
@@ -180,10 +183,6 @@ class Builder(object):
self.stat_ = {}
# for conditionsets
self.conditionsets_ = {}
- # We will often use exactly the same locations (i.e. the font's masters)
- # for a large number of variable scalars. Instead of creating a model
- # for each, let's share the models.
- self.model_cache = {}
def build(self, tables=None, debug=False):
if self.parseTree is None:
@@ -392,6 +391,10 @@ class Builder(object):
return user_name_id
def buildFeatureParams(self, tag):
+ # by convention, a missing name ID is represented by 0xffff.
+ # the spec says that these fields can be 'NULL', but 'NULL' is not
+ # well defined for the purpose of nameIDs?
+ NO_NAME_ID = 0xFFFF
params = None
if tag == "size":
params = otTables.FeatureParamsSize()
@@ -418,17 +421,17 @@ class Builder(object):
params = otTables.FeatureParamsCharacterVariants()
params.Format = 0
params.FeatUILabelNameID = self.cv_parameters_ids_.get(
- (tag, "FeatUILabelNameID"), 0
+ (tag, "FeatUILabelNameID"), NO_NAME_ID
)
params.FeatUITooltipTextNameID = self.cv_parameters_ids_.get(
- (tag, "FeatUITooltipTextNameID"), 0
+ (tag, "FeatUITooltipTextNameID"), NO_NAME_ID
)
params.SampleTextNameID = self.cv_parameters_ids_.get(
- (tag, "SampleTextNameID"), 0
+ (tag, "SampleTextNameID"), NO_NAME_ID
)
params.NumNamedParameters = self.cv_num_named_params_.get(tag, 0)
params.FirstParamUILabelNameID = self.cv_parameters_ids_.get(
- (tag, "ParamUILabelNameID_0"), 0
+ (tag, "ParamUILabelNameID_0"), NO_NAME_ID
)
params.CharCount = len(self.cv_characters_[tag])
params.Character = self.cv_characters_[tag]
@@ -805,7 +808,6 @@ class Builder(object):
gdef.remap_device_varidxes(varidx_map)
if "GPOS" in self.font:
self.font["GPOS"].table.remap_device_varidxes(varidx_map)
- self.model_cache.clear()
if any(
(
gdef.GlyphClassDef,
@@ -1439,6 +1441,19 @@ class Builder(object):
if sub is None:
sub = self.get_chained_lookup_(location, MultipleSubstBuilder)
sub.mapping[glyph] = replacements
+ # https://github.com/fonttools/fonttools/issues/4016
+ # If the last rule has the same context and lookup, merge the glyph
+ # into it instead of creating a new rule (avoids unnecessary subtables).
+ if chain.rules and not chain.rules[-1].is_subtable_break:
+ last = chain.rules[-1]
+ if (
+ last.prefix == prefix
+ and last.suffix == suffix
+ and last.lookups == [sub]
+ ):
+ assert len(last.glyphs) == 1
+ last.glyphs[0].add(glyph)
+ return
chain.rules.append(ChainContextualRule(prefix, [{glyph}], suffix, [sub]))
def add_ligature_subst_chained_(
@@ -1725,19 +1740,24 @@ class Builder(object):
self.conditionsets_[key] = value
- def makeVariablePos(self, location, varscalar):
- if not self.varstorebuilder:
+ def makeVariablePos(
+ self, location, varscalar: VariableScalar
+ ) -> tuple[int, int | None]:
+ """Make a pos statement from a VariableScalar, returning the default
+ value, and optionally the variation index if the scalar genuinely
+ requires variation too."""
+
+ if self.varstorebuilder is None or self.scalar_builder is None:
raise FeatureLibError(
"Can't define a variable scalar in a non-variable font", location
)
- varscalar.axes = self.axes
if not varscalar.does_vary:
- return varscalar.default, None
+ return self.scalar_builder.default_value(varscalar), None
try:
- default, index = varscalar.add_to_variation_store(
- self.varstorebuilder, self.model_cache, self.font.get("avar")
+ default, index = self.scalar_builder.add_to_variation_store(
+ varscalar, self.varstorebuilder
)
except VarLibError as e:
raise FeatureLibError(
diff --git a/contrib/python/fonttools/fontTools/feaLib/parser.py b/contrib/python/fonttools/fontTools/feaLib/parser.py
index 0e211e0032a..9233b8739fc 100644
--- a/contrib/python/fonttools/fontTools/feaLib/parser.py
+++ b/contrib/python/fonttools/fontTools/feaLib/parser.py
@@ -1953,6 +1953,7 @@ class Parser(object):
return self.ast.FontRevisionStatement(version, location=location)
def parse_conditionset_(self):
+ location = self.cur_token_location_
name = self.expect_name_()
conditions = {}
@@ -1987,7 +1988,7 @@ class Parser(object):
finalname = self.expect_name_()
if finalname != name:
raise FeatureLibError('Expected "%s"' % name, self.cur_token_location_)
- return self.ast.ConditionsetStatement(name, conditions)
+ return self.ast.ConditionsetStatement(name, conditions, location=location)
def parse_block_(
self, block, vertical, stylisticset=None, size_feature=False, cv_feature=None
diff --git a/contrib/python/fonttools/fontTools/feaLib/variableScalar.py b/contrib/python/fonttools/fontTools/feaLib/variableScalar.py
index 31f1bd19f2d..b20c68fd0cb 100644
--- a/contrib/python/fonttools/fontTools/feaLib/variableScalar.py
+++ b/contrib/python/fonttools/fontTools/feaLib/variableScalar.py
@@ -1,18 +1,47 @@
-from fontTools.varLib.models import VariationModel, normalizeValue, piecewiseLinearMap
+from __future__ import annotations
+from collections.abc import Mapping
+from dataclasses import dataclass
-def Location(loc):
- return tuple(sorted(loc.items()))
+from fontTools.designspaceLib import DesignSpaceDocument
+from fontTools.ttLib.ttFont import TTFont
+from fontTools.varLib.models import (
+ VariationModel,
+ noRound,
+ normalizeValue,
+ piecewiseLinearMap,
+)
+
+import typing
+import warnings
+
+if typing.TYPE_CHECKING:
+ from typing import Self
+
+LocationTuple = tuple[tuple[str, float], ...]
+"""A hashable location."""
+
+
+def Location(location: Mapping[str, float]) -> LocationTuple:
+ """Create a hashable location from a dictionary-like location."""
+ return tuple(sorted(location.items()))
class VariableScalar:
"""A scalar with different values at different points in the designspace."""
- def __init__(self, location_value={}):
- self.values = {}
- self.axes = {}
- for location, value in location_value.items():
- self.add_value(location, value)
+ values: dict[LocationTuple, int]
+ """The values across various user-locations. Must always include the default
+ location by time of building."""
+
+ def __init__(self, location_value=None):
+ self.values = {
+ Location(location): value
+ for location, value in (location_value or {}).items()
+ }
+ # Deprecated: only used by the add_to_variation_store() backwards-compat
+ # shim. New code should use VariableScalarBuilder instead.
+ self.axes = []
def __repr__(self):
items = []
@@ -27,92 +56,210 @@ class VariableScalar:
return "(" + (" ".join(items)) + ")"
@property
- def does_vary(self):
+ def does_vary(self) -> bool:
values = list(self.values.values())
return any(v != values[0] for v in values[1:])
- @property
- def axes_dict(self):
+ def add_value(self, location: Mapping[str, float], value: int):
+ self.values[Location(location)] = value
+
+ def add_to_variation_store(self, store_builder, model_cache=None, avar=None):
+ """Deprecated: use VariableScalarBuilder.add_to_variation_store() instead."""
+ warnings.warn(
+ "VariableScalar.add_to_variation_store() is deprecated. "
+ "Use VariableScalarBuilder.add_to_variation_store() instead.",
+ DeprecationWarning,
+ stacklevel=2,
+ )
if not self.axes:
raise ValueError(
- ".axes must be defined on variable scalar before interpolating"
+ ".axes must be defined on variable scalar before calling "
+ "add_to_variation_store()"
)
- return {ax.axisTag: ax for ax in self.axes}
+ builder = VariableScalarBuilder(
+ axis_triples={
+ ax.axisTag: (ax.minValue, ax.defaultValue, ax.maxValue)
+ for ax in self.axes
+ },
+ axis_mappings=({} if avar is None else dict(avar.segments)),
+ model_cache=model_cache if model_cache is not None else {},
+ )
+ return builder.add_to_variation_store(self, store_builder)
- def _normalized_location(self, location):
- location = self.fix_location(location)
- normalized_location = {}
- for axtag in location.keys():
- if axtag not in self.axes_dict:
+
+@dataclass
+class VariableScalarBuilder:
+ """A helper class for building variable scalars, or otherwise interrogating
+ their variation model for interpolation or similar."""
+
+ axis_triples: dict[str, tuple[float, float, float]]
+ """Minimum, default, and maximum for each axis in user-coordinates."""
+ axis_mappings: dict[str, Mapping[float, float]]
+ """Optional mappings from normalized user-coordinates to normalized
+ design-coordinates."""
+
+ model_cache: dict[tuple[LocationTuple, ...], VariationModel]
+ """We often use the same exact locations (i.e. font sources) for a large
+ number of variable scalars. Instead of creating a model for each, cache
+ them. Cache by user-location to avoid repeated mapping computations."""
+
+ @classmethod
+ def from_ttf(cls, ttf: TTFont) -> Self:
+ return cls(
+ axis_triples={
+ axis.axisTag: (axis.minValue, axis.defaultValue, axis.maxValue)
+ for axis in ttf["fvar"].axes
+ },
+ axis_mappings=(
+ {}
+ if (avar := ttf.get("avar")) is None
+ else {axis: segments for axis, segments in avar.segments.items()}
+ ),
+ model_cache={},
+ )
+
+ @classmethod
+ def from_designspace(cls, doc: DesignSpaceDocument) -> Self:
+ return cls(
+ axis_triples={
+ axis.tag: (axis.minimum, axis.default, axis.maximum)
+ for axis in doc.axes
+ },
+ axis_mappings={
+ axis.tag: {
+ normalizeValue(
+ user, (axis.minimum, axis.default, axis.maximum)
+ ): normalizeValue(
+ design,
+ (
+ axis.map_forward(axis.minimum),
+ axis.map_forward(axis.default),
+ axis.map_forward(axis.maximum),
+ ),
+ )
+ for user, design in axis.map
+ }
+ for axis in doc.axes
+ if axis.map
+ },
+ model_cache={},
+ )
+
+ def _fully_specify_location(self, location: LocationTuple) -> LocationTuple:
+ """Validate and fully-specify a user-space location by filling in
+ missing axes with their user-space defaults."""
+
+ full = {}
+ for axtag, value in location:
+ if axtag not in self.axis_triples:
raise ValueError("Unknown axis %s in %s" % (axtag, location))
- axis = self.axes_dict[axtag]
- normalized_location[axtag] = normalizeValue(
- location[axtag], (axis.minValue, axis.defaultValue, axis.maxValue)
- )
+ full[axtag] = value
- return Location(normalized_location)
+ for axtag, (_, axis_default, _) in self.axis_triples.items():
+ if axtag not in full:
+ full[axtag] = axis_default
- def fix_location(self, location):
- location = dict(location)
- for tag, axis in self.axes_dict.items():
- if tag not in location:
- location[tag] = axis.defaultValue
- return location
+ return Location(full)
- def add_value(self, location, value):
- if self.axes:
- location = self.fix_location(location)
+ def _normalize_location(self, location: LocationTuple) -> dict[str, float]:
+ """Normalize a user-space location, applying avar mappings if present.
- self.values[Location(location)] = value
+ TODO: This only handles avar1 (per-axis piecewise linear mappings),
+ not avar2 (multi-dimensional mappings).
+ """
- def fix_all_locations(self):
- self.values = {
- Location(self.fix_location(l)): v for l, v in self.values.items()
- }
+ result = {}
+ for axtag, value in location:
+ axis_min, axis_default, axis_max = self.axis_triples[axtag]
+ normalized = normalizeValue(value, (axis_min, axis_default, axis_max))
+ mapping = self.axis_mappings.get(axtag)
+ if mapping is not None:
+ normalized = piecewiseLinearMap(normalized, mapping)
+ result[axtag] = normalized
- @property
- def default(self):
- self.fix_all_locations()
- key = Location({ax.axisTag: ax.defaultValue for ax in self.axes})
- if key not in self.values:
- raise ValueError("Default value could not be found")
- # I *guess* we could interpolate one, but I don't know how.
- return self.values[key]
+ return result
- def value_at_location(self, location, model_cache=None, avar=None):
- loc = Location(location)
- if loc in self.values.keys():
- return self.values[loc]
- values = list(self.values.values())
- loc = dict(self._normalized_location(loc))
- return self.model(model_cache, avar).interpolateFromMasters(loc, values)
+ def _full_locations_and_values(
+ self, scalar: VariableScalar
+ ) -> list[tuple[LocationTuple, int]]:
+ """Return a list of (fully-specified user-space location, value) pairs,
+ preserving order and length of scalar.values."""
- def model(self, model_cache=None, avar=None):
- if model_cache is not None:
- key = tuple(self.values.keys())
- if key in model_cache:
- return model_cache[key]
- locations = [dict(self._normalized_location(k)) for k in self.values.keys()]
- if avar is not None:
- mapping = avar.segments
- locations = [
- {
- k: piecewiseLinearMap(v, mapping[k]) if k in mapping else v
- for k, v in location.items()
- }
- for location in locations
- ]
- m = VariationModel(locations)
- if model_cache is not None:
- model_cache[key] = m
- return m
+ return [
+ (self._fully_specify_location(loc), val)
+ for loc, val in scalar.values.items()
+ ]
- def get_deltas_and_supports(self, model_cache=None, avar=None):
- values = list(self.values.values())
- return self.model(model_cache, avar).getDeltasAndSupports(values)
+ def default_value(self, scalar: VariableScalar) -> int:
+ """Get the default value of a variable scalar."""
- def add_to_variation_store(self, store_builder, model_cache=None, avar=None):
- deltas, supports = self.get_deltas_and_supports(model_cache, avar)
+ default_loc = Location(
+ {tag: default for tag, (_, default, _) in self.axis_triples.items()}
+ )
+ for location, value in self._full_locations_and_values(scalar):
+ if location == default_loc:
+ return value
+
+ raise ValueError("Default value could not be found")
+
+ def value_at_location(
+ self, scalar: VariableScalar, location: LocationTuple
+ ) -> float:
+ """Interpolate the value of a scalar from a user-location."""
+
+ location = self._fully_specify_location(location)
+ pairs = self._full_locations_and_values(scalar)
+
+ # If user location matches exactly, no axis mapping or variation model needed.
+ for loc, val in pairs:
+ if loc == location:
+ return val
+
+ values = [val for _, val in pairs]
+ normalized_location = self._normalize_location(location)
+
+ value = self.model(scalar).interpolateFromMasters(normalized_location, values)
+ if value is None:
+ raise ValueError("Insufficient number of values to interpolate")
+
+ return value
+
+ def model(self, scalar: VariableScalar) -> VariationModel:
+ """Return a variation model based on a scalar's values.
+
+ Variable scalars with the same fully-specified user-locations will use
+ the same cached variation model."""
+
+ pairs = self._full_locations_and_values(scalar)
+ cache_key = tuple(loc for loc, _ in pairs)
+
+ cached_model = self.model_cache.get(cache_key)
+ if cached_model is not None:
+ return cached_model
+
+ normalized_locations = [self._normalize_location(loc) for loc, _ in pairs]
+ axisOrder = list(self.axis_triples.keys())
+ model = self.model_cache[cache_key] = VariationModel(
+ normalized_locations, axisOrder=axisOrder
+ )
+
+ return model
+
+ def get_deltas_and_supports(self, scalar: VariableScalar):
+ """Calculate deltas and supports from this scalar's variation model."""
+ values = list(scalar.values.values())
+ return self.model(scalar).getDeltasAndSupports(values, round=round)
+
+ def add_to_variation_store(
+ self, scalar: VariableScalar, store_builder
+ ) -> tuple[int, int]:
+ """Serialize this scalar's variation model to a store, returning the
+ default value and variation index."""
+
+ deltas, supports = self.get_deltas_and_supports(scalar)
store_builder.setSupports(supports)
- index = store_builder.storeDeltas(deltas)
- return int(self.default), index
+ index = store_builder.storeDeltas(deltas, round=noRound)
+
+ # NOTE: Default value should be an exact integer by construction of
+ # VariableScalar.
+ return int(self.default_value(scalar)), index
diff --git a/contrib/python/fonttools/fontTools/pens/pointPen.py b/contrib/python/fonttools/fontTools/pens/pointPen.py
index 3091b86921b..a6568bb7c57 100644
--- a/contrib/python/fonttools/fontTools/pens/pointPen.py
+++ b/contrib/python/fonttools/fontTools/pens/pointPen.py
@@ -344,7 +344,17 @@ class SegmentToPointPen(AbstractPen):
def closePath(self):
if self.contour is None:
raise PenError("Contour missing required initial moveTo")
- if len(self.contour) > 1 and self.contour[0][0] == self.contour[-1][0]:
+
+ # Remove the last point if it's a duplicate of the first, but only if both
+ # are on-curve points (segmentType is not None); for quad blobs
+ # (all off-curve) every point must be preserved:
+ # https://github.com/fonttools/fonttools/issues/4014
+ if (
+ len(self.contour) > 1
+ and (self.contour[0][0] == self.contour[-1][0])
+ and self.contour[0][1] is not None
+ and self.contour[-1][1] is not None
+ ):
self.contour[0] = self.contour[-1]
del self.contour[-1]
else:
diff --git a/contrib/python/fonttools/fontTools/qu2cu/cli.py b/contrib/python/fonttools/fontTools/qu2cu/cli.py
index 101e938a6fc..c3fee2db6c3 100644
--- a/contrib/python/fonttools/fontTools/qu2cu/cli.py
+++ b/contrib/python/fonttools/fontTools/qu2cu/cli.py
@@ -22,7 +22,8 @@ def _font_to_cubic(input_path, output_path=None, **kwargs):
"all_cubic": kwargs["all_cubic"],
}
- assert "gvar" not in font, "Cannot convert variable font"
+ if "gvar" in font:
+ raise ValueError("Cannot convert variable font")
glyphSet = font.getGlyphSet()
glyphOrder = font.getGlyphOrder()
glyf = font["glyf"]
@@ -87,6 +88,9 @@ def _main(args=None):
options = parser.parse_args(args)
+ if options.conversion_error <= 0:
+ parser.error("--conversion-error must be greater than zero")
+
if not options.verbose:
level = "WARNING"
elif options.verbose == 1:
diff --git a/contrib/python/fonttools/fontTools/qu2cu/qu2cu.py b/contrib/python/fonttools/fontTools/qu2cu/qu2cu.py
index 8fe3e18b086..958029e3b6e 100644
--- a/contrib/python/fonttools/fontTools/qu2cu/qu2cu.py
+++ b/contrib/python/fonttools/fontTools/qu2cu/qu2cu.py
@@ -175,6 +175,22 @@ def add_implicit_on_curves(p):
Point = Union[Tuple[float, float], complex]
+def _raise_incompatible_point(point, previous_point):
+ raise ValueError(
+ f"Quadratic splines must connect end-to-start; got {previous_point!r} then {point!r}"
+ )
+
+
+def _validate_spline_length(spline):
+ if len(spline) < 3:
+ raise ValueError("Quadratic splines must contain at least 3 points")
+
+
+def _validate_positive_tolerance(max_err):
+ if max_err <= 0:
+ raise ValueError("max_err must be greater than zero")
+
+
@cython.locals(
cost=cython.int,
is_complex=cython.int,
@@ -198,6 +214,7 @@ def quadratic_to_curves(
Returns:
The output is a list of tuples of points. Points are represented
in the same format as the input, either as 2-tuples or complex numbers.
+ If ``quads`` is empty, returns an empty list.
Each tuple is either of length three, for a quadratic curve, or four,
for a cubic curve. Each curve's last point is the same as the next
@@ -209,7 +226,17 @@ def quadratic_to_curves(
max_err: absolute error tolerance; defaults to 0.5
all_cubic: if True, only cubic curves are generated; defaults to False
+
+ Raises:
+ ValueError: if an input spline has fewer than 3 points, or if adjacent
+ splines do not connect end-to-start.
"""
+ if not quads:
+ return []
+ _validate_positive_tolerance(max_err)
+ for spline in quads:
+ _validate_spline_length(spline)
+
is_complex = type(quads[0][0]) is complex
if not is_complex:
quads = [[complex(x, y) for (x, y) in p] for p in quads]
@@ -218,7 +245,8 @@ def quadratic_to_curves(
costs = [1]
cost = 1
for p in quads:
- assert q[-1] == p[0]
+ if q[-1] != p[0]:
+ _raise_incompatible_point(p[0], q[-1])
for i in range(len(p) - 2):
cost += 1
costs.append(cost)
diff --git a/contrib/python/fonttools/fontTools/svgLib/path/shapes.py b/contrib/python/fonttools/fontTools/svgLib/path/shapes.py
index 3f22e6c6a3e..112d17fd4ea 100644
--- a/contrib/python/fonttools/fontTools/svgLib/path/shapes.py
+++ b/contrib/python/fonttools/fontTools/svgLib/path/shapes.py
@@ -173,6 +173,8 @@ class PathBuilder(object):
self.A(rx, ry, cx - rx, cy, large_arc=1)
def add_path_from_element(self, el):
+ if not isinstance(el.tag, str):
+ return False
tag = _strip_xml_ns(el.tag)
parse_fn = getattr(self, "_parse_%s" % tag.lower(), None)
if not callable(parse_fn):
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/D_S_I_G_.py b/contrib/python/fonttools/fontTools/ttLib/tables/D_S_I_G_.py
index f89cc29e49f..deb7d58d92e 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/D_S_I_G_.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/D_S_I_G_.py
@@ -1,7 +1,16 @@
-from fontTools.misc.textTools import bytesjoin, strjoin, tobytes, tostr, safeEval
+from __future__ import annotations
+
+import base64
+from typing import TYPE_CHECKING
+
from fontTools.misc import sstruct
+from fontTools.misc.textTools import bytesjoin, safeEval, strjoin, tobytes, tostr
+
from . import DefaultTable
-import base64
+
+if TYPE_CHECKING:
+ from fontTools.misc.xmlWriter import XMLWriter
+ from fontTools.ttLib import TTFont
DSIG_HeaderFormat = """
> # big endian
@@ -46,11 +55,12 @@ class table_D_S_I_G_(DefaultTable.DefaultTable):
See also https://learn.microsoft.com/en-us/typography/opentype/spec/dsig
"""
- def decompile(self, data, ttFont):
+ def decompile(self, data: bytes, ttFont: TTFont) -> None:
dummy, newData = sstruct.unpack2(DSIG_HeaderFormat, data, self)
assert self.ulVersion == 1, "DSIG ulVersion must be 1"
assert self.usFlag & ~1 == 0, "DSIG usFlag must be 0x1 or 0x0"
- self.signatureRecords = sigrecs = []
+ self.signatureRecords: list[SignatureRecord] = []
+ sigrecs = self.signatureRecords
for n in range(self.usNumSigs):
sigrec, newData = sstruct.unpack2(
DSIG_SignatureFormat, newData, SignatureRecord()
@@ -71,7 +81,7 @@ class table_D_S_I_G_(DefaultTable.DefaultTable):
)
sigrec.pkcs7 = newData[: sigrec.cbSignature]
- def compile(self, ttFont):
+ def compile(self, ttFont: TTFont) -> bytes:
packed = sstruct.pack(DSIG_HeaderFormat, self)
headers = [packed]
offset = len(packed) + self.usNumSigs * sstruct.calcsize(DSIG_SignatureFormat)
@@ -92,7 +102,7 @@ class table_D_S_I_G_(DefaultTable.DefaultTable):
data.append(b"\0")
return bytesjoin(headers + data)
- def toXML(self, xmlWriter, ttFont):
+ def toXML(self, xmlWriter: XMLWriter, ttFont: TTFont) -> None:
xmlWriter.comment(
"note that the Digital Signature will be invalid after recompilation!"
)
@@ -139,11 +149,11 @@ def b64encode(b):
return strjoin(items)
-class SignatureRecord(object):
- def __repr__(self):
+class SignatureRecord:
+ def __repr__(self) -> str:
return "<%s: %s>" % (self.__class__.__name__, self.__dict__)
- def toXML(self, writer, ttFont):
+ def toXML(self, writer, ttFont: TTFont) -> None:
writer.begintag(self.__class__.__name__, format=self.ulFormat)
writer.newline()
writer.write_noindent("-----BEGIN PKCS7-----\n")
@@ -151,8 +161,10 @@ class SignatureRecord(object):
writer.write_noindent("-----END PKCS7-----\n")
writer.endtag(self.__class__.__name__)
- def fromXML(self, name, attrs, content, ttFont):
- self.ulFormat = safeEval(attrs["format"])
- self.usReserved1 = safeEval(attrs.get("reserved1", "0"))
- self.usReserved2 = safeEval(attrs.get("reserved2", "0"))
+ def fromXML(
+ self, name: str, attrs: dict[str, str], content: str, ttFont: TTFont
+ ) -> None:
+ self.ulFormat: int = safeEval(attrs["format"])
+ self.usReserved1: int = safeEval(attrs.get("reserved1", "0"))
+ self.usReserved2: int = safeEval(attrs.get("reserved2", "0"))
self.pkcs7 = base64.b64decode(tobytes(strjoin(filter(pem_spam, content))))
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py b/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py
index 92f2aa6523a..9ed065dd4a7 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/DefaultTable.py
@@ -1,22 +1,34 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
from fontTools.misc.textTools import Tag
from fontTools.ttLib import getClassTag
+if TYPE_CHECKING:
+ from typing import Any
+
+ from fontTools.misc.xmlWriter import XMLWriter
+ from fontTools.ttLib import TTFont
-class DefaultTable(object):
- dependencies = []
- def __init__(self, tag=None):
+class DefaultTable:
+ dependencies: list[str] = []
+
+ def __init__(self, tag: str | bytes | None = None) -> None:
if tag is None:
tag = getClassTag(self.__class__)
self.tableTag = Tag(tag)
- def decompile(self, data, ttFont):
+ def decompile(self, data: bytes, ttFont: TTFont) -> None:
self.data = data
- def compile(self, ttFont):
+ def compile(self, ttFont: TTFont) -> bytes:
return self.data
- def toXML(self, writer, ttFont, **kwargs):
+ def toXML(
+ self, writer: XMLWriter, ttFont: TTFont, **kwargs: dict[str, Any]
+ ) -> None:
if hasattr(self, "ERROR"):
writer.comment("An error occurred during the decompilation of this table")
writer.newline()
@@ -28,22 +40,24 @@ class DefaultTable(object):
writer.endtag("hexdata")
writer.newline()
- def fromXML(self, name, attrs, content, ttFont):
- from fontTools.misc.textTools import readHex
+ def fromXML(
+ self, name: str, attrs: dict[str, str], content: str, ttFont: TTFont
+ ) -> None:
from fontTools import ttLib
+ from fontTools.misc.textTools import readHex
if name != "hexdata":
raise ttLib.TTLibError("can't handle '%s' element" % name)
self.decompile(readHex(content), ttFont)
- def __repr__(self):
+ def __repr__(self) -> str:
return "<'%s' table at %x>" % (self.tableTag, id(self))
- def __eq__(self, other):
+ def __eq__(self, other: Any) -> bool:
if type(self) != type(other):
return NotImplemented
return self.__dict__ == other.__dict__
- def __ne__(self, other):
+ def __ne__(self, other: Any) -> bool:
result = self.__eq__(other)
return result if result is NotImplemented else not result
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py b/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py
deleted file mode 100644
index 070c61919e1..00000000000
--- a/contrib/python/fonttools/fontTools/ttLib/tables/G_M_A_P_.py
+++ /dev/null
@@ -1,148 +0,0 @@
-from fontTools.misc import sstruct
-from fontTools.misc.textTools import tobytes, tostr, safeEval
-from . import DefaultTable
-
-GMAPFormat = """
- > # big endian
- tableVersionMajor: H
- tableVersionMinor: H
- flags: H
- recordsCount: H
- recordsOffset: H
- fontNameLength: H
-"""
-# psFontName is a byte string which follows the record above. This is zero padded
-# to the beginning of the records array. The recordsOffsst is 32 bit aligned.
-
-GMAPRecordFormat1 = """
- > # big endian
- UV: L
- cid: H
- gid: H
- ggid: H
- name: 32s
-"""
-
-
-class GMAPRecord(object):
- def __init__(self, uv=0, cid=0, gid=0, ggid=0, name=""):
- self.UV = uv
- self.cid = cid
- self.gid = gid
- self.ggid = ggid
- self.name = name
-
- def toXML(self, writer, ttFont):
- writer.begintag("GMAPRecord")
- writer.newline()
- writer.simpletag("UV", value=self.UV)
- writer.newline()
- writer.simpletag("cid", value=self.cid)
- writer.newline()
- writer.simpletag("gid", value=self.gid)
- writer.newline()
- writer.simpletag("glyphletGid", value=self.gid)
- writer.newline()
- writer.simpletag("GlyphletName", value=self.name)
- writer.newline()
- writer.endtag("GMAPRecord")
- writer.newline()
-
- def fromXML(self, name, attrs, content, ttFont):
- value = attrs["value"]
- if name == "GlyphletName":
- self.name = value
- else:
- setattr(self, name, safeEval(value))
-
- def compile(self, ttFont):
- if self.UV is None:
- self.UV = 0
- nameLen = len(self.name)
- if nameLen < 32:
- self.name = self.name + "\0" * (32 - nameLen)
- data = sstruct.pack(GMAPRecordFormat1, self)
- return data
-
- def __repr__(self):
- return (
- "GMAPRecord[ UV: "
- + str(self.UV)
- + ", cid: "
- + str(self.cid)
- + ", gid: "
- + str(self.gid)
- + ", ggid: "
- + str(self.ggid)
- + ", Glyphlet Name: "
- + str(self.name)
- + " ]"
- )
-
-
-class table_G_M_A_P_(DefaultTable.DefaultTable):
- """Glyphlets GMAP table
-
- The ``GMAP`` table is used by Adobe's SING Glyphlets.
-
- See also https://web.archive.org/web/20080627183635/http://www.adobe.com/devnet/opentype/gdk/topic.html
- """
-
- dependencies = []
-
- def decompile(self, data, ttFont):
- dummy, newData = sstruct.unpack2(GMAPFormat, data, self)
- self.psFontName = tostr(newData[: self.fontNameLength])
- assert (
- self.recordsOffset % 4
- ) == 0, "GMAP error: recordsOffset is not 32 bit aligned."
- newData = data[self.recordsOffset :]
- self.gmapRecords = []
- for i in range(self.recordsCount):
- gmapRecord, newData = sstruct.unpack2(
- GMAPRecordFormat1, newData, GMAPRecord()
- )
- gmapRecord.name = gmapRecord.name.strip("\0")
- self.gmapRecords.append(gmapRecord)
-
- def compile(self, ttFont):
- self.recordsCount = len(self.gmapRecords)
- self.fontNameLength = len(self.psFontName)
- self.recordsOffset = 4 * (((self.fontNameLength + 12) + 3) // 4)
- data = sstruct.pack(GMAPFormat, self)
- data = data + tobytes(self.psFontName)
- data = data + b"\0" * (self.recordsOffset - len(data))
- for record in self.gmapRecords:
- data = data + record.compile(ttFont)
- return data
-
- def toXML(self, writer, ttFont):
- writer.comment("Most of this table will be recalculated by the compiler")
- writer.newline()
- formatstring, names, fixes = sstruct.getformat(GMAPFormat)
- for name in names:
- value = getattr(self, name)
- writer.simpletag(name, value=value)
- writer.newline()
- writer.simpletag("PSFontName", value=self.psFontName)
- writer.newline()
- for gmapRecord in self.gmapRecords:
- gmapRecord.toXML(writer, ttFont)
-
- def fromXML(self, name, attrs, content, ttFont):
- if name == "GMAPRecord":
- if not hasattr(self, "gmapRecords"):
- self.gmapRecords = []
- gmapRecord = GMAPRecord()
- self.gmapRecords.append(gmapRecord)
- for element in content:
- if isinstance(element, str):
- continue
- name, attrs, content = element
- gmapRecord.fromXML(name, attrs, content, ttFont)
- else:
- value = attrs["value"]
- if name == "PSFontName":
- self.psFontName = value
- else:
- setattr(self, name, safeEval(value))
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/G_P_K_G_.py b/contrib/python/fonttools/fontTools/ttLib/tables/G_P_K_G_.py
deleted file mode 100644
index 0da99fcda40..00000000000
--- a/contrib/python/fonttools/fontTools/ttLib/tables/G_P_K_G_.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from fontTools.misc import sstruct
-from fontTools.misc.textTools import bytesjoin, safeEval, readHex
-from . import DefaultTable
-import sys
-import array
-
-GPKGFormat = """
- > # big endian
- version: H
- flags: H
- numGMAPs: H
- numGlyplets: H
-"""
-# psFontName is a byte string which follows the record above. This is zero padded
-# to the beginning of the records array. The recordsOffsst is 32 bit aligned.
-
-
-class table_G_P_K_G_(DefaultTable.DefaultTable):
- """Glyphlets GPKG table
-
- The ``GPKG`` table is used by Adobe's SING Glyphlets.
-
- See also https://web.archive.org/web/20080627183635/http://www.adobe.com/devnet/opentype/gdk/topic.html
- """
-
- def decompile(self, data, ttFont):
- dummy, newData = sstruct.unpack2(GPKGFormat, data, self)
-
- GMAPoffsets = array.array("I")
- endPos = (self.numGMAPs + 1) * 4
- GMAPoffsets.frombytes(newData[:endPos])
- if sys.byteorder != "big":
- GMAPoffsets.byteswap()
- self.GMAPs = []
- for i in range(self.numGMAPs):
- start = GMAPoffsets[i]
- end = GMAPoffsets[i + 1]
- self.GMAPs.append(data[start:end])
- pos = endPos
- endPos = pos + (self.numGlyplets + 1) * 4
- glyphletOffsets = array.array("I")
- glyphletOffsets.frombytes(newData[pos:endPos])
- if sys.byteorder != "big":
- glyphletOffsets.byteswap()
- self.glyphlets = []
- for i in range(self.numGlyplets):
- start = glyphletOffsets[i]
- end = glyphletOffsets[i + 1]
- self.glyphlets.append(data[start:end])
-
- def compile(self, ttFont):
- self.numGMAPs = len(self.GMAPs)
- self.numGlyplets = len(self.glyphlets)
- GMAPoffsets = [0] * (self.numGMAPs + 1)
- glyphletOffsets = [0] * (self.numGlyplets + 1)
-
- dataList = [sstruct.pack(GPKGFormat, self)]
-
- pos = len(dataList[0]) + (self.numGMAPs + 1) * 4 + (self.numGlyplets + 1) * 4
- GMAPoffsets[0] = pos
- for i in range(1, self.numGMAPs + 1):
- pos += len(self.GMAPs[i - 1])
- GMAPoffsets[i] = pos
- gmapArray = array.array("I", GMAPoffsets)
- if sys.byteorder != "big":
- gmapArray.byteswap()
- dataList.append(gmapArray.tobytes())
-
- glyphletOffsets[0] = pos
- for i in range(1, self.numGlyplets + 1):
- pos += len(self.glyphlets[i - 1])
- glyphletOffsets[i] = pos
- glyphletArray = array.array("I", glyphletOffsets)
- if sys.byteorder != "big":
- glyphletArray.byteswap()
- dataList.append(glyphletArray.tobytes())
- dataList += self.GMAPs
- dataList += self.glyphlets
- data = bytesjoin(dataList)
- return data
-
- def toXML(self, writer, ttFont):
- writer.comment("Most of this table will be recalculated by the compiler")
- writer.newline()
- formatstring, names, fixes = sstruct.getformat(GPKGFormat)
- for name in names:
- value = getattr(self, name)
- writer.simpletag(name, value=value)
- writer.newline()
-
- writer.begintag("GMAPs")
- writer.newline()
- for gmapData in self.GMAPs:
- writer.begintag("hexdata")
- writer.newline()
- writer.dumphex(gmapData)
- writer.endtag("hexdata")
- writer.newline()
- writer.endtag("GMAPs")
- writer.newline()
-
- writer.begintag("glyphlets")
- writer.newline()
- for glyphletData in self.glyphlets:
- writer.begintag("hexdata")
- writer.newline()
- writer.dumphex(glyphletData)
- writer.endtag("hexdata")
- writer.newline()
- writer.endtag("glyphlets")
- writer.newline()
-
- def fromXML(self, name, attrs, content, ttFont):
- if name == "GMAPs":
- if not hasattr(self, "GMAPs"):
- self.GMAPs = []
- for element in content:
- if isinstance(element, str):
- continue
- itemName, itemAttrs, itemContent = element
- if itemName == "hexdata":
- self.GMAPs.append(readHex(itemContent))
- elif name == "glyphlets":
- if not hasattr(self, "glyphlets"):
- self.glyphlets = []
- for element in content:
- if isinstance(element, str):
- continue
- itemName, itemAttrs, itemContent = element
- if itemName == "hexdata":
- self.glyphlets.append(readHex(itemContent))
- else:
- setattr(self, name, safeEval(attrs["value"]))
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py b/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py
deleted file mode 100644
index 6a6f8bbf840..00000000000
--- a/contrib/python/fonttools/fontTools/ttLib/tables/M_E_T_A_.py
+++ /dev/null
@@ -1,352 +0,0 @@
-from fontTools.misc import sstruct
-from fontTools.misc.textTools import byteord, safeEval
-from . import DefaultTable
-import pdb
-import struct
-
-
-METAHeaderFormat = """
- > # big endian
- tableVersionMajor: H
- tableVersionMinor: H
- metaEntriesVersionMajor: H
- metaEntriesVersionMinor: H
- unicodeVersion: L
- metaFlags: H
- nMetaRecs: H
-"""
-# This record is followed by nMetaRecs of METAGlyphRecordFormat.
-# This in turn is followd by as many METAStringRecordFormat entries
-# as specified by the METAGlyphRecordFormat entries
-# this is followed by the strings specifried in the METAStringRecordFormat
-METAGlyphRecordFormat = """
- > # big endian
- glyphID: H
- nMetaEntry: H
-"""
-# This record is followd by a variable data length field:
-# USHORT or ULONG hdrOffset
-# Offset from start of META table to the beginning
-# of this glyphs array of ns Metadata string entries.
-# Size determined by metaFlags field
-# METAGlyphRecordFormat entries must be sorted by glyph ID
-
-METAStringRecordFormat = """
- > # big endian
- labelID: H
- stringLen: H
-"""
-# This record is followd by a variable data length field:
-# USHORT or ULONG stringOffset
-# METAStringRecordFormat entries must be sorted in order of labelID
-# There may be more than one entry with the same labelID
-# There may be more than one strign with the same content.
-
-# Strings shall be Unicode UTF-8 encoded, and null-terminated.
-
-METALabelDict = {
- 0: "MojikumiX4051", # An integer in the range 1-20
- 1: "UNIUnifiedBaseChars",
- 2: "BaseFontName",
- 3: "Language",
- 4: "CreationDate",
- 5: "FoundryName",
- 6: "FoundryCopyright",
- 7: "OwnerURI",
- 8: "WritingScript",
- 10: "StrokeCount",
- 11: "IndexingRadical",
-}
-
-
-def getLabelString(labelID):
- try:
- label = METALabelDict[labelID]
- except KeyError:
- label = "Unknown label"
- return str(label)
-
-
-class table_M_E_T_A_(DefaultTable.DefaultTable):
- """Glyphlets META table
-
- The ``META`` table is used by Adobe's SING Glyphlets.
-
- See also https://web.archive.org/web/20080627183635/http://www.adobe.com/devnet/opentype/gdk/topic.html
- """
-
- dependencies = []
-
- def decompile(self, data, ttFont):
- dummy, newData = sstruct.unpack2(METAHeaderFormat, data, self)
- self.glyphRecords = []
- for i in range(self.nMetaRecs):
- glyphRecord, newData = sstruct.unpack2(
- METAGlyphRecordFormat, newData, GlyphRecord()
- )
- if self.metaFlags == 0:
- [glyphRecord.offset] = struct.unpack(">H", newData[:2])
- newData = newData[2:]
- elif self.metaFlags == 1:
- [glyphRecord.offset] = struct.unpack(">H", newData[:4])
- newData = newData[4:]
- else:
- assert 0, (
- "The metaFlags field in the META table header has a value other than 0 or 1 :"
- + str(self.metaFlags)
- )
- glyphRecord.stringRecs = []
- newData = data[glyphRecord.offset :]
- for j in range(glyphRecord.nMetaEntry):
- stringRec, newData = sstruct.unpack2(
- METAStringRecordFormat, newData, StringRecord()
- )
- if self.metaFlags == 0:
- [stringRec.offset] = struct.unpack(">H", newData[:2])
- newData = newData[2:]
- else:
- [stringRec.offset] = struct.unpack(">H", newData[:4])
- newData = newData[4:]
- stringRec.string = data[
- stringRec.offset : stringRec.offset + stringRec.stringLen
- ]
- glyphRecord.stringRecs.append(stringRec)
- self.glyphRecords.append(glyphRecord)
-
- def compile(self, ttFont):
- offsetOK = 0
- self.nMetaRecs = len(self.glyphRecords)
- count = 0
- while offsetOK != 1:
- count = count + 1
- if count > 4:
- pdb.set_trace()
- metaData = sstruct.pack(METAHeaderFormat, self)
- stringRecsOffset = len(metaData) + self.nMetaRecs * (
- 6 + 2 * (self.metaFlags & 1)
- )
- stringRecSize = 6 + 2 * (self.metaFlags & 1)
- for glyphRec in self.glyphRecords:
- glyphRec.offset = stringRecsOffset
- if (glyphRec.offset > 65535) and ((self.metaFlags & 1) == 0):
- self.metaFlags = self.metaFlags + 1
- offsetOK = -1
- break
- metaData = metaData + glyphRec.compile(self)
- stringRecsOffset = stringRecsOffset + (
- glyphRec.nMetaEntry * stringRecSize
- )
- # this will be the String Record offset for the next GlyphRecord.
- if offsetOK == -1:
- offsetOK = 0
- continue
-
- # metaData now contains the header and all of the GlyphRecords. Its length should bw
- # the offset to the first StringRecord.
- stringOffset = stringRecsOffset
- for glyphRec in self.glyphRecords:
- assert glyphRec.offset == len(
- metaData
- ), "Glyph record offset did not compile correctly! for rec:" + str(
- glyphRec
- )
- for stringRec in glyphRec.stringRecs:
- stringRec.offset = stringOffset
- if (stringRec.offset > 65535) and ((self.metaFlags & 1) == 0):
- self.metaFlags = self.metaFlags + 1
- offsetOK = -1
- break
- metaData = metaData + stringRec.compile(self)
- stringOffset = stringOffset + stringRec.stringLen
- if offsetOK == -1:
- offsetOK = 0
- continue
-
- if ((self.metaFlags & 1) == 1) and (stringOffset < 65536):
- self.metaFlags = self.metaFlags - 1
- continue
- else:
- offsetOK = 1
-
- # metaData now contains the header and all of the GlyphRecords and all of the String Records.
- # Its length should be the offset to the first string datum.
- for glyphRec in self.glyphRecords:
- for stringRec in glyphRec.stringRecs:
- assert stringRec.offset == len(
- metaData
- ), "String offset did not compile correctly! for string:" + str(
- stringRec.string
- )
- metaData = metaData + stringRec.string
-
- return metaData
-
- def toXML(self, writer, ttFont):
- writer.comment(
- "Lengths and number of entries in this table will be recalculated by the compiler"
- )
- writer.newline()
- formatstring, names, fixes = sstruct.getformat(METAHeaderFormat)
- for name in names:
- value = getattr(self, name)
- writer.simpletag(name, value=value)
- writer.newline()
- for glyphRec in self.glyphRecords:
- glyphRec.toXML(writer, ttFont)
-
- def fromXML(self, name, attrs, content, ttFont):
- if name == "GlyphRecord":
- if not hasattr(self, "glyphRecords"):
- self.glyphRecords = []
- glyphRec = GlyphRecord()
- self.glyphRecords.append(glyphRec)
- for element in content:
- if isinstance(element, str):
- continue
- name, attrs, content = element
- glyphRec.fromXML(name, attrs, content, ttFont)
- glyphRec.offset = -1
- glyphRec.nMetaEntry = len(glyphRec.stringRecs)
- else:
- setattr(self, name, safeEval(attrs["value"]))
-
-
-class GlyphRecord(object):
- def __init__(self):
- self.glyphID = -1
- self.nMetaEntry = -1
- self.offset = -1
- self.stringRecs = []
-
- def toXML(self, writer, ttFont):
- writer.begintag("GlyphRecord")
- writer.newline()
- writer.simpletag("glyphID", value=self.glyphID)
- writer.newline()
- writer.simpletag("nMetaEntry", value=self.nMetaEntry)
- writer.newline()
- for stringRec in self.stringRecs:
- stringRec.toXML(writer, ttFont)
- writer.endtag("GlyphRecord")
- writer.newline()
-
- def fromXML(self, name, attrs, content, ttFont):
- if name == "StringRecord":
- stringRec = StringRecord()
- self.stringRecs.append(stringRec)
- for element in content:
- if isinstance(element, str):
- continue
- stringRec.fromXML(name, attrs, content, ttFont)
- stringRec.stringLen = len(stringRec.string)
- else:
- setattr(self, name, safeEval(attrs["value"]))
-
- def compile(self, parentTable):
- data = sstruct.pack(METAGlyphRecordFormat, self)
- if parentTable.metaFlags == 0:
- datum = struct.pack(">H", self.offset)
- elif parentTable.metaFlags == 1:
- datum = struct.pack(">L", self.offset)
- data = data + datum
- return data
-
- def __repr__(self):
- return (
- "GlyphRecord[ glyphID: "
- + str(self.glyphID)
- + ", nMetaEntry: "
- + str(self.nMetaEntry)
- + ", offset: "
- + str(self.offset)
- + " ]"
- )
-
-
-# XXX The following two functions are really broken around UTF-8 vs Unicode
-
-
-def mapXMLToUTF8(string):
- uString = str()
- strLen = len(string)
- i = 0
- while i < strLen:
- prefixLen = 0
- if string[i : i + 3] == "&#x":
- prefixLen = 3
- elif string[i : i + 7] == "&amp;#x":
- prefixLen = 7
- if prefixLen:
- i = i + prefixLen
- j = i
- while string[i] != ";":
- i = i + 1
- valStr = string[j:i]
-
- uString = uString + chr(eval("0x" + valStr))
- else:
- uString = uString + chr(byteord(string[i]))
- i = i + 1
-
- return uString.encode("utf_8")
-
-
-def mapUTF8toXML(string):
- uString = string.decode("utf_8")
- string = ""
- for uChar in uString:
- i = ord(uChar)
- if (i < 0x80) and (i > 0x1F):
- string = string + uChar
- else:
- string = string + "&#x" + hex(i)[2:] + ";"
- return string
-
-
-class StringRecord(object):
- def toXML(self, writer, ttFont):
- writer.begintag("StringRecord")
- writer.newline()
- writer.simpletag("labelID", value=self.labelID)
- writer.comment(getLabelString(self.labelID))
- writer.newline()
- writer.newline()
- writer.simpletag("string", value=mapUTF8toXML(self.string))
- writer.newline()
- writer.endtag("StringRecord")
- writer.newline()
-
- def fromXML(self, name, attrs, content, ttFont):
- for element in content:
- if isinstance(element, str):
- continue
- name, attrs, content = element
- value = attrs["value"]
- if name == "string":
- self.string = mapXMLToUTF8(value)
- else:
- setattr(self, name, safeEval(value))
-
- def compile(self, parentTable):
- data = sstruct.pack(METAStringRecordFormat, self)
- if parentTable.metaFlags == 0:
- datum = struct.pack(">H", self.offset)
- elif parentTable.metaFlags == 1:
- datum = struct.pack(">L", self.offset)
- data = data + datum
- return data
-
- def __repr__(self):
- return (
- "StringRecord [ labelID: "
- + str(self.labelID)
- + " aka "
- + getLabelString(self.labelID)
- + ", offset: "
- + str(self.offset)
- + ", length: "
- + str(self.stringLen)
- + ", string: "
- + self.string
- + " ]"
- )
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py b/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py
deleted file mode 100644
index 1a367a92f2f..00000000000
--- a/contrib/python/fonttools/fontTools/ttLib/tables/S_I_N_G_.py
+++ /dev/null
@@ -1,99 +0,0 @@
-from fontTools.misc import sstruct
-from fontTools.misc.textTools import bytechr, byteord, tobytes, tostr, safeEval
-from . import DefaultTable
-
-SINGFormat = """
- > # big endian
- tableVersionMajor: H
- tableVersionMinor: H
- glyphletVersion: H
- permissions: h
- mainGID: H
- unitsPerEm: H
- vertAdvance: h
- vertOrigin: h
- uniqueName: 28s
- METAMD5: 16s
- nameLength: 1s
-"""
-# baseGlyphName is a byte string which follows the record above.
-
-
-class table_S_I_N_G_(DefaultTable.DefaultTable):
- """Glyphlets SING table
-
- The ``SING`` table is used by Adobe's SING Glyphlets.
-
- See also https://web.archive.org/web/20080627183635/http://www.adobe.com/devnet/opentype/gdk/topic.html
- """
-
- dependencies = []
-
- def decompile(self, data, ttFont):
- dummy, rest = sstruct.unpack2(SINGFormat, data, self)
- self.uniqueName = self.decompileUniqueName(self.uniqueName)
- self.nameLength = byteord(self.nameLength)
- assert len(rest) == self.nameLength
- self.baseGlyphName = tostr(rest)
-
- rawMETAMD5 = self.METAMD5
- self.METAMD5 = "[" + hex(byteord(self.METAMD5[0]))
- for char in rawMETAMD5[1:]:
- self.METAMD5 = self.METAMD5 + ", " + hex(byteord(char))
- self.METAMD5 = self.METAMD5 + "]"
-
- def decompileUniqueName(self, data):
- name = ""
- for char in data:
- val = byteord(char)
- if val == 0:
- break
- if (val > 31) or (val < 128):
- name += chr(val)
- else:
- octString = oct(val)
- if len(octString) > 3:
- octString = octString[1:] # chop off that leading zero.
- elif len(octString) < 3:
- octString.zfill(3)
- name += "\\" + octString
- return name
-
- def compile(self, ttFont):
- d = self.__dict__.copy()
- d["nameLength"] = bytechr(len(self.baseGlyphName))
- d["uniqueName"] = self.compilecompileUniqueName(self.uniqueName, 28)
- METAMD5List = eval(self.METAMD5)
- d["METAMD5"] = b""
- for val in METAMD5List:
- d["METAMD5"] += bytechr(val)
- assert len(d["METAMD5"]) == 16, "Failed to pack 16 byte MD5 hash in SING table"
- data = sstruct.pack(SINGFormat, d)
- data = data + tobytes(self.baseGlyphName)
- return data
-
- def compilecompileUniqueName(self, name, length):
- nameLen = len(name)
- if length <= nameLen:
- name = name[: length - 1] + "\000"
- else:
- name += (nameLen - length) * "\000"
- return name
-
- def toXML(self, writer, ttFont):
- writer.comment("Most of this table will be recalculated by the compiler")
- writer.newline()
- formatstring, names, fixes = sstruct.getformat(SINGFormat)
- for name in names:
- value = getattr(self, name)
- writer.simpletag(name, value=value)
- writer.newline()
- writer.simpletag("baseGlyphName", value=self.baseGlyphName)
- writer.newline()
-
- def fromXML(self, name, attrs, content, ttFont):
- value = attrs["value"]
- if name in ["uniqueName", "METAMD5", "baseGlyphName"]:
- setattr(self, name, value)
- else:
- setattr(self, name, safeEval(value))
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py
index bd6217e2ed9..4916f91d993 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/TupleVariation.py
@@ -8,10 +8,10 @@ from fontTools.misc.fixedTools import (
from fontTools.misc.textTools import safeEval
import array
from collections import Counter, defaultdict
-import io
import logging
import struct
import sys
+import warnings
# https://www.microsoft.com/typography/otspec/otvarcommonformats.htm
@@ -620,6 +620,12 @@ class TupleVariation(object):
def optimize(self, origCoords, endPts, tolerance=0.5, isComposite=False):
from fontTools.varLib.iup import iup_delta_optimize
+ if isComposite:
+ warnings.warn(
+ "The isComposite argument is deprecated and may be removed in a future version",
+ DeprecationWarning,
+ )
+
if None in self.coordinates:
return # already optimized
@@ -627,10 +633,6 @@ class TupleVariation(object):
self.coordinates, origCoords, endPts, tolerance=tolerance
)
if None in deltaOpt:
- if isComposite and all(d is None for d in deltaOpt):
- # Fix for macOS composites
- # https://github.com/fonttools/fonttools/issues/1381
- deltaOpt = [(0, 0)] + [None] * (len(deltaOpt) - 1)
# Use "optimized" version only if smaller...
varOpt = TupleVariation(self.axes, deltaOpt)
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/__init__.py b/contrib/python/fonttools/fontTools/ttLib/tables/__init__.py
index b111097a804..fd98db78223 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/__init__.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/__init__.py
@@ -19,8 +19,6 @@ def _moduleFinderHint():
from . import F_F_T_M_
from . import F__e_a_t
from . import G_D_E_F_
- from . import G_M_A_P_
- from . import G_P_K_G_
from . import G_P_O_S_
from . import G_S_U_B_
from . import G_V_A_R_
@@ -30,10 +28,8 @@ def _moduleFinderHint():
from . import J_S_T_F_
from . import L_T_S_H_
from . import M_A_T_H_
- from . import M_E_T_A_
from . import M_V_A_R_
from . import O_S_2f_2
- from . import S_I_N_G_
from . import S_T_A_T_
from . import S_V_G_
from . import S__i_l_f
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py b/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py
index e935313a18c..18d4bc64187 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_c_m_a_p.py
@@ -204,6 +204,14 @@ class table__c_m_a_p(DefaultTable.DefaultTable):
def compile(self, ttFont):
self.tables.sort() # sort according to the spec; see CmapSubtable.__lt__()
+ keys = [(t.platformID, t.platEncID, t.language) for t in self.tables]
+ seen = set()
+ duplicates = {k for k in keys if k in seen or seen.add(k)}
+ if duplicates:
+ raise ValueError(
+ "cmap subtables have duplicate (platformID, platEncID, language) "
+ f"entries, which the OpenType spec does not allow: {duplicates}"
+ )
numSubTables = len(self.tables)
totalOffset = 4 + 8 * numSubTables
data = struct.pack(">HH", self.tableVersion, numSubTables)
@@ -371,13 +379,11 @@ class CmapSubtable(object):
getattr(self, "platformID", None),
getattr(self, "platEncID", None),
getattr(self, "language", None),
- self.__dict__,
)
otherTuple = (
getattr(other, "platformID", None),
getattr(other, "platEncID", None),
getattr(other, "language", None),
- other.__dict__,
)
return selfTuple < otherTuple
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py b/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py
index f2536cb288a..a7e61d01347 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_f_v_a_r.py
@@ -51,8 +51,6 @@ class table__f_v_a_r(DefaultTable.DefaultTable):
See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6fvar.html
"""
- dependencies = ["name"]
-
def __init__(self, tag=None):
DefaultTable.DefaultTable.__init__(self, tag)
self.axes = []
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py
index 3dea653baa0..ffc7eaadbc7 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_g_l_y_f.py
@@ -84,8 +84,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
"""
- dependencies = ["fvar"]
-
# this attribute controls the amount of padding applied to glyph data upon compile.
# Glyph lenghts are aligned to multiples of the specified value.
# Allowed values are (0, 1, 2, 4). '0' means no padding; '1' (default) also means
@@ -93,9 +91,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
padding = 1
def decompile(self, data, ttFont):
- self.axisTags = (
- [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
- )
loca = ttFont["loca"]
pos = int(loca[0])
nextPos = 0
@@ -135,10 +130,6 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
def compile(self, ttFont):
optimizeSpeed = ttFont.cfg[ttLib.OPTIMIZE_FONT_SPEED]
-
- self.axisTags = (
- [axis.axisTag for axis in ttFont["fvar"].axes] if "fvar" in ttFont else []
- )
if not hasattr(self, "glyphOrder"):
self.glyphOrder = ttFont.getGlyphOrder()
padding = self.padding
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/_t_r_a_k.py b/contrib/python/fonttools/fontTools/ttLib/tables/_t_r_a_k.py
index b0e8e19e08a..c49509e6fd1 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/_t_r_a_k.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/_t_r_a_k.py
@@ -65,8 +65,6 @@ class table__t_r_a_k(DefaultTable.DefaultTable):
See also https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6trak.html
"""
- dependencies = ["name"]
-
def compile(self, ttFont):
dataList = []
offset = TRAK_HEADER_FORMAT_SIZE
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py
index 75bfd7f6ca5..f2ba809b215 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/otConverters.py
@@ -430,6 +430,8 @@ class NameID(UShort):
xmlWriter.write(" ")
if name:
xmlWriter.comment(name)
+ elif value == 0xFFFF:
+ xmlWriter.comment("None")
else:
xmlWriter.comment("missing from name table")
log.warning("name id %d missing from name table" % value)
diff --git a/contrib/python/fonttools/fontTools/ttLib/tables/ttProgram.py b/contrib/python/fonttools/fontTools/ttLib/tables/ttProgram.py
index 32a4ec8b20f..41ea098a86f 100644
--- a/contrib/python/fonttools/fontTools/ttLib/tables/ttProgram.py
+++ b/contrib/python/fonttools/fontTools/ttLib/tables/ttProgram.py
@@ -157,7 +157,7 @@ instructions = [
# fmt: on
-def bitRepr(value, bits):
+def bitRepr(value: int, bits: int) -> str:
s = ""
for i in range(bits):
s = "01"[value & 0x1] + s
@@ -168,7 +168,9 @@ def bitRepr(value, bits):
_mnemonicPat = re.compile(r"[A-Z][A-Z0-9]*$")
-def _makeDict(instructionList):
+def _makeDict(
+ instructionList: list[tuple[int, str, int, str, int, int]],
+) -> tuple[dict, dict]:
opcodeDict = {}
mnemonicDict = {}
for op, mnemonic, argBits, name, pops, pushes in instructionList:
@@ -188,10 +190,10 @@ opcodeDict, mnemonicDict = _makeDict(instructions)
class tt_instructions_error(Exception):
- def __init__(self, error):
+ def __init__(self, error: str) -> None:
self.error = error
- def __str__(self):
+ def __str__(self) -> str:
return "TT instructions error: %s" % repr(self.error)
@@ -209,14 +211,14 @@ _indentRE = re.compile(r"^FDEF|IF|ELSE\[ \]\t.+")
_unindentRE = re.compile(r"^ELSE|ENDF|EIF\[ \]\t.+")
-def _skipWhite(data, pos):
+def _skipWhite(data: str, pos: int) -> int:
m = _whiteRE.match(data, pos)
newPos = m.regs[0][1]
assert newPos >= pos
return newPos
-class Program(object):
+class Program:
def __init__(self) -> None:
pass
@@ -225,7 +227,7 @@ class Program(object):
if hasattr(self, "assembly"):
del self.assembly
- def fromAssembly(self, assembly: List[str] | str) -> None:
+ def fromAssembly(self, assembly: list[str] | str) -> None:
if isinstance(assembly, list):
self.assembly = assembly
elif isinstance(assembly, str):
diff --git a/contrib/python/fonttools/fontTools/ttLib/ttFont.py b/contrib/python/fonttools/fontTools/ttLib/ttFont.py
index 4407e567a36..925250511d7 100644
--- a/contrib/python/fonttools/fontTools/ttLib/ttFont.py
+++ b/contrib/python/fonttools/fontTools/ttLib/ttFont.py
@@ -41,8 +41,6 @@ if TYPE_CHECKING:
E_B_L_C_,
F_F_T_M_,
G_D_E_F_,
- G_M_A_P_,
- G_P_K_G_,
G_P_O_S_,
G_S_U_B_,
G_V_A_R_,
@@ -50,9 +48,7 @@ if TYPE_CHECKING:
J_S_T_F_,
L_T_S_H_,
M_A_T_H_,
- M_E_T_A_,
M_V_A_R_,
- S_I_N_G_,
S_T_A_T_,
S_V_G_,
T_S_I__0,
@@ -442,9 +438,9 @@ class TTFont(object):
"""Export the font as TTX (an XML-based text file), or as a series of text
files when splitTables is true. In the latter case, the 'fileOrPath'
argument should be a path to a directory.
- The 'tables' argument must either be false (dump all tables) or a
- list of tables to dump. The 'skipTables' argument may be a list of tables
- to skip, but only when the 'tables' argument is false.
+ The 'tables' argument must either be falsy (None or empty list, meaning
+ dump all tables) or a non-empty list of tables to dump. The 'skipTables'
+ argument may be a list of tables to skip, but only when 'tables' is falsy.
"""
writer = xmlWriter.XMLWriter(fileOrPath, newlinestr=newlinestr)
@@ -647,10 +643,6 @@ class TTFont(object):
@overload
def __getitem__(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_: ...
@overload
- def __getitem__(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_: ...
- @overload
- def __getitem__(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_: ...
- @overload
def __getitem__(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_: ...
@overload
def __getitem__(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_: ...
@@ -665,12 +657,8 @@ class TTFont(object):
@overload
def __getitem__(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_: ...
@overload
- def __getitem__(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_: ...
- @overload
def __getitem__(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_: ...
@overload
- def __getitem__(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_: ...
- @overload
def __getitem__(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_: ...
@overload
def __getitem__(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_: ...
@@ -879,10 +867,6 @@ class TTFont(object):
@overload
def get(self, tag: Literal["GDEF"]) -> G_D_E_F_.table_G_D_E_F_ | None: ...
@overload
- def get(self, tag: Literal["GMAP"]) -> G_M_A_P_.table_G_M_A_P_ | None: ...
- @overload
- def get(self, tag: Literal["GPKG"]) -> G_P_K_G_.table_G_P_K_G_ | None: ...
- @overload
def get(self, tag: Literal["GPOS"]) -> G_P_O_S_.table_G_P_O_S_ | None: ...
@overload
def get(self, tag: Literal["GSUB"]) -> G_S_U_B_.table_G_S_U_B_ | None: ...
@@ -897,12 +881,8 @@ class TTFont(object):
@overload
def get(self, tag: Literal["MATH"]) -> M_A_T_H_.table_M_A_T_H_ | None: ...
@overload
- def get(self, tag: Literal["META"]) -> M_E_T_A_.table_M_E_T_A_ | None: ...
- @overload
def get(self, tag: Literal["MVAR"]) -> M_V_A_R_.table_M_V_A_R_ | None: ...
@overload
- def get(self, tag: Literal["SING"]) -> S_I_N_G_.table_S_I_N_G_ | None: ...
- @overload
def get(self, tag: Literal["STAT"]) -> S_T_A_T_.table_S_T_A_T_ | None: ...
@overload
def get(self, tag: Literal["SVG "]) -> S_V_G_.table_S_V_G_ | None: ...
diff --git a/contrib/python/fonttools/fontTools/ufoLib/glifLib.py b/contrib/python/fonttools/fontTools/ufoLib/glifLib.py
index 040c31c4990..c9929b2f69b 100644
--- a/contrib/python/fonttools/fontTools/ufoLib/glifLib.py
+++ b/contrib/python/fonttools/fontTools/ufoLib/glifLib.py
@@ -1230,9 +1230,6 @@ def _readGlyphFromTreeFormat1(
unicodes = []
haveSeenAdvance = haveSeenOutline = haveSeenLib = haveSeenNote = False
for element in tree:
- if glyphObject is None:
- continue
-
if element.tag == "outline":
if validate:
if haveSeenOutline:
@@ -1245,6 +1242,10 @@ def _readGlyphFromTreeFormat1(
raise GlifLibError("Invalid outline structure.")
haveSeenOutline = True
buildOutlineFormat1(glyphObject, pointPen, element, validate)
+ elif glyphObject is None:
+ # Skip remaining elements if no glyphObject, but outline is still
+ # processed above to allow drawing via pointPen without a glyphObject.
+ continue
elif element.tag == "advance":
if validate and haveSeenAdvance:
raise GlifLibError("The advance element occurs more than once.")
@@ -1299,8 +1300,6 @@ def _readGlyphFromTreeFormat2(
)
identifiers: set[str] = set()
for element in tree:
- if glyphObject is None:
- continue
if element.tag == "outline":
if validate:
if haveSeenOutline:
@@ -1316,6 +1315,10 @@ def _readGlyphFromTreeFormat2(
buildOutlineFormat2(
glyphObject, pointPen, element, identifiers, validate
)
+ elif glyphObject is None:
+ # Skip remaining elements if no glyphObject, but outline is still
+ # processed above to allow drawing via pointPen without a glyphObject.
+ continue
elif element.tag == "advance":
if validate and haveSeenAdvance:
raise GlifLibError("The advance element occurs more than once.")
diff --git a/contrib/python/fonttools/fontTools/ufoLib/validators.py b/contrib/python/fonttools/fontTools/ufoLib/validators.py
index 54c65fb6cf6..baed5688d04 100644
--- a/contrib/python/fonttools/fontTools/ufoLib/validators.py
+++ b/contrib/python/fonttools/fontTools/ufoLib/validators.py
@@ -623,19 +623,8 @@ def guidelineValidator(value: Any) -> bool:
"""
if not genericDictValidator(value, _guidelineDictPrototype):
return False
- x = value.get("x")
- y = value.get("y")
+
angle = value.get("angle")
- # x or y must be present
- if x is None and y is None:
- return False
- # if x or y are None, angle must not be present
- if x is None or y is None:
- if angle is not None:
- return False
- # if x and y are defined, angle must be defined
- if x is not None and y is not None and angle is None:
- return False
# angle must be between 0 and 360
if angle is not None:
if angle < 0:
diff --git a/contrib/python/fonttools/fontTools/varLib/__init__.py b/contrib/python/fonttools/fontTools/varLib/__init__.py
index c19bd151588..34bf8fc2015 100644
--- a/contrib/python/fonttools/fontTools/varLib/__init__.py
+++ b/contrib/python/fonttools/fontTools/varLib/__init__.py
@@ -382,25 +382,7 @@ def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
continue
var = TupleVariation(support, delta)
if optimize:
- delta_opt = iup_delta_optimize(
- delta, origCoords, endPts, tolerance=tolerance
- )
-
- if None in delta_opt:
- # Use "optimized" version only if smaller...
- var_opt = TupleVariation(support, delta_opt)
-
- axis_tags = sorted(
- support.keys()
- ) # Shouldn't matter that this is different from fvar...?
- tupleData, auxData = var.compile(axis_tags)
- unoptimized_len = len(tupleData) + len(auxData)
- tupleData, auxData = var_opt.compile(axis_tags)
- optimized_len = len(tupleData) + len(auxData)
-
- if optimized_len < unoptimized_len:
- var = var_opt
-
+ var.optimize(origCoords, endPts, tolerance=tolerance)
gvar.variations[glyph].append(var)
@@ -768,7 +750,7 @@ def _add_MVAR(font, masterModel, master_ttfs, axisTags):
# and unilaterally/arbitrarily define a sentinel value to distinguish the case
# when a post table is present in a given master simply because that's where
# the glyph names in TrueType must be stored, but the underline values are not
- # meant to be used for building MVAR's deltas. The value of -0x8000 (-36768)
+ # meant to be used for building MVAR's deltas. The value of -0x8000 (-32768)
# the minimum FWord (int16) value, was chosen for its unlikelyhood to appear
# in real-world underline position/thickness values.
specialTags = {"unds": -0x8000, "undo": -0x8000}
@@ -864,7 +846,6 @@ def _merge_OTL(font, model, master_fonts, axisTags):
GDEF = font["GDEF"].table
assert GDEF.Version <= 0x00010002
except KeyError:
- font["GDEF"] = newTable("GDEF")
GDEFTable = font["GDEF"] = newTable("GDEF")
GDEF = GDEFTable.table = ot.GDEF()
GDEF.GlyphClassDef = None
@@ -1154,7 +1135,6 @@ def drop_implied_oncurve_points(*masters: TTFont) -> int:
https://developer.apple.com/fonts/TrueType-Reference-Manual/RM01/Chap1.html
"""
- count = 0
glyph_masters = defaultdict(list)
# multiple DS source may point to the same TTFont object and we want to
# avoid processing the same glyph twice as they are modified in-place
@@ -1569,6 +1549,7 @@ def main(args=None):
filename = vf.name + ".{ext}"
vf_name_to_output_path[vf.name] = os.path.join(output_dir, filename)
+ vf_names_to_build = {vf.name for vf in vfs_to_build}
finder = MasterFinder(options.master_finder)
vfs = build_many(
@@ -1576,6 +1557,7 @@ def main(args=None):
finder,
exclude=options.exclude,
optimize=options.optimize,
+ skip_vf=lambda name: name not in vf_names_to_build,
colr_layer_reuse=options.colr_layer_reuse,
drop_implied_oncurves=options.drop_implied_oncurves,
)
diff --git a/contrib/python/fonttools/fontTools/varLib/avar/build.py b/contrib/python/fonttools/fontTools/varLib/avar/build.py
index e70925cbd74..d9706705db8 100644
--- a/contrib/python/fonttools/fontTools/varLib/avar/build.py
+++ b/contrib/python/fonttools/fontTools/varLib/avar/build.py
@@ -9,8 +9,8 @@ def build(font, designspace_file):
ds = load_designspace(designspace_file, require_sources=False)
if not "fvar" in font:
- # if "name" not in font:
- font["name"] = newTable("name")
+ if "name" not in font:
+ font["name"] = newTable("name")
_add_fvar(font, ds.axes, ds.instances)
axisTags = [a.axisTag for a in font["fvar"].axes]
diff --git a/contrib/python/fonttools/fontTools/varLib/avar/map.py b/contrib/python/fonttools/fontTools/varLib/avar/map.py
index 68eed6c83eb..a5331f109b8 100644
--- a/contrib/python/fonttools/fontTools/varLib/avar/map.py
+++ b/contrib/python/fonttools/fontTools/varLib/avar/map.py
@@ -16,6 +16,9 @@ def map(
fvar = font["fvar"]
axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes}
+ unknownAxes = sorted(tag for tag in location if tag not in axes)
+ if unknownAxes:
+ raise ValueError(f"Unknown axis tag(s): {', '.join(unknownAxes)}")
if not inputNormalized:
location = {
@@ -83,17 +86,30 @@ def main(args=None):
if "fvar" not in font:
parser.error(f"Font '{options.font}' does not contain an 'fvar' table.")
- location = {
- tag: float(value) for tag, value in (item.split("=") for item in options.coords)
- }
+ location = {}
+ for item in options.coords:
+ tag, sep, value = item.partition("=")
+ if not sep or not tag or not value:
+ parser.error(
+ f"Invalid coordinate {item!r}. Expected AXIS=value, e.g. wght=500"
+ )
+ try:
+ location[tag] = float(value)
+ except ValueError:
+ parser.error(
+ f"Invalid coordinate value in {item!r}. Expected a number after '='"
+ )
- mapped = map(
- font,
- location,
- inputNormalized=options.i,
- outputNormalized=options.o,
- dropZeroes=not options.f,
- )
+ try:
+ mapped = map(
+ font,
+ location,
+ inputNormalized=options.i,
+ outputNormalized=options.o,
+ dropZeroes=not options.f,
+ )
+ except ValueError as e:
+ parser.error(str(e))
assert mapped is not None
for tag in mapped:
diff --git a/contrib/python/fonttools/fontTools/varLib/avar/unbuild.py b/contrib/python/fonttools/fontTools/varLib/avar/unbuild.py
index d592bd73104..ddf677902ce 100644
--- a/contrib/python/fonttools/fontTools/varLib/avar/unbuild.py
+++ b/contrib/python/fonttools/fontTools/varLib/avar/unbuild.py
@@ -81,7 +81,7 @@ def mappings_from_avar(font, denormalize=True):
axisTags = [a.axisTag for a in fvarAxes]
axisIndexes = {a.axisTag: i for i, a in enumerate(fvarAxes)}
if "avar" not in font:
- return {}, {}
+ return {}, []
avar = font["avar"]
axisMaps = {
tag: seg
@@ -185,7 +185,10 @@ def unbuild(font, f=sys.stdout):
if "name" in font:
name = font["name"]
- axisNames = {axis.axisTag: name.getDebugName(axis.axisNameID) for axis in axes}
+ axisNames = {
+ axis.axisTag: name.getDebugName(axis.axisNameID) or axis.axisTag
+ for axis in axes
+ }
else:
axisNames = {a.axisTag: a.axisTag for a in axes}
diff --git a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py
index 2993bf38bfa..dc6bf18e586 100644
--- a/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py
+++ b/contrib/python/fonttools/fontTools/varLib/instancer/__init__.py
@@ -954,11 +954,8 @@ def _instantiateGvarGlyph(
# preserve backwards compatibility.
# See 0010a3cd9aa25f84a3a6250dafb119743d32aa40
coordinates.toInt()
-
- isComposite = glyf[glyphname].isComposite()
-
for var in tupleVarStore:
- var.optimize(coordinates, endPts, isComposite=isComposite)
+ var.optimize(coordinates, endPts)
def instantiateGvarGlyph(varfont, glyphname, axisLimits, optimize=True):
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatable.py b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
index c5d7ecf525e..64b3a6d1968 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatable.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatable.py
@@ -116,12 +116,12 @@ class Glyph:
# Add mirrored rotations
add_isomorphisms(points.value, isomorphisms, True)
- def draw(self, pen, countor_idx=None):
- if countor_idx is None:
+ def draw(self, pen, contour_idx=None):
+ if contour_idx is None:
for contour in self.recordings:
contour.draw(pen)
else:
- self.recordings[countor_idx].draw(pen)
+ self.recordings[contour_idx].draw(pen)
def test_gen(
@@ -168,7 +168,7 @@ def test_gen(
for glyph_name in glyphs:
log.info("Testing glyph %s", glyph_name)
allGlyphs = [Glyph(glyph_name, glyphset) for glyphset in glyphsets]
- if len([1 for glyph in allGlyphs if glyph is not None]) <= 1:
+ if len([1 for glyph in allGlyphs if not glyph.doesnt_exist]) <= 1:
continue
for master_idx, (glyph, glyphset, name) in enumerate(
zip(allGlyphs, glyphsets, names)
@@ -219,8 +219,8 @@ def test_gen(
# Basic compatibility checks
#
- m1 = glyph0.nodeTypes
- m0 = glyph1.nodeTypes
+ m0 = glyph0.nodeTypes
+ m1 = glyph1.nodeTypes
if len(m0) != len(m1):
yield (
glyph_name,
@@ -387,7 +387,7 @@ def test_gen(
):
if overweight:
expectedSize = max(size0, size1)
- continue
+ continue # disabled
else:
expectedSize = sqrt(size0 * size1)
@@ -467,7 +467,7 @@ def test_gen(
if pt0_prev[1] and pt1_prev[1]:
# At least one off-curve is required
continue
- if pt0_prev[1] and pt1_prev[1]:
+ if pt0_next[1] and pt1_next[1]:
# At least one off-curve is required
continue
@@ -826,7 +826,7 @@ def main(args=None):
# The spec says vsindex can only appear once and must be the first
# operator in the charstring, but we support multiple.
# https://github.com/harfbuzz/boring-expansion-spec/issues/158
- for op in enumerate(cs.program):
+ for op in cs.program:
if op == "blend":
vsindices.add(vsindex)
elif op == "vsindex":
@@ -1048,7 +1048,7 @@ def main(args=None):
)
elif p["type"] == InterpolatableProblem.NODE_INCOMPATIBILITY:
print(
- " Node %o incompatible in path %i: %s in %s, %s in %s"
+ " Node %d incompatible in path %i: %s in %s, %s in %s"
% (
p["node"],
p["path"],
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py b/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py
index 67b9ea27c68..bbe072c83cf 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatableHelpers.py
@@ -5,7 +5,6 @@ from fontTools.pens.recordingPen import RecordingPen, DecomposingRecordingPen
from fontTools.misc.transform import Transform
from collections import defaultdict, deque
from math import sqrt, copysign, atan2, pi
-from enum import Enum
import itertools
import logging
@@ -42,7 +41,7 @@ class InterpolatableProblem:
def sort_problems(problems):
- """Sort problems by severity, then by glyph name, then by problem message."""
+ """Sort problem groups by their most severe problem type."""
return dict(
sorted(
problems.items(),
@@ -66,7 +65,6 @@ def rot_list(l, k):
class PerContourPen(BasePen):
def __init__(self, Pen, glyphset=None):
BasePen.__init__(self, glyphset)
- self._glyphset = glyphset
self._Pen = Pen
self._pen = None
self.value = []
@@ -212,8 +210,6 @@ def contour_vector_from_stats(stats):
def matching_for_vectors(m0, m1):
n = len(m0)
- identity_matching = list(range(n))
-
costs = [[vdiff_hypot2(v0, v1) for v1 in m1] for v0 in m0]
(
matching,
@@ -307,9 +303,9 @@ def find_parents_and_order(glyphsets, locations, *, discrete_axes=set()):
if all(v == 0 for k, v in l.items() if k not in discrete_axes)
]
if bases:
- logging.info("Found %s base masters: %s", len(bases), bases)
+ log.info("Found %s base masters: %s", len(bases), bases)
else:
- logging.warning("No base master location found")
+ log.warning("No base master location found")
# Form a minimum spanning tree of the locations
try:
@@ -362,38 +358,3 @@ def find_parents_and_order(glyphsets, locations, *, discrete_axes=set()):
log.info("Parents: %s", parents)
log.info("Order: %s", order)
return parents, order
-
-
-def transform_from_stats(stats, inverse=False):
- # https://cookierobotics.com/007/
- a = stats.varianceX
- b = stats.covariance
- c = stats.varianceY
-
- delta = (((a - c) * 0.5) ** 2 + b * b) ** 0.5
- lambda1 = (a + c) * 0.5 + delta # Major eigenvalue
- lambda2 = (a + c) * 0.5 - delta # Minor eigenvalue
- theta = atan2(lambda1 - a, b) if b != 0 else (pi * 0.5 if a < c else 0)
- trans = Transform()
-
- if lambda2 < 0:
- # XXX This is a hack.
- # The problem is that the covariance matrix is singular.
- # This happens when the contour is a line, or a circle.
- # In that case, the covariance matrix is not a good
- # representation of the contour.
- # We should probably detect this earlier and avoid
- # computing the covariance matrix in the first place.
- # But for now, we just avoid the division by zero.
- lambda2 = 0
-
- if inverse:
- trans = trans.translate(-stats.meanX, -stats.meanY)
- trans = trans.rotate(-theta)
- trans = trans.scale(1 / sqrt(lambda1), 1 / sqrt(lambda2))
- else:
- trans = trans.scale(sqrt(lambda1), sqrt(lambda2))
- trans = trans.rotate(theta)
- trans = trans.translate(stats.meanX, stats.meanY)
-
- return trans
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py b/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py
index 3c206c6ee2e..6685cd763a0 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatablePlot.py
@@ -13,12 +13,7 @@ from fontTools.pens.pointPen import (
PointToSegmentPen,
ReverseContourPointPen,
)
-from fontTools.varLib.interpolatableHelpers import (
- PerContourOrComponentPen,
- SimpleRecordingPointPen,
-)
from itertools import cycle
-from functools import wraps
from io import BytesIO
import cairo
import math
@@ -141,7 +136,6 @@ class InterpolatablePlot:
):
pad = self.pad
width = self.width - 3 * self.pad
- height = self.height - 2 * self.pad
x = y = pad
self.draw_label(
@@ -163,7 +157,8 @@ class InterpolatablePlot:
y += self.font_size + self.pad
try:
- h = hashlib.sha1(open(file, "rb").read()).hexdigest()
+ with open(file, "rb") as f:
+ h = hashlib.sha1(f.read()).hexdigest()
self.draw_label("sha1: %s" % h, x=x + pad, y=y, width=width)
y += self.font_size
except IsADirectoryError:
@@ -359,12 +354,12 @@ class InterpolatablePlot:
y += self.title_font_size
glyphs_per_problem = defaultdict(set)
- for glyphname, problems in sorted(problems.items()):
- for problem in problems:
+ for glyphname, glyph_problems in sorted(problems.items()):
+ for problem in glyph_problems:
glyphs_per_problem[problem["type"]].add(glyphname)
- if "nothing" in glyphs_per_problem:
- del glyphs_per_problem["nothing"]
+ if InterpolatableProblem.NOTHING in glyphs_per_problem:
+ del glyphs_per_problem[InterpolatableProblem.NOTHING]
for problem_type in sorted(
glyphs_per_problem, key=lambda x: InterpolatableProblem.severity[x]
@@ -867,7 +862,7 @@ class InterpolatablePlot:
if scale is None:
scale = self.panel_width / glyph_width
else:
- scale = min(scale, self.panel_height / glyph_height)
+ scale = min(scale, self.panel_width / glyph_width)
if glyph_height:
if scale is None:
scale = self.panel_height / glyph_height
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py b/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py
index 36885297984..f593e2bd4db 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatableTestContourOrder.py
@@ -8,7 +8,7 @@ def test_contour_order(glyph0, glyph1):
# We try matching both the StatisticsControlPen vector
# and the StatisticsPen vector.
#
- # If either method found a identity matching, accept it.
+ # If either method found an identity matching, accept it.
# This is crucial for fonts like Kablammo[MORF].ttf and
# Nabla[EDPT,EHLT].ttf, since they really confuse the
# StatisticsPen vector because of their area=0 contours.
@@ -43,28 +43,27 @@ def test_contour_order(glyph0, glyph1):
# test will fix them.
#
# Reverse the sign of the area (0); the rest stay the same.
- if not done:
- m1ControlReversed = [(-m[0],) + m[1:] for m in m1Control]
- (
- matching_control_reversed,
- matching_cost_control_reversed,
- identity_cost_control_reversed,
- ) = matching_for_vectors(m0Control, m1ControlReversed)
- done = matching_cost_control_reversed == identity_cost_control_reversed
+ m1ControlReversed = [(-m[0],) + m[1:] for m in m1Control]
+ (
+ matching_control_reversed,
+ matching_cost_control_reversed,
+ identity_cost_control_reversed,
+ ) = matching_for_vectors(m0Control, m1ControlReversed)
+ done = matching_cost_control_reversed == identity_cost_control_reversed
if not done:
m1GreenReversed = [(-m[0],) + m[1:] for m in m1Green]
(
- matching_control_reversed,
+ matching_green_reversed,
matching_cost_green_reversed,
identity_cost_green_reversed,
) = matching_for_vectors(m0Green, m1GreenReversed)
done = matching_cost_green_reversed == identity_cost_green_reversed
if not done:
- # Otherwise, use the worst of the two matchings.
+ # Otherwise, use the best of the two matchings.
if (
- matching_cost_control / identity_cost_control
- < matching_cost_green / identity_cost_green
+ matching_cost_control * identity_cost_green
+ < matching_cost_green * identity_cost_control
):
matching = matching_control
matching_cost = matching_cost_control
diff --git a/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py b/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py
index e91dacf288b..6c619ebe689 100644
--- a/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py
+++ b/contrib/python/fonttools/fontTools/varLib/interpolatableTestStartingPoint.py
@@ -45,8 +45,8 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching):
# This is a 2x2 matrix.
transforms = []
for vector in (m0Vectors[ix], m1Vectors[ix]):
- meanX = vector[1]
- meanY = vector[2]
+ # meanX = vector[1]
+ # meanY = vector[2]
stddevX = vector[3] * 0.5
stddevY = vector[4] * 0.5
correlation = vector[5]
@@ -68,6 +68,8 @@ def test_starting_point(glyph0, glyph1, ix, tolerance, matching):
# we are doing anyway...
# trans = trans.translate(meanX, meanY)
trans = trans.rotate(theta)
+ if lambda2 < 0:
+ lambda2 = 0
trans = trans.scale(sqrt(lambda1), sqrt(lambda2))
transforms.append(trans)
diff --git a/contrib/python/fonttools/fontTools/varLib/models.py b/contrib/python/fonttools/fontTools/varLib/models.py
index 1d836aeb163..05f92c252e0 100644
--- a/contrib/python/fonttools/fontTools/varLib/models.py
+++ b/contrib/python/fonttools/fontTools/varLib/models.py
@@ -10,6 +10,7 @@ __all__ = [
"VariationModel",
]
+from collections.abc import Mapping
from typing import TYPE_CHECKING
from fontTools.misc.roundTools import noRound
from .errors import VariationModelError
@@ -274,6 +275,8 @@ class VariationModel(object):
def __init__(
self, locations, axisOrder=None, extrapolate=False, *, axisRanges=None
):
+ locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
+
if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
raise VariationModelError("Locations must be unique.")
@@ -287,8 +290,6 @@ class VariationModel(object):
allAxes = {axis for loc in locations for axis in loc.keys()}
axisRanges = {axis: (-1, 1) for axis in allAxes}
self.axisRanges = axisRanges
-
- locations = [{k: v for k, v in loc.items() if v != 0.0} for loc in locations]
keyFunc = self.getMasterLocationsSortKeyFunc(
locations, axisOrder=self.axisOrder
)
@@ -313,7 +314,12 @@ class VariationModel(object):
key = tuple(v is not None for v in items)
subModel = self._subModels.get(key)
if subModel is None:
- subModel = VariationModel(subList(key, self.origLocations), self.axisOrder)
+ subModel = VariationModel(
+ subList(key, self.origLocations),
+ self.axisOrder,
+ extrapolate=self.extrapolate,
+ axisRanges=self.axisRanges,
+ )
self._subModels[key] = subModel
return subModel, subList(key, items)
@@ -401,7 +407,7 @@ class VariationModel(object):
locAxes = set(region.keys())
# Walk over previous masters now
for prev_region in regions[:i]:
- # Master with different axes do not participte
+ # Master with different axes do not participate
if set(prev_region.keys()) != locAxes:
continue
# If it's NOT in the current box, it does not participate
@@ -572,7 +578,7 @@ class VariationModel(object):
return self.interpolateFromDeltasAndScalars(deltas, scalars)
-def piecewiseLinearMap(v, mapping):
+def piecewiseLinearMap(v: float, mapping: Mapping[float, float]) -> float:
keys = mapping.keys()
if not keys:
return v
diff --git a/contrib/python/fonttools/ya.make b/contrib/python/fonttools/ya.make
index 3f8f9d4e88a..da4de1fc839 100644
--- a/contrib/python/fonttools/ya.make
+++ b/contrib/python/fonttools/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(4.61.1)
+VERSION(4.62.0)
LICENSE(MIT)
@@ -49,6 +49,11 @@ PY_SRCS(
fontTools/designspaceLib/split.py
fontTools/designspaceLib/statNames.py
fontTools/designspaceLib/types.py
+ fontTools/diff/__init__.py
+ fontTools/diff/__main__.py
+ fontTools/diff/color.py
+ fontTools/diff/diff.py
+ fontTools/diff/utils.py
fontTools/encodings/MacRoman.py
fontTools/encodings/StandardEncoding.py
fontTools/encodings/__init__.py
@@ -202,8 +207,6 @@ PY_SRCS(
fontTools/ttLib/tables/F_F_T_M_.py
fontTools/ttLib/tables/F__e_a_t.py
fontTools/ttLib/tables/G_D_E_F_.py
- fontTools/ttLib/tables/G_M_A_P_.py
- fontTools/ttLib/tables/G_P_K_G_.py
fontTools/ttLib/tables/G_P_O_S_.py
fontTools/ttLib/tables/G_S_U_B_.py
fontTools/ttLib/tables/G_V_A_R_.py
@@ -213,10 +216,8 @@ PY_SRCS(
fontTools/ttLib/tables/J_S_T_F_.py
fontTools/ttLib/tables/L_T_S_H_.py
fontTools/ttLib/tables/M_A_T_H_.py
- fontTools/ttLib/tables/M_E_T_A_.py
fontTools/ttLib/tables/M_V_A_R_.py
fontTools/ttLib/tables/O_S_2f_2.py
- fontTools/ttLib/tables/S_I_N_G_.py
fontTools/ttLib/tables/S_T_A_T_.py
fontTools/ttLib/tables/S_V_G_.py
fontTools/ttLib/tables/S__i_l_f.py