Merge remote-tracking branch 'origin/main' into mutator-trivial-fixes
This commit is contained in:
commit
53b13263e9
2
.github/workflows/publish.yml
vendored
2
.github/workflows/publish.yml
vendored
@ -7,7 +7,7 @@ on:
|
||||
push:
|
||||
# Sequence of patterns matched against refs/tags
|
||||
tags:
|
||||
- 'v*' # Push events to matching v*, i.e. v1.0, v20.15.10
|
||||
- '*.*.*' # e.g. 1.0.0 or 20.15.10
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
|
9
.github/workflows/test.yml
vendored
9
.github/workflows/test.yml
vendored
@ -2,13 +2,15 @@ name: Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
branches: [main]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
# https://github.community/t/github-actions-does-not-respect-skip-ci/17325/8
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.x
|
||||
@ -22,6 +24,7 @@ jobs:
|
||||
|
||||
test:
|
||||
runs-on: ${{ matrix.platform }}
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: [3.6, 3.7, 3.8, 3.9]
|
||||
@ -61,6 +64,7 @@ jobs:
|
||||
|
||||
test-cython:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python 3.x
|
||||
@ -74,6 +78,7 @@ jobs:
|
||||
|
||||
test-pypy3:
|
||||
runs-on: ubuntu-latest
|
||||
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Python pypy3
|
||||
|
@ -5,4 +5,5 @@ ttFont
|
||||
.. automodule:: fontTools.ttLib.ttFont
|
||||
:inherited-members:
|
||||
:members:
|
||||
:undoc-members:
|
||||
:undoc-members:
|
||||
:private-members:
|
||||
|
@ -1,6 +1,94 @@
|
||||
######
|
||||
varLib
|
||||
######
|
||||
##################################
|
||||
varLib: OpenType Variation Support
|
||||
##################################
|
||||
|
||||
The ``fontTools.varLib`` package contains a number of classes and routines
|
||||
for handling, building and interpolating variable font data. These routines
|
||||
rely on a common set of concepts, many of which are equivalent to concepts
|
||||
in the OpenType Specification, but some of which are unique to ``varLib``.
|
||||
|
||||
Terminology
|
||||
-----------
|
||||
|
||||
axis
|
||||
"A designer-determined variable in a font face design that can be used to
|
||||
derive multiple, variant designs within a family." (OpenType Specification)
|
||||
An axis has a minimum value, a maximum value and a default value.
|
||||
|
||||
designspace
|
||||
The n-dimensional space formed by the font's axes. (OpenType Specification
|
||||
calls this the "design-variation space")
|
||||
|
||||
scalar
|
||||
A value which is able to be varied at different points in the designspace:
|
||||
for example, the horizontal advance width of the glyph "a" is a scalar.
|
||||
However, see also *support scalar* below.
|
||||
|
||||
default location
|
||||
A point in the designspace whose coordinates are the default value of
|
||||
all axes.
|
||||
|
||||
location
|
||||
A point in the designspace, specified as a set of coordinates on one or
|
||||
more axes. In the context of ``varLib``, a location is a dictionary with
|
||||
the keys being the axis tags and the values being the coordinates on the
|
||||
respective axis. A ``varLib`` location dictionary may be "sparse", in the
|
||||
sense that axes defined in the font may be omitted from the location's
|
||||
coordinates, in which case the default value of the axis is assumed.
|
||||
For example, given a font having a ``wght`` axis ranging from 200-1000
|
||||
with default 400, and a ``wdth`` axis ranging 100-300 with default 150,
|
||||
the location ``{"wdth": 200}`` represents the point ``wght=400,wdth=200``.
|
||||
|
||||
master
|
||||
The value of a scalar at a given location. **Note that this is a
|
||||
considerably more general concept than the usual type design sense of
|
||||
the term "master".**
|
||||
|
||||
normalized location
|
||||
While the range of an axis is determined by its minimum and maximum values
|
||||
as set by the designer, locations are specified internally to the font binary
|
||||
in the range -1 to 1, with 0 being the default, -1 being the minimum and
|
||||
1 being the maximum. A normalized location is one which is scaled to the
|
||||
range (-1,1) on all of its axes. Note that as the range from minimum to
|
||||
default and from default to maximum on a given axis may differ (for
|
||||
example, given ``wght min=200 default=500 max=1000``, the difference
|
||||
between a normalized location -1 of a normalized location of 0 represents a
|
||||
difference of 300 units while the difference between a normalized location
|
||||
of 0 and a normalized location of 1 represents a difference of 700 units),
|
||||
a location is scaled by a different factor depending on whether it is above
|
||||
or below the axis' default value.
|
||||
|
||||
support
|
||||
While designers tend to think in terms of masters - that is, a precise
|
||||
location having a particular value - OpenType Variations specifies the
|
||||
variation of scalars in terms of deltas which are themselves composed of
|
||||
the combined contributions of a set of triangular regions, each having
|
||||
a contribution value of 0 at its minimum value, rising linearly to its
|
||||
full contribution at the *peak* and falling linearly to zero from the
|
||||
peak to the maximum value. The OpenType Specification calls these "regions",
|
||||
while ``varLib`` calls them "supports" (a mathematical term used in real
|
||||
analysis) and expresses them as a dictionary mapping each axis tag to a
|
||||
tuple ``(min, peak, max)``.
|
||||
|
||||
box
|
||||
``varLib`` uses the term "box" to denote the minimum and maximum "corners" of
|
||||
a support, ignoring its peak value.
|
||||
|
||||
delta
|
||||
The term "delta" is used in OpenType Variations in two senses. In the
|
||||
more general sense, a delta is the difference between a scalar at a
|
||||
given location and its value at the default location. Additionally, inside
|
||||
the font, variation data is stored as a mapping between supports and deltas.
|
||||
The delta (in the first sense) is computed by summing the product of the
|
||||
delta of each support by a factor representing the support's contribution
|
||||
at this location (see "support scalar" below).
|
||||
|
||||
support scalar
|
||||
When interpolating a set of variation data, the support scalar represents
|
||||
the scalar multiplier of the support's contribution at this location. For
|
||||
example, the support scalar will be 1 at the support's peak location, and
|
||||
0 below its minimum or above its maximum.
|
||||
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
version = __version__ = "4.17.2.dev0"
|
||||
version = __version__ = "4.21.2.dev0"
|
||||
|
||||
__all__ = ["version", "log", "configLogger"]
|
||||
|
@ -6,6 +6,7 @@ import collections
|
||||
import copy
|
||||
import enum
|
||||
from functools import partial
|
||||
from math import ceil, log
|
||||
from typing import (
|
||||
Any,
|
||||
Dict,
|
||||
@ -24,7 +25,6 @@ from fontTools.misc.fixedTools import fixedToFloat
|
||||
from fontTools.ttLib.tables import C_O_L_R_
|
||||
from fontTools.ttLib.tables import C_P_A_L_
|
||||
from fontTools.ttLib.tables import _n_a_m_e
|
||||
from fontTools.ttLib.tables.otBase import BaseTable
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.ttLib.tables.otTables import (
|
||||
ExtendMode,
|
||||
@ -34,6 +34,12 @@ from fontTools.ttLib.tables.otTables import (
|
||||
VariableInt,
|
||||
)
|
||||
from .errors import ColorLibError
|
||||
from .geometry import round_start_circle_stable_containment
|
||||
from .table_builder import (
|
||||
convertTupleClass,
|
||||
BuildCallback,
|
||||
TableBuilder,
|
||||
)
|
||||
|
||||
|
||||
# TODO move type aliases to colorLib.types?
|
||||
@ -43,21 +49,87 @@ _PaintInput = Union[int, _Kwargs, ot.Paint, Tuple[str, "_PaintInput"]]
|
||||
_PaintInputList = Sequence[_PaintInput]
|
||||
_ColorGlyphsDict = Dict[str, Union[_PaintInputList, _PaintInput]]
|
||||
_ColorGlyphsV0Dict = Dict[str, Sequence[Tuple[str, int]]]
|
||||
_Number = Union[int, float]
|
||||
_ScalarInput = Union[_Number, VariableValue, Tuple[_Number, int]]
|
||||
_ColorStopTuple = Tuple[_ScalarInput, int]
|
||||
_ColorStopInput = Union[_ColorStopTuple, _Kwargs, ot.ColorStop]
|
||||
_ColorStopsList = Sequence[_ColorStopInput]
|
||||
_ExtendInput = Union[int, str, ExtendMode]
|
||||
_CompositeInput = Union[int, str, CompositeMode]
|
||||
_ColorLineInput = Union[_Kwargs, ot.ColorLine]
|
||||
_PointTuple = Tuple[_ScalarInput, _ScalarInput]
|
||||
_AffineTuple = Tuple[
|
||||
_ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput, _ScalarInput
|
||||
]
|
||||
_AffineInput = Union[_AffineTuple, ot.Affine2x3]
|
||||
|
||||
|
||||
MAX_PAINT_COLR_LAYER_COUNT = 255
|
||||
_DEFAULT_ALPHA = VariableFloat(1.0)
|
||||
_MAX_REUSE_LEN = 32
|
||||
|
||||
|
||||
def _beforeBuildPaintVarRadialGradient(paint, source, srcMapFn=lambda v: v):
|
||||
# normalize input types (which may or may not specify a varIdx)
|
||||
x0 = convertTupleClass(VariableFloat, source["x0"])
|
||||
y0 = convertTupleClass(VariableFloat, source["y0"])
|
||||
r0 = convertTupleClass(VariableFloat, source["r0"])
|
||||
x1 = convertTupleClass(VariableFloat, source["x1"])
|
||||
y1 = convertTupleClass(VariableFloat, source["y1"])
|
||||
r1 = convertTupleClass(VariableFloat, source["r1"])
|
||||
|
||||
# TODO apparently no builder_test confirms this works (?)
|
||||
|
||||
# avoid abrupt change after rounding when c0 is near c1's perimeter
|
||||
c = round_start_circle_stable_containment(
|
||||
(x0.value, y0.value), r0.value, (x1.value, y1.value), r1.value
|
||||
)
|
||||
x0, y0 = x0._replace(value=c.centre[0]), y0._replace(value=c.centre[1])
|
||||
r0 = r0._replace(value=c.radius)
|
||||
|
||||
# update source to ensure paint is built with corrected values
|
||||
source["x0"] = srcMapFn(x0)
|
||||
source["y0"] = srcMapFn(y0)
|
||||
source["r0"] = srcMapFn(r0)
|
||||
source["x1"] = srcMapFn(x1)
|
||||
source["y1"] = srcMapFn(y1)
|
||||
source["r1"] = srcMapFn(r1)
|
||||
|
||||
return paint, source
|
||||
|
||||
|
||||
def _beforeBuildPaintRadialGradient(paint, source):
|
||||
return _beforeBuildPaintVarRadialGradient(paint, source, lambda v: v.value)
|
||||
|
||||
|
||||
def _defaultColorIndex():
|
||||
colorIndex = ot.ColorIndex()
|
||||
colorIndex.Alpha = _DEFAULT_ALPHA.value
|
||||
return colorIndex
|
||||
|
||||
|
||||
def _defaultVarColorIndex():
|
||||
colorIndex = ot.VarColorIndex()
|
||||
colorIndex.Alpha = _DEFAULT_ALPHA
|
||||
return colorIndex
|
||||
|
||||
|
||||
def _defaultColorLine():
|
||||
colorLine = ot.ColorLine()
|
||||
colorLine.Extend = ExtendMode.PAD
|
||||
return colorLine
|
||||
|
||||
|
||||
def _defaultVarColorLine():
|
||||
colorLine = ot.VarColorLine()
|
||||
colorLine.Extend = ExtendMode.PAD
|
||||
return colorLine
|
||||
|
||||
|
||||
def _buildPaintCallbacks():
|
||||
return {
|
||||
(
|
||||
BuildCallback.BEFORE_BUILD,
|
||||
ot.Paint,
|
||||
ot.PaintFormat.PaintRadialGradient,
|
||||
): _beforeBuildPaintRadialGradient,
|
||||
(
|
||||
BuildCallback.BEFORE_BUILD,
|
||||
ot.Paint,
|
||||
ot.PaintFormat.PaintVarRadialGradient,
|
||||
): _beforeBuildPaintVarRadialGradient,
|
||||
(BuildCallback.CREATE_DEFAULT, ot.ColorIndex): _defaultColorIndex,
|
||||
(BuildCallback.CREATE_DEFAULT, ot.VarColorIndex): _defaultVarColorIndex,
|
||||
(BuildCallback.CREATE_DEFAULT, ot.ColorLine): _defaultColorLine,
|
||||
(BuildCallback.CREATE_DEFAULT, ot.VarColorLine): _defaultVarColorLine,
|
||||
}
|
||||
|
||||
|
||||
def populateCOLRv0(
|
||||
@ -110,7 +182,6 @@ def buildCOLR(
|
||||
varStore: Optional[ot.VarStore] = None,
|
||||
) -> C_O_L_R_.table_C_O_L_R_:
|
||||
"""Build COLR table from color layers mapping.
|
||||
|
||||
Args:
|
||||
colorGlyphs: map of base glyph name to, either list of (layer glyph name,
|
||||
color palette index) tuples for COLRv0; or a single Paint (dict) or
|
||||
@ -122,7 +193,6 @@ def buildCOLR(
|
||||
glyphMap: a map from glyph names to glyph indices, as returned from
|
||||
TTFont.getReverseGlyphMap(), to optionally sort base records by GID.
|
||||
varStore: Optional ItemVarationStore for deltas associated with v1 layer.
|
||||
|
||||
Return:
|
||||
A new COLR table.
|
||||
"""
|
||||
@ -159,7 +229,7 @@ def buildCOLR(
|
||||
self.version = colr.Version = version
|
||||
|
||||
if version == 0:
|
||||
self._fromOTTable(colr)
|
||||
self.ColorLayers = self._decompileColorLayersV0(colr)
|
||||
else:
|
||||
colr.VarStore = varStore
|
||||
self.table = colr
|
||||
@ -293,8 +363,6 @@ def buildCPAL(
|
||||
# COLR v1 tables
|
||||
# See draft proposal at: https://github.com/googlefonts/colr-gradients-spec
|
||||
|
||||
_DEFAULT_ALPHA = VariableFloat(1.0)
|
||||
|
||||
|
||||
def _is_colrv0_layer(layer: Any) -> bool:
|
||||
# Consider as COLRv0 layer any sequence of length 2 (be it tuple or list) in which
|
||||
@ -326,138 +394,15 @@ def _split_color_glyphs_by_version(
|
||||
return colorGlyphsV0, colorGlyphsV1
|
||||
|
||||
|
||||
def _to_variable_value(
|
||||
value: _ScalarInput,
|
||||
minValue: _Number,
|
||||
maxValue: _Number,
|
||||
cls: Type[VariableValue],
|
||||
) -> VariableValue:
|
||||
if not isinstance(value, cls):
|
||||
try:
|
||||
it = iter(value)
|
||||
except TypeError: # not iterable
|
||||
value = cls(value)
|
||||
else:
|
||||
value = cls._make(it)
|
||||
if value.value < minValue:
|
||||
raise OverflowError(f"{cls.__name__}: {value.value} < {minValue}")
|
||||
if value.value > maxValue:
|
||||
raise OverflowError(f"{cls.__name__}: {value.value} < {maxValue}")
|
||||
return value
|
||||
|
||||
|
||||
_to_variable_f16dot16_float = partial(
|
||||
_to_variable_value,
|
||||
cls=VariableFloat,
|
||||
minValue=-(2 ** 15),
|
||||
maxValue=fixedToFloat(2 ** 31 - 1, 16),
|
||||
)
|
||||
_to_variable_f2dot14_float = partial(
|
||||
_to_variable_value,
|
||||
cls=VariableFloat,
|
||||
minValue=-2.0,
|
||||
maxValue=fixedToFloat(2 ** 15 - 1, 14),
|
||||
)
|
||||
_to_variable_int16 = partial(
|
||||
_to_variable_value,
|
||||
cls=VariableInt,
|
||||
minValue=-(2 ** 15),
|
||||
maxValue=2 ** 15 - 1,
|
||||
)
|
||||
_to_variable_uint16 = partial(
|
||||
_to_variable_value,
|
||||
cls=VariableInt,
|
||||
minValue=0,
|
||||
maxValue=2 ** 16,
|
||||
)
|
||||
|
||||
|
||||
def buildColorIndex(
|
||||
paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
|
||||
) -> ot.ColorIndex:
|
||||
self = ot.ColorIndex()
|
||||
self.PaletteIndex = int(paletteIndex)
|
||||
self.Alpha = _to_variable_f2dot14_float(alpha)
|
||||
return self
|
||||
|
||||
|
||||
def buildColorStop(
|
||||
offset: _ScalarInput,
|
||||
paletteIndex: int,
|
||||
alpha: _ScalarInput = _DEFAULT_ALPHA,
|
||||
) -> ot.ColorStop:
|
||||
self = ot.ColorStop()
|
||||
self.StopOffset = _to_variable_f2dot14_float(offset)
|
||||
self.Color = buildColorIndex(paletteIndex, alpha)
|
||||
return self
|
||||
|
||||
|
||||
def _to_enum_value(v: Union[str, int, T], enumClass: Type[T]) -> T:
|
||||
if isinstance(v, enumClass):
|
||||
return v
|
||||
elif isinstance(v, str):
|
||||
try:
|
||||
return getattr(enumClass, v.upper())
|
||||
except AttributeError:
|
||||
raise ValueError(f"{v!r} is not a valid {enumClass.__name__}")
|
||||
return enumClass(v)
|
||||
|
||||
|
||||
def _to_extend_mode(v: _ExtendInput) -> ExtendMode:
|
||||
return _to_enum_value(v, ExtendMode)
|
||||
|
||||
|
||||
def _to_composite_mode(v: _CompositeInput) -> CompositeMode:
|
||||
return _to_enum_value(v, CompositeMode)
|
||||
|
||||
|
||||
def buildColorLine(
|
||||
stops: _ColorStopsList, extend: _ExtendInput = ExtendMode.PAD
|
||||
) -> ot.ColorLine:
|
||||
self = ot.ColorLine()
|
||||
self.Extend = _to_extend_mode(extend)
|
||||
self.StopCount = len(stops)
|
||||
self.ColorStop = [
|
||||
stop
|
||||
if isinstance(stop, ot.ColorStop)
|
||||
else buildColorStop(**stop)
|
||||
if isinstance(stop, collections.abc.Mapping)
|
||||
else buildColorStop(*stop)
|
||||
for stop in stops
|
||||
]
|
||||
return self
|
||||
|
||||
|
||||
def _to_color_line(obj):
|
||||
if isinstance(obj, ot.ColorLine):
|
||||
return obj
|
||||
elif isinstance(obj, collections.abc.Mapping):
|
||||
return buildColorLine(**obj)
|
||||
raise TypeError(obj)
|
||||
|
||||
|
||||
def _as_tuple(obj) -> Tuple[Any, ...]:
|
||||
# start simple, who even cares about cyclic graphs or interesting field types
|
||||
def _tuple_safe(value):
|
||||
if isinstance(value, enum.Enum):
|
||||
return value
|
||||
elif hasattr(value, "__dict__"):
|
||||
return tuple((k, _tuple_safe(v)) for k, v in value.__dict__.items())
|
||||
elif isinstance(value, collections.abc.MutableSequence):
|
||||
return tuple(_tuple_safe(e) for e in value)
|
||||
return value
|
||||
|
||||
return tuple(_tuple_safe(obj))
|
||||
|
||||
|
||||
def _reuse_ranges(num_layers: int) -> Generator[Tuple[int, int], None, None]:
|
||||
# TODO feels like something itertools might have already
|
||||
for lbound in range(num_layers):
|
||||
# TODO may want a max length to limit scope of search
|
||||
# Reuse of very large #s of layers is relatively unlikely
|
||||
# +2: we want sequences of at least 2
|
||||
# otData handles single-record duplication
|
||||
for ubound in range(lbound + 2, num_layers + 1):
|
||||
for ubound in range(
|
||||
lbound + 2, min(num_layers + 1, lbound + 2 + _MAX_REUSE_LEN)
|
||||
):
|
||||
yield (lbound, ubound)
|
||||
|
||||
|
||||
@ -465,162 +410,133 @@ class LayerV1ListBuilder:
|
||||
slices: List[ot.Paint]
|
||||
layers: List[ot.Paint]
|
||||
reusePool: Mapping[Tuple[Any, ...], int]
|
||||
tuples: Mapping[int, Tuple[Any, ...]]
|
||||
keepAlive: List[ot.Paint] # we need id to remain valid
|
||||
|
||||
def __init__(self):
|
||||
self.slices = []
|
||||
self.layers = []
|
||||
self.reusePool = {}
|
||||
self.tuples = {}
|
||||
self.keepAlive = []
|
||||
|
||||
def buildPaintSolid(
|
||||
self, paletteIndex: int, alpha: _ScalarInput = _DEFAULT_ALPHA
|
||||
) -> ot.Paint:
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintSolid)
|
||||
ot_paint.Color = buildColorIndex(paletteIndex, alpha)
|
||||
return ot_paint
|
||||
# We need to intercept construction of PaintColrLayers
|
||||
callbacks = _buildPaintCallbacks()
|
||||
callbacks[
|
||||
(
|
||||
BuildCallback.BEFORE_BUILD,
|
||||
ot.Paint,
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
)
|
||||
] = self._beforeBuildPaintColrLayers
|
||||
self.tableBuilder = TableBuilder(callbacks)
|
||||
|
||||
def buildPaintLinearGradient(
|
||||
self,
|
||||
colorLine: _ColorLineInput,
|
||||
p0: _PointTuple,
|
||||
p1: _PointTuple,
|
||||
p2: Optional[_PointTuple] = None,
|
||||
) -> ot.Paint:
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintLinearGradient)
|
||||
ot_paint.ColorLine = _to_color_line(colorLine)
|
||||
def _paint_tuple(self, paint: ot.Paint):
|
||||
# start simple, who even cares about cyclic graphs or interesting field types
|
||||
def _tuple_safe(value):
|
||||
if isinstance(value, enum.Enum):
|
||||
return value
|
||||
elif hasattr(value, "__dict__"):
|
||||
return tuple(
|
||||
(k, _tuple_safe(v)) for k, v in sorted(value.__dict__.items())
|
||||
)
|
||||
elif isinstance(value, collections.abc.MutableSequence):
|
||||
return tuple(_tuple_safe(e) for e in value)
|
||||
return value
|
||||
|
||||
if p2 is None:
|
||||
p2 = copy.copy(p1)
|
||||
for i, (x, y) in enumerate((p0, p1, p2)):
|
||||
setattr(ot_paint, f"x{i}", _to_variable_int16(x))
|
||||
setattr(ot_paint, f"y{i}", _to_variable_int16(y))
|
||||
# Cache the tuples for individual Paint instead of the whole sequence
|
||||
# because the seq could be a transient slice
|
||||
result = self.tuples.get(id(paint), None)
|
||||
if result is None:
|
||||
result = _tuple_safe(paint)
|
||||
self.tuples[id(paint)] = result
|
||||
self.keepAlive.append(paint)
|
||||
return result
|
||||
|
||||
return ot_paint
|
||||
def _as_tuple(self, paints: Sequence[ot.Paint]) -> Tuple[Any, ...]:
|
||||
return tuple(self._paint_tuple(p) for p in paints)
|
||||
|
||||
def buildPaintRadialGradient(
|
||||
self,
|
||||
colorLine: _ColorLineInput,
|
||||
c0: _PointTuple,
|
||||
c1: _PointTuple,
|
||||
r0: _ScalarInput,
|
||||
r1: _ScalarInput,
|
||||
) -> ot.Paint:
|
||||
# COLR layers is unusual in that it modifies shared state
|
||||
# so we need a callback into an object
|
||||
def _beforeBuildPaintColrLayers(self, dest, source):
|
||||
paint = ot.Paint()
|
||||
paint.Format = int(ot.PaintFormat.PaintColrLayers)
|
||||
self.slices.append(paint)
|
||||
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintRadialGradient)
|
||||
ot_paint.ColorLine = _to_color_line(colorLine)
|
||||
# Sketchy gymnastics: a sequence input will have dropped it's layers
|
||||
# into NumLayers; get it back
|
||||
if isinstance(source.get("NumLayers", None), collections.abc.Sequence):
|
||||
layers = source["NumLayers"]
|
||||
else:
|
||||
layers = source["Layers"]
|
||||
|
||||
for i, (x, y), r in [(0, c0, r0), (1, c1, r1)]:
|
||||
setattr(ot_paint, f"x{i}", _to_variable_int16(x))
|
||||
setattr(ot_paint, f"y{i}", _to_variable_int16(y))
|
||||
setattr(ot_paint, f"r{i}", _to_variable_uint16(r))
|
||||
# Convert maps seqs or whatever into typed objects
|
||||
layers = [self.buildPaint(l) for l in layers]
|
||||
|
||||
return ot_paint
|
||||
|
||||
def buildPaintGlyph(self, glyph: str, paint: _PaintInput) -> ot.Paint:
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintGlyph)
|
||||
ot_paint.Glyph = glyph
|
||||
ot_paint.Paint = self.buildPaint(paint)
|
||||
return ot_paint
|
||||
|
||||
def buildPaintColrGlyph(self, glyph: str) -> ot.Paint:
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintColrGlyph)
|
||||
ot_paint.Glyph = glyph
|
||||
return ot_paint
|
||||
|
||||
def buildPaintTransform(
|
||||
self, transform: _AffineInput, paint: _PaintInput
|
||||
) -> ot.Paint:
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintTransform)
|
||||
if not isinstance(transform, ot.Affine2x3):
|
||||
transform = buildAffine2x3(transform)
|
||||
ot_paint.Transform = transform
|
||||
ot_paint.Paint = self.buildPaint(paint)
|
||||
return ot_paint
|
||||
|
||||
def buildPaintComposite(
|
||||
self,
|
||||
mode: _CompositeInput,
|
||||
source: _PaintInput,
|
||||
backdrop: _PaintInput,
|
||||
):
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintComposite)
|
||||
ot_paint.SourcePaint = self.buildPaint(source)
|
||||
ot_paint.CompositeMode = _to_composite_mode(mode)
|
||||
ot_paint.BackdropPaint = self.buildPaint(backdrop)
|
||||
return ot_paint
|
||||
|
||||
def buildColrLayers(self, paints: List[_PaintInput]) -> ot.Paint:
|
||||
ot_paint = ot.Paint()
|
||||
ot_paint.Format = int(ot.Paint.Format.PaintColrLayers)
|
||||
self.slices.append(ot_paint)
|
||||
|
||||
paints = [self.buildPaint(p) for p in paints]
|
||||
# No reason to have a colr layers with just one entry
|
||||
if len(layers) == 1:
|
||||
return layers[0], {}
|
||||
|
||||
# Look for reuse, with preference to longer sequences
|
||||
# This may make the layer list smaller
|
||||
found_reuse = True
|
||||
while found_reuse:
|
||||
found_reuse = False
|
||||
|
||||
ranges = sorted(
|
||||
_reuse_ranges(len(paints)),
|
||||
_reuse_ranges(len(layers)),
|
||||
key=lambda t: (t[1] - t[0], t[1], t[0]),
|
||||
reverse=True,
|
||||
)
|
||||
for lbound, ubound in ranges:
|
||||
reuse_lbound = self.reusePool.get(_as_tuple(paints[lbound:ubound]), -1)
|
||||
reuse_lbound = self.reusePool.get(
|
||||
self._as_tuple(layers[lbound:ubound]), -1
|
||||
)
|
||||
if reuse_lbound == -1:
|
||||
continue
|
||||
new_slice = ot.Paint()
|
||||
new_slice.Format = int(ot.Paint.Format.PaintColrLayers)
|
||||
new_slice.Format = int(ot.PaintFormat.PaintColrLayers)
|
||||
new_slice.NumLayers = ubound - lbound
|
||||
new_slice.FirstLayerIndex = reuse_lbound
|
||||
paints = paints[:lbound] + [new_slice] + paints[ubound:]
|
||||
layers = layers[:lbound] + [new_slice] + layers[ubound:]
|
||||
found_reuse = True
|
||||
break
|
||||
|
||||
ot_paint.NumLayers = len(paints)
|
||||
ot_paint.FirstLayerIndex = len(self.layers)
|
||||
self.layers.extend(paints)
|
||||
# The layer list is now final; if it's too big we need to tree it
|
||||
is_tree = len(layers) > MAX_PAINT_COLR_LAYER_COUNT
|
||||
layers = _build_n_ary_tree(layers, n=MAX_PAINT_COLR_LAYER_COUNT)
|
||||
|
||||
# Register our parts for reuse
|
||||
for lbound, ubound in _reuse_ranges(len(paints)):
|
||||
self.reusePool[_as_tuple(paints[lbound:ubound])] = (
|
||||
lbound + ot_paint.FirstLayerIndex
|
||||
)
|
||||
# We now have a tree of sequences with Paint leaves.
|
||||
# Convert the sequences into PaintColrLayers.
|
||||
def listToColrLayers(layer):
|
||||
if isinstance(layer, collections.abc.Sequence):
|
||||
return self.buildPaint(
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintColrLayers,
|
||||
"Layers": [listToColrLayers(l) for l in layer],
|
||||
}
|
||||
)
|
||||
return layer
|
||||
|
||||
return ot_paint
|
||||
layers = [listToColrLayers(l) for l in layers]
|
||||
|
||||
paint.NumLayers = len(layers)
|
||||
paint.FirstLayerIndex = len(self.layers)
|
||||
self.layers.extend(layers)
|
||||
|
||||
# Register our parts for reuse provided we aren't a tree
|
||||
# If we are a tree the leaves registered for reuse and that will suffice
|
||||
if not is_tree:
|
||||
for lbound, ubound in _reuse_ranges(len(layers)):
|
||||
self.reusePool[self._as_tuple(layers[lbound:ubound])] = (
|
||||
lbound + paint.FirstLayerIndex
|
||||
)
|
||||
|
||||
# we've fully built dest; empty source prevents generalized build from kicking in
|
||||
return paint, {}
|
||||
|
||||
def buildPaint(self, paint: _PaintInput) -> ot.Paint:
|
||||
if isinstance(paint, ot.Paint):
|
||||
return paint
|
||||
elif isinstance(paint, int):
|
||||
paletteIndex = paint
|
||||
return self.buildPaintSolid(paletteIndex)
|
||||
elif isinstance(paint, tuple):
|
||||
layerGlyph, paint = paint
|
||||
return self.buildPaintGlyph(layerGlyph, paint)
|
||||
elif isinstance(paint, list):
|
||||
# implicit PaintColrLayers for a list of > 1
|
||||
if len(paint) == 0:
|
||||
raise ValueError("An empty list is hard to paint")
|
||||
elif len(paint) == 1:
|
||||
return self.buildPaint(paint[0])
|
||||
else:
|
||||
return self.buildColrLayers(paint)
|
||||
elif isinstance(paint, collections.abc.Mapping):
|
||||
kwargs = dict(paint)
|
||||
fmt = kwargs.pop("format")
|
||||
try:
|
||||
return LayerV1ListBuilder._buildFunctions[fmt](self, **kwargs)
|
||||
except KeyError:
|
||||
raise NotImplementedError(fmt)
|
||||
raise TypeError(f"Not sure what to do with {type(paint).__name__}: {paint!r}")
|
||||
return self.tableBuilder.build(ot.Paint, paint)
|
||||
|
||||
def build(self) -> ot.LayerV1List:
|
||||
layers = ot.LayerV1List()
|
||||
@ -629,31 +545,6 @@ class LayerV1ListBuilder:
|
||||
return layers
|
||||
|
||||
|
||||
LayerV1ListBuilder._buildFunctions = {
|
||||
pf.value: getattr(LayerV1ListBuilder, "build" + pf.name)
|
||||
for pf in ot.Paint.Format
|
||||
if pf != ot.Paint.Format.PaintColrLayers
|
||||
}
|
||||
|
||||
|
||||
def buildAffine2x3(transform: _AffineTuple) -> ot.Affine2x3:
|
||||
if len(transform) != 6:
|
||||
raise ValueError(f"Expected 6-tuple of floats, found: {transform!r}")
|
||||
self = ot.Affine2x3()
|
||||
# COLRv1 Affine2x3 uses the same column-major order to serialize a 2D
|
||||
# Affine Transformation as the one used by fontTools.misc.transform.
|
||||
# However, for historical reasons, the labels 'xy' and 'yx' are swapped.
|
||||
# Their fundamental meaning is the same though.
|
||||
# COLRv1 Affine2x3 follows the names found in FreeType and Cairo.
|
||||
# In all case, the second element in the 6-tuple correspond to the
|
||||
# y-part of the x basis vector, and the third to the x-part of the y
|
||||
# basis vector.
|
||||
# See https://github.com/googlefonts/colr-gradients-spec/pull/85
|
||||
for i, attr in enumerate(("xx", "yx", "xy", "yy", "dx", "dy")):
|
||||
setattr(self, attr, _to_variable_f16dot16_float(transform[i]))
|
||||
return self
|
||||
|
||||
|
||||
def buildBaseGlyphV1Record(
|
||||
baseGlyph: str, layerBuilder: LayerV1ListBuilder, paint: _PaintInput
|
||||
) -> ot.BaseGlyphV1List:
|
||||
@ -702,3 +593,45 @@ def buildColrV1(
|
||||
glyphs.BaseGlyphCount = len(baseGlyphs)
|
||||
glyphs.BaseGlyphV1Record = baseGlyphs
|
||||
return (layers, glyphs)
|
||||
|
||||
|
||||
def _build_n_ary_tree(leaves, n):
|
||||
"""Build N-ary tree from sequence of leaf nodes.
|
||||
|
||||
Return a list of lists where each non-leaf node is a list containing
|
||||
max n nodes.
|
||||
"""
|
||||
if not leaves:
|
||||
return []
|
||||
|
||||
assert n > 1
|
||||
|
||||
depth = ceil(log(len(leaves), n))
|
||||
|
||||
if depth <= 1:
|
||||
return list(leaves)
|
||||
|
||||
# Fully populate complete subtrees of root until we have enough leaves left
|
||||
root = []
|
||||
unassigned = None
|
||||
full_step = n ** (depth - 1)
|
||||
for i in range(0, len(leaves), full_step):
|
||||
subtree = leaves[i : i + full_step]
|
||||
if len(subtree) < full_step:
|
||||
unassigned = subtree
|
||||
break
|
||||
while len(subtree) > n:
|
||||
subtree = [subtree[k : k + n] for k in range(0, len(subtree), n)]
|
||||
root.append(subtree)
|
||||
|
||||
if unassigned:
|
||||
# Recurse to fill the last subtree, which is the only partially populated one
|
||||
subtree = _build_n_ary_tree(unassigned, n)
|
||||
if len(subtree) <= n - len(root):
|
||||
# replace last subtree with its children if they can still fit
|
||||
root.extend(subtree)
|
||||
else:
|
||||
root.append(subtree)
|
||||
assert len(root) <= n
|
||||
|
||||
return root
|
||||
|
145
Lib/fontTools/colorLib/geometry.py
Normal file
145
Lib/fontTools/colorLib/geometry.py
Normal file
@ -0,0 +1,145 @@
|
||||
"""Helpers for manipulating 2D points and vectors in COLR table."""
|
||||
|
||||
from math import copysign, cos, hypot, pi
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
|
||||
|
||||
def _vector_between(origin, target):
|
||||
return (target[0] - origin[0], target[1] - origin[1])
|
||||
|
||||
|
||||
def _round_point(pt):
|
||||
return (otRound(pt[0]), otRound(pt[1]))
|
||||
|
||||
|
||||
def _unit_vector(vec):
|
||||
length = hypot(*vec)
|
||||
if length == 0:
|
||||
return None
|
||||
return (vec[0] / length, vec[1] / length)
|
||||
|
||||
|
||||
# This is the same tolerance used by Skia's SkTwoPointConicalGradient.cpp to detect
|
||||
# when a radial gradient's focal point lies on the end circle.
|
||||
_NEARLY_ZERO = 1 / (1 << 12) # 0.000244140625
|
||||
|
||||
|
||||
# The unit vector's X and Y components are respectively
|
||||
# U = (cos(α), sin(α))
|
||||
# where α is the angle between the unit vector and the positive x axis.
|
||||
_UNIT_VECTOR_THRESHOLD = cos(3 / 8 * pi) # == sin(1/8 * pi) == 0.38268343236508984
|
||||
|
||||
|
||||
def _rounding_offset(direction):
|
||||
# Return 2-tuple of -/+ 1.0 or 0.0 approximately based on the direction vector.
|
||||
# We divide the unit circle in 8 equal slices oriented towards the cardinal
|
||||
# (N, E, S, W) and intermediate (NE, SE, SW, NW) directions. To each slice we
|
||||
# map one of the possible cases: -1, 0, +1 for either X and Y coordinate.
|
||||
# E.g. Return (+1.0, -1.0) if unit vector is oriented towards SE, or
|
||||
# (-1.0, 0.0) if it's pointing West, etc.
|
||||
uv = _unit_vector(direction)
|
||||
if not uv:
|
||||
return (0, 0)
|
||||
|
||||
result = []
|
||||
for uv_component in uv:
|
||||
if -_UNIT_VECTOR_THRESHOLD <= uv_component < _UNIT_VECTOR_THRESHOLD:
|
||||
# unit vector component near 0: direction almost orthogonal to the
|
||||
# direction of the current axis, thus keep coordinate unchanged
|
||||
result.append(0)
|
||||
else:
|
||||
# nudge coord by +/- 1.0 in direction of unit vector
|
||||
result.append(copysign(1.0, uv_component))
|
||||
return tuple(result)
|
||||
|
||||
|
||||
class Circle:
|
||||
def __init__(self, centre, radius):
|
||||
self.centre = centre
|
||||
self.radius = radius
|
||||
|
||||
def __repr__(self):
|
||||
return f"Circle(centre={self.centre}, radius={self.radius})"
|
||||
|
||||
def round(self):
|
||||
return Circle(_round_point(self.centre), otRound(self.radius))
|
||||
|
||||
def inside(self, outer_circle):
|
||||
dist = self.radius + hypot(*_vector_between(self.centre, outer_circle.centre))
|
||||
return (
|
||||
abs(outer_circle.radius - dist) <= _NEARLY_ZERO
|
||||
or outer_circle.radius > dist
|
||||
)
|
||||
|
||||
def concentric(self, other):
|
||||
return self.centre == other.centre
|
||||
|
||||
def move(self, dx, dy):
|
||||
self.centre = (self.centre[0] + dx, self.centre[1] + dy)
|
||||
|
||||
|
||||
def round_start_circle_stable_containment(c0, r0, c1, r1):
|
||||
"""Round start circle so that it stays inside/outside end circle after rounding.
|
||||
|
||||
The rounding of circle coordinates to integers may cause an abrupt change
|
||||
if the start circle c0 is so close to the end circle c1's perimiter that
|
||||
it ends up falling outside (or inside) as a result of the rounding.
|
||||
To keep the gradient unchanged, we nudge it in the right direction.
|
||||
|
||||
See:
|
||||
https://github.com/googlefonts/colr-gradients-spec/issues/204
|
||||
https://github.com/googlefonts/picosvg/issues/158
|
||||
"""
|
||||
start, end = Circle(c0, r0), Circle(c1, r1)
|
||||
|
||||
inside_before_round = start.inside(end)
|
||||
|
||||
round_start = start.round()
|
||||
round_end = end.round()
|
||||
inside_after_round = round_start.inside(round_end)
|
||||
|
||||
if inside_before_round == inside_after_round:
|
||||
return round_start
|
||||
elif inside_after_round:
|
||||
# start was outside before rounding: we need to push start away from end
|
||||
direction = _vector_between(round_end.centre, round_start.centre)
|
||||
radius_delta = +1.0
|
||||
else:
|
||||
# start was inside before rounding: we need to push start towards end
|
||||
direction = _vector_between(round_start.centre, round_end.centre)
|
||||
radius_delta = -1.0
|
||||
dx, dy = _rounding_offset(direction)
|
||||
|
||||
# At most 2 iterations ought to be enough to converge. Before the loop, we
|
||||
# know the start circle didn't keep containment after normal rounding; thus
|
||||
# we continue adjusting by -/+ 1.0 until containment is restored.
|
||||
# Normal rounding can at most move each coordinates -/+0.5; in the worst case
|
||||
# both the start and end circle's centres and radii will be rounded in opposite
|
||||
# directions, e.g. when they move along a 45 degree diagonal:
|
||||
# c0 = (1.5, 1.5) ===> (2.0, 2.0)
|
||||
# r0 = 0.5 ===> 1.0
|
||||
# c1 = (0.499, 0.499) ===> (0.0, 0.0)
|
||||
# r1 = 2.499 ===> 2.0
|
||||
# In this example, the relative distance between the circles, calculated
|
||||
# as r1 - (r0 + distance(c0, c1)) is initially 0.57437 (c0 is inside c1), and
|
||||
# -1.82842 after rounding (c0 is now outside c1). Nudging c0 by -1.0 on both
|
||||
# x and y axes moves it towards c1 by hypot(-1.0, -1.0) = 1.41421. Two of these
|
||||
# moves cover twice that distance, which is enough to restore containment.
|
||||
max_attempts = 2
|
||||
for _ in range(max_attempts):
|
||||
if round_start.concentric(round_end):
|
||||
# can't move c0 towards c1 (they are the same), so we change the radius
|
||||
round_start.radius += radius_delta
|
||||
assert round_start.radius >= 0
|
||||
else:
|
||||
round_start.move(dx, dy)
|
||||
if inside_before_round == round_start.inside(round_end):
|
||||
break
|
||||
else: # likely a bug
|
||||
raise AssertionError(
|
||||
f"Rounding circle {start} "
|
||||
f"{'inside' if inside_before_round else 'outside'} "
|
||||
f"{end} failed after {max_attempts} attempts!"
|
||||
)
|
||||
|
||||
return round_start
|
234
Lib/fontTools/colorLib/table_builder.py
Normal file
234
Lib/fontTools/colorLib/table_builder.py
Normal file
@ -0,0 +1,234 @@
|
||||
"""
|
||||
colorLib.table_builder: Generic helper for filling in BaseTable derivatives from tuples and maps and such.
|
||||
|
||||
"""
|
||||
|
||||
import collections
|
||||
import enum
|
||||
from fontTools.ttLib.tables.otBase import (
|
||||
BaseTable,
|
||||
FormatSwitchingBaseTable,
|
||||
UInt8FormatSwitchingBaseTable,
|
||||
)
|
||||
from fontTools.ttLib.tables.otConverters import (
|
||||
ComputedInt,
|
||||
SimpleValue,
|
||||
Struct,
|
||||
Short,
|
||||
UInt8,
|
||||
UShort,
|
||||
VarInt16,
|
||||
VarUInt16,
|
||||
IntValue,
|
||||
FloatValue,
|
||||
)
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
|
||||
|
||||
class BuildCallback(enum.Enum):
|
||||
"""Keyed on (BEFORE_BUILD, class[, Format if available]).
|
||||
Receives (dest, source).
|
||||
Should return (dest, source), which can be new objects.
|
||||
"""
|
||||
|
||||
BEFORE_BUILD = enum.auto()
|
||||
|
||||
"""Keyed on (AFTER_BUILD, class[, Format if available]).
|
||||
Receives (dest).
|
||||
Should return dest, which can be a new object.
|
||||
"""
|
||||
AFTER_BUILD = enum.auto()
|
||||
|
||||
"""Keyed on (CREATE_DEFAULT, class).
|
||||
Receives no arguments.
|
||||
Should return a new instance of class.
|
||||
"""
|
||||
CREATE_DEFAULT = enum.auto()
|
||||
|
||||
|
||||
def _assignable(convertersByName):
|
||||
return {k: v for k, v in convertersByName.items() if not isinstance(v, ComputedInt)}
|
||||
|
||||
|
||||
def convertTupleClass(tupleClass, value):
|
||||
if isinstance(value, tupleClass):
|
||||
return value
|
||||
if isinstance(value, tuple):
|
||||
return tupleClass(*value)
|
||||
return tupleClass(value)
|
||||
|
||||
|
||||
def _isNonStrSequence(value):
|
||||
return isinstance(value, collections.abc.Sequence) and not isinstance(value, str)
|
||||
|
||||
|
||||
def _set_format(dest, source):
|
||||
if _isNonStrSequence(source):
|
||||
assert len(source) > 0, f"{type(dest)} needs at least format from {source}"
|
||||
dest.Format = source[0]
|
||||
source = source[1:]
|
||||
elif isinstance(source, collections.abc.Mapping):
|
||||
assert "Format" in source, f"{type(dest)} needs at least Format from {source}"
|
||||
dest.Format = source["Format"]
|
||||
else:
|
||||
raise ValueError(f"Not sure how to populate {type(dest)} from {source}")
|
||||
|
||||
assert isinstance(
|
||||
dest.Format, collections.abc.Hashable
|
||||
), f"{type(dest)} Format is not hashable: {dest.Format}"
|
||||
assert (
|
||||
dest.Format in dest.convertersByName
|
||||
), f"{dest.Format} invalid Format of {cls}"
|
||||
|
||||
return source
|
||||
|
||||
|
||||
class TableBuilder:
|
||||
"""
|
||||
Helps to populate things derived from BaseTable from maps, tuples, etc.
|
||||
|
||||
A table of lifecycle callbacks may be provided to add logic beyond what is possible
|
||||
based on otData info for the target class. See BuildCallbacks.
|
||||
"""
|
||||
|
||||
def __init__(self, callbackTable=None):
|
||||
if callbackTable is None:
|
||||
callbackTable = {}
|
||||
self._callbackTable = callbackTable
|
||||
|
||||
def _convert(self, dest, field, converter, value):
|
||||
tupleClass = getattr(converter, "tupleClass", None)
|
||||
enumClass = getattr(converter, "enumClass", None)
|
||||
|
||||
if tupleClass:
|
||||
value = convertTupleClass(tupleClass, value)
|
||||
|
||||
elif enumClass:
|
||||
if isinstance(value, enumClass):
|
||||
pass
|
||||
elif isinstance(value, str):
|
||||
try:
|
||||
value = getattr(enumClass, value.upper())
|
||||
except AttributeError:
|
||||
raise ValueError(f"{value} is not a valid {enumClass}")
|
||||
else:
|
||||
value = enumClass(value)
|
||||
|
||||
elif isinstance(converter, IntValue):
|
||||
value = otRound(value)
|
||||
elif isinstance(converter, FloatValue):
|
||||
value = float(value)
|
||||
|
||||
elif isinstance(converter, Struct):
|
||||
if converter.repeat:
|
||||
if _isNonStrSequence(value):
|
||||
value = [self.build(converter.tableClass, v) for v in value]
|
||||
else:
|
||||
value = [self.build(converter.tableClass, value)]
|
||||
setattr(dest, converter.repeat, len(value))
|
||||
else:
|
||||
value = self.build(converter.tableClass, value)
|
||||
elif callable(converter):
|
||||
value = converter(value)
|
||||
|
||||
setattr(dest, field, value)
|
||||
|
||||
def build(self, cls, source):
|
||||
assert issubclass(cls, BaseTable)
|
||||
|
||||
if isinstance(source, cls):
|
||||
return source
|
||||
|
||||
callbackKey = (cls,)
|
||||
dest = self._callbackTable.get(
|
||||
(BuildCallback.CREATE_DEFAULT,) + callbackKey, lambda: cls()
|
||||
)()
|
||||
assert isinstance(dest, cls)
|
||||
|
||||
convByName = _assignable(cls.convertersByName)
|
||||
skippedFields = set()
|
||||
|
||||
# For format switchers we need to resolve converters based on format
|
||||
if issubclass(cls, FormatSwitchingBaseTable):
|
||||
source = _set_format(dest, source)
|
||||
|
||||
convByName = _assignable(convByName[dest.Format])
|
||||
skippedFields.add("Format")
|
||||
callbackKey = (cls, dest.Format)
|
||||
|
||||
# Convert sequence => mapping so before thunk only has to handle one format
|
||||
if _isNonStrSequence(source):
|
||||
# Sequence (typically list or tuple) assumed to match fields in declaration order
|
||||
assert len(source) <= len(
|
||||
convByName
|
||||
), f"Sequence of {len(source)} too long for {cls}; expected <= {len(convByName)} values"
|
||||
source = dict(zip(convByName.keys(), source))
|
||||
|
||||
dest, source = self._callbackTable.get(
|
||||
(BuildCallback.BEFORE_BUILD,) + callbackKey, lambda d, s: (d, s)
|
||||
)(dest, source)
|
||||
|
||||
if isinstance(source, collections.abc.Mapping):
|
||||
for field, value in source.items():
|
||||
if field in skippedFields:
|
||||
continue
|
||||
converter = convByName.get(field, None)
|
||||
if not converter:
|
||||
raise ValueError(
|
||||
f"Unrecognized field {field} for {cls}; expected one of {sorted(convByName.keys())}"
|
||||
)
|
||||
self._convert(dest, field, converter, value)
|
||||
else:
|
||||
# let's try as a 1-tuple
|
||||
dest = self.build(cls, (source,))
|
||||
|
||||
dest = self._callbackTable.get(
|
||||
(BuildCallback.AFTER_BUILD,) + callbackKey, lambda d: d
|
||||
)(dest)
|
||||
|
||||
return dest
|
||||
|
||||
|
||||
class TableUnbuilder:
|
||||
def __init__(self, callbackTable=None):
|
||||
if callbackTable is None:
|
||||
callbackTable = {}
|
||||
self._callbackTable = callbackTable
|
||||
|
||||
def unbuild(self, table):
|
||||
assert isinstance(table, BaseTable)
|
||||
|
||||
source = {}
|
||||
|
||||
callbackKey = (type(table),)
|
||||
if isinstance(table, FormatSwitchingBaseTable):
|
||||
source["Format"] = int(table.Format)
|
||||
callbackKey += (table.Format,)
|
||||
|
||||
for converter in table.getConverters():
|
||||
if isinstance(converter, ComputedInt):
|
||||
continue
|
||||
value = getattr(table, converter.name)
|
||||
|
||||
tupleClass = getattr(converter, "tupleClass", None)
|
||||
enumClass = getattr(converter, "enumClass", None)
|
||||
if tupleClass:
|
||||
source[converter.name] = tuple(value)
|
||||
elif enumClass:
|
||||
source[converter.name] = value.name.lower()
|
||||
elif isinstance(converter, Struct):
|
||||
if converter.repeat:
|
||||
source[converter.name] = [self.unbuild(v) for v in value]
|
||||
else:
|
||||
source[converter.name] = self.unbuild(value)
|
||||
elif isinstance(converter, SimpleValue):
|
||||
# "simple" values (e.g. int, float, str) need no further un-building
|
||||
source[converter.name] = value
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"Don't know how unbuild {value!r} with {converter!r}"
|
||||
)
|
||||
|
||||
source = self._callbackTable.get(callbackKey, lambda s: s)(source)
|
||||
|
||||
return source
|
79
Lib/fontTools/colorLib/unbuilder.py
Normal file
79
Lib/fontTools/colorLib/unbuilder.py
Normal file
@ -0,0 +1,79 @@
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from .table_builder import TableUnbuilder
|
||||
|
||||
|
||||
def unbuildColrV1(layerV1List, baseGlyphV1List):
|
||||
unbuilder = LayerV1ListUnbuilder(layerV1List.Paint)
|
||||
return {
|
||||
rec.BaseGlyph: unbuilder.unbuildPaint(rec.Paint)
|
||||
for rec in baseGlyphV1List.BaseGlyphV1Record
|
||||
}
|
||||
|
||||
|
||||
def _flatten(lst):
|
||||
for el in lst:
|
||||
if isinstance(el, list):
|
||||
yield from _flatten(el)
|
||||
else:
|
||||
yield el
|
||||
|
||||
|
||||
class LayerV1ListUnbuilder:
|
||||
def __init__(self, layers):
|
||||
self.layers = layers
|
||||
|
||||
callbacks = {
|
||||
(
|
||||
ot.Paint,
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
): self._unbuildPaintColrLayers,
|
||||
}
|
||||
self.tableUnbuilder = TableUnbuilder(callbacks)
|
||||
|
||||
def unbuildPaint(self, paint):
|
||||
assert isinstance(paint, ot.Paint)
|
||||
return self.tableUnbuilder.unbuild(paint)
|
||||
|
||||
def _unbuildPaintColrLayers(self, source):
|
||||
assert source["Format"] == ot.PaintFormat.PaintColrLayers
|
||||
|
||||
layers = list(
|
||||
_flatten(
|
||||
[
|
||||
self.unbuildPaint(childPaint)
|
||||
for childPaint in self.layers[
|
||||
source["FirstLayerIndex"] : source["FirstLayerIndex"]
|
||||
+ source["NumLayers"]
|
||||
]
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
if len(layers) == 1:
|
||||
return layers[0]
|
||||
|
||||
return {"Format": source["Format"], "Layers": layers}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from pprint import pprint
|
||||
import sys
|
||||
from fontTools.ttLib import TTFont
|
||||
|
||||
try:
|
||||
fontfile = sys.argv[1]
|
||||
except IndexError:
|
||||
sys.exit("usage: fonttools colorLib.unbuilder FONTFILE")
|
||||
|
||||
font = TTFont(fontfile)
|
||||
colr = font["COLR"]
|
||||
if colr.version < 1:
|
||||
sys.exit(f"error: No COLR table version=1 found in {fontfile}")
|
||||
|
||||
colorGlyphs = unbuildColrV1(
|
||||
colr.table.LayerV1List,
|
||||
colr.table.BaseGlyphV1List,
|
||||
ignoreVarIdx=not colr.table.VarStore,
|
||||
)
|
||||
|
||||
pprint(colorGlyphs)
|
@ -16,43 +16,29 @@ class ExtendCodec(codecs.Codec):
|
||||
self.info = codecs.CodecInfo(name=self.name, encode=self.encode, decode=self.decode)
|
||||
codecs.register_error(name, self.error)
|
||||
|
||||
def encode(self, input, errors='strict'):
|
||||
assert errors == 'strict'
|
||||
#return codecs.encode(input, self.base_encoding, self.name), len(input)
|
||||
|
||||
# The above line could totally be all we needed, relying on the error
|
||||
# handling to replace the unencodable Unicode characters with our extended
|
||||
# byte sequences.
|
||||
#
|
||||
# However, there seems to be a design bug in Python (probably intentional):
|
||||
# the error handler for encoding is supposed to return a **Unicode** character,
|
||||
# that then needs to be encodable itself... Ugh.
|
||||
#
|
||||
# So we implement what codecs.encode() should have been doing: which is expect
|
||||
# error handler to return bytes() to be added to the output.
|
||||
#
|
||||
# This seems to have been fixed in Python 3.3. We should try using that and
|
||||
# use fallback only if that failed.
|
||||
# https://docs.python.org/3.3/library/codecs.html#codecs.register_error
|
||||
|
||||
def _map(self, mapper, output_type, exc_type, input, errors):
|
||||
base_error_handler = codecs.lookup_error(errors)
|
||||
length = len(input)
|
||||
out = b''
|
||||
out = output_type()
|
||||
while input:
|
||||
# first try to use self.error as the error handler
|
||||
try:
|
||||
part = codecs.encode(input, self.base_encoding)
|
||||
part = mapper(input, self.base_encoding, errors=self.name)
|
||||
out += part
|
||||
input = '' # All converted
|
||||
except UnicodeEncodeError as e:
|
||||
# Convert the correct part
|
||||
out += codecs.encode(input[:e.start], self.base_encoding)
|
||||
replacement, pos = self.error(e)
|
||||
break # All converted
|
||||
except exc_type as e:
|
||||
# else convert the correct part, handle error as requested and continue
|
||||
out += mapper(input[:e.start], self.base_encoding, self.name)
|
||||
replacement, pos = base_error_handler(e)
|
||||
out += replacement
|
||||
input = input[pos:]
|
||||
return out, length
|
||||
|
||||
def encode(self, input, errors='strict'):
|
||||
return self._map(codecs.encode, bytes, UnicodeEncodeError, input, errors)
|
||||
|
||||
def decode(self, input, errors='strict'):
|
||||
assert errors == 'strict'
|
||||
return codecs.decode(input, self.base_encoding, self.name), len(input)
|
||||
return self._map(codecs.decode, str, UnicodeDecodeError, input, errors)
|
||||
|
||||
def error(self, e):
|
||||
if isinstance(e, UnicodeDecodeError):
|
||||
|
@ -4,6 +4,7 @@ from fontTools.feaLib.location import FeatureLibLocation
|
||||
from fontTools.misc.encodingTools import getEncoding
|
||||
from collections import OrderedDict
|
||||
import itertools
|
||||
from typing import NamedTuple
|
||||
|
||||
SHIFT = " " * 4
|
||||
|
||||
@ -28,12 +29,15 @@ __all__ = [
|
||||
"Anchor",
|
||||
"AnchorDefinition",
|
||||
"AttachStatement",
|
||||
"AxisValueLocationStatement",
|
||||
"BaseAxis",
|
||||
"CVParametersNameStatement",
|
||||
"ChainContextPosStatement",
|
||||
"ChainContextSubstStatement",
|
||||
"CharacterStatement",
|
||||
"CursivePosStatement",
|
||||
"ElidedFallbackName",
|
||||
"ElidedFallbackNameID",
|
||||
"Expression",
|
||||
"FeatureNameStatement",
|
||||
"FeatureReferenceStatement",
|
||||
@ -62,6 +66,9 @@ __all__ = [
|
||||
"SingleSubstStatement",
|
||||
"SizeParameters",
|
||||
"Statement",
|
||||
"STATAxisValueStatement",
|
||||
"STATDesignAxisStatement",
|
||||
"STATNameStatement",
|
||||
"SubtableStatement",
|
||||
"TableBlock",
|
||||
"ValueRecord",
|
||||
@ -188,6 +195,21 @@ class Comment(Element):
|
||||
return self.text
|
||||
|
||||
|
||||
class NullGlyph(Expression):
|
||||
"""The NULL glyph, used in glyph deletion substitutions."""
|
||||
|
||||
def __init__(self, location=None):
|
||||
Expression.__init__(self, location)
|
||||
#: The name itself as a string
|
||||
|
||||
def glyphSet(self):
|
||||
"""The glyphs in this class as a tuple of :class:`GlyphName` objects."""
|
||||
return ()
|
||||
|
||||
def asFea(self, indent=""):
|
||||
return "NULL"
|
||||
|
||||
|
||||
class GlyphName(Expression):
|
||||
"""A single glyph name, such as ``cedilla``."""
|
||||
|
||||
@ -237,7 +259,7 @@ class GlyphClass(Expression):
|
||||
|
||||
def add_range(self, start, end, glyphs):
|
||||
"""Add a range (e.g. ``A-Z``) to the class. ``start`` and ``end``
|
||||
are either :class:`GlyphName` objects or strings representing the
|
||||
are either :class:`GlyphName` objects or strings representing the
|
||||
start and end glyphs in the class, and ``glyphs`` is the full list of
|
||||
:class:`GlyphName` objects in the range."""
|
||||
if self.curr < len(self.glyphs):
|
||||
@ -532,7 +554,7 @@ class MarkClass(object):
|
||||
|
||||
|
||||
class MarkClassDefinition(Statement):
|
||||
"""A single ``markClass`` statement. The ``markClass`` should be a
|
||||
"""A single ``markClass`` statement. The ``markClass`` should be a
|
||||
:class:`MarkClass` object, the ``anchor`` an :class:`Anchor` object,
|
||||
and the ``glyphs`` parameter should be a `glyph-containing object`_ .
|
||||
|
||||
@ -834,7 +856,7 @@ class IgnorePosStatement(Statement):
|
||||
"""An ``ignore pos`` statement, containing `one or more` contexts to ignore.
|
||||
|
||||
``chainContexts`` should be a list of ``(prefix, glyphs, suffix)`` tuples,
|
||||
with each of ``prefix``, ``glyphs`` and ``suffix`` being
|
||||
with each of ``prefix``, ``glyphs`` and ``suffix`` being
|
||||
`glyph-containing objects`_ ."""
|
||||
|
||||
def __init__(self, chainContexts, location=None):
|
||||
@ -1131,7 +1153,7 @@ class MarkBasePosStatement(Statement):
|
||||
def asFea(self, indent=""):
|
||||
res = "pos base {}".format(self.base.asFea())
|
||||
for a, m in self.marks:
|
||||
res += " {} mark @{}".format(a.asFea(), m.name)
|
||||
res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
|
||||
res += ";"
|
||||
return res
|
||||
|
||||
@ -1150,7 +1172,7 @@ class MarkLigPosStatement(Statement):
|
||||
# ... add definitions to mark classes...
|
||||
|
||||
glyph = GlyphName("lam_meem_jeem")
|
||||
marks = [
|
||||
marks = [
|
||||
[ (Anchor(625,1800), m1) ], # Attachments on 1st component (lam)
|
||||
[ (Anchor(376,-378), m2) ], # Attachments on 2nd component (meem)
|
||||
[ ] # No attachments on the jeem
|
||||
@ -1177,10 +1199,15 @@ class MarkLigPosStatement(Statement):
|
||||
for l in self.marks:
|
||||
temp = ""
|
||||
if l is None or not len(l):
|
||||
temp = " <anchor NULL>"
|
||||
temp = "\n" + indent + SHIFT * 2 + "<anchor NULL>"
|
||||
else:
|
||||
for a, m in l:
|
||||
temp += " {} mark @{}".format(a.asFea(), m.name)
|
||||
temp += (
|
||||
"\n"
|
||||
+ indent
|
||||
+ SHIFT * 2
|
||||
+ "{} mark @{}".format(a.asFea(), m.name)
|
||||
)
|
||||
ligs.append(temp)
|
||||
res += ("\n" + indent + SHIFT + "ligComponent").join(ligs)
|
||||
res += ";"
|
||||
@ -1203,7 +1230,7 @@ class MarkMarkPosStatement(Statement):
|
||||
def asFea(self, indent=""):
|
||||
res = "pos mark {}".format(self.baseMarks.asFea())
|
||||
for a, m in self.marks:
|
||||
res += " {} mark @{}".format(a.asFea(), m.name)
|
||||
res += "\n" + indent + SHIFT + "{} mark @{}".format(a.asFea(), m.name)
|
||||
res += ";"
|
||||
return res
|
||||
|
||||
@ -1246,8 +1273,9 @@ class MultipleSubstStatement(Statement):
|
||||
res += " " + " ".join(map(asFea, self.suffix))
|
||||
else:
|
||||
res += asFea(self.glyph)
|
||||
replacement = self.replacement or [NullGlyph()]
|
||||
res += " by "
|
||||
res += " ".join(map(asFea, self.replacement))
|
||||
res += " ".join(map(asFea, replacement))
|
||||
res += ";"
|
||||
return res
|
||||
|
||||
@ -1683,6 +1711,16 @@ class FeatureNameStatement(NameRecord):
|
||||
return '{} {}"{}";'.format(tag, plat, self.string)
|
||||
|
||||
|
||||
class STATNameStatement(NameRecord):
|
||||
"""Represents a STAT table ``name`` statement."""
|
||||
|
||||
def asFea(self, indent=""):
|
||||
plat = simplify_name_attributes(self.platformID, self.platEncID, self.langID)
|
||||
if plat != "":
|
||||
plat += " "
|
||||
return 'name {}"{}";'.format(plat, self.string)
|
||||
|
||||
|
||||
class SizeParameters(Statement):
|
||||
"""A ``parameters`` statement."""
|
||||
|
||||
@ -1861,3 +1899,132 @@ class VheaField(Statement):
|
||||
fields = ("VertTypoAscender", "VertTypoDescender", "VertTypoLineGap")
|
||||
keywords = dict([(x.lower(), x) for x in fields])
|
||||
return "{} {};".format(keywords[self.key], self.value)
|
||||
|
||||
|
||||
class STATDesignAxisStatement(Statement):
|
||||
"""A STAT table Design Axis
|
||||
|
||||
Args:
|
||||
tag (str): a 4 letter axis tag
|
||||
axisOrder (int): an int
|
||||
names (list): a list of :class:`STATNameStatement` objects
|
||||
"""
|
||||
|
||||
def __init__(self, tag, axisOrder, names, location=None):
|
||||
Statement.__init__(self, location)
|
||||
self.tag = tag
|
||||
self.axisOrder = axisOrder
|
||||
self.names = names
|
||||
self.location = location
|
||||
|
||||
def build(self, builder):
|
||||
builder.addDesignAxis(self, self.location)
|
||||
|
||||
def asFea(self, indent=""):
|
||||
indent += SHIFT
|
||||
res = f"DesignAxis {self.tag} {self.axisOrder} {{ \n"
|
||||
res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
|
||||
res += "};"
|
||||
return res
|
||||
|
||||
|
||||
class ElidedFallbackName(Statement):
|
||||
"""STAT table ElidedFallbackName
|
||||
|
||||
Args:
|
||||
names: a list of :class:`STATNameStatement` objects
|
||||
"""
|
||||
|
||||
def __init__(self, names, location=None):
|
||||
Statement.__init__(self, location)
|
||||
self.names = names
|
||||
self.location = location
|
||||
|
||||
def build(self, builder):
|
||||
builder.setElidedFallbackName(self.names, self.location)
|
||||
|
||||
def asFea(self, indent=""):
|
||||
indent += SHIFT
|
||||
res = "ElidedFallbackName { \n"
|
||||
res += ("\n" + indent).join([s.asFea(indent=indent) for s in self.names]) + "\n"
|
||||
res += "};"
|
||||
return res
|
||||
|
||||
|
||||
class ElidedFallbackNameID(Statement):
|
||||
"""STAT table ElidedFallbackNameID
|
||||
|
||||
Args:
|
||||
value: an int pointing to an existing name table name ID
|
||||
"""
|
||||
|
||||
def __init__(self, value, location=None):
|
||||
Statement.__init__(self, location)
|
||||
self.value = value
|
||||
self.location = location
|
||||
|
||||
def build(self, builder):
|
||||
builder.setElidedFallbackName(self.value, self.location)
|
||||
|
||||
def asFea(self, indent=""):
|
||||
return f"ElidedFallbackNameID {self.value};"
|
||||
|
||||
|
||||
class STATAxisValueStatement(Statement):
|
||||
"""A STAT table Axis Value Record
|
||||
|
||||
Args:
|
||||
names (list): a list of :class:`STATNameStatement` objects
|
||||
locations (list): a list of :class:`AxisValueLocationStatement` objects
|
||||
flags (int): an int
|
||||
"""
|
||||
|
||||
def __init__(self, names, locations, flags, location=None):
|
||||
Statement.__init__(self, location)
|
||||
self.names = names
|
||||
self.locations = locations
|
||||
self.flags = flags
|
||||
|
||||
def build(self, builder):
|
||||
builder.addAxisValueRecord(self, self.location)
|
||||
|
||||
def asFea(self, indent=""):
|
||||
res = "AxisValue {\n"
|
||||
for location in self.locations:
|
||||
res += location.asFea()
|
||||
|
||||
for nameRecord in self.names:
|
||||
res += nameRecord.asFea()
|
||||
res += "\n"
|
||||
|
||||
if self.flags:
|
||||
flags = ["OlderSiblingFontAttribute", "ElidableAxisValueName"]
|
||||
flagStrings = []
|
||||
curr = 1
|
||||
for i in range(len(flags)):
|
||||
if self.flags & curr != 0:
|
||||
flagStrings.append(flags[i])
|
||||
curr = curr << 1
|
||||
res += f"flag {' '.join(flagStrings)};\n"
|
||||
res += "};"
|
||||
return res
|
||||
|
||||
|
||||
class AxisValueLocationStatement(Statement):
|
||||
"""
|
||||
A STAT table Axis Value Location
|
||||
|
||||
Args:
|
||||
tag (str): a 4 letter axis tag
|
||||
values (list): a list of ints and/or floats
|
||||
"""
|
||||
|
||||
def __init__(self, tag, values, location=None):
|
||||
Statement.__init__(self, location)
|
||||
self.tag = tag
|
||||
self.values = values
|
||||
|
||||
def asFea(self, res=""):
|
||||
res += f"location {self.tag} "
|
||||
res += f"{' '.join(str(i) for i in self.values)};\n"
|
||||
return res
|
||||
|
@ -98,6 +98,7 @@ class Builder(object):
|
||||
"hhea",
|
||||
"name",
|
||||
"vhea",
|
||||
"STAT",
|
||||
]
|
||||
)
|
||||
|
||||
@ -159,6 +160,8 @@ class Builder(object):
|
||||
self.hhea_ = {}
|
||||
# for table 'vhea'
|
||||
self.vhea_ = {}
|
||||
# for table 'STAT'
|
||||
self.stat_ = {}
|
||||
|
||||
def build(self, tables=None, debug=False):
|
||||
if self.parseTree is None:
|
||||
@ -188,6 +191,8 @@ class Builder(object):
|
||||
self.build_name()
|
||||
if "OS/2" in tables:
|
||||
self.build_OS_2()
|
||||
if "STAT" in tables:
|
||||
self.build_STAT()
|
||||
for tag in ("GPOS", "GSUB"):
|
||||
if tag not in tables:
|
||||
continue
|
||||
@ -510,6 +515,140 @@ class Builder(object):
|
||||
if version >= 5:
|
||||
checkattr(table, ("usLowerOpticalPointSize", "usUpperOpticalPointSize"))
|
||||
|
||||
def setElidedFallbackName(self, value, location):
|
||||
# ElidedFallbackName is a convenience method for setting
|
||||
# ElidedFallbackNameID so only one can be allowed
|
||||
for token in ("ElidedFallbackName", "ElidedFallbackNameID"):
|
||||
if token in self.stat_:
|
||||
raise FeatureLibError(
|
||||
f"{token} is already set.",
|
||||
location,
|
||||
)
|
||||
if isinstance(value, int):
|
||||
self.stat_["ElidedFallbackNameID"] = value
|
||||
elif isinstance(value, list):
|
||||
self.stat_["ElidedFallbackName"] = value
|
||||
else:
|
||||
raise AssertionError(value)
|
||||
|
||||
def addDesignAxis(self, designAxis, location):
|
||||
if "DesignAxes" not in self.stat_:
|
||||
self.stat_["DesignAxes"] = []
|
||||
if designAxis.tag in (r.tag for r in self.stat_["DesignAxes"]):
|
||||
raise FeatureLibError(
|
||||
f'DesignAxis already defined for tag "{designAxis.tag}".',
|
||||
location,
|
||||
)
|
||||
if designAxis.axisOrder in (r.axisOrder for r in self.stat_["DesignAxes"]):
|
||||
raise FeatureLibError(
|
||||
f"DesignAxis already defined for axis number {designAxis.axisOrder}.",
|
||||
location,
|
||||
)
|
||||
self.stat_["DesignAxes"].append(designAxis)
|
||||
|
||||
def addAxisValueRecord(self, axisValueRecord, location):
|
||||
if "AxisValueRecords" not in self.stat_:
|
||||
self.stat_["AxisValueRecords"] = []
|
||||
# Check for duplicate AxisValueRecords
|
||||
for record_ in self.stat_["AxisValueRecords"]:
|
||||
if (
|
||||
{n.asFea() for n in record_.names}
|
||||
== {n.asFea() for n in axisValueRecord.names}
|
||||
and {n.asFea() for n in record_.locations}
|
||||
== {n.asFea() for n in axisValueRecord.locations}
|
||||
and record_.flags == axisValueRecord.flags
|
||||
):
|
||||
raise FeatureLibError(
|
||||
"An AxisValueRecord with these values is already defined.",
|
||||
location,
|
||||
)
|
||||
self.stat_["AxisValueRecords"].append(axisValueRecord)
|
||||
|
||||
def build_STAT(self):
|
||||
if not self.stat_:
|
||||
return
|
||||
|
||||
axes = self.stat_.get("DesignAxes")
|
||||
if not axes:
|
||||
raise FeatureLibError("DesignAxes not defined", None)
|
||||
axisValueRecords = self.stat_.get("AxisValueRecords")
|
||||
axisValues = {}
|
||||
format4_locations = []
|
||||
for tag in axes:
|
||||
axisValues[tag.tag] = []
|
||||
if axisValueRecords is not None:
|
||||
for avr in axisValueRecords:
|
||||
valuesDict = {}
|
||||
if avr.flags > 0:
|
||||
valuesDict["flags"] = avr.flags
|
||||
if len(avr.locations) == 1:
|
||||
location = avr.locations[0]
|
||||
values = location.values
|
||||
if len(values) == 1: # format1
|
||||
valuesDict.update({"value": values[0], "name": avr.names})
|
||||
if len(values) == 2: # format3
|
||||
valuesDict.update(
|
||||
{
|
||||
"value": values[0],
|
||||
"linkedValue": values[1],
|
||||
"name": avr.names,
|
||||
}
|
||||
)
|
||||
if len(values) == 3: # format2
|
||||
nominal, minVal, maxVal = values
|
||||
valuesDict.update(
|
||||
{
|
||||
"nominalValue": nominal,
|
||||
"rangeMinValue": minVal,
|
||||
"rangeMaxValue": maxVal,
|
||||
"name": avr.names,
|
||||
}
|
||||
)
|
||||
axisValues[location.tag].append(valuesDict)
|
||||
else:
|
||||
valuesDict.update(
|
||||
{
|
||||
"location": {i.tag: i.values[0] for i in avr.locations},
|
||||
"name": avr.names,
|
||||
}
|
||||
)
|
||||
format4_locations.append(valuesDict)
|
||||
|
||||
designAxes = [
|
||||
{
|
||||
"ordering": a.axisOrder,
|
||||
"tag": a.tag,
|
||||
"name": a.names,
|
||||
"values": axisValues[a.tag],
|
||||
}
|
||||
for a in axes
|
||||
]
|
||||
|
||||
nameTable = self.font.get("name")
|
||||
if not nameTable: # this only happens for unit tests
|
||||
nameTable = self.font["name"] = newTable("name")
|
||||
nameTable.names = []
|
||||
|
||||
if "ElidedFallbackNameID" in self.stat_:
|
||||
nameID = self.stat_["ElidedFallbackNameID"]
|
||||
name = nameTable.getDebugName(nameID)
|
||||
if not name:
|
||||
raise FeatureLibError(
|
||||
f"ElidedFallbackNameID {nameID} points "
|
||||
"to a nameID that does not exist in the "
|
||||
'"name" table',
|
||||
None,
|
||||
)
|
||||
elif "ElidedFallbackName" in self.stat_:
|
||||
nameID = self.stat_["ElidedFallbackName"]
|
||||
|
||||
otl.buildStatTable(
|
||||
self.font,
|
||||
designAxes,
|
||||
locations=format4_locations,
|
||||
elidedFallbackName=nameID,
|
||||
)
|
||||
|
||||
def build_codepages_(self, pages):
|
||||
pages2bits = {
|
||||
1252: 0,
|
||||
@ -718,8 +857,10 @@ class Builder(object):
|
||||
str(ix)
|
||||
]._replace(feature=key)
|
||||
except KeyError:
|
||||
warnings.warn("feaLib.Builder subclass needs upgrading to "
|
||||
"stash debug information. See fonttools#2065.")
|
||||
warnings.warn(
|
||||
"feaLib.Builder subclass needs upgrading to "
|
||||
"stash debug information. See fonttools#2065."
|
||||
)
|
||||
|
||||
feature_key = (feature_tag, lookup_indices)
|
||||
feature_index = feature_indices.get(feature_key)
|
||||
|
@ -314,10 +314,15 @@ class Parser(object):
|
||||
location,
|
||||
)
|
||||
|
||||
def parse_glyphclass_(self, accept_glyphname):
|
||||
def parse_glyphclass_(self, accept_glyphname, accept_null=False):
|
||||
# Parses a glyph class, either named or anonymous, or (if
|
||||
# ``bool(accept_glyphname)``) a glyph name.
|
||||
# ``bool(accept_glyphname)``) a glyph name. If ``bool(accept_null)`` then
|
||||
# also accept the special NULL glyph.
|
||||
if accept_glyphname and self.next_token_type_ in (Lexer.NAME, Lexer.CID):
|
||||
if accept_null and self.next_token_ == "NULL":
|
||||
# If you want a glyph called NULL, you should escape it.
|
||||
self.advance_lexer_()
|
||||
return self.ast.NullGlyph(location=self.cur_token_location_)
|
||||
glyph = self.expect_glyph_()
|
||||
self.check_glyph_name_in_glyph_set(glyph)
|
||||
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
|
||||
@ -375,7 +380,8 @@ class Parser(object):
|
||||
self.expect_symbol_("-")
|
||||
range_end = self.expect_cid_()
|
||||
self.check_glyph_name_in_glyph_set(
|
||||
f"cid{range_start:05d}", f"cid{range_end:05d}",
|
||||
f"cid{range_start:05d}",
|
||||
f"cid{range_end:05d}",
|
||||
)
|
||||
glyphs.add_cid_range(
|
||||
range_start,
|
||||
@ -804,7 +810,7 @@ class Parser(object):
|
||||
if self.next_token_ == "by":
|
||||
keyword = self.expect_keyword_("by")
|
||||
while self.next_token_ != ";":
|
||||
gc = self.parse_glyphclass_(accept_glyphname=True)
|
||||
gc = self.parse_glyphclass_(accept_glyphname=True, accept_null=True)
|
||||
new.append(gc)
|
||||
elif self.next_token_ == "from":
|
||||
keyword = self.expect_keyword_("from")
|
||||
@ -837,6 +843,11 @@ class Parser(object):
|
||||
|
||||
num_lookups = len([l for l in lookups if l is not None])
|
||||
|
||||
is_deletion = False
|
||||
if len(new) == 1 and len(new[0].glyphSet()) == 0:
|
||||
new = [] # Deletion
|
||||
is_deletion = True
|
||||
|
||||
# GSUB lookup type 1: Single substitution.
|
||||
# Format A: "substitute a by a.sc;"
|
||||
# Format B: "substitute [one.fitted one.oldstyle] by one;"
|
||||
@ -863,8 +874,10 @@ class Parser(object):
|
||||
not reverse
|
||||
and len(old) == 1
|
||||
and len(old[0].glyphSet()) == 1
|
||||
and len(new) > 1
|
||||
and max([len(n.glyphSet()) for n in new]) == 1
|
||||
and (
|
||||
(len(new) > 1 and max([len(n.glyphSet()) for n in new]) == 1)
|
||||
or len(new) == 0
|
||||
)
|
||||
and num_lookups == 0
|
||||
):
|
||||
return self.ast.MultipleSubstStatement(
|
||||
@ -936,7 +949,7 @@ class Parser(object):
|
||||
)
|
||||
|
||||
# If there are remaining glyphs to parse, this is an invalid GSUB statement
|
||||
if len(new) != 0:
|
||||
if len(new) != 0 or is_deletion:
|
||||
raise FeatureLibError("Invalid substitution statement", location)
|
||||
|
||||
# GSUB lookup type 6: Chaining contextual substitution.
|
||||
@ -990,6 +1003,7 @@ class Parser(object):
|
||||
"name": self.parse_table_name_,
|
||||
"BASE": self.parse_table_BASE_,
|
||||
"OS/2": self.parse_table_OS_2_,
|
||||
"STAT": self.parse_table_STAT_,
|
||||
}.get(name)
|
||||
if handler:
|
||||
handler(table)
|
||||
@ -1149,6 +1163,35 @@ class Parser(object):
|
||||
unescaped = self.unescape_string_(string, encoding)
|
||||
return platformID, platEncID, langID, unescaped
|
||||
|
||||
def parse_stat_name_(self):
|
||||
platEncID = None
|
||||
langID = None
|
||||
if self.next_token_type_ in Lexer.NUMBERS:
|
||||
platformID = self.expect_any_number_()
|
||||
location = self.cur_token_location_
|
||||
if platformID not in (1, 3):
|
||||
raise FeatureLibError("Expected platform id 1 or 3", location)
|
||||
if self.next_token_type_ in Lexer.NUMBERS:
|
||||
platEncID = self.expect_any_number_()
|
||||
langID = self.expect_any_number_()
|
||||
else:
|
||||
platformID = 3
|
||||
location = self.cur_token_location_
|
||||
|
||||
if platformID == 1: # Macintosh
|
||||
platEncID = platEncID or 0 # Roman
|
||||
langID = langID or 0 # English
|
||||
else: # 3, Windows
|
||||
platEncID = platEncID or 1 # Unicode
|
||||
langID = langID or 0x0409 # English
|
||||
|
||||
string = self.expect_string_()
|
||||
encoding = getEncoding(platformID, platEncID, langID)
|
||||
if encoding is None:
|
||||
raise FeatureLibError("Unsupported encoding", location)
|
||||
unescaped = self.unescape_string_(string, encoding)
|
||||
return platformID, platEncID, langID, unescaped
|
||||
|
||||
def parse_nameid_(self):
|
||||
assert self.cur_token_ == "nameid", self.cur_token_
|
||||
location, nameID = self.cur_token_location_, self.expect_any_number_()
|
||||
@ -1270,6 +1313,198 @@ class Parser(object):
|
||||
elif self.cur_token_ == ";":
|
||||
continue
|
||||
|
||||
def parse_STAT_ElidedFallbackName(self):
|
||||
assert self.is_cur_keyword_("ElidedFallbackName")
|
||||
self.expect_symbol_("{")
|
||||
names = []
|
||||
while self.next_token_ != "}" or self.cur_comments_:
|
||||
self.advance_lexer_()
|
||||
if self.is_cur_keyword_("name"):
|
||||
platformID, platEncID, langID, string = self.parse_stat_name_()
|
||||
nameRecord = self.ast.STATNameStatement(
|
||||
"stat",
|
||||
platformID,
|
||||
platEncID,
|
||||
langID,
|
||||
string,
|
||||
location=self.cur_token_location_,
|
||||
)
|
||||
names.append(nameRecord)
|
||||
else:
|
||||
if self.cur_token_ != ";":
|
||||
raise FeatureLibError(
|
||||
f"Unexpected token {self.cur_token_} " f"in ElidedFallbackName",
|
||||
self.cur_token_location_,
|
||||
)
|
||||
self.expect_symbol_("}")
|
||||
if not names:
|
||||
raise FeatureLibError('Expected "name"', self.cur_token_location_)
|
||||
return names
|
||||
|
||||
def parse_STAT_design_axis(self):
|
||||
assert self.is_cur_keyword_("DesignAxis")
|
||||
names = []
|
||||
axisTag = self.expect_tag_()
|
||||
if (
|
||||
axisTag not in ("ital", "opsz", "slnt", "wdth", "wght")
|
||||
and not axisTag.isupper()
|
||||
):
|
||||
log.warning(f"Unregistered axis tag {axisTag} should be uppercase.")
|
||||
axisOrder = self.expect_number_()
|
||||
self.expect_symbol_("{")
|
||||
while self.next_token_ != "}" or self.cur_comments_:
|
||||
self.advance_lexer_()
|
||||
if self.cur_token_type_ is Lexer.COMMENT:
|
||||
continue
|
||||
elif self.is_cur_keyword_("name"):
|
||||
location = self.cur_token_location_
|
||||
platformID, platEncID, langID, string = self.parse_stat_name_()
|
||||
name = self.ast.STATNameStatement(
|
||||
"stat", platformID, platEncID, langID, string, location=location
|
||||
)
|
||||
names.append(name)
|
||||
elif self.cur_token_ == ";":
|
||||
continue
|
||||
else:
|
||||
raise FeatureLibError(
|
||||
f'Expected "name", got {self.cur_token_}', self.cur_token_location_
|
||||
)
|
||||
|
||||
self.expect_symbol_("}")
|
||||
return self.ast.STATDesignAxisStatement(
|
||||
axisTag, axisOrder, names, self.cur_token_location_
|
||||
)
|
||||
|
||||
def parse_STAT_axis_value_(self):
|
||||
assert self.is_cur_keyword_("AxisValue")
|
||||
self.expect_symbol_("{")
|
||||
locations = []
|
||||
names = []
|
||||
flags = 0
|
||||
while self.next_token_ != "}" or self.cur_comments_:
|
||||
self.advance_lexer_(comments=True)
|
||||
if self.cur_token_type_ is Lexer.COMMENT:
|
||||
continue
|
||||
elif self.is_cur_keyword_("name"):
|
||||
location = self.cur_token_location_
|
||||
platformID, platEncID, langID, string = self.parse_stat_name_()
|
||||
name = self.ast.STATNameStatement(
|
||||
"stat", platformID, platEncID, langID, string, location=location
|
||||
)
|
||||
names.append(name)
|
||||
elif self.is_cur_keyword_("location"):
|
||||
location = self.parse_STAT_location()
|
||||
locations.append(location)
|
||||
elif self.is_cur_keyword_("flag"):
|
||||
flags = self.expect_stat_flags()
|
||||
elif self.cur_token_ == ";":
|
||||
continue
|
||||
else:
|
||||
raise FeatureLibError(
|
||||
f"Unexpected token {self.cur_token_} " f"in AxisValue",
|
||||
self.cur_token_location_,
|
||||
)
|
||||
self.expect_symbol_("}")
|
||||
if not names:
|
||||
raise FeatureLibError('Expected "Axis Name"', self.cur_token_location_)
|
||||
if not locations:
|
||||
raise FeatureLibError('Expected "Axis location"', self.cur_token_location_)
|
||||
if len(locations) > 1:
|
||||
for location in locations:
|
||||
if len(location.values) > 1:
|
||||
raise FeatureLibError(
|
||||
"Only one value is allowed in a "
|
||||
"Format 4 Axis Value Record, but "
|
||||
f"{len(location.values)} were found.",
|
||||
self.cur_token_location_,
|
||||
)
|
||||
format4_tags = []
|
||||
for location in locations:
|
||||
tag = location.tag
|
||||
if tag in format4_tags:
|
||||
raise FeatureLibError(
|
||||
f"Axis tag {tag} already " "defined.", self.cur_token_location_
|
||||
)
|
||||
format4_tags.append(tag)
|
||||
|
||||
return self.ast.STATAxisValueStatement(
|
||||
names, locations, flags, self.cur_token_location_
|
||||
)
|
||||
|
||||
def parse_STAT_location(self):
|
||||
values = []
|
||||
tag = self.expect_tag_()
|
||||
if len(tag.strip()) != 4:
|
||||
raise FeatureLibError(
|
||||
f"Axis tag {self.cur_token_} must be 4 " "characters",
|
||||
self.cur_token_location_,
|
||||
)
|
||||
|
||||
while self.next_token_ != ";":
|
||||
if self.next_token_type_ is Lexer.FLOAT:
|
||||
value = self.expect_float_()
|
||||
values.append(value)
|
||||
elif self.next_token_type_ is Lexer.NUMBER:
|
||||
value = self.expect_number_()
|
||||
values.append(value)
|
||||
else:
|
||||
raise FeatureLibError(
|
||||
f'Unexpected value "{self.next_token_}". '
|
||||
"Expected integer or float.",
|
||||
self.next_token_location_,
|
||||
)
|
||||
if len(values) == 3:
|
||||
nominal, min_val, max_val = values
|
||||
if nominal < min_val or nominal > max_val:
|
||||
raise FeatureLibError(
|
||||
f"Default value {nominal} is outside "
|
||||
f"of specified range "
|
||||
f"{min_val}-{max_val}.",
|
||||
self.next_token_location_,
|
||||
)
|
||||
return self.ast.AxisValueLocationStatement(tag, values)
|
||||
|
||||
def parse_table_STAT_(self, table):
|
||||
statements = table.statements
|
||||
design_axes = []
|
||||
while self.next_token_ != "}" or self.cur_comments_:
|
||||
self.advance_lexer_(comments=True)
|
||||
if self.cur_token_type_ is Lexer.COMMENT:
|
||||
statements.append(
|
||||
self.ast.Comment(self.cur_token_, location=self.cur_token_location_)
|
||||
)
|
||||
elif self.cur_token_type_ is Lexer.NAME:
|
||||
if self.is_cur_keyword_("ElidedFallbackName"):
|
||||
names = self.parse_STAT_ElidedFallbackName()
|
||||
statements.append(self.ast.ElidedFallbackName(names))
|
||||
elif self.is_cur_keyword_("ElidedFallbackNameID"):
|
||||
value = self.expect_number_()
|
||||
statements.append(self.ast.ElidedFallbackNameID(value))
|
||||
self.expect_symbol_(";")
|
||||
elif self.is_cur_keyword_("DesignAxis"):
|
||||
designAxis = self.parse_STAT_design_axis()
|
||||
design_axes.append(designAxis.tag)
|
||||
statements.append(designAxis)
|
||||
self.expect_symbol_(";")
|
||||
elif self.is_cur_keyword_("AxisValue"):
|
||||
axisValueRecord = self.parse_STAT_axis_value_()
|
||||
for location in axisValueRecord.locations:
|
||||
if location.tag not in design_axes:
|
||||
# Tag must be defined in a DesignAxis before it
|
||||
# can be referenced
|
||||
raise FeatureLibError(
|
||||
"DesignAxis not defined for " f"{location.tag}.",
|
||||
self.cur_token_location_,
|
||||
)
|
||||
statements.append(axisValueRecord)
|
||||
self.expect_symbol_(";")
|
||||
else:
|
||||
raise FeatureLibError(
|
||||
f"Unexpected token {self.cur_token_}", self.cur_token_location_
|
||||
)
|
||||
elif self.cur_token_ == ";":
|
||||
continue
|
||||
|
||||
def parse_base_tag_list_(self):
|
||||
# Parses BASE table entries. (See `section 9.a <https://adobe-type-tools.github.io/afdko/OpenTypeFeatureFileSpecification.html#9.a>`_)
|
||||
assert self.cur_token_ in (
|
||||
@ -1771,7 +2006,7 @@ class Parser(object):
|
||||
raise FeatureLibError("Expected a tag", self.cur_token_location_)
|
||||
if len(self.cur_token_) > 4:
|
||||
raise FeatureLibError(
|
||||
"Tags can not be longer than 4 characters", self.cur_token_location_
|
||||
"Tags cannot be longer than 4 characters", self.cur_token_location_
|
||||
)
|
||||
return (self.cur_token_ + " ")[:4]
|
||||
|
||||
@ -1843,6 +2078,32 @@ class Parser(object):
|
||||
"Expected an integer or floating-point number", self.cur_token_location_
|
||||
)
|
||||
|
||||
def expect_stat_flags(self):
|
||||
value = 0
|
||||
flags = {
|
||||
"OlderSiblingFontAttribute": 1,
|
||||
"ElidableAxisValueName": 2,
|
||||
}
|
||||
while self.next_token_ != ";":
|
||||
if self.next_token_ in flags:
|
||||
name = self.expect_name_()
|
||||
value = value | flags[name]
|
||||
else:
|
||||
raise FeatureLibError(
|
||||
f"Unexpected STAT flag {self.cur_token_}", self.cur_token_location_
|
||||
)
|
||||
return value
|
||||
|
||||
def expect_stat_values_(self):
|
||||
if self.next_token_type_ == Lexer.FLOAT:
|
||||
return self.expect_float_()
|
||||
elif self.next_token_type_ is Lexer.NUMBER:
|
||||
return self.expect_number_()
|
||||
else:
|
||||
raise FeatureLibError(
|
||||
"Expected an integer or floating-point number", self.cur_token_location_
|
||||
)
|
||||
|
||||
def expect_string_(self):
|
||||
self.advance_lexer_()
|
||||
if self.cur_token_type_ is Lexer.STRING:
|
||||
|
@ -1,4 +1,3 @@
|
||||
|
||||
__all__ = ["FontBuilder"]
|
||||
|
||||
"""
|
||||
@ -136,192 +135,192 @@ from .ttLib.tables._c_m_a_p import cmap_classes
|
||||
from .ttLib.tables._n_a_m_e import NameRecord, makeName
|
||||
from .misc.timeTools import timestampNow
|
||||
import struct
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
_headDefaults = dict(
|
||||
tableVersion = 1.0,
|
||||
fontRevision = 1.0,
|
||||
checkSumAdjustment = 0,
|
||||
magicNumber = 0x5F0F3CF5,
|
||||
flags = 0x0003,
|
||||
unitsPerEm = 1000,
|
||||
created = 0,
|
||||
modified = 0,
|
||||
xMin = 0,
|
||||
yMin = 0,
|
||||
xMax = 0,
|
||||
yMax = 0,
|
||||
macStyle = 0,
|
||||
lowestRecPPEM = 3,
|
||||
fontDirectionHint = 2,
|
||||
indexToLocFormat = 0,
|
||||
glyphDataFormat = 0,
|
||||
tableVersion=1.0,
|
||||
fontRevision=1.0,
|
||||
checkSumAdjustment=0,
|
||||
magicNumber=0x5F0F3CF5,
|
||||
flags=0x0003,
|
||||
unitsPerEm=1000,
|
||||
created=0,
|
||||
modified=0,
|
||||
xMin=0,
|
||||
yMin=0,
|
||||
xMax=0,
|
||||
yMax=0,
|
||||
macStyle=0,
|
||||
lowestRecPPEM=3,
|
||||
fontDirectionHint=2,
|
||||
indexToLocFormat=0,
|
||||
glyphDataFormat=0,
|
||||
)
|
||||
|
||||
_maxpDefaultsTTF = dict(
|
||||
tableVersion = 0x00010000,
|
||||
numGlyphs = 0,
|
||||
maxPoints = 0,
|
||||
maxContours = 0,
|
||||
maxCompositePoints = 0,
|
||||
maxCompositeContours = 0,
|
||||
maxZones = 2,
|
||||
maxTwilightPoints = 0,
|
||||
maxStorage = 0,
|
||||
maxFunctionDefs = 0,
|
||||
maxInstructionDefs = 0,
|
||||
maxStackElements = 0,
|
||||
maxSizeOfInstructions = 0,
|
||||
maxComponentElements = 0,
|
||||
maxComponentDepth = 0,
|
||||
tableVersion=0x00010000,
|
||||
numGlyphs=0,
|
||||
maxPoints=0,
|
||||
maxContours=0,
|
||||
maxCompositePoints=0,
|
||||
maxCompositeContours=0,
|
||||
maxZones=2,
|
||||
maxTwilightPoints=0,
|
||||
maxStorage=0,
|
||||
maxFunctionDefs=0,
|
||||
maxInstructionDefs=0,
|
||||
maxStackElements=0,
|
||||
maxSizeOfInstructions=0,
|
||||
maxComponentElements=0,
|
||||
maxComponentDepth=0,
|
||||
)
|
||||
_maxpDefaultsOTF = dict(
|
||||
tableVersion = 0x00005000,
|
||||
numGlyphs = 0,
|
||||
tableVersion=0x00005000,
|
||||
numGlyphs=0,
|
||||
)
|
||||
|
||||
_postDefaults = dict(
|
||||
formatType = 3.0,
|
||||
italicAngle = 0,
|
||||
underlinePosition = 0,
|
||||
underlineThickness = 0,
|
||||
isFixedPitch = 0,
|
||||
minMemType42 = 0,
|
||||
maxMemType42 = 0,
|
||||
minMemType1 = 0,
|
||||
maxMemType1 = 0,
|
||||
formatType=3.0,
|
||||
italicAngle=0,
|
||||
underlinePosition=0,
|
||||
underlineThickness=0,
|
||||
isFixedPitch=0,
|
||||
minMemType42=0,
|
||||
maxMemType42=0,
|
||||
minMemType1=0,
|
||||
maxMemType1=0,
|
||||
)
|
||||
|
||||
_hheaDefaults = dict(
|
||||
tableVersion = 0x00010000,
|
||||
ascent = 0,
|
||||
descent = 0,
|
||||
lineGap = 0,
|
||||
advanceWidthMax = 0,
|
||||
minLeftSideBearing = 0,
|
||||
minRightSideBearing = 0,
|
||||
xMaxExtent = 0,
|
||||
caretSlopeRise = 1,
|
||||
caretSlopeRun = 0,
|
||||
caretOffset = 0,
|
||||
reserved0 = 0,
|
||||
reserved1 = 0,
|
||||
reserved2 = 0,
|
||||
reserved3 = 0,
|
||||
metricDataFormat = 0,
|
||||
numberOfHMetrics = 0,
|
||||
tableVersion=0x00010000,
|
||||
ascent=0,
|
||||
descent=0,
|
||||
lineGap=0,
|
||||
advanceWidthMax=0,
|
||||
minLeftSideBearing=0,
|
||||
minRightSideBearing=0,
|
||||
xMaxExtent=0,
|
||||
caretSlopeRise=1,
|
||||
caretSlopeRun=0,
|
||||
caretOffset=0,
|
||||
reserved0=0,
|
||||
reserved1=0,
|
||||
reserved2=0,
|
||||
reserved3=0,
|
||||
metricDataFormat=0,
|
||||
numberOfHMetrics=0,
|
||||
)
|
||||
|
||||
_vheaDefaults = dict(
|
||||
tableVersion = 0x00010000,
|
||||
ascent = 0,
|
||||
descent = 0,
|
||||
lineGap = 0,
|
||||
advanceHeightMax = 0,
|
||||
minTopSideBearing = 0,
|
||||
minBottomSideBearing = 0,
|
||||
yMaxExtent = 0,
|
||||
caretSlopeRise = 0,
|
||||
caretSlopeRun = 0,
|
||||
reserved0 = 0,
|
||||
reserved1 = 0,
|
||||
reserved2 = 0,
|
||||
reserved3 = 0,
|
||||
reserved4 = 0,
|
||||
metricDataFormat = 0,
|
||||
numberOfVMetrics = 0,
|
||||
tableVersion=0x00010000,
|
||||
ascent=0,
|
||||
descent=0,
|
||||
lineGap=0,
|
||||
advanceHeightMax=0,
|
||||
minTopSideBearing=0,
|
||||
minBottomSideBearing=0,
|
||||
yMaxExtent=0,
|
||||
caretSlopeRise=0,
|
||||
caretSlopeRun=0,
|
||||
reserved0=0,
|
||||
reserved1=0,
|
||||
reserved2=0,
|
||||
reserved3=0,
|
||||
reserved4=0,
|
||||
metricDataFormat=0,
|
||||
numberOfVMetrics=0,
|
||||
)
|
||||
|
||||
_nameIDs = dict(
|
||||
copyright = 0,
|
||||
familyName = 1,
|
||||
styleName = 2,
|
||||
uniqueFontIdentifier = 3,
|
||||
fullName = 4,
|
||||
version = 5,
|
||||
psName = 6,
|
||||
trademark = 7,
|
||||
manufacturer = 8,
|
||||
designer = 9,
|
||||
description = 10,
|
||||
vendorURL = 11,
|
||||
designerURL = 12,
|
||||
licenseDescription = 13,
|
||||
licenseInfoURL = 14,
|
||||
# reserved = 15,
|
||||
typographicFamily = 16,
|
||||
typographicSubfamily = 17,
|
||||
compatibleFullName = 18,
|
||||
sampleText = 19,
|
||||
postScriptCIDFindfontName = 20,
|
||||
wwsFamilyName = 21,
|
||||
wwsSubfamilyName = 22,
|
||||
lightBackgroundPalette = 23,
|
||||
darkBackgroundPalette = 24,
|
||||
variationsPostScriptNamePrefix = 25,
|
||||
copyright=0,
|
||||
familyName=1,
|
||||
styleName=2,
|
||||
uniqueFontIdentifier=3,
|
||||
fullName=4,
|
||||
version=5,
|
||||
psName=6,
|
||||
trademark=7,
|
||||
manufacturer=8,
|
||||
designer=9,
|
||||
description=10,
|
||||
vendorURL=11,
|
||||
designerURL=12,
|
||||
licenseDescription=13,
|
||||
licenseInfoURL=14,
|
||||
# reserved = 15,
|
||||
typographicFamily=16,
|
||||
typographicSubfamily=17,
|
||||
compatibleFullName=18,
|
||||
sampleText=19,
|
||||
postScriptCIDFindfontName=20,
|
||||
wwsFamilyName=21,
|
||||
wwsSubfamilyName=22,
|
||||
lightBackgroundPalette=23,
|
||||
darkBackgroundPalette=24,
|
||||
variationsPostScriptNamePrefix=25,
|
||||
)
|
||||
|
||||
# to insert in setupNameTable doc string:
|
||||
# print("\n".join(("%s (nameID %s)" % (k, v)) for k, v in sorted(_nameIDs.items(), key=lambda x: x[1])))
|
||||
|
||||
_panoseDefaults = dict(
|
||||
bFamilyType = 0,
|
||||
bSerifStyle = 0,
|
||||
bWeight = 0,
|
||||
bProportion = 0,
|
||||
bContrast = 0,
|
||||
bStrokeVariation = 0,
|
||||
bArmStyle = 0,
|
||||
bLetterForm = 0,
|
||||
bMidline = 0,
|
||||
bXHeight = 0,
|
||||
bFamilyType=0,
|
||||
bSerifStyle=0,
|
||||
bWeight=0,
|
||||
bProportion=0,
|
||||
bContrast=0,
|
||||
bStrokeVariation=0,
|
||||
bArmStyle=0,
|
||||
bLetterForm=0,
|
||||
bMidline=0,
|
||||
bXHeight=0,
|
||||
)
|
||||
|
||||
_OS2Defaults = dict(
|
||||
version = 3,
|
||||
xAvgCharWidth = 0,
|
||||
usWeightClass = 400,
|
||||
usWidthClass = 5,
|
||||
fsType = 0x0004, # default: Preview & Print embedding
|
||||
ySubscriptXSize = 0,
|
||||
ySubscriptYSize = 0,
|
||||
ySubscriptXOffset = 0,
|
||||
ySubscriptYOffset = 0,
|
||||
ySuperscriptXSize = 0,
|
||||
ySuperscriptYSize = 0,
|
||||
ySuperscriptXOffset = 0,
|
||||
ySuperscriptYOffset = 0,
|
||||
yStrikeoutSize = 0,
|
||||
yStrikeoutPosition = 0,
|
||||
sFamilyClass = 0,
|
||||
panose = _panoseDefaults,
|
||||
ulUnicodeRange1 = 0,
|
||||
ulUnicodeRange2 = 0,
|
||||
ulUnicodeRange3 = 0,
|
||||
ulUnicodeRange4 = 0,
|
||||
achVendID = "????",
|
||||
fsSelection = 0,
|
||||
usFirstCharIndex = 0,
|
||||
usLastCharIndex = 0,
|
||||
sTypoAscender = 0,
|
||||
sTypoDescender = 0,
|
||||
sTypoLineGap = 0,
|
||||
usWinAscent = 0,
|
||||
usWinDescent = 0,
|
||||
ulCodePageRange1 = 0,
|
||||
ulCodePageRange2 = 0,
|
||||
sxHeight = 0,
|
||||
sCapHeight = 0,
|
||||
usDefaultChar = 0, # .notdef
|
||||
usBreakChar = 32, # space
|
||||
usMaxContext = 0,
|
||||
usLowerOpticalPointSize = 0,
|
||||
usUpperOpticalPointSize = 0,
|
||||
version=3,
|
||||
xAvgCharWidth=0,
|
||||
usWeightClass=400,
|
||||
usWidthClass=5,
|
||||
fsType=0x0004, # default: Preview & Print embedding
|
||||
ySubscriptXSize=0,
|
||||
ySubscriptYSize=0,
|
||||
ySubscriptXOffset=0,
|
||||
ySubscriptYOffset=0,
|
||||
ySuperscriptXSize=0,
|
||||
ySuperscriptYSize=0,
|
||||
ySuperscriptXOffset=0,
|
||||
ySuperscriptYOffset=0,
|
||||
yStrikeoutSize=0,
|
||||
yStrikeoutPosition=0,
|
||||
sFamilyClass=0,
|
||||
panose=_panoseDefaults,
|
||||
ulUnicodeRange1=0,
|
||||
ulUnicodeRange2=0,
|
||||
ulUnicodeRange3=0,
|
||||
ulUnicodeRange4=0,
|
||||
achVendID="????",
|
||||
fsSelection=0,
|
||||
usFirstCharIndex=0,
|
||||
usLastCharIndex=0,
|
||||
sTypoAscender=0,
|
||||
sTypoDescender=0,
|
||||
sTypoLineGap=0,
|
||||
usWinAscent=0,
|
||||
usWinDescent=0,
|
||||
ulCodePageRange1=0,
|
||||
ulCodePageRange2=0,
|
||||
sxHeight=0,
|
||||
sCapHeight=0,
|
||||
usDefaultChar=0, # .notdef
|
||||
usBreakChar=32, # space
|
||||
usMaxContext=0,
|
||||
usLowerOpticalPointSize=0,
|
||||
usUpperOpticalPointSize=0,
|
||||
)
|
||||
|
||||
|
||||
class FontBuilder(object):
|
||||
|
||||
def __init__(self, unitsPerEm=None, font=None, isTTF=True):
|
||||
"""Initialize a FontBuilder instance.
|
||||
|
||||
@ -395,7 +394,7 @@ class FontBuilder(object):
|
||||
"""
|
||||
subTables = []
|
||||
highestUnicode = max(cmapping)
|
||||
if highestUnicode > 0xffff:
|
||||
if highestUnicode > 0xFFFF:
|
||||
cmapping_3_1 = dict((k, v) for k, v in cmapping.items() if k < 0x10000)
|
||||
subTable_3_10 = buildCmapSubTable(cmapping, 12, 3, 10)
|
||||
subTables.append(subTable_3_10)
|
||||
@ -408,7 +407,9 @@ class FontBuilder(object):
|
||||
except struct.error:
|
||||
# format 4 overflowed, fall back to format 12
|
||||
if not allowFallback:
|
||||
raise ValueError("cmap format 4 subtable overflowed; sort glyph order by unicode to fix.")
|
||||
raise ValueError(
|
||||
"cmap format 4 subtable overflowed; sort glyph order by unicode to fix."
|
||||
)
|
||||
format = 12
|
||||
subTable_3_1 = buildCmapSubTable(cmapping_3_1, format, 3, 1)
|
||||
subTables.append(subTable_3_1)
|
||||
@ -489,17 +490,33 @@ class FontBuilder(object):
|
||||
"""
|
||||
if "xAvgCharWidth" not in values:
|
||||
gs = self.font.getGlyphSet()
|
||||
widths = [gs[glyphName].width for glyphName in gs.keys() if gs[glyphName].width > 0]
|
||||
widths = [
|
||||
gs[glyphName].width
|
||||
for glyphName in gs.keys()
|
||||
if gs[glyphName].width > 0
|
||||
]
|
||||
values["xAvgCharWidth"] = int(round(sum(widths) / float(len(widths))))
|
||||
self._initTableWithValues("OS/2", _OS2Defaults, values)
|
||||
if not ("ulUnicodeRange1" in values or "ulUnicodeRange2" in values or
|
||||
"ulUnicodeRange3" in values or "ulUnicodeRange3" in values):
|
||||
assert "cmap" in self.font, "the 'cmap' table must be setup before the 'OS/2' table"
|
||||
if not (
|
||||
"ulUnicodeRange1" in values
|
||||
or "ulUnicodeRange2" in values
|
||||
or "ulUnicodeRange3" in values
|
||||
or "ulUnicodeRange3" in values
|
||||
):
|
||||
assert (
|
||||
"cmap" in self.font
|
||||
), "the 'cmap' table must be setup before the 'OS/2' table"
|
||||
self.font["OS/2"].recalcUnicodeRanges(self.font)
|
||||
|
||||
def setupCFF(self, psName, fontInfo, charStringsDict, privateDict):
|
||||
from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
|
||||
GlobalSubrsIndex, PrivateDict
|
||||
from .cffLib import (
|
||||
CFFFontSet,
|
||||
TopDictIndex,
|
||||
TopDict,
|
||||
CharStrings,
|
||||
GlobalSubrsIndex,
|
||||
PrivateDict,
|
||||
)
|
||||
|
||||
assert not self.isTTF
|
||||
self.font.sfntVersion = "OTTO"
|
||||
@ -528,7 +545,9 @@ class FontBuilder(object):
|
||||
scale = 1 / self.font["head"].unitsPerEm
|
||||
topDict.FontMatrix = [scale, 0, 0, scale, 0, 0]
|
||||
|
||||
charStrings = CharStrings(None, topDict.charset, globalSubrs, private, fdSelect, fdArray)
|
||||
charStrings = CharStrings(
|
||||
None, topDict.charset, globalSubrs, private, fdSelect, fdArray
|
||||
)
|
||||
for glyphName, charString in charStringsDict.items():
|
||||
charString.private = private
|
||||
charString.globalSubrs = globalSubrs
|
||||
@ -541,8 +560,16 @@ class FontBuilder(object):
|
||||
self.font["CFF "].cff = fontSet
|
||||
|
||||
def setupCFF2(self, charStringsDict, fdArrayList=None, regions=None):
|
||||
from .cffLib import CFFFontSet, TopDictIndex, TopDict, CharStrings, \
|
||||
GlobalSubrsIndex, PrivateDict, FDArrayIndex, FontDict
|
||||
from .cffLib import (
|
||||
CFFFontSet,
|
||||
TopDictIndex,
|
||||
TopDict,
|
||||
CharStrings,
|
||||
GlobalSubrsIndex,
|
||||
PrivateDict,
|
||||
FDArrayIndex,
|
||||
FontDict,
|
||||
)
|
||||
|
||||
assert not self.isTTF
|
||||
self.font.sfntVersion = "OTTO"
|
||||
@ -628,10 +655,40 @@ class FontBuilder(object):
|
||||
self.calcGlyphBounds()
|
||||
|
||||
def setupFvar(self, axes, instances):
|
||||
"""Adds an font variations table to the font.
|
||||
|
||||
Args:
|
||||
axes (list): See below.
|
||||
instances (list): See below.
|
||||
|
||||
``axes`` should be a list of axes, with each axis either supplied as
|
||||
a py:class:`.designspaceLib.AxisDescriptor` object, or a tuple in the
|
||||
format ```tupletag, minValue, defaultValue, maxValue, name``.
|
||||
The ``name`` is either a string, or a dict, mapping language codes
|
||||
to strings, to allow localized name table entries.
|
||||
|
||||
```instances`` should be a list of instances, with each instance either
|
||||
supplied as a py:class:`.designspaceLib.InstanceDescriptor` object, or a
|
||||
dict with keys ``location`` (mapping of axis tags to float values),
|
||||
``stylename`` and (optionally) ``postscriptfontname``.
|
||||
The ``stylename`` is either a string, or a dict, mapping language codes
|
||||
to strings, to allow localized name table entries.
|
||||
"""
|
||||
|
||||
addFvar(self.font, axes, instances)
|
||||
|
||||
def setupAvar(self, axes):
|
||||
"""Adds an axis variations table to the font.
|
||||
|
||||
Args:
|
||||
axes (list): A list of py:class:`.designspaceLib.AxisDescriptor` objects.
|
||||
"""
|
||||
from .varLib import _add_avar
|
||||
|
||||
_add_avar(self.font, OrderedDict(enumerate(axes))) # Only values are used
|
||||
|
||||
def setupGvar(self, variations):
|
||||
gvar = self.font["gvar"] = newTable('gvar')
|
||||
gvar = self.font["gvar"] = newTable("gvar")
|
||||
gvar.version = 1
|
||||
gvar.reserved = 0
|
||||
gvar.variations = variations
|
||||
@ -650,7 +707,7 @@ class FontBuilder(object):
|
||||
The `metrics` argument must be a dict, mapping glyph names to
|
||||
`(width, leftSidebearing)` tuples.
|
||||
"""
|
||||
self.setupMetrics('hmtx', metrics)
|
||||
self.setupMetrics("hmtx", metrics)
|
||||
|
||||
def setupVerticalMetrics(self, metrics):
|
||||
"""Create a new `vmtx` table, for horizontal metrics.
|
||||
@ -658,7 +715,7 @@ class FontBuilder(object):
|
||||
The `metrics` argument must be a dict, mapping glyph names to
|
||||
`(height, topSidebearing)` tuples.
|
||||
"""
|
||||
self.setupMetrics('vmtx', metrics)
|
||||
self.setupMetrics("vmtx", metrics)
|
||||
|
||||
def setupMetrics(self, tableTag, metrics):
|
||||
"""See `setupHorizontalMetrics()` and `setupVerticalMetrics()`."""
|
||||
@ -699,8 +756,14 @@ class FontBuilder(object):
|
||||
bag[vorg] = 1
|
||||
else:
|
||||
bag[vorg] += 1
|
||||
defaultVerticalOrigin = sorted(bag, key=lambda vorg: bag[vorg], reverse=True)[0]
|
||||
self._initTableWithValues("VORG", {}, dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin))
|
||||
defaultVerticalOrigin = sorted(
|
||||
bag, key=lambda vorg: bag[vorg], reverse=True
|
||||
)[0]
|
||||
self._initTableWithValues(
|
||||
"VORG",
|
||||
{},
|
||||
dict(VOriginRecords={}, defaultVertOriginY=defaultVerticalOrigin),
|
||||
)
|
||||
vorgTable = self.font["VORG"]
|
||||
vorgTable.majorVersion = 1
|
||||
vorgTable.minorVersion = 0
|
||||
@ -711,7 +774,7 @@ class FontBuilder(object):
|
||||
"""Create a new `post` table and initialize it with default values,
|
||||
which can be overridden by keyword arguments.
|
||||
"""
|
||||
isCFF2 = 'CFF2' in self.font
|
||||
isCFF2 = "CFF2" in self.font
|
||||
postTable = self._initTableWithValues("post", _postDefaults, values)
|
||||
if (self.isTTF or isCFF2) and keepGlyphNames:
|
||||
postTable.formatType = 2.0
|
||||
@ -735,10 +798,10 @@ class FontBuilder(object):
|
||||
happy. This does not properly sign the font.
|
||||
"""
|
||||
values = dict(
|
||||
ulVersion = 1,
|
||||
usFlag = 0,
|
||||
usNumSigs = 0,
|
||||
signatureRecords = [],
|
||||
ulVersion=1,
|
||||
usFlag=0,
|
||||
usNumSigs=0,
|
||||
signatureRecords=[],
|
||||
)
|
||||
self._initTableWithValues("DSIG", {}, values)
|
||||
|
||||
@ -754,7 +817,10 @@ class FontBuilder(object):
|
||||
`fontTools.feaLib` for details.
|
||||
"""
|
||||
from .feaLib.builder import addOpenTypeFeaturesFromString
|
||||
addOpenTypeFeaturesFromString(self.font, features, filename=filename, tables=tables)
|
||||
|
||||
addOpenTypeFeaturesFromString(
|
||||
self.font, features, filename=filename, tables=tables
|
||||
)
|
||||
|
||||
def addFeatureVariations(self, conditionalSubstitutions, featureTag="rvrn"):
|
||||
"""Add conditional substitutions to a Variable Font.
|
||||
@ -770,14 +836,17 @@ class FontBuilder(object):
|
||||
self.font, conditionalSubstitutions, featureTag=featureTag
|
||||
)
|
||||
|
||||
def setupCOLR(self, colorLayers):
|
||||
def setupCOLR(self, colorLayers, version=None, varStore=None):
|
||||
"""Build new COLR table using color layers dictionary.
|
||||
|
||||
Cf. `fontTools.colorLib.builder.buildCOLR`.
|
||||
"""
|
||||
from fontTools.colorLib.builder import buildCOLR
|
||||
|
||||
self.font["COLR"] = buildCOLR(colorLayers)
|
||||
glyphMap = self.font.getReverseGlyphMap()
|
||||
self.font["COLR"] = buildCOLR(
|
||||
colorLayers, version=version, glyphMap=glyphMap, varStore=varStore
|
||||
)
|
||||
|
||||
def setupCPAL(
|
||||
self,
|
||||
@ -800,7 +869,7 @@ class FontBuilder(object):
|
||||
paletteTypes=paletteTypes,
|
||||
paletteLabels=paletteLabels,
|
||||
paletteEntryLabels=paletteEntryLabels,
|
||||
nameTable=self.font.get("name")
|
||||
nameTable=self.font.get("name"),
|
||||
)
|
||||
|
||||
def setupStat(self, axes, locations=None, elidedFallbackName=2):
|
||||
@ -810,6 +879,7 @@ class FontBuilder(object):
|
||||
the arguments.
|
||||
"""
|
||||
from .otlLib.builder import buildStatTable
|
||||
|
||||
buildStatTable(self.font, axes, locations, elidedFallbackName)
|
||||
|
||||
|
||||
@ -823,32 +893,58 @@ def buildCmapSubTable(cmapping, format, platformID, platEncID):
|
||||
|
||||
|
||||
def addFvar(font, axes, instances):
|
||||
from .misc.py23 import Tag, tounicode
|
||||
from .ttLib.tables._f_v_a_r import Axis, NamedInstance
|
||||
from .designspaceLib import AxisDescriptor
|
||||
|
||||
assert axes
|
||||
|
||||
fvar = newTable('fvar')
|
||||
nameTable = font['name']
|
||||
fvar = newTable("fvar")
|
||||
nameTable = font["name"]
|
||||
|
||||
for tag, minValue, defaultValue, maxValue, name in axes:
|
||||
for axis_def in axes:
|
||||
axis = Axis()
|
||||
axis.axisTag = Tag(tag)
|
||||
axis.minValue, axis.defaultValue, axis.maxValue = minValue, defaultValue, maxValue
|
||||
axis.axisNameID = nameTable.addName(tounicode(name))
|
||||
|
||||
if isinstance(axis_def, tuple):
|
||||
(
|
||||
axis.axisTag,
|
||||
axis.minValue,
|
||||
axis.defaultValue,
|
||||
axis.maxValue,
|
||||
name,
|
||||
) = axis_def
|
||||
else:
|
||||
(axis.axisTag, axis.minValue, axis.defaultValue, axis.maxValue, name) = (
|
||||
axis_def.tag,
|
||||
axis_def.minimum,
|
||||
axis_def.default,
|
||||
axis_def.maximum,
|
||||
axis_def.name,
|
||||
)
|
||||
|
||||
if isinstance(name, str):
|
||||
name = dict(en=name)
|
||||
|
||||
axis.axisNameID = nameTable.addMultilingualName(name, ttFont=font)
|
||||
fvar.axes.append(axis)
|
||||
|
||||
for instance in instances:
|
||||
coordinates = instance['location']
|
||||
name = tounicode(instance['stylename'])
|
||||
psname = instance.get('postscriptfontname')
|
||||
if isinstance(instance, dict):
|
||||
coordinates = instance["location"]
|
||||
name = instance["stylename"]
|
||||
psname = instance.get("postscriptfontname")
|
||||
else:
|
||||
coordinates = instance.location
|
||||
name = instance.localisedStyleName or instance.styleName
|
||||
psname = instance.postScriptFontName
|
||||
|
||||
if isinstance(name, str):
|
||||
name = dict(en=name)
|
||||
|
||||
inst = NamedInstance()
|
||||
inst.subfamilyNameID = nameTable.addName(name)
|
||||
inst.subfamilyNameID = nameTable.addMultilingualName(name, ttFont=font)
|
||||
if psname is not None:
|
||||
psname = tounicode(psname)
|
||||
inst.postscriptNameID = nameTable.addName(psname)
|
||||
inst.coordinates = coordinates
|
||||
fvar.instances.append(inst)
|
||||
|
||||
font['fvar'] = fvar
|
||||
font["fvar"] = fvar
|
||||
|
@ -4,9 +4,10 @@ so on.
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
from numbers import Number
|
||||
from fontTools.misc.vector import Vector as _Vector
|
||||
import math
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
|
||||
def calcBounds(array):
|
||||
"""Calculate the bounding rectangle of a 2D points array.
|
||||
@ -228,6 +229,19 @@ def rectCenter(rect):
|
||||
(xMin, yMin, xMax, yMax) = rect
|
||||
return (xMin+xMax)/2, (yMin+yMax)/2
|
||||
|
||||
def rectArea(rect):
|
||||
"""Determine rectangle area.
|
||||
|
||||
Args:
|
||||
rect: Bounding rectangle, expressed as tuples
|
||||
``(xMin, yMin, xMax, yMax)``.
|
||||
|
||||
Returns:
|
||||
The area of the rectangle.
|
||||
"""
|
||||
(xMin, yMin, xMax, yMax) = rect
|
||||
return (yMax - yMin) * (xMax - xMin)
|
||||
|
||||
def intRect(rect):
|
||||
"""Round a rectangle to integer values.
|
||||
|
||||
@ -248,107 +262,14 @@ def intRect(rect):
|
||||
return (xMin, yMin, xMax, yMax)
|
||||
|
||||
|
||||
class Vector(object):
|
||||
"""A math-like vector.
|
||||
class Vector(_Vector):
|
||||
|
||||
Represents an n-dimensional numeric vector. ``Vector`` objects support
|
||||
vector addition and subtraction, scalar multiplication and division,
|
||||
negation, rounding, and comparison tests.
|
||||
|
||||
Attributes:
|
||||
values: Sequence of values stored in the vector.
|
||||
"""
|
||||
|
||||
def __init__(self, values, keep=False):
|
||||
"""Initialize a vector. If ``keep`` is true, values will be copied."""
|
||||
self.values = values if keep else list(values)
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.values[index]
|
||||
|
||||
def __len__(self):
|
||||
return len(self.values)
|
||||
|
||||
def __repr__(self):
|
||||
return "Vector(%s)" % self.values
|
||||
|
||||
def _vectorOp(self, other, op):
|
||||
if isinstance(other, Vector):
|
||||
assert len(self.values) == len(other.values)
|
||||
a = self.values
|
||||
b = other.values
|
||||
return [op(a[i], b[i]) for i in range(len(self.values))]
|
||||
if isinstance(other, Number):
|
||||
return [op(v, other) for v in self.values]
|
||||
raise NotImplementedError
|
||||
|
||||
def _scalarOp(self, other, op):
|
||||
if isinstance(other, Number):
|
||||
return [op(v, other) for v in self.values]
|
||||
raise NotImplementedError
|
||||
|
||||
def _unaryOp(self, op):
|
||||
return [op(v) for v in self.values]
|
||||
|
||||
def __add__(self, other):
|
||||
return Vector(self._vectorOp(other, operator.add), keep=True)
|
||||
def __iadd__(self, other):
|
||||
self.values = self._vectorOp(other, operator.add)
|
||||
return self
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other):
|
||||
return Vector(self._vectorOp(other, operator.sub), keep=True)
|
||||
def __isub__(self, other):
|
||||
self.values = self._vectorOp(other, operator.sub)
|
||||
return self
|
||||
def __rsub__(self, other):
|
||||
return other + (-self)
|
||||
|
||||
def __mul__(self, other):
|
||||
return Vector(self._scalarOp(other, operator.mul), keep=True)
|
||||
def __imul__(self, other):
|
||||
self.values = self._scalarOp(other, operator.mul)
|
||||
return self
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __truediv__(self, other):
|
||||
return Vector(self._scalarOp(other, operator.div), keep=True)
|
||||
def __itruediv__(self, other):
|
||||
self.values = self._scalarOp(other, operator.div)
|
||||
return self
|
||||
|
||||
def __pos__(self):
|
||||
return Vector(self._unaryOp(operator.pos), keep=True)
|
||||
def __neg__(self):
|
||||
return Vector(self._unaryOp(operator.neg), keep=True)
|
||||
def __round__(self):
|
||||
return Vector(self._unaryOp(round), keep=True)
|
||||
def toInt(self):
|
||||
"""Synonym for ``round``."""
|
||||
return self.__round__()
|
||||
|
||||
def __eq__(self, other):
|
||||
if type(other) == Vector:
|
||||
return self.values == other.values
|
||||
else:
|
||||
return self.values == other
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __bool__(self):
|
||||
return any(self.values)
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __abs__(self):
|
||||
return math.sqrt(sum([x*x for x in self.values]))
|
||||
def dot(self, other):
|
||||
"""Performs vector dot product, returning sum of
|
||||
``a[0] * b[0], a[1] * b[1], ...``"""
|
||||
a = self.values
|
||||
b = other.values if type(other) == Vector else b
|
||||
assert len(a) == len(b)
|
||||
return sum([a[i] * b[i] for i in range(len(a))])
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(
|
||||
"fontTools.misc.arrayTools.Vector has been deprecated, please use "
|
||||
"fontTools.misc.vector.Vector instead.",
|
||||
DeprecationWarning,
|
||||
)
|
||||
|
||||
|
||||
def pairwise(iterable, reverse=False):
|
||||
|
@ -2,9 +2,13 @@
|
||||
"""fontTools.misc.bezierTools.py -- tools for working with Bezier path segments.
|
||||
"""
|
||||
|
||||
from fontTools.misc.arrayTools import calcBounds
|
||||
from fontTools.misc.arrayTools import calcBounds, sectRect, rectArea
|
||||
from fontTools.misc.transform import Offset, Identity
|
||||
from fontTools.misc.py23 import *
|
||||
import math
|
||||
from collections import namedtuple
|
||||
|
||||
Intersection = namedtuple("Intersection", ["pt", "t1", "t2"])
|
||||
|
||||
|
||||
__all__ = [
|
||||
@ -25,6 +29,14 @@ __all__ = [
|
||||
"splitCubicAtT",
|
||||
"solveQuadratic",
|
||||
"solveCubic",
|
||||
"quadraticPointAtT",
|
||||
"cubicPointAtT",
|
||||
"linePointAtT",
|
||||
"segmentPointAtT",
|
||||
"lineLineIntersections",
|
||||
"curveLineIntersections",
|
||||
"curveCurveIntersections",
|
||||
"segmentSegmentIntersections",
|
||||
]
|
||||
|
||||
|
||||
@ -42,23 +54,31 @@ def calcCubicArcLength(pt1, pt2, pt3, pt4, tolerance=0.005):
|
||||
Returns:
|
||||
Arc length value.
|
||||
"""
|
||||
return calcCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance)
|
||||
return calcCubicArcLengthC(
|
||||
complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4), tolerance
|
||||
)
|
||||
|
||||
|
||||
def _split_cubic_into_two(p0, p1, p2, p3):
|
||||
mid = (p0 + 3 * (p1 + p2) + p3) * .125
|
||||
deriv3 = (p3 + p2 - p1 - p0) * .125
|
||||
return ((p0, (p0 + p1) * .5, mid - deriv3, mid),
|
||||
(mid, mid + deriv3, (p2 + p3) * .5, p3))
|
||||
mid = (p0 + 3 * (p1 + p2) + p3) * 0.125
|
||||
deriv3 = (p3 + p2 - p1 - p0) * 0.125
|
||||
return (
|
||||
(p0, (p0 + p1) * 0.5, mid - deriv3, mid),
|
||||
(mid, mid + deriv3, (p2 + p3) * 0.5, p3),
|
||||
)
|
||||
|
||||
|
||||
def _calcCubicArcLengthCRecurse(mult, p0, p1, p2, p3):
|
||||
arch = abs(p0-p3)
|
||||
box = abs(p0-p1) + abs(p1-p2) + abs(p2-p3)
|
||||
if arch * mult >= box:
|
||||
return (arch + box) * .5
|
||||
else:
|
||||
one,two = _split_cubic_into_two(p0,p1,p2,p3)
|
||||
return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(mult, *two)
|
||||
arch = abs(p0 - p3)
|
||||
box = abs(p0 - p1) + abs(p1 - p2) + abs(p2 - p3)
|
||||
if arch * mult >= box:
|
||||
return (arch + box) * 0.5
|
||||
else:
|
||||
one, two = _split_cubic_into_two(p0, p1, p2, p3)
|
||||
return _calcCubicArcLengthCRecurse(mult, *one) + _calcCubicArcLengthCRecurse(
|
||||
mult, *two
|
||||
)
|
||||
|
||||
|
||||
def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005):
|
||||
"""Calculates the arc length for a cubic Bezier segment.
|
||||
@ -70,7 +90,7 @@ def calcCubicArcLengthC(pt1, pt2, pt3, pt4, tolerance=0.005):
|
||||
Returns:
|
||||
Arc length value.
|
||||
"""
|
||||
mult = 1. + 1.5 * tolerance # The 1.5 is a empirical hack; no math
|
||||
mult = 1.0 + 1.5 * tolerance # The 1.5 is a empirical hack; no math
|
||||
return _calcCubicArcLengthCRecurse(mult, pt1, pt2, pt3, pt4)
|
||||
|
||||
|
||||
@ -85,7 +105,7 @@ def _dot(v1, v2):
|
||||
def _intSecAtan(x):
|
||||
# In : sympy.integrate(sp.sec(sp.atan(x)))
|
||||
# Out: x*sqrt(x**2 + 1)/2 + asinh(x)/2
|
||||
return x * math.sqrt(x**2 + 1)/2 + math.asinh(x)/2
|
||||
return x * math.sqrt(x ** 2 + 1) / 2 + math.asinh(x) / 2
|
||||
|
||||
|
||||
def calcQuadraticArcLength(pt1, pt2, pt3):
|
||||
@ -141,16 +161,16 @@ def calcQuadraticArcLengthC(pt1, pt2, pt3):
|
||||
d = d1 - d0
|
||||
n = d * 1j
|
||||
scale = abs(n)
|
||||
if scale == 0.:
|
||||
return abs(pt3-pt1)
|
||||
origDist = _dot(n,d0)
|
||||
if scale == 0.0:
|
||||
return abs(pt3 - pt1)
|
||||
origDist = _dot(n, d0)
|
||||
if abs(origDist) < epsilon:
|
||||
if _dot(d0,d1) >= 0:
|
||||
return abs(pt3-pt1)
|
||||
if _dot(d0, d1) >= 0:
|
||||
return abs(pt3 - pt1)
|
||||
a, b = abs(d0), abs(d1)
|
||||
return (a*a + b*b) / (a+b)
|
||||
x0 = _dot(d,d0) / origDist
|
||||
x1 = _dot(d,d1) / origDist
|
||||
return (a * a + b * b) / (a + b)
|
||||
x0 = _dot(d, d0) / origDist
|
||||
x1 = _dot(d, d1) / origDist
|
||||
Len = abs(2 * (_intSecAtan(x1) - _intSecAtan(x0)) * origDist / (scale * (x1 - x0)))
|
||||
return Len
|
||||
|
||||
@ -190,13 +210,17 @@ def approximateQuadraticArcLengthC(pt1, pt2, pt3):
|
||||
# to be integrated with the best-matching fifth-degree polynomial
|
||||
# approximation of it.
|
||||
#
|
||||
#https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature
|
||||
# https://en.wikipedia.org/wiki/Gaussian_quadrature#Gauss.E2.80.93Legendre_quadrature
|
||||
|
||||
# abs(BezierCurveC[2].diff(t).subs({t:T})) for T in sorted(.5, .5±sqrt(3/5)/2),
|
||||
# weighted 5/18, 8/18, 5/18 respectively.
|
||||
v0 = abs(-0.492943519233745*pt1 + 0.430331482911935*pt2 + 0.0626120363218102*pt3)
|
||||
v1 = abs(pt3-pt1)*0.4444444444444444
|
||||
v2 = abs(-0.0626120363218102*pt1 - 0.430331482911935*pt2 + 0.492943519233745*pt3)
|
||||
v0 = abs(
|
||||
-0.492943519233745 * pt1 + 0.430331482911935 * pt2 + 0.0626120363218102 * pt3
|
||||
)
|
||||
v1 = abs(pt3 - pt1) * 0.4444444444444444
|
||||
v2 = abs(
|
||||
-0.0626120363218102 * pt1 - 0.430331482911935 * pt2 + 0.492943519233745 * pt3
|
||||
)
|
||||
|
||||
return v0 + v1 + v2
|
||||
|
||||
@ -220,14 +244,18 @@ def calcQuadraticBounds(pt1, pt2, pt3):
|
||||
(0.0, 0.0, 100, 100)
|
||||
"""
|
||||
(ax, ay), (bx, by), (cx, cy) = calcQuadraticParameters(pt1, pt2, pt3)
|
||||
ax2 = ax*2.0
|
||||
ay2 = ay*2.0
|
||||
ax2 = ax * 2.0
|
||||
ay2 = ay * 2.0
|
||||
roots = []
|
||||
if ax2 != 0:
|
||||
roots.append(-bx/ax2)
|
||||
roots.append(-bx / ax2)
|
||||
if ay2 != 0:
|
||||
roots.append(-by/ay2)
|
||||
points = [(ax*t*t + bx*t + cx, ay*t*t + by*t + cy) for t in roots if 0 <= t < 1] + [pt1, pt3]
|
||||
roots.append(-by / ay2)
|
||||
points = [
|
||||
(ax * t * t + bx * t + cx, ay * t * t + by * t + cy)
|
||||
for t in roots
|
||||
if 0 <= t < 1
|
||||
] + [pt1, pt3]
|
||||
return calcBounds(points)
|
||||
|
||||
|
||||
@ -256,7 +284,9 @@ def approximateCubicArcLength(pt1, pt2, pt3, pt4):
|
||||
>>> approximateCubicArcLength((0, 0), (50, 0), (100, -50), (-50, 0)) # cusp
|
||||
154.80848416537057
|
||||
"""
|
||||
return approximateCubicArcLengthC(complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4))
|
||||
return approximateCubicArcLengthC(
|
||||
complex(*pt1), complex(*pt2), complex(*pt3), complex(*pt4)
|
||||
)
|
||||
|
||||
|
||||
def approximateCubicArcLengthC(pt1, pt2, pt3, pt4):
|
||||
@ -276,11 +306,21 @@ def approximateCubicArcLengthC(pt1, pt2, pt3, pt4):
|
||||
|
||||
# abs(BezierCurveC[3].diff(t).subs({t:T})) for T in sorted(0, .5±(3/7)**.5/2, .5, 1),
|
||||
# weighted 1/20, 49/180, 32/90, 49/180, 1/20 respectively.
|
||||
v0 = abs(pt2-pt1)*.15
|
||||
v1 = abs(-0.558983582205757*pt1 + 0.325650248872424*pt2 + 0.208983582205757*pt3 + 0.024349751127576*pt4)
|
||||
v2 = abs(pt4-pt1+pt3-pt2)*0.26666666666666666
|
||||
v3 = abs(-0.024349751127576*pt1 - 0.208983582205757*pt2 - 0.325650248872424*pt3 + 0.558983582205757*pt4)
|
||||
v4 = abs(pt4-pt3)*.15
|
||||
v0 = abs(pt2 - pt1) * 0.15
|
||||
v1 = abs(
|
||||
-0.558983582205757 * pt1
|
||||
+ 0.325650248872424 * pt2
|
||||
+ 0.208983582205757 * pt3
|
||||
+ 0.024349751127576 * pt4
|
||||
)
|
||||
v2 = abs(pt4 - pt1 + pt3 - pt2) * 0.26666666666666666
|
||||
v3 = abs(
|
||||
-0.024349751127576 * pt1
|
||||
- 0.208983582205757 * pt2
|
||||
- 0.325650248872424 * pt3
|
||||
+ 0.558983582205757 * pt4
|
||||
)
|
||||
v4 = abs(pt4 - pt3) * 0.15
|
||||
|
||||
return v0 + v1 + v2 + v3 + v4
|
||||
|
||||
@ -313,7 +353,13 @@ def calcCubicBounds(pt1, pt2, pt3, pt4):
|
||||
yRoots = [t for t in solveQuadratic(ay3, by2, cy) if 0 <= t < 1]
|
||||
roots = xRoots + yRoots
|
||||
|
||||
points = [(ax*t*t*t + bx*t*t + cx * t + dx, ay*t*t*t + by*t*t + cy * t + dy) for t in roots] + [pt1, pt4]
|
||||
points = [
|
||||
(
|
||||
ax * t * t * t + bx * t * t + cx * t + dx,
|
||||
ay * t * t * t + by * t * t + cy * t + dy,
|
||||
)
|
||||
for t in roots
|
||||
] + [pt1, pt4]
|
||||
return calcBounds(points)
|
||||
|
||||
|
||||
@ -356,8 +402,8 @@ def splitLine(pt1, pt2, where, isHorizontal):
|
||||
pt1x, pt1y = pt1
|
||||
pt2x, pt2y = pt2
|
||||
|
||||
ax = (pt2x - pt1x)
|
||||
ay = (pt2y - pt1y)
|
||||
ax = pt2x - pt1x
|
||||
ay = pt2y - pt1y
|
||||
|
||||
bx = pt1x
|
||||
by = pt1y
|
||||
@ -410,8 +456,9 @@ def splitQuadratic(pt1, pt2, pt3, where, isHorizontal):
|
||||
((50, 50), (75, 50), (100, 0))
|
||||
"""
|
||||
a, b, c = calcQuadraticParameters(pt1, pt2, pt3)
|
||||
solutions = solveQuadratic(a[isHorizontal], b[isHorizontal],
|
||||
c[isHorizontal] - where)
|
||||
solutions = solveQuadratic(
|
||||
a[isHorizontal], b[isHorizontal], c[isHorizontal] - where
|
||||
)
|
||||
solutions = sorted([t for t in solutions if 0 <= t < 1])
|
||||
if not solutions:
|
||||
return [(pt1, pt2, pt3)]
|
||||
@ -446,8 +493,9 @@ def splitCubic(pt1, pt2, pt3, pt4, where, isHorizontal):
|
||||
((92.5259, 25), (95.202, 17.5085), (97.7062, 9.17517), (100, 1.77636e-15))
|
||||
"""
|
||||
a, b, c, d = calcCubicParameters(pt1, pt2, pt3, pt4)
|
||||
solutions = solveCubic(a[isHorizontal], b[isHorizontal], c[isHorizontal],
|
||||
d[isHorizontal] - where)
|
||||
solutions = solveCubic(
|
||||
a[isHorizontal], b[isHorizontal], c[isHorizontal], d[isHorizontal] - where
|
||||
)
|
||||
solutions = sorted([t for t in solutions if 0 <= t < 1])
|
||||
if not solutions:
|
||||
return [(pt1, pt2, pt3, pt4)]
|
||||
@ -512,17 +560,17 @@ def _splitQuadraticAtT(a, b, c, *ts):
|
||||
cx, cy = c
|
||||
for i in range(len(ts) - 1):
|
||||
t1 = ts[i]
|
||||
t2 = ts[i+1]
|
||||
delta = (t2 - t1)
|
||||
t2 = ts[i + 1]
|
||||
delta = t2 - t1
|
||||
# calc new a, b and c
|
||||
delta_2 = delta*delta
|
||||
delta_2 = delta * delta
|
||||
a1x = ax * delta_2
|
||||
a1y = ay * delta_2
|
||||
b1x = (2*ax*t1 + bx) * delta
|
||||
b1y = (2*ay*t1 + by) * delta
|
||||
t1_2 = t1*t1
|
||||
c1x = ax*t1_2 + bx*t1 + cx
|
||||
c1y = ay*t1_2 + by*t1 + cy
|
||||
b1x = (2 * ax * t1 + bx) * delta
|
||||
b1y = (2 * ay * t1 + by) * delta
|
||||
t1_2 = t1 * t1
|
||||
c1x = ax * t1_2 + bx * t1 + cx
|
||||
c1y = ay * t1_2 + by * t1 + cy
|
||||
|
||||
pt1, pt2, pt3 = calcQuadraticPoints((a1x, a1y), (b1x, b1y), (c1x, c1y))
|
||||
segments.append((pt1, pt2, pt3))
|
||||
@ -540,24 +588,26 @@ def _splitCubicAtT(a, b, c, d, *ts):
|
||||
dx, dy = d
|
||||
for i in range(len(ts) - 1):
|
||||
t1 = ts[i]
|
||||
t2 = ts[i+1]
|
||||
delta = (t2 - t1)
|
||||
t2 = ts[i + 1]
|
||||
delta = t2 - t1
|
||||
|
||||
delta_2 = delta*delta
|
||||
delta_3 = delta*delta_2
|
||||
t1_2 = t1*t1
|
||||
t1_3 = t1*t1_2
|
||||
delta_2 = delta * delta
|
||||
delta_3 = delta * delta_2
|
||||
t1_2 = t1 * t1
|
||||
t1_3 = t1 * t1_2
|
||||
|
||||
# calc new a, b, c and d
|
||||
a1x = ax * delta_3
|
||||
a1y = ay * delta_3
|
||||
b1x = (3*ax*t1 + bx) * delta_2
|
||||
b1y = (3*ay*t1 + by) * delta_2
|
||||
c1x = (2*bx*t1 + cx + 3*ax*t1_2) * delta
|
||||
c1y = (2*by*t1 + cy + 3*ay*t1_2) * delta
|
||||
d1x = ax*t1_3 + bx*t1_2 + cx*t1 + dx
|
||||
d1y = ay*t1_3 + by*t1_2 + cy*t1 + dy
|
||||
pt1, pt2, pt3, pt4 = calcCubicPoints((a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y))
|
||||
b1x = (3 * ax * t1 + bx) * delta_2
|
||||
b1y = (3 * ay * t1 + by) * delta_2
|
||||
c1x = (2 * bx * t1 + cx + 3 * ax * t1_2) * delta
|
||||
c1y = (2 * by * t1 + cy + 3 * ay * t1_2) * delta
|
||||
d1x = ax * t1_3 + bx * t1_2 + cx * t1 + dx
|
||||
d1y = ay * t1_3 + by * t1_2 + cy * t1 + dy
|
||||
pt1, pt2, pt3, pt4 = calcCubicPoints(
|
||||
(a1x, a1y), (b1x, b1y), (c1x, c1y), (d1x, d1y)
|
||||
)
|
||||
segments.append((pt1, pt2, pt3, pt4))
|
||||
return segments
|
||||
|
||||
@ -569,8 +619,7 @@ def _splitCubicAtT(a, b, c, d, *ts):
|
||||
from math import sqrt, acos, cos, pi
|
||||
|
||||
|
||||
def solveQuadratic(a, b, c,
|
||||
sqrt=sqrt):
|
||||
def solveQuadratic(a, b, c, sqrt=sqrt):
|
||||
"""Solve a quadratic equation.
|
||||
|
||||
Solves *a*x*x + b*x + c = 0* where a, b and c are real.
|
||||
@ -590,13 +639,13 @@ def solveQuadratic(a, b, c,
|
||||
roots = []
|
||||
else:
|
||||
# We have a linear equation with 1 root.
|
||||
roots = [-c/b]
|
||||
roots = [-c / b]
|
||||
else:
|
||||
# We have a true quadratic equation. Apply the quadratic formula to find two roots.
|
||||
DD = b*b - 4.0*a*c
|
||||
DD = b * b - 4.0 * a * c
|
||||
if DD >= 0.0:
|
||||
rDD = sqrt(DD)
|
||||
roots = [(-b+rDD)/2.0/a, (-b-rDD)/2.0/a]
|
||||
roots = [(-b + rDD) / 2.0 / a, (-b - rDD) / 2.0 / a]
|
||||
else:
|
||||
# complex roots, ignore
|
||||
roots = []
|
||||
@ -646,52 +695,52 @@ def solveCubic(a, b, c, d):
|
||||
# returns unreliable results, so we fall back to quad.
|
||||
return solveQuadratic(b, c, d)
|
||||
a = float(a)
|
||||
a1 = b/a
|
||||
a2 = c/a
|
||||
a3 = d/a
|
||||
a1 = b / a
|
||||
a2 = c / a
|
||||
a3 = d / a
|
||||
|
||||
Q = (a1*a1 - 3.0*a2)/9.0
|
||||
R = (2.0*a1*a1*a1 - 9.0*a1*a2 + 27.0*a3)/54.0
|
||||
Q = (a1 * a1 - 3.0 * a2) / 9.0
|
||||
R = (2.0 * a1 * a1 * a1 - 9.0 * a1 * a2 + 27.0 * a3) / 54.0
|
||||
|
||||
R2 = R*R
|
||||
Q3 = Q*Q*Q
|
||||
R2 = R * R
|
||||
Q3 = Q * Q * Q
|
||||
R2 = 0 if R2 < epsilon else R2
|
||||
Q3 = 0 if abs(Q3) < epsilon else Q3
|
||||
|
||||
R2_Q3 = R2 - Q3
|
||||
|
||||
if R2 == 0. and Q3 == 0.:
|
||||
x = round(-a1/3.0, epsilonDigits)
|
||||
if R2 == 0.0 and Q3 == 0.0:
|
||||
x = round(-a1 / 3.0, epsilonDigits)
|
||||
return [x, x, x]
|
||||
elif R2_Q3 <= epsilon * .5:
|
||||
elif R2_Q3 <= epsilon * 0.5:
|
||||
# The epsilon * .5 above ensures that Q3 is not zero.
|
||||
theta = acos(max(min(R/sqrt(Q3), 1.0), -1.0))
|
||||
rQ2 = -2.0*sqrt(Q)
|
||||
a1_3 = a1/3.0
|
||||
x0 = rQ2*cos(theta/3.0) - a1_3
|
||||
x1 = rQ2*cos((theta+2.0*pi)/3.0) - a1_3
|
||||
x2 = rQ2*cos((theta+4.0*pi)/3.0) - a1_3
|
||||
theta = acos(max(min(R / sqrt(Q3), 1.0), -1.0))
|
||||
rQ2 = -2.0 * sqrt(Q)
|
||||
a1_3 = a1 / 3.0
|
||||
x0 = rQ2 * cos(theta / 3.0) - a1_3
|
||||
x1 = rQ2 * cos((theta + 2.0 * pi) / 3.0) - a1_3
|
||||
x2 = rQ2 * cos((theta + 4.0 * pi) / 3.0) - a1_3
|
||||
x0, x1, x2 = sorted([x0, x1, x2])
|
||||
# Merge roots that are close-enough
|
||||
if x1 - x0 < epsilon and x2 - x1 < epsilon:
|
||||
x0 = x1 = x2 = round((x0 + x1 + x2) / 3., epsilonDigits)
|
||||
x0 = x1 = x2 = round((x0 + x1 + x2) / 3.0, epsilonDigits)
|
||||
elif x1 - x0 < epsilon:
|
||||
x0 = x1 = round((x0 + x1) / 2., epsilonDigits)
|
||||
x0 = x1 = round((x0 + x1) / 2.0, epsilonDigits)
|
||||
x2 = round(x2, epsilonDigits)
|
||||
elif x2 - x1 < epsilon:
|
||||
x0 = round(x0, epsilonDigits)
|
||||
x1 = x2 = round((x1 + x2) / 2., epsilonDigits)
|
||||
x1 = x2 = round((x1 + x2) / 2.0, epsilonDigits)
|
||||
else:
|
||||
x0 = round(x0, epsilonDigits)
|
||||
x1 = round(x1, epsilonDigits)
|
||||
x2 = round(x2, epsilonDigits)
|
||||
return [x0, x1, x2]
|
||||
else:
|
||||
x = pow(sqrt(R2_Q3)+abs(R), 1/3.0)
|
||||
x = x + Q/x
|
||||
x = pow(sqrt(R2_Q3) + abs(R), 1 / 3.0)
|
||||
x = x + Q / x
|
||||
if R >= 0.0:
|
||||
x = -x
|
||||
x = round(x - a1/3.0, epsilonDigits)
|
||||
x = round(x - a1 / 3.0, epsilonDigits)
|
||||
return [x]
|
||||
|
||||
|
||||
@ -699,6 +748,7 @@ def solveCubic(a, b, c, d):
|
||||
# Conversion routines for points to parameters and vice versa
|
||||
#
|
||||
|
||||
|
||||
def calcQuadraticParameters(pt1, pt2, pt3):
|
||||
x2, y2 = pt2
|
||||
x3, y3 = pt3
|
||||
@ -753,10 +803,399 @@ def calcCubicPoints(a, b, c, d):
|
||||
return (x1, y1), (x2, y2), (x3, y3), (x4, y4)
|
||||
|
||||
|
||||
#
|
||||
# Point at time
|
||||
#
|
||||
|
||||
|
||||
def linePointAtT(pt1, pt2, t):
|
||||
"""Finds the point at time `t` on a line.
|
||||
|
||||
Args:
|
||||
pt1, pt2: Coordinates of the line as 2D tuples.
|
||||
t: The time along the line.
|
||||
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
return ((pt1[0] * (1 - t) + pt2[0] * t), (pt1[1] * (1 - t) + pt2[1] * t))
|
||||
|
||||
|
||||
def quadraticPointAtT(pt1, pt2, pt3, t):
|
||||
"""Finds the point at time `t` on a quadratic curve.
|
||||
|
||||
Args:
|
||||
pt1, pt2, pt3: Coordinates of the curve as 2D tuples.
|
||||
t: The time along the curve.
|
||||
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
x = (1 - t) * (1 - t) * pt1[0] + 2 * (1 - t) * t * pt2[0] + t * t * pt3[0]
|
||||
y = (1 - t) * (1 - t) * pt1[1] + 2 * (1 - t) * t * pt2[1] + t * t * pt3[1]
|
||||
return (x, y)
|
||||
|
||||
|
||||
def cubicPointAtT(pt1, pt2, pt3, pt4, t):
|
||||
"""Finds the point at time `t` on a cubic curve.
|
||||
|
||||
Args:
|
||||
pt1, pt2, pt3, pt4: Coordinates of the curve as 2D tuples.
|
||||
t: The time along the curve.
|
||||
|
||||
Returns:
|
||||
A 2D tuple with the coordinates of the point.
|
||||
"""
|
||||
x = (
|
||||
(1 - t) * (1 - t) * (1 - t) * pt1[0]
|
||||
+ 3 * (1 - t) * (1 - t) * t * pt2[0]
|
||||
+ 3 * (1 - t) * t * t * pt3[0]
|
||||
+ t * t * t * pt4[0]
|
||||
)
|
||||
y = (
|
||||
(1 - t) * (1 - t) * (1 - t) * pt1[1]
|
||||
+ 3 * (1 - t) * (1 - t) * t * pt2[1]
|
||||
+ 3 * (1 - t) * t * t * pt3[1]
|
||||
+ t * t * t * pt4[1]
|
||||
)
|
||||
return (x, y)
|
||||
|
||||
|
||||
def segmentPointAtT(seg, t):
|
||||
if len(seg) == 2:
|
||||
return linePointAtT(*seg, t)
|
||||
elif len(seg) == 3:
|
||||
return quadraticPointAtT(*seg, t)
|
||||
elif len(seg) == 4:
|
||||
return cubicPointAtT(*seg, t)
|
||||
raise ValueError("Unknown curve degree")
|
||||
|
||||
|
||||
#
|
||||
# Intersection finders
|
||||
#
|
||||
|
||||
|
||||
def _line_t_of_pt(s, e, pt):
|
||||
sx, sy = s
|
||||
ex, ey = e
|
||||
px, py = pt
|
||||
if not math.isclose(sx, ex):
|
||||
return (px - sx) / (ex - sx)
|
||||
if not math.isclose(sy, ey):
|
||||
return (py - sy) / (ey - sy)
|
||||
# Line is a point!
|
||||
return -1
|
||||
|
||||
|
||||
def _both_points_are_on_same_side_of_origin(a, b, origin):
|
||||
xDiff = (a[0] - origin[0]) * (b[0] - origin[0])
|
||||
yDiff = (a[1] - origin[1]) * (b[1] - origin[1])
|
||||
return not (xDiff <= 0.0 and yDiff <= 0.0)
|
||||
|
||||
|
||||
def lineLineIntersections(s1, e1, s2, e2):
|
||||
"""Finds intersections between two line segments.
|
||||
|
||||
Args:
|
||||
s1, e1: Coordinates of the first line as 2D tuples.
|
||||
s2, e2: Coordinates of the second line as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
|
||||
>>> a = lineLineIntersections( (310,389), (453, 222), (289, 251), (447, 367))
|
||||
>>> len(a)
|
||||
1
|
||||
>>> intersection = a[0]
|
||||
>>> intersection.pt
|
||||
(374.44882952482897, 313.73458370177315)
|
||||
>>> (intersection.t1, intersection.t2)
|
||||
(0.45069111555824454, 0.5408153767394238)
|
||||
"""
|
||||
s1x, s1y = s1
|
||||
e1x, e1y = e1
|
||||
s2x, s2y = s2
|
||||
e2x, e2y = e2
|
||||
if (
|
||||
math.isclose(s2x, e2x) and math.isclose(s1x, e1x) and not math.isclose(s1x, s2x)
|
||||
): # Parallel vertical
|
||||
return []
|
||||
if (
|
||||
math.isclose(s2y, e2y) and math.isclose(s1y, e1y) and not math.isclose(s1y, s2y)
|
||||
): # Parallel horizontal
|
||||
return []
|
||||
if math.isclose(s2x, e2x) and math.isclose(s2y, e2y): # Line segment is tiny
|
||||
return []
|
||||
if math.isclose(s1x, e1x) and math.isclose(s1y, e1y): # Line segment is tiny
|
||||
return []
|
||||
if math.isclose(e1x, s1x):
|
||||
x = s1x
|
||||
slope34 = (e2y - s2y) / (e2x - s2x)
|
||||
y = slope34 * (x - s2x) + s2y
|
||||
pt = (x, y)
|
||||
return [
|
||||
Intersection(
|
||||
pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
|
||||
)
|
||||
]
|
||||
if math.isclose(s2x, e2x):
|
||||
x = s2x
|
||||
slope12 = (e1y - s1y) / (e1x - s1x)
|
||||
y = slope12 * (x - s1x) + s1y
|
||||
pt = (x, y)
|
||||
return [
|
||||
Intersection(
|
||||
pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
|
||||
)
|
||||
]
|
||||
|
||||
slope12 = (e1y - s1y) / (e1x - s1x)
|
||||
slope34 = (e2y - s2y) / (e2x - s2x)
|
||||
if math.isclose(slope12, slope34):
|
||||
return []
|
||||
x = (slope12 * s1x - s1y - slope34 * s2x + s2y) / (slope12 - slope34)
|
||||
y = slope12 * (x - s1x) + s1y
|
||||
pt = (x, y)
|
||||
if _both_points_are_on_same_side_of_origin(
|
||||
pt, e1, s1
|
||||
) and _both_points_are_on_same_side_of_origin(pt, s2, e2):
|
||||
return [
|
||||
Intersection(
|
||||
pt=pt, t1=_line_t_of_pt(s1, e1, pt), t2=_line_t_of_pt(s2, e2, pt)
|
||||
)
|
||||
]
|
||||
return []
|
||||
|
||||
|
||||
def _alignment_transformation(segment):
|
||||
# Returns a transformation which aligns a segment horizontally at the
|
||||
# origin. Apply this transformation to curves and root-find to find
|
||||
# intersections with the segment.
|
||||
start = segment[0]
|
||||
end = segment[-1]
|
||||
angle = math.atan2(end[1] - start[1], end[0] - start[0])
|
||||
return Identity.rotate(-angle).translate(-start[0], -start[1])
|
||||
|
||||
|
||||
def _curve_line_intersections_t(curve, line):
|
||||
aligned_curve = _alignment_transformation(line).transformPoints(curve)
|
||||
if len(curve) == 3:
|
||||
a, b, c = calcQuadraticParameters(*aligned_curve)
|
||||
intersections = solveQuadratic(a[1], b[1], c[1])
|
||||
elif len(curve) == 4:
|
||||
a, b, c, d = calcCubicParameters(*aligned_curve)
|
||||
intersections = solveCubic(a[1], b[1], c[1], d[1])
|
||||
else:
|
||||
raise ValueError("Unknown curve degree")
|
||||
return sorted([i for i in intersections if 0.0 <= i <= 1])
|
||||
|
||||
|
||||
def curveLineIntersections(curve, line):
|
||||
"""Finds intersections between a curve and a line.
|
||||
|
||||
Args:
|
||||
curve: List of coordinates of the curve segment as 2D tuples.
|
||||
line: List of coordinates of the line segment as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
>>> curve = [ (100, 240), (30, 60), (210, 230), (160, 30) ]
|
||||
>>> line = [ (25, 260), (230, 20) ]
|
||||
>>> intersections = curveLineIntersections(curve, line)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(84.90010344084885, 189.87306176459828)
|
||||
"""
|
||||
if len(curve) == 3:
|
||||
pointFinder = quadraticPointAtT
|
||||
elif len(curve) == 4:
|
||||
pointFinder = cubicPointAtT
|
||||
else:
|
||||
raise ValueError("Unknown curve degree")
|
||||
intersections = []
|
||||
for t in _curve_line_intersections_t(curve, line):
|
||||
pt = pointFinder(*curve, t)
|
||||
intersections.append(Intersection(pt=pt, t1=t, t2=_line_t_of_pt(*line, pt)))
|
||||
return intersections
|
||||
|
||||
|
||||
def _curve_bounds(c):
|
||||
if len(c) == 3:
|
||||
return calcQuadraticBounds(*c)
|
||||
elif len(c) == 4:
|
||||
return calcCubicBounds(*c)
|
||||
raise ValueError("Unknown curve degree")
|
||||
|
||||
|
||||
def _split_segment_at_t(c, t):
|
||||
if len(c) == 2:
|
||||
s, e = c
|
||||
midpoint = linePointAtT(s, e, t)
|
||||
return [(s, midpoint), (midpoint, e)]
|
||||
if len(c) == 3:
|
||||
return splitQuadraticAtT(*c, t)
|
||||
elif len(c) == 4:
|
||||
return splitCubicAtT(*c, t)
|
||||
raise ValueError("Unknown curve degree")
|
||||
|
||||
|
||||
def _curve_curve_intersections_t(
|
||||
curve1, curve2, precision=1e-3, range1=None, range2=None
|
||||
):
|
||||
bounds1 = _curve_bounds(curve1)
|
||||
bounds2 = _curve_bounds(curve2)
|
||||
|
||||
if not range1:
|
||||
range1 = (0.0, 1.0)
|
||||
if not range2:
|
||||
range2 = (0.0, 1.0)
|
||||
|
||||
# If bounds don't intersect, go home
|
||||
intersects, _ = sectRect(bounds1, bounds2)
|
||||
if not intersects:
|
||||
return []
|
||||
|
||||
def midpoint(r):
|
||||
return 0.5 * (r[0] + r[1])
|
||||
|
||||
# If they do overlap but they're tiny, approximate
|
||||
if rectArea(bounds1) < precision and rectArea(bounds2) < precision:
|
||||
return [(midpoint(range1), midpoint(range2))]
|
||||
|
||||
c11, c12 = _split_segment_at_t(curve1, 0.5)
|
||||
c11_range = (range1[0], midpoint(range1))
|
||||
c12_range = (midpoint(range1), range1[1])
|
||||
|
||||
c21, c22 = _split_segment_at_t(curve2, 0.5)
|
||||
c21_range = (range2[0], midpoint(range2))
|
||||
c22_range = (midpoint(range2), range2[1])
|
||||
|
||||
found = []
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c11, c21, precision, range1=c11_range, range2=c21_range
|
||||
)
|
||||
)
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c12, c21, precision, range1=c12_range, range2=c21_range
|
||||
)
|
||||
)
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c11, c22, precision, range1=c11_range, range2=c22_range
|
||||
)
|
||||
)
|
||||
found.extend(
|
||||
_curve_curve_intersections_t(
|
||||
c12, c22, precision, range1=c12_range, range2=c22_range
|
||||
)
|
||||
)
|
||||
|
||||
unique_key = lambda ts: (int(ts[0] / precision), int(ts[1] / precision))
|
||||
seen = set()
|
||||
unique_values = []
|
||||
|
||||
for ts in found:
|
||||
key = unique_key(ts)
|
||||
if key in seen:
|
||||
continue
|
||||
seen.add(key)
|
||||
unique_values.append(ts)
|
||||
|
||||
return unique_values
|
||||
|
||||
|
||||
def curveCurveIntersections(curve1, curve2):
|
||||
"""Finds intersections between a curve and a curve.
|
||||
|
||||
Args:
|
||||
curve1: List of coordinates of the first curve segment as 2D tuples.
|
||||
curve2: List of coordinates of the second curve segment as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
>>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ]
|
||||
>>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ]
|
||||
>>> intersections = curveCurveIntersections(curve1, curve2)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(81.7831487395506, 109.88904552375288)
|
||||
"""
|
||||
intersection_ts = _curve_curve_intersections_t(curve1, curve2)
|
||||
return [
|
||||
Intersection(pt=segmentPointAtT(curve1, ts[0]), t1=ts[0], t2=ts[1])
|
||||
for ts in intersection_ts
|
||||
]
|
||||
|
||||
|
||||
def segmentSegmentIntersections(seg1, seg2):
|
||||
"""Finds intersections between two segments.
|
||||
|
||||
Args:
|
||||
seg1: List of coordinates of the first segment as 2D tuples.
|
||||
seg2: List of coordinates of the second segment as 2D tuples.
|
||||
|
||||
Returns:
|
||||
A list of ``Intersection`` objects, each object having ``pt``, ``t1``
|
||||
and ``t2`` attributes containing the intersection point, time on first
|
||||
segment and time on second segment respectively.
|
||||
|
||||
Examples::
|
||||
>>> curve1 = [ (10,100), (90,30), (40,140), (220,220) ]
|
||||
>>> curve2 = [ (5,150), (180,20), (80,250), (210,190) ]
|
||||
>>> intersections = segmentSegmentIntersections(curve1, curve2)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(81.7831487395506, 109.88904552375288)
|
||||
>>> curve3 = [ (100, 240), (30, 60), (210, 230), (160, 30) ]
|
||||
>>> line = [ (25, 260), (230, 20) ]
|
||||
>>> intersections = segmentSegmentIntersections(curve3, line)
|
||||
>>> len(intersections)
|
||||
3
|
||||
>>> intersections[0].pt
|
||||
(84.90010344084885, 189.87306176459828)
|
||||
|
||||
"""
|
||||
# Arrange by degree
|
||||
swapped = False
|
||||
if len(seg2) > len(seg1):
|
||||
seg2, seg1 = seg1, seg2
|
||||
swapped = True
|
||||
if len(seg1) > 2:
|
||||
if len(seg2) > 2:
|
||||
intersections = curveCurveIntersections(seg1, seg2)
|
||||
else:
|
||||
intersections = curveLineIntersections(seg1, seg2)
|
||||
elif len(seg1) == 2 and len(seg2) == 2:
|
||||
intersections = lineLineIntersections(*seg1, *seg2)
|
||||
else:
|
||||
raise ValueError("Couldn't work out which intersection function to use")
|
||||
if not swapped:
|
||||
return intersections
|
||||
return [Intersection(pt=i.pt, t1=i.t2, t2=i.t1) for i in intersections]
|
||||
|
||||
|
||||
def _segmentrepr(obj):
|
||||
"""
|
||||
>>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
|
||||
'(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
|
||||
>>> _segmentrepr([1, [2, 3], [], [[2, [3, 4], [0.1, 2.2]]]])
|
||||
'(1, (2, 3), (), ((2, (3, 4), (0.1, 2.2))))'
|
||||
"""
|
||||
try:
|
||||
it = iter(obj)
|
||||
@ -773,7 +1212,9 @@ def printSegments(segments):
|
||||
for segment in segments:
|
||||
print(_segmentrepr(segment))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
import doctest
|
||||
|
||||
sys.exit(doctest.testmod().failed)
|
||||
|
@ -543,7 +543,7 @@ def load(
|
||||
if not hasattr(fp, "read"):
|
||||
raise AttributeError("'%s' object has no attribute 'read'" % type(fp).__name__)
|
||||
target = PlistTarget(use_builtin_types=use_builtin_types, dict_type=dict_type)
|
||||
parser = etree.XMLParser(target=target) # type: ignore
|
||||
parser = etree.XMLParser(target=target)
|
||||
result = etree.parse(fp, parser=parser)
|
||||
# lxml returns the target object directly, while ElementTree wraps
|
||||
# it as the root of an ElementTree object
|
||||
|
138
Lib/fontTools/misc/vector.py
Normal file
138
Lib/fontTools/misc/vector.py
Normal file
@ -0,0 +1,138 @@
|
||||
from numbers import Number
|
||||
import math
|
||||
import operator
|
||||
import warnings
|
||||
|
||||
|
||||
class Vector(tuple):
|
||||
|
||||
"""A math-like vector.
|
||||
|
||||
Represents an n-dimensional numeric vector. ``Vector`` objects support
|
||||
vector addition and subtraction, scalar multiplication and division,
|
||||
negation, rounding, and comparison tests.
|
||||
"""
|
||||
|
||||
def __new__(cls, values, keep=False):
|
||||
if keep is not False:
|
||||
warnings.warn(
|
||||
"the 'keep' argument has been deprecated",
|
||||
DeprecationWarning,
|
||||
)
|
||||
if type(values) == Vector:
|
||||
# No need to create a new object
|
||||
return values
|
||||
return super().__new__(cls, values)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__name__}({super().__repr__()})"
|
||||
|
||||
def _vectorOp(self, other, op):
|
||||
if isinstance(other, Vector):
|
||||
assert len(self) == len(other)
|
||||
return self.__class__(op(a, b) for a, b in zip(self, other))
|
||||
if isinstance(other, Number):
|
||||
return self.__class__(op(v, other) for v in self)
|
||||
raise NotImplementedError()
|
||||
|
||||
def _scalarOp(self, other, op):
|
||||
if isinstance(other, Number):
|
||||
return self.__class__(op(v, other) for v in self)
|
||||
raise NotImplementedError()
|
||||
|
||||
def _unaryOp(self, op):
|
||||
return self.__class__(op(v) for v in self)
|
||||
|
||||
def __add__(self, other):
|
||||
return self._vectorOp(other, operator.add)
|
||||
|
||||
__radd__ = __add__
|
||||
|
||||
def __sub__(self, other):
|
||||
return self._vectorOp(other, operator.sub)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self._vectorOp(other, _operator_rsub)
|
||||
|
||||
def __mul__(self, other):
|
||||
return self._scalarOp(other, operator.mul)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __truediv__(self, other):
|
||||
return self._scalarOp(other, operator.truediv)
|
||||
|
||||
def __rtruediv__(self, other):
|
||||
return self._scalarOp(other, _operator_rtruediv)
|
||||
|
||||
def __pos__(self):
|
||||
return self._unaryOp(operator.pos)
|
||||
|
||||
def __neg__(self):
|
||||
return self._unaryOp(operator.neg)
|
||||
|
||||
def __round__(self):
|
||||
return self._unaryOp(round)
|
||||
|
||||
def __eq__(self, other):
|
||||
if isinstance(other, list):
|
||||
# bw compat Vector([1, 2, 3]) == [1, 2, 3]
|
||||
other = tuple(other)
|
||||
return super().__eq__(other)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __bool__(self):
|
||||
return any(self)
|
||||
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __abs__(self):
|
||||
return math.sqrt(sum(x * x for x in self))
|
||||
|
||||
def length(self):
|
||||
"""Return the length of the vector. Equivalent to abs(vector)."""
|
||||
return abs(self)
|
||||
|
||||
def normalized(self):
|
||||
"""Return the normalized vector of the vector."""
|
||||
return self / abs(self)
|
||||
|
||||
def dot(self, other):
|
||||
"""Performs vector dot product, returning the sum of
|
||||
``a[0] * b[0], a[1] * b[1], ...``"""
|
||||
assert len(self) == len(other)
|
||||
return sum(a * b for a, b in zip(self, other))
|
||||
|
||||
# Deprecated methods/properties
|
||||
|
||||
def toInt(self):
|
||||
warnings.warn(
|
||||
"the 'toInt' method has been deprecated, use round(vector) instead",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return self.__round__()
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
warnings.warn(
|
||||
"the 'values' attribute has been deprecated, use "
|
||||
"the vector object itself instead",
|
||||
DeprecationWarning,
|
||||
)
|
||||
return list(self)
|
||||
|
||||
@values.setter
|
||||
def values(self, values):
|
||||
raise AttributeError(
|
||||
"can't set attribute, the 'values' attribute has been deprecated",
|
||||
)
|
||||
|
||||
|
||||
def _operator_rsub(a, b):
|
||||
return operator.sub(b, a)
|
||||
|
||||
|
||||
def _operator_rtruediv(a, b):
|
||||
return operator.truediv(b, a)
|
@ -9,6 +9,7 @@ from fontTools.ttLib.tables.otBase import (
|
||||
CountReference,
|
||||
)
|
||||
from fontTools.ttLib.tables import otBase
|
||||
from fontTools.feaLib.ast import STATNameStatement
|
||||
from fontTools.otlLib.error import OpenTypeLibError
|
||||
import logging
|
||||
import copy
|
||||
@ -94,9 +95,10 @@ def buildLookup(subtables, flags=0, markFilterSet=None):
|
||||
subtables = [st for st in subtables if st is not None]
|
||||
if not subtables:
|
||||
return None
|
||||
assert all(t.LookupType == subtables[0].LookupType for t in subtables), (
|
||||
"all subtables must have the same LookupType; got %s"
|
||||
% repr([t.LookupType for t in subtables])
|
||||
assert all(
|
||||
t.LookupType == subtables[0].LookupType for t in subtables
|
||||
), "all subtables must have the same LookupType; got %s" % repr(
|
||||
[t.LookupType for t in subtables]
|
||||
)
|
||||
self = ot.Lookup()
|
||||
self.LookupType = subtables[0].LookupType
|
||||
@ -314,9 +316,10 @@ class ChainContextualRuleset:
|
||||
classdefbuilder = ClassDefBuilder(useClass0=False)
|
||||
for position in context:
|
||||
for glyphset in position:
|
||||
if not classdefbuilder.canAdd(glyphset):
|
||||
glyphs = set(glyphset)
|
||||
if not classdefbuilder.canAdd(glyphs):
|
||||
return None
|
||||
classdefbuilder.add(glyphset)
|
||||
classdefbuilder.add(glyphs)
|
||||
return classdefbuilder
|
||||
|
||||
|
||||
@ -2573,7 +2576,10 @@ class ClassDefBuilder(object):
|
||||
return
|
||||
self.classes_.add(glyphs)
|
||||
for glyph in glyphs:
|
||||
assert glyph not in self.glyphs_
|
||||
if glyph in self.glyphs_:
|
||||
raise OpenTypeLibError(
|
||||
f"Glyph {glyph} is already present in class.", None
|
||||
)
|
||||
self.glyphs_[glyph] = glyphs
|
||||
|
||||
def classes(self):
|
||||
@ -2685,8 +2691,8 @@ def buildStatTable(ttFont, axes, locations=None, elidedFallbackName=2):
|
||||
]
|
||||
|
||||
The optional 'elidedFallbackName' argument can be a name ID (int),
|
||||
a string, or a dictionary containing multilingual names. It
|
||||
translates to the ElidedFallbackNameID field.
|
||||
a string, a dictionary containing multilingual names, or a list of
|
||||
STATNameStatements. It translates to the ElidedFallbackNameID field.
|
||||
|
||||
The 'ttFont' argument must be a TTFont instance that already has a
|
||||
'name' table. If a 'STAT' table already exists, it will be
|
||||
@ -2795,6 +2801,20 @@ def _addName(nameTable, value, minNameID=0):
|
||||
names = dict(en=value)
|
||||
elif isinstance(value, dict):
|
||||
names = value
|
||||
elif isinstance(value, list):
|
||||
nameID = nameTable._findUnusedNameID()
|
||||
for nameRecord in value:
|
||||
if isinstance(nameRecord, STATNameStatement):
|
||||
nameTable.setName(
|
||||
nameRecord.string,
|
||||
nameID,
|
||||
nameRecord.platformID,
|
||||
nameRecord.platEncID,
|
||||
nameRecord.langID,
|
||||
)
|
||||
else:
|
||||
raise TypeError("value must be a list of STATNameStatements")
|
||||
return nameID
|
||||
else:
|
||||
raise TypeError("value must be int, str or dict")
|
||||
raise TypeError("value must be int, str, dict or list")
|
||||
return nameTable.addMultilingualName(names, minNameID=minNameID)
|
||||
|
@ -1,3 +1 @@
|
||||
"""Empty __init__.py file to signal Python this directory is a package."""
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""Calculate the area of a glyph."""
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
|
@ -36,26 +36,27 @@ Coordinates are usually expressed as (x, y) tuples, but generally any
|
||||
sequence of length 2 will do.
|
||||
"""
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from typing import Any, Tuple
|
||||
|
||||
from fontTools.misc.loggingTools import LogMixin
|
||||
|
||||
__all__ = ["AbstractPen", "NullPen", "BasePen",
|
||||
"decomposeSuperBezierSegment", "decomposeQuadraticSegment"]
|
||||
|
||||
|
||||
class AbstractPen(object):
|
||||
class AbstractPen:
|
||||
|
||||
def moveTo(self, pt):
|
||||
def moveTo(self, pt: Tuple[float, float]) -> None:
|
||||
"""Begin a new sub path, set the current point to 'pt'. You must
|
||||
end each sub path with a call to pen.closePath() or pen.endPath().
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def lineTo(self, pt):
|
||||
def lineTo(self, pt: Tuple[float, float]) -> None:
|
||||
"""Draw a straight line from the current point to 'pt'."""
|
||||
raise NotImplementedError
|
||||
|
||||
def curveTo(self, *points):
|
||||
def curveTo(self, *points: Tuple[float, float]) -> None:
|
||||
"""Draw a cubic bezier with an arbitrary number of control points.
|
||||
|
||||
The last point specified is on-curve, all others are off-curve
|
||||
@ -76,7 +77,7 @@ class AbstractPen(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
def qCurveTo(self, *points: Tuple[float, float]) -> None:
|
||||
"""Draw a whole string of quadratic curve segments.
|
||||
|
||||
The last point specified is on-curve, all others are off-curve
|
||||
@ -93,19 +94,23 @@ class AbstractPen(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def closePath(self):
|
||||
def closePath(self) -> None:
|
||||
"""Close the current sub path. You must call either pen.closePath()
|
||||
or pen.endPath() after each sub path.
|
||||
"""
|
||||
pass
|
||||
|
||||
def endPath(self):
|
||||
def endPath(self) -> None:
|
||||
"""End the current sub path, but don't close it. You must call
|
||||
either pen.closePath() or pen.endPath() after each sub path.
|
||||
"""
|
||||
pass
|
||||
|
||||
def addComponent(self, glyphName, transformation):
|
||||
def addComponent(
|
||||
self,
|
||||
glyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float]
|
||||
) -> None:
|
||||
"""Add a sub glyph. The 'transformation' argument must be a 6-tuple
|
||||
containing an affine transformation, or a Transform object from the
|
||||
fontTools.misc.transform module. More precisely: it should be a
|
||||
@ -114,7 +119,7 @@ class AbstractPen(object):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NullPen(object):
|
||||
class NullPen(AbstractPen):
|
||||
|
||||
"""A pen that does nothing.
|
||||
"""
|
||||
@ -147,6 +152,10 @@ class LoggingPen(LogMixin, AbstractPen):
|
||||
pass
|
||||
|
||||
|
||||
class MissingComponentError(KeyError):
|
||||
"""Indicates a component pointing to a non-existent glyph in the glyphset."""
|
||||
|
||||
|
||||
class DecomposingPen(LoggingPen):
|
||||
|
||||
""" Implements a 'addComponent' method that decomposes components
|
||||
@ -155,10 +164,12 @@ class DecomposingPen(LoggingPen):
|
||||
|
||||
You must override moveTo, lineTo, curveTo and qCurveTo. You may
|
||||
additionally override closePath, endPath and addComponent.
|
||||
|
||||
By default a warning message is logged when a base glyph is missing;
|
||||
set the class variable ``skipMissingComponents`` to False if you want
|
||||
to raise a :class:`MissingComponentError` exception.
|
||||
"""
|
||||
|
||||
# By default a warning message is logged when a base glyph is missing;
|
||||
# set this to False if you want to raise a 'KeyError' exception
|
||||
skipMissingComponents = True
|
||||
|
||||
def __init__(self, glyphSet):
|
||||
@ -176,7 +187,7 @@ class DecomposingPen(LoggingPen):
|
||||
glyph = self.glyphSet[glyphName]
|
||||
except KeyError:
|
||||
if not self.skipMissingComponents:
|
||||
raise
|
||||
raise MissingComponentError(glyphName)
|
||||
self.log.warning(
|
||||
"glyph '%s' is missing from glyphSet; skipped" % glyphName)
|
||||
else:
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.arrayTools import updateBounds, pointInRect, unionRect
|
||||
from fontTools.misc.bezierTools import calcCubicBounds, calcQuadraticBounds
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
from fontTools.pens.recordingPen import RecordingPen
|
||||
|
@ -1,6 +1,7 @@
|
||||
# Modified from https://github.com/adobe-type-tools/psautohint/blob/08b346865710ed3c172f1eb581d6ef243b203f99/python/psautohint/ufoFont.py#L800-L838
|
||||
import hashlib
|
||||
|
||||
from fontTools.pens.basePen import MissingComponentError
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
|
||||
|
||||
@ -69,5 +70,8 @@ class HashPointPen(AbstractPointPen):
|
||||
):
|
||||
tr = "".join([f"{t:+}" for t in transformation])
|
||||
self.data.append("[")
|
||||
self.glyphset[baseGlyphName].drawPoints(self)
|
||||
try:
|
||||
self.glyphset[baseGlyphName].drawPoints(self)
|
||||
except KeyError:
|
||||
raise MissingComponentError(baseGlyphName)
|
||||
self.data.append(f"({tr})]")
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""Pen calculating 0th, 1st, and 2nd moments of area of glyph shapes.
|
||||
This is low-level, autogenerated pen. Use statisticsPen instead."""
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Calculate the perimeter of a glyph."""
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.misc.bezierTools import approximateQuadraticArcLengthC, calcQuadraticArcLengthC, approximateCubicArcLengthC, calcCubicArcLengthC
|
||||
import math
|
||||
|
@ -2,7 +2,6 @@
|
||||
for shapes.
|
||||
"""
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from fontTools.misc.bezierTools import solveQuadratic, solveCubic
|
||||
|
||||
|
@ -11,8 +11,11 @@ steps through all the points in a call from glyph.drawPoints().
|
||||
This allows the caller to provide more data for each point.
|
||||
For instance, whether or not a point is smooth, and its name.
|
||||
"""
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
|
||||
import math
|
||||
from typing import Any, List, Optional, Tuple
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
|
||||
__all__ = [
|
||||
"AbstractPointPen",
|
||||
@ -24,26 +27,36 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class AbstractPointPen(object):
|
||||
"""
|
||||
Baseclass for all PointPens.
|
||||
"""
|
||||
class AbstractPointPen:
|
||||
"""Baseclass for all PointPens."""
|
||||
|
||||
def beginPath(self, identifier=None, **kwargs):
|
||||
def beginPath(self, identifier: Optional[str] = None, **kwargs: Any) -> None:
|
||||
"""Start a new sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def endPath(self):
|
||||
def endPath(self) -> None:
|
||||
"""End the current sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addPoint(self, pt, segmentType=None, smooth=False, name=None,
|
||||
identifier=None, **kwargs):
|
||||
def addPoint(
|
||||
self,
|
||||
pt: Tuple[float, float],
|
||||
segmentType: Optional[str] = None,
|
||||
smooth: bool = False,
|
||||
name: Optional[str] = None,
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Add a point to the current sub path."""
|
||||
raise NotImplementedError
|
||||
|
||||
def addComponent(self, baseGlyphName, transformation, identifier=None,
|
||||
**kwargs):
|
||||
def addComponent(
|
||||
self,
|
||||
baseGlyphName: str,
|
||||
transformation: Tuple[float, float, float, float, float, float],
|
||||
identifier: Optional[str] = None,
|
||||
**kwargs: Any
|
||||
) -> None:
|
||||
"""Add a sub glyph."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
from Quartz.CoreGraphics import CGPathCreateMutable, CGPathMoveToPoint
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""Pen recording operations that can be accessed or replayed."""
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import AbstractPen, DecomposingPen
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
from reportlab.graphics.shapes import Path
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.arrayTools import pairwise
|
||||
from fontTools.pens.filterPen import ContourFilterPen
|
||||
|
||||
|
@ -1,6 +1,5 @@
|
||||
"""Pen calculating area, center of mass, variance and standard-deviation,
|
||||
covariance and correlation, and slant, of glyph shapes."""
|
||||
from fontTools.misc.py23 import *
|
||||
import math
|
||||
from fontTools.pens.momentsPen import MomentsPen
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2009 Type Supply LLC
|
||||
# Author: Tal Leming
|
||||
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
from fontTools.misc.psCharStrings import T2CharString
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
@ -1,5 +1,4 @@
|
||||
"""Pen multiplexing drawing to one or more pens."""
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.filterPen import FilterPen, FilterPointPen
|
||||
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from array import array
|
||||
from fontTools.misc.fixedTools import MAX_F2DOT14, otRound, floatToFixedToFloat
|
||||
from fontTools.pens.basePen import LoggingPen
|
||||
@ -15,22 +14,34 @@ __all__ = ["TTGlyphPen"]
|
||||
class TTGlyphPen(LoggingPen):
|
||||
"""Pen used for drawing to a TrueType glyph.
|
||||
|
||||
If `handleOverflowingTransforms` is True, the components' transform values
|
||||
are checked that they don't overflow the limits of a F2Dot14 number:
|
||||
-2.0 <= v < +2.0. If any transform value exceeds these, the composite
|
||||
glyph is decomposed.
|
||||
An exception to this rule is done for values that are very close to +2.0
|
||||
(both for consistency with the -2.0 case, and for the relative frequency
|
||||
these occur in real fonts). When almost +2.0 values occur (and all other
|
||||
values are within the range -2.0 <= x <= +2.0), they are clamped to the
|
||||
maximum positive value that can still be encoded as an F2Dot14: i.e.
|
||||
1.99993896484375.
|
||||
If False, no check is done and all components are translated unmodified
|
||||
into the glyf table, followed by an inevitable `struct.error` once an
|
||||
attempt is made to compile them.
|
||||
This pen can be used to construct or modify glyphs in a TrueType format
|
||||
font. After using the pen to draw, use the ``.glyph()`` method to retrieve
|
||||
a :py:class:`~._g_l_y_f.Glyph` object representing the glyph.
|
||||
"""
|
||||
|
||||
def __init__(self, glyphSet, handleOverflowingTransforms=True):
|
||||
"""Construct a new pen.
|
||||
|
||||
Args:
|
||||
glyphSet (ttLib._TTGlyphSet): A glyphset object, used to resolve components.
|
||||
handleOverflowingTransforms (bool): See below.
|
||||
|
||||
If ``handleOverflowingTransforms`` is True, the components' transform values
|
||||
are checked that they don't overflow the limits of a F2Dot14 number:
|
||||
-2.0 <= v < +2.0. If any transform value exceeds these, the composite
|
||||
glyph is decomposed.
|
||||
|
||||
An exception to this rule is done for values that are very close to +2.0
|
||||
(both for consistency with the -2.0 case, and for the relative frequency
|
||||
these occur in real fonts). When almost +2.0 values occur (and all other
|
||||
values are within the range -2.0 <= x <= +2.0), they are clamped to the
|
||||
maximum positive value that can still be encoded as an F2Dot14: i.e.
|
||||
1.99993896484375.
|
||||
|
||||
If False, no check is done and all components are translated unmodified
|
||||
into the glyf table, followed by an inevitable ``struct.error`` once an
|
||||
attempt is made to compile them.
|
||||
"""
|
||||
self.glyphSet = glyphSet
|
||||
self.handleOverflowingTransforms = handleOverflowingTransforms
|
||||
self.init()
|
||||
@ -61,6 +72,9 @@ class TTGlyphPen(LoggingPen):
|
||||
assert self._isClosed(), '"move"-type point must begin a new contour.'
|
||||
self._addPoint(pt, 1)
|
||||
|
||||
def curveTo(self, *points):
|
||||
raise NotImplementedError
|
||||
|
||||
def qCurveTo(self, *points):
|
||||
assert len(points) >= 1
|
||||
for pt in points[:-1]:
|
||||
@ -136,6 +150,7 @@ class TTGlyphPen(LoggingPen):
|
||||
return components
|
||||
|
||||
def glyph(self, componentFlags=0x4):
|
||||
"""Returns a :py:class:`~._g_l_y_f.Glyph` object representing the glyph."""
|
||||
assert self._isClosed(), "Didn't close last contour."
|
||||
|
||||
components = self._buildComponents(componentFlags)
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import BasePen
|
||||
|
||||
|
||||
|
@ -14,7 +14,7 @@ import sys
|
||||
import struct
|
||||
import array
|
||||
import logging
|
||||
from collections import Counter
|
||||
from collections import Counter, defaultdict
|
||||
from types import MethodType
|
||||
|
||||
__usage__ = "pyftsubset font-file [glyph...] [--option=value]..."
|
||||
@ -1983,27 +1983,130 @@ def subset_glyphs(self, s):
|
||||
else:
|
||||
assert False, "unknown 'prop' format %s" % prop.Format
|
||||
|
||||
def _paint_glyph_names(paint, colr):
|
||||
result = set()
|
||||
|
||||
def callback(paint):
|
||||
if paint.Format in {
|
||||
otTables.PaintFormat.PaintGlyph,
|
||||
otTables.PaintFormat.PaintColrGlyph,
|
||||
}:
|
||||
result.add(paint.Glyph)
|
||||
|
||||
paint.traverse(colr, callback)
|
||||
return result
|
||||
|
||||
@_add_method(ttLib.getTableClass('COLR'))
|
||||
def closure_glyphs(self, s):
|
||||
if self.version > 0:
|
||||
# on decompiling COLRv1, we only keep around the raw otTables
|
||||
# but for subsetting we need dicts with fully decompiled layers;
|
||||
# we store them temporarily in the C_O_L_R_ instance and delete
|
||||
# them after we have finished subsetting.
|
||||
self.ColorLayers = self._decompileColorLayersV0(self.table)
|
||||
self.ColorLayersV1 = {
|
||||
rec.BaseGlyph: rec.Paint
|
||||
for rec in self.table.BaseGlyphV1List.BaseGlyphV1Record
|
||||
}
|
||||
|
||||
decompose = s.glyphs
|
||||
while decompose:
|
||||
layers = set()
|
||||
for g in decompose:
|
||||
for l in self.ColorLayers.get(g, []):
|
||||
layers.add(l.name)
|
||||
for layer in self.ColorLayers.get(g, []):
|
||||
layers.add(layer.name)
|
||||
|
||||
if self.version > 0:
|
||||
paint = self.ColorLayersV1.get(g)
|
||||
if paint is not None:
|
||||
layers.update(_paint_glyph_names(paint, self.table))
|
||||
|
||||
layers -= s.glyphs
|
||||
s.glyphs.update(layers)
|
||||
decompose = layers
|
||||
|
||||
@_add_method(ttLib.getTableClass('COLR'))
|
||||
def subset_glyphs(self, s):
|
||||
self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers}
|
||||
return bool(self.ColorLayers)
|
||||
from fontTools.colorLib.unbuilder import unbuildColrV1
|
||||
from fontTools.colorLib.builder import buildColrV1, populateCOLRv0
|
||||
|
||||
self.ColorLayers = {g: self.ColorLayers[g] for g in s.glyphs if g in self.ColorLayers}
|
||||
if self.version == 0:
|
||||
return bool(self.ColorLayers)
|
||||
|
||||
colorGlyphsV1 = unbuildColrV1(self.table.LayerV1List, self.table.BaseGlyphV1List)
|
||||
self.table.LayerV1List, self.table.BaseGlyphV1List = buildColrV1(
|
||||
{g: colorGlyphsV1[g] for g in colorGlyphsV1 if g in s.glyphs}
|
||||
)
|
||||
del self.ColorLayersV1
|
||||
|
||||
layersV0 = self.ColorLayers
|
||||
if not self.table.BaseGlyphV1List.BaseGlyphV1Record:
|
||||
# no more COLRv1 glyphs: downgrade to version 0
|
||||
self.version = 0
|
||||
del self.table
|
||||
return bool(layersV0)
|
||||
|
||||
if layersV0:
|
||||
populateCOLRv0(
|
||||
self.table,
|
||||
{
|
||||
g: [(layer.name, layer.colorID) for layer in layersV0[g]]
|
||||
for g in layersV0
|
||||
},
|
||||
)
|
||||
del self.ColorLayers
|
||||
|
||||
# TODO: also prune ununsed varIndices in COLR.VarStore
|
||||
return True
|
||||
|
||||
# TODO: prune unused palettes
|
||||
@_add_method(ttLib.getTableClass('CPAL'))
|
||||
def prune_post_subset(self, font, options):
|
||||
return True
|
||||
colr = font.get("COLR")
|
||||
if not colr: # drop CPAL if COLR was subsetted to empty
|
||||
return False
|
||||
|
||||
colors_by_index = defaultdict(list)
|
||||
|
||||
def collect_colors_by_index(paint):
|
||||
if hasattr(paint, "Color"): # either solid colors...
|
||||
colors_by_index[paint.Color.PaletteIndex].append(paint.Color)
|
||||
elif hasattr(paint, "ColorLine"): # ... or gradient color stops
|
||||
for stop in paint.ColorLine.ColorStop:
|
||||
colors_by_index[stop.Color.PaletteIndex].append(stop.Color)
|
||||
|
||||
if colr.version == 0:
|
||||
for layers in colr.ColorLayers.values():
|
||||
for layer in layers:
|
||||
colors_by_index[layer.colorID].append(layer)
|
||||
else:
|
||||
if colr.table.LayerRecordArray:
|
||||
for layer in colr.table.LayerRecordArray.LayerRecord:
|
||||
colors_by_index[layer.PaletteIndex].append(layer)
|
||||
for record in colr.table.BaseGlyphV1List.BaseGlyphV1Record:
|
||||
record.Paint.traverse(colr.table, collect_colors_by_index)
|
||||
|
||||
retained_palette_indices = set(colors_by_index.keys())
|
||||
for palette in self.palettes:
|
||||
palette[:] = [c for i, c in enumerate(palette) if i in retained_palette_indices]
|
||||
assert len(palette) == len(retained_palette_indices)
|
||||
|
||||
for new_index, old_index in enumerate(sorted(retained_palette_indices)):
|
||||
for record in colors_by_index[old_index]:
|
||||
if hasattr(record, "colorID"): # v0
|
||||
record.colorID = new_index
|
||||
elif hasattr(record, "PaletteIndex"): # v1
|
||||
record.PaletteIndex = new_index
|
||||
else:
|
||||
raise AssertionError(record)
|
||||
|
||||
self.numPaletteEntries = len(self.palettes[0])
|
||||
|
||||
if self.version == 1:
|
||||
self.paletteEntryLabels = [
|
||||
label for i, label in self.paletteEntryLabels if i in retained_palette_indices
|
||||
]
|
||||
return bool(self.numPaletteEntries)
|
||||
|
||||
@_add_method(otTables.MathGlyphConstruction)
|
||||
def closure_glyphs(self, glyphs):
|
||||
@ -2207,7 +2310,17 @@ def prune_pre_subset(self, font, options):
|
||||
@_add_method(ttLib.getTableClass('cmap'))
|
||||
def subset_glyphs(self, s):
|
||||
s.glyphs = None # We use s.glyphs_requested and s.unicodes_requested only
|
||||
|
||||
tables_format12_bmp = []
|
||||
table_plat0_enc3 = {} # Unicode platform, Unicode BMP only, keyed by language
|
||||
table_plat3_enc1 = {} # Windows platform, Unicode BMP, keyed by language
|
||||
|
||||
for t in self.tables:
|
||||
if t.platformID == 0 and t.platEncID == 3:
|
||||
table_plat0_enc3[t.language] = t
|
||||
if t.platformID == 3 and t.platEncID == 1:
|
||||
table_plat3_enc1[t.language] = t
|
||||
|
||||
if t.format == 14:
|
||||
# TODO(behdad) We drop all the default-UVS mappings
|
||||
# for glyphs_requested. So it's the caller's responsibility to make
|
||||
@ -2219,16 +2332,38 @@ def subset_glyphs(self, s):
|
||||
elif t.isUnicode():
|
||||
t.cmap = {u:g for u,g in t.cmap.items()
|
||||
if g in s.glyphs_requested or u in s.unicodes_requested}
|
||||
# Collect format 12 tables that hold only basic multilingual plane
|
||||
# codepoints.
|
||||
if t.format == 12 and t.cmap and max(t.cmap.keys()) < 0x10000:
|
||||
tables_format12_bmp.append(t)
|
||||
else:
|
||||
t.cmap = {u:g for u,g in t.cmap.items()
|
||||
if g in s.glyphs_requested}
|
||||
|
||||
# Fomat 12 tables are redundant if they contain just the same BMP codepoints
|
||||
# their little BMP-only encoding siblings contain.
|
||||
for t in tables_format12_bmp:
|
||||
if (
|
||||
t.platformID == 0 # Unicode platform
|
||||
and t.platEncID == 4 # Unicode full repertoire
|
||||
and t.language in table_plat0_enc3 # Have a BMP-only sibling?
|
||||
and table_plat0_enc3[t.language].cmap == t.cmap
|
||||
):
|
||||
t.cmap.clear()
|
||||
elif (
|
||||
t.platformID == 3 # Windows platform
|
||||
and t.platEncID == 10 # Unicode full repertoire
|
||||
and t.language in table_plat3_enc1 # Have a BMP-only sibling?
|
||||
and table_plat3_enc1[t.language].cmap == t.cmap
|
||||
):
|
||||
t.cmap.clear()
|
||||
|
||||
self.tables = [t for t in self.tables
|
||||
if (t.cmap if t.format != 14 else t.uvsDict)]
|
||||
self.numSubTables = len(self.tables)
|
||||
# TODO(behdad) Convert formats when needed.
|
||||
# In particular, if we have a format=12 without non-BMP
|
||||
# characters, either drop format=12 one or convert it
|
||||
# to format=4 if there's not one.
|
||||
# characters, convert it to format=4 if there's not one.
|
||||
return True # Required table
|
||||
|
||||
@_add_method(ttLib.getTableClass('DSIG'))
|
||||
|
@ -14,9 +14,11 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
||||
ttFont['COLR'][<glyphName>] = <value> will set the color layers for any glyph.
|
||||
"""
|
||||
|
||||
def _fromOTTable(self, table):
|
||||
self.version = 0
|
||||
self.ColorLayers = colorLayerLists = {}
|
||||
@staticmethod
|
||||
def _decompileColorLayersV0(table):
|
||||
if not table.LayerRecordArray:
|
||||
return {}
|
||||
colorLayerLists = {}
|
||||
layerRecords = table.LayerRecordArray.LayerRecord
|
||||
numLayerRecords = len(layerRecords)
|
||||
for baseRec in table.BaseGlyphRecordArray.BaseGlyphRecord:
|
||||
@ -31,6 +33,7 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
||||
LayerRecord(layerRec.LayerGlyph, layerRec.PaletteIndex)
|
||||
)
|
||||
colorLayerLists[baseGlyph] = layers
|
||||
return colorLayerLists
|
||||
|
||||
def _toOTTable(self, ttFont):
|
||||
from . import otTables
|
||||
@ -61,12 +64,12 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
||||
table = tableClass()
|
||||
table.decompile(reader, ttFont)
|
||||
|
||||
if table.Version == 0:
|
||||
self._fromOTTable(table)
|
||||
self.version = table.Version
|
||||
if self.version == 0:
|
||||
self.ColorLayers = self._decompileColorLayersV0(table)
|
||||
else:
|
||||
# for new versions, keep the raw otTables around
|
||||
self.table = table
|
||||
self.version = table.Version
|
||||
|
||||
def compile(self, ttFont):
|
||||
from .otBase import OTTableWriter
|
||||
@ -120,6 +123,7 @@ class table_C_O_L_R_(DefaultTable.DefaultTable):
|
||||
self.table = tableClass()
|
||||
self.table.fromXML(name, attrs, content, ttFont)
|
||||
self.table.populateDefaults()
|
||||
self.version = self.table.Version
|
||||
|
||||
def __getitem__(self, glyphName):
|
||||
if not isinstance(glyphName, str):
|
||||
|
@ -59,14 +59,20 @@ def buildConverters(tableSpec, tableNamespace):
|
||||
converterClass = Struct
|
||||
else:
|
||||
converterClass = eval(tp, tableNamespace, converterMapping)
|
||||
if tp in ('MortChain', 'MortSubtable', 'MorxChain'):
|
||||
|
||||
conv = converterClass(name, repeat, aux)
|
||||
|
||||
if conv.tableClass:
|
||||
# A "template" such as OffsetTo(AType) knowss the table class already
|
||||
tableClass = conv.tableClass
|
||||
elif tp in ('MortChain', 'MortSubtable', 'MorxChain'):
|
||||
tableClass = tableNamespace.get(tp)
|
||||
else:
|
||||
tableClass = tableNamespace.get(tableName)
|
||||
if tableClass is not None:
|
||||
conv = converterClass(name, repeat, aux, tableClass=tableClass)
|
||||
else:
|
||||
conv = converterClass(name, repeat, aux)
|
||||
|
||||
if not conv.tableClass:
|
||||
conv.tableClass = tableClass
|
||||
|
||||
if name in ["SubTable", "ExtSubTable", "SubStruct"]:
|
||||
conv.lookupTypes = tableNamespace['lookupTypes']
|
||||
# also create reverse mapping
|
||||
@ -332,6 +338,18 @@ class NameID(UShort):
|
||||
log.warning("name id %d missing from name table" % value)
|
||||
xmlWriter.newline()
|
||||
|
||||
class STATFlags(UShort):
|
||||
def xmlWrite(self, xmlWriter, font, value, name, attrs):
|
||||
xmlWriter.simpletag(name, attrs + [("value", value)])
|
||||
flags = []
|
||||
if value & 0x01:
|
||||
flags.append("OlderSiblingFontAttribute")
|
||||
if value & 0x02:
|
||||
flags.append("ElidableAxisValueName")
|
||||
if flags:
|
||||
xmlWriter.write(" ")
|
||||
xmlWriter.comment(" ".join(flags))
|
||||
xmlWriter.newline()
|
||||
|
||||
class FloatValue(SimpleValue):
|
||||
@staticmethod
|
||||
@ -1739,7 +1757,6 @@ converterMapping = {
|
||||
"int8": Int8,
|
||||
"int16": Short,
|
||||
"uint8": UInt8,
|
||||
"uint8": UInt8,
|
||||
"uint16": UShort,
|
||||
"uint24": UInt24,
|
||||
"uint32": ULong,
|
||||
@ -1764,6 +1781,7 @@ converterMapping = {
|
||||
"LookupFlag": LookupFlag,
|
||||
"ExtendMode": ExtendMode,
|
||||
"CompositeMode": CompositeMode,
|
||||
"STATFlags": STATFlags,
|
||||
|
||||
# AAT
|
||||
"CIDGlyphMap": CIDGlyphMap,
|
||||
|
@ -872,7 +872,7 @@ otData = [
|
||||
('AxisValueFormat1', [
|
||||
('uint16', 'Format', None, None, 'Format, = 1'),
|
||||
('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'),
|
||||
('uint16', 'Flags', None, None, 'Flags.'),
|
||||
('STATFlags', 'Flags', None, None, 'Flags.'),
|
||||
('NameID', 'ValueNameID', None, None, ''),
|
||||
('Fixed', 'Value', None, None, ''),
|
||||
]),
|
||||
@ -880,7 +880,7 @@ otData = [
|
||||
('AxisValueFormat2', [
|
||||
('uint16', 'Format', None, None, 'Format, = 2'),
|
||||
('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'),
|
||||
('uint16', 'Flags', None, None, 'Flags.'),
|
||||
('STATFlags', 'Flags', None, None, 'Flags.'),
|
||||
('NameID', 'ValueNameID', None, None, ''),
|
||||
('Fixed', 'NominalValue', None, None, ''),
|
||||
('Fixed', 'RangeMinValue', None, None, ''),
|
||||
@ -890,7 +890,7 @@ otData = [
|
||||
('AxisValueFormat3', [
|
||||
('uint16', 'Format', None, None, 'Format, = 3'),
|
||||
('uint16', 'AxisIndex', None, None, 'Index into the axis record array identifying the axis of design variation to which the axis value record applies.'),
|
||||
('uint16', 'Flags', None, None, 'Flags.'),
|
||||
('STATFlags', 'Flags', None, None, 'Flags.'),
|
||||
('NameID', 'ValueNameID', None, None, ''),
|
||||
('Fixed', 'Value', None, None, ''),
|
||||
('Fixed', 'LinkedValue', None, None, ''),
|
||||
@ -899,7 +899,7 @@ otData = [
|
||||
('AxisValueFormat4', [
|
||||
('uint16', 'Format', None, None, 'Format, = 4'),
|
||||
('uint16', 'AxisCount', None, None, 'The total number of axes contributing to this axis-values combination.'),
|
||||
('uint16', 'Flags', None, None, 'Flags.'),
|
||||
('STATFlags', 'Flags', None, None, 'Flags.'),
|
||||
('NameID', 'ValueNameID', None, None, ''),
|
||||
('struct', 'AxisValueRecord', 'AxisCount', 0, 'Array of AxisValue records that provide the combination of axis values, one for each contributing axis. '),
|
||||
]),
|
||||
@ -1588,7 +1588,24 @@ otData = [
|
||||
('LOffset', 'Paint', 'LayerCount', 0, 'Array of offsets to Paint tables, from the start of the LayerV1List table.'),
|
||||
]),
|
||||
|
||||
# COLRv1 Affine2x3 uses the same column-major order to serialize a 2D
|
||||
# Affine Transformation as the one used by fontTools.misc.transform.
|
||||
# However, for historical reasons, the labels 'xy' and 'yx' are swapped.
|
||||
# Their fundamental meaning is the same though.
|
||||
# COLRv1 Affine2x3 follows the names found in FreeType and Cairo.
|
||||
# In all case, the second element in the 6-tuple correspond to the
|
||||
# y-part of the x basis vector, and the third to the x-part of the y
|
||||
# basis vector.
|
||||
# See https://github.com/googlefonts/colr-gradients-spec/pull/85
|
||||
('Affine2x3', [
|
||||
('Fixed', 'xx', None, None, 'x-part of x basis vector'),
|
||||
('Fixed', 'yx', None, None, 'y-part of x basis vector'),
|
||||
('Fixed', 'xy', None, None, 'x-part of y basis vector'),
|
||||
('Fixed', 'yy', None, None, 'y-part of y basis vector'),
|
||||
('Fixed', 'dx', None, None, 'Translation in x direction'),
|
||||
('Fixed', 'dy', None, None, 'Translation in y direction'),
|
||||
]),
|
||||
('VarAffine2x3', [
|
||||
('VarFixed', 'xx', None, None, 'x-part of x basis vector'),
|
||||
('VarFixed', 'yx', None, None, 'y-part of x basis vector'),
|
||||
('VarFixed', 'xy', None, None, 'x-part of y basis vector'),
|
||||
@ -1598,35 +1615,67 @@ otData = [
|
||||
]),
|
||||
|
||||
('ColorIndex', [
|
||||
('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
|
||||
('F2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
|
||||
]),
|
||||
('VarColorIndex', [
|
||||
('uint16', 'PaletteIndex', None, None, 'Index value to use with a selected color palette.'),
|
||||
('VarF2Dot14', 'Alpha', None, None, 'Values outsided [0.,1.] reserved'),
|
||||
]),
|
||||
|
||||
('ColorStop', [
|
||||
('VarF2Dot14', 'StopOffset', None, None, ''),
|
||||
('F2Dot14', 'StopOffset', None, None, ''),
|
||||
('ColorIndex', 'Color', None, None, ''),
|
||||
]),
|
||||
('VarColorStop', [
|
||||
('VarF2Dot14', 'StopOffset', None, None, ''),
|
||||
('VarColorIndex', 'Color', None, None, ''),
|
||||
]),
|
||||
|
||||
('ColorLine', [
|
||||
('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
|
||||
('uint16', 'StopCount', None, None, 'Number of Color stops.'),
|
||||
('ColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
|
||||
]),
|
||||
('VarColorLine', [
|
||||
('ExtendMode', 'Extend', None, None, 'Enum {PAD = 0, REPEAT = 1, REFLECT = 2}'),
|
||||
('uint16', 'StopCount', None, None, 'Number of Color stops.'),
|
||||
('VarColorStop', 'ColorStop', 'StopCount', 0, 'Array of Color stops.'),
|
||||
]),
|
||||
|
||||
# PaintColrLayers
|
||||
('PaintFormat1', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 1'),
|
||||
('uint8', 'NumLayers', None, None, 'Number of offsets to Paint to read from LayerV1List.'),
|
||||
('uint32', 'FirstLayerIndex', None, None, 'Index into LayerV1List.'),
|
||||
]),
|
||||
|
||||
# PaintSolid
|
||||
('PaintFormat2', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 2'),
|
||||
('ColorIndex', 'Color', None, None, 'A solid color paint.'),
|
||||
]),
|
||||
|
||||
# PaintVarSolid
|
||||
('PaintFormat3', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 3'),
|
||||
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
|
||||
('VarColorIndex', 'Color', None, None, 'A solid color paint.'),
|
||||
]),
|
||||
|
||||
# PaintLinearGradient
|
||||
('PaintFormat4', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 4'),
|
||||
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintLinearGradient table) to ColorLine subtable.'),
|
||||
('int16', 'x0', None, None, ''),
|
||||
('int16', 'y0', None, None, ''),
|
||||
('int16', 'x1', None, None, ''),
|
||||
('int16', 'y1', None, None, ''),
|
||||
('int16', 'x2', None, None, ''),
|
||||
('int16', 'y2', None, None, ''),
|
||||
]),
|
||||
# PaintVarLinearGradient
|
||||
('PaintFormat5', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
|
||||
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarLinearGradient table) to VarColorLine subtable.'),
|
||||
('VarInt16', 'x0', None, None, ''),
|
||||
('VarInt16', 'y0', None, None, ''),
|
||||
('VarInt16', 'x1', None, None, ''),
|
||||
@ -1635,9 +1684,21 @@ otData = [
|
||||
('VarInt16', 'y2', None, None, ''),
|
||||
]),
|
||||
|
||||
('PaintFormat4', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 4'),
|
||||
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of Paint table) to ColorLine subtable.'),
|
||||
# PaintRadialGradient
|
||||
('PaintFormat6', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'),
|
||||
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintRadialGradient table) to ColorLine subtable.'),
|
||||
('int16', 'x0', None, None, ''),
|
||||
('int16', 'y0', None, None, ''),
|
||||
('uint16', 'r0', None, None, ''),
|
||||
('int16', 'x1', None, None, ''),
|
||||
('int16', 'y1', None, None, ''),
|
||||
('uint16', 'r1', None, None, ''),
|
||||
]),
|
||||
# PaintVarRadialGradient
|
||||
('PaintFormat7', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
|
||||
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarRadialGradient table) to VarColorLine subtable.'),
|
||||
('VarInt16', 'x0', None, None, ''),
|
||||
('VarInt16', 'y0', None, None, ''),
|
||||
('VarUInt16', 'r0', None, None, ''),
|
||||
@ -1646,25 +1707,105 @@ otData = [
|
||||
('VarUInt16', 'r1', None, None, ''),
|
||||
]),
|
||||
|
||||
('PaintFormat5', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 5'),
|
||||
# PaintSweepGradient
|
||||
('PaintFormat8', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 8'),
|
||||
('Offset24', 'ColorLine', None, None, 'Offset (from beginning of PaintSweepGradient table) to ColorLine subtable.'),
|
||||
('int16', 'centerX', None, None, 'Center x coordinate.'),
|
||||
('int16', 'centerY', None, None, 'Center y coordinate.'),
|
||||
('Fixed', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
|
||||
('Fixed', 'endAngle', None, None, 'End of the angular range of the gradient.'),
|
||||
]),
|
||||
# PaintVarSweepGradient
|
||||
('PaintFormat9', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 9'),
|
||||
('LOffset24To(VarColorLine)', 'ColorLine', None, None, 'Offset (from beginning of PaintVarSweepGradient table) to VarColorLine subtable.'),
|
||||
('VarInt16', 'centerX', None, None, 'Center x coordinate.'),
|
||||
('VarInt16', 'centerY', None, None, 'Center y coordinate.'),
|
||||
('VarFixed', 'startAngle', None, None, 'Start of the angular range of the gradient.'),
|
||||
('VarFixed', 'endAngle', None, None, 'End of the angular range of the gradient.'),
|
||||
]),
|
||||
|
||||
# PaintGlyph
|
||||
('PaintFormat10', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 10'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintGlyph table) to Paint subtable.'),
|
||||
('GlyphID', 'Glyph', None, None, 'Glyph ID for the source outline.'),
|
||||
]),
|
||||
|
||||
('PaintFormat6', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 6'),
|
||||
# PaintColrGlyph
|
||||
('PaintFormat11', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 11'),
|
||||
('GlyphID', 'Glyph', None, None, 'Virtual glyph ID for a BaseGlyphV1List base glyph.'),
|
||||
]),
|
||||
|
||||
('PaintFormat7', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 7'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTransformed table) to Paint subtable.'),
|
||||
('Affine2x3', 'Transform', None, None, 'Offset (from beginning of PaintTrasformed table) to Affine2x3 subtable.'),
|
||||
# PaintTransform
|
||||
('PaintFormat12', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 12'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTransform table) to Paint subtable.'),
|
||||
('Affine2x3', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
|
||||
]),
|
||||
# PaintVarTransform
|
||||
('PaintFormat13', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 13'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTransform table) to Paint subtable.'),
|
||||
('VarAffine2x3', 'Transform', None, None, '2x3 matrix for 2D affine transformations.'),
|
||||
]),
|
||||
|
||||
('PaintFormat8', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 8'),
|
||||
# PaintTranslate
|
||||
('PaintFormat14', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 14'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintTranslate table) to Paint subtable.'),
|
||||
('Fixed', 'dx', None, None, 'Translation in x direction.'),
|
||||
('Fixed', 'dy', None, None, 'Translation in y direction.'),
|
||||
]),
|
||||
# PaintVarTranslate
|
||||
('PaintFormat15', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 15'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarTranslate table) to Paint subtable.'),
|
||||
('VarFixed', 'dx', None, None, 'Translation in x direction.'),
|
||||
('VarFixed', 'dy', None, None, 'Translation in y direction.'),
|
||||
]),
|
||||
|
||||
# PaintRotate
|
||||
('PaintFormat16', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 16'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintRotate table) to Paint subtable.'),
|
||||
('Fixed', 'angle', None, None, ''),
|
||||
('Fixed', 'centerX', None, None, ''),
|
||||
('Fixed', 'centerY', None, None, ''),
|
||||
]),
|
||||
# PaintVarRotate
|
||||
('PaintFormat17', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 17'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarRotate table) to Paint subtable.'),
|
||||
('VarFixed', 'angle', None, None, ''),
|
||||
('VarFixed', 'centerX', None, None, ''),
|
||||
('VarFixed', 'centerY', None, None, ''),
|
||||
]),
|
||||
|
||||
# PaintSkew
|
||||
('PaintFormat18', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 18'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintSkew table) to Paint subtable.'),
|
||||
('Fixed', 'xSkewAngle', None, None, ''),
|
||||
('Fixed', 'ySkewAngle', None, None, ''),
|
||||
('Fixed', 'centerX', None, None, ''),
|
||||
('Fixed', 'centerY', None, None, ''),
|
||||
]),
|
||||
# PaintVarSkew
|
||||
('PaintFormat19', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 19'),
|
||||
('Offset24', 'Paint', None, None, 'Offset (from beginning of PaintVarSkew table) to Paint subtable.'),
|
||||
('VarFixed', 'xSkewAngle', None, None, ''),
|
||||
('VarFixed', 'ySkewAngle', None, None, ''),
|
||||
('VarFixed', 'centerX', None, None, ''),
|
||||
('VarFixed', 'centerY', None, None, ''),
|
||||
]),
|
||||
|
||||
# PaintComposite
|
||||
('PaintFormat20', [
|
||||
('uint8', 'PaintFormat', None, None, 'Format identifier-format = 20'),
|
||||
('LOffset24To(Paint)', 'SourcePaint', None, None, 'Offset (from beginning of PaintComposite table) to source Paint subtable.'),
|
||||
('CompositeMode', 'CompositeMode', None, None, 'A CompositeMode enumeration value.'),
|
||||
('LOffset24To(Paint)', 'BackdropPaint', None, None, 'Offset (from beginning of PaintComposite table) to backdrop Paint subtable.'),
|
||||
|
@ -1324,21 +1324,34 @@ class CompositeMode(IntEnum):
|
||||
HSL_LUMINOSITY = 26
|
||||
|
||||
|
||||
class Paint(getFormatSwitchingBaseTableClass("uint8")):
|
||||
class PaintFormat(IntEnum):
|
||||
PaintColrLayers = 1
|
||||
PaintSolid = 2
|
||||
PaintVarSolid = 3,
|
||||
PaintLinearGradient = 4
|
||||
PaintVarLinearGradient = 5
|
||||
PaintRadialGradient = 6
|
||||
PaintVarRadialGradient = 7
|
||||
PaintSweepGradient = 8
|
||||
PaintVarSweepGradient = 9
|
||||
PaintGlyph = 10
|
||||
PaintColrGlyph = 11
|
||||
PaintTransform = 12
|
||||
PaintVarTransform = 13
|
||||
PaintTranslate = 14
|
||||
PaintVarTranslate = 15
|
||||
PaintRotate = 16
|
||||
PaintVarRotate = 17
|
||||
PaintSkew = 18
|
||||
PaintVarSkew = 19
|
||||
PaintComposite = 20
|
||||
|
||||
class Format(IntEnum):
|
||||
PaintColrLayers = 1
|
||||
PaintSolid = 2
|
||||
PaintLinearGradient = 3
|
||||
PaintRadialGradient = 4
|
||||
PaintGlyph = 5
|
||||
PaintColrGlyph = 6
|
||||
PaintTransform = 7
|
||||
PaintComposite = 8
|
||||
|
||||
class Paint(getFormatSwitchingBaseTableClass("uint8")):
|
||||
|
||||
def getFormatName(self):
|
||||
try:
|
||||
return self.__class__.Format(self.Format).name
|
||||
return PaintFormat(self.Format).name
|
||||
except ValueError:
|
||||
raise NotImplementedError(f"Unknown Paint format: {self.Format}")
|
||||
|
||||
@ -1354,6 +1367,40 @@ class Paint(getFormatSwitchingBaseTableClass("uint8")):
|
||||
xmlWriter.endtag(tableName)
|
||||
xmlWriter.newline()
|
||||
|
||||
def getChildren(self, colr):
|
||||
if self.Format == PaintFormat.PaintColrLayers:
|
||||
return colr.LayerV1List.Paint[
|
||||
self.FirstLayerIndex : self.FirstLayerIndex + self.NumLayers
|
||||
]
|
||||
|
||||
if self.Format == PaintFormat.PaintColrGlyph:
|
||||
for record in colr.BaseGlyphV1List.BaseGlyphV1Record:
|
||||
if record.BaseGlyph == self.Glyph:
|
||||
return [record.Paint]
|
||||
else:
|
||||
raise KeyError(f"{self.Glyph!r} not in colr.BaseGlyphV1List")
|
||||
|
||||
children = []
|
||||
for conv in self.getConverters():
|
||||
if conv.tableClass is not None and issubclass(conv.tableClass, type(self)):
|
||||
children.append(getattr(self, conv.name))
|
||||
|
||||
return children
|
||||
|
||||
def traverse(self, colr: COLR, callback):
|
||||
"""Depth-first traversal of graph rooted at self, callback on each node."""
|
||||
if not callable(callback):
|
||||
raise TypeError("callback must be callable")
|
||||
stack = [self]
|
||||
visited = set()
|
||||
while stack:
|
||||
current = stack.pop()
|
||||
if id(current) in visited:
|
||||
continue
|
||||
callback(current)
|
||||
visited.add(id(current))
|
||||
stack.extend(reversed(current.getChildren(colr)))
|
||||
|
||||
|
||||
# For each subtable format there is a class. However, we don't really distinguish
|
||||
# between "field name" and "format name": often these are the same. Yet there's
|
||||
|
@ -700,6 +700,13 @@ class _TTGlyphSet(object):
|
||||
"""
|
||||
|
||||
def __init__(self, ttFont, glyphs, glyphType):
|
||||
"""Construct a new glyphset.
|
||||
|
||||
Args:
|
||||
font (TTFont): The font object (used to get metrics).
|
||||
glyphs (dict): A dictionary mapping glyph names to ``_TTGlyph`` objects.
|
||||
glyphType (class): Either ``_TTGlyphCFF`` or ``_TTGlyphGlyf``.
|
||||
"""
|
||||
self._glyphs = glyphs
|
||||
self._hmtx = ttFont['hmtx']
|
||||
self._vmtx = ttFont['vmtx'] if 'vmtx' in ttFont else None
|
||||
@ -740,6 +747,13 @@ class _TTGlyph(object):
|
||||
"""
|
||||
|
||||
def __init__(self, glyphset, glyph, horizontalMetrics, verticalMetrics=None):
|
||||
"""Construct a new _TTGlyph.
|
||||
|
||||
Args:
|
||||
glyphset (_TTGlyphSet): A glyphset object used to resolve components.
|
||||
glyph (ttLib.tables._g_l_y_f.Glyph): The glyph object.
|
||||
horizontalMetrics (int, int): The glyph's width and left sidebearing.
|
||||
"""
|
||||
self._glyphset = glyphset
|
||||
self._glyph = glyph
|
||||
self.width, self.lsb = horizontalMetrics
|
||||
@ -749,7 +763,7 @@ class _TTGlyph(object):
|
||||
self.height, self.tsb = None, None
|
||||
|
||||
def draw(self, pen):
|
||||
"""Draw the glyph onto Pen. See fontTools.pens.basePen for details
|
||||
"""Draw the glyph onto ``pen``. See fontTools.pens.basePen for details
|
||||
how that works.
|
||||
"""
|
||||
self._glyph.draw(pen)
|
||||
|
@ -11,7 +11,7 @@ from fontTools.ttLib import (TTFont, TTLibError, getTableModule, getTableClass,
|
||||
from fontTools.ttLib.sfnt import (SFNTReader, SFNTWriter, DirectoryEntry,
|
||||
WOFFFlavorData, sfntDirectoryFormat, sfntDirectorySize, SFNTDirectoryEntry,
|
||||
sfntDirectoryEntrySize, calcChecksum)
|
||||
from fontTools.ttLib.tables import ttProgram
|
||||
from fontTools.ttLib.tables import ttProgram, _g_l_y_f
|
||||
import logging
|
||||
|
||||
|
||||
@ -19,7 +19,10 @@ log = logging.getLogger("fontTools.ttLib.woff2")
|
||||
|
||||
haveBrotli = False
|
||||
try:
|
||||
import brotli
|
||||
try:
|
||||
import brotlicffi as brotli
|
||||
except ImportError:
|
||||
import brotli
|
||||
haveBrotli = True
|
||||
except ImportError:
|
||||
pass
|
||||
@ -931,7 +934,7 @@ class WOFF2GlyfTable(getTableClass('glyf')):
|
||||
flags = array.array('B')
|
||||
triplets = array.array('B')
|
||||
for i in range(len(coordinates)):
|
||||
onCurve = glyph.flags[i]
|
||||
onCurve = glyph.flags[i] & _g_l_y_f.flagOnCurve
|
||||
x, y = coordinates[i]
|
||||
absX = abs(x)
|
||||
absY = abs(y)
|
||||
|
@ -20,7 +20,7 @@ API *will* change in near future.
|
||||
"""
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.fixedTools import otRound
|
||||
from fontTools.misc.arrayTools import Vector
|
||||
from fontTools.misc.vector import Vector
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables._f_v_a_r import Axis, NamedInstance
|
||||
from fontTools.ttLib.tables._g_l_y_f import GlyphCoordinates
|
||||
|
@ -413,7 +413,7 @@ def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):
|
||||
# in the PrivatDict, so we will build the default data for vsindex = 0.
|
||||
if not vsindex_dict:
|
||||
key = (True,) * num_masters
|
||||
_add_new_vsindex(model, key, masterSupports, vsindex_dict,
|
||||
_add_new_vsindex(masterModel, key, masterSupports, vsindex_dict,
|
||||
vsindex_by_key, varDataList)
|
||||
cvData = CVarData(varDataList=varDataList, masterSupports=masterSupports,
|
||||
vsindex_dict=vsindex_dict)
|
||||
|
@ -10,7 +10,7 @@ from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
|
||||
from collections import OrderedDict
|
||||
|
||||
from .errors import VarLibValidationError
|
||||
from .errors import VarLibError, VarLibValidationError
|
||||
|
||||
|
||||
def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
|
||||
@ -298,6 +298,11 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
|
||||
varFeatureIndex = gsub.FeatureList.FeatureRecord.index(varFeature)
|
||||
|
||||
for scriptRecord in gsub.ScriptList.ScriptRecord:
|
||||
if scriptRecord.Script.DefaultLangSys is None:
|
||||
raise VarLibError(
|
||||
"Feature variations require that the script "
|
||||
f"'{scriptRecord.ScriptTag}' defines a default language system."
|
||||
)
|
||||
langSystems = [lsr.LangSys for lsr in scriptRecord.Script.LangSysRecord]
|
||||
for langSys in [scriptRecord.Script.DefaultLangSys] + langSystems:
|
||||
langSys.FeatureIndex.append(varFeatureIndex)
|
||||
|
@ -84,6 +84,7 @@ from fontTools import subset # noqa: F401
|
||||
from fontTools.varLib import builder
|
||||
from fontTools.varLib.mvar import MVAR_ENTRIES
|
||||
from fontTools.varLib.merger import MutatorMerger
|
||||
from fontTools.varLib.instancer import names
|
||||
from contextlib import contextmanager
|
||||
import collections
|
||||
from copy import deepcopy
|
||||
@ -1008,6 +1009,13 @@ def instantiateSTAT(varfont, axisLimits):
|
||||
):
|
||||
return # STAT table empty, nothing to do
|
||||
|
||||
log.info("Instantiating STAT table")
|
||||
newAxisValueTables = axisValuesFromAxisLimits(stat, axisLimits)
|
||||
stat.AxisValueArray.AxisValue = newAxisValueTables
|
||||
stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
|
||||
|
||||
|
||||
def axisValuesFromAxisLimits(stat, axisLimits):
|
||||
location, axisRanges = splitAxisLocationAndRanges(axisLimits, rangeType=AxisRange)
|
||||
|
||||
def isAxisValueOutsideLimits(axisTag, axisValue):
|
||||
@ -1019,8 +1027,6 @@ def instantiateSTAT(varfont, axisLimits):
|
||||
return True
|
||||
return False
|
||||
|
||||
log.info("Instantiating STAT table")
|
||||
|
||||
# only keep AxisValues whose axis is not pinned nor restricted, or is pinned at the
|
||||
# exact (nominal) value, or is restricted but the value is within the new range
|
||||
designAxes = stat.DesignAxisRecord.Axis
|
||||
@ -1050,53 +1056,7 @@ def instantiateSTAT(varfont, axisLimits):
|
||||
else:
|
||||
log.warn("Unknown AxisValue table format (%s); ignored", axisValueFormat)
|
||||
newAxisValueTables.append(axisValueTable)
|
||||
|
||||
stat.AxisValueArray.AxisValue = newAxisValueTables
|
||||
stat.AxisValueCount = len(stat.AxisValueArray.AxisValue)
|
||||
|
||||
|
||||
def getVariationNameIDs(varfont):
|
||||
used = []
|
||||
if "fvar" in varfont:
|
||||
fvar = varfont["fvar"]
|
||||
for axis in fvar.axes:
|
||||
used.append(axis.axisNameID)
|
||||
for instance in fvar.instances:
|
||||
used.append(instance.subfamilyNameID)
|
||||
if instance.postscriptNameID != 0xFFFF:
|
||||
used.append(instance.postscriptNameID)
|
||||
if "STAT" in varfont:
|
||||
stat = varfont["STAT"].table
|
||||
for axis in stat.DesignAxisRecord.Axis if stat.DesignAxisRecord else ():
|
||||
used.append(axis.AxisNameID)
|
||||
for value in stat.AxisValueArray.AxisValue if stat.AxisValueArray else ():
|
||||
used.append(value.ValueNameID)
|
||||
# nameIDs <= 255 are reserved by OT spec so we don't touch them
|
||||
return {nameID for nameID in used if nameID > 255}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def pruningUnusedNames(varfont):
|
||||
origNameIDs = getVariationNameIDs(varfont)
|
||||
|
||||
yield
|
||||
|
||||
log.info("Pruning name table")
|
||||
exclude = origNameIDs - getVariationNameIDs(varfont)
|
||||
varfont["name"].names[:] = [
|
||||
record for record in varfont["name"].names if record.nameID not in exclude
|
||||
]
|
||||
if "ltag" in varfont:
|
||||
# Drop the whole 'ltag' table if all the language-dependent Unicode name
|
||||
# records that reference it have been dropped.
|
||||
# TODO: Only prune unused ltag tags, renumerating langIDs accordingly.
|
||||
# Note ltag can also be used by feat or morx tables, so check those too.
|
||||
if not any(
|
||||
record
|
||||
for record in varfont["name"].names
|
||||
if record.platformID == 0 and record.langID != 0xFFFF
|
||||
):
|
||||
del varfont["ltag"]
|
||||
return newAxisValueTables
|
||||
|
||||
|
||||
def setMacOverlapFlags(glyfTable):
|
||||
@ -1187,6 +1147,7 @@ def instantiateVariableFont(
|
||||
inplace=False,
|
||||
optimize=True,
|
||||
overlap=OverlapMode.KEEP_AND_SET_FLAGS,
|
||||
updateFontNames=False,
|
||||
):
|
||||
"""Instantiate variable font, either fully or partially.
|
||||
|
||||
@ -1219,6 +1180,11 @@ def instantiateVariableFont(
|
||||
contours and components, you can pass OverlapMode.REMOVE. Note that this
|
||||
requires the skia-pathops package (available to pip install).
|
||||
The overlap parameter only has effect when generating full static instances.
|
||||
updateFontNames (bool): if True, update the instantiated font's name table using
|
||||
the Axis Value Tables from the STAT table. The name table will be updated so
|
||||
it conforms to the R/I/B/BI model. If the STAT table is missing or
|
||||
an Axis Value table is missing for a given axis coordinate, a ValueError will
|
||||
be raised.
|
||||
"""
|
||||
# 'overlap' used to be bool and is now enum; for backward compat keep accepting bool
|
||||
overlap = OverlapMode(int(overlap))
|
||||
@ -1234,6 +1200,10 @@ def instantiateVariableFont(
|
||||
if not inplace:
|
||||
varfont = deepcopy(varfont)
|
||||
|
||||
if updateFontNames:
|
||||
log.info("Updating name table")
|
||||
names.updateNameTable(varfont, axisLimits)
|
||||
|
||||
if "gvar" in varfont:
|
||||
instantiateGvar(varfont, normalizedLimits, optimize=optimize)
|
||||
|
||||
@ -1256,7 +1226,7 @@ def instantiateVariableFont(
|
||||
if "avar" in varfont:
|
||||
instantiateAvar(varfont, axisLimits)
|
||||
|
||||
with pruningUnusedNames(varfont):
|
||||
with names.pruningUnusedNames(varfont):
|
||||
if "STAT" in varfont:
|
||||
instantiateSTAT(varfont, axisLimits)
|
||||
|
||||
@ -1377,6 +1347,12 @@ def parseArgs(args):
|
||||
help="Merge overlapping contours and components (only applicable "
|
||||
"when generating a full instance). Requires skia-pathops",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--update-name-table",
|
||||
action="store_true",
|
||||
help="Update the instantiated font's `name` table. Input font must have "
|
||||
"a STAT table with Axis Value Tables",
|
||||
)
|
||||
loggingGroup = parser.add_mutually_exclusive_group(required=False)
|
||||
loggingGroup.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
||||
@ -1428,6 +1404,7 @@ def main(args=None):
|
||||
inplace=True,
|
||||
optimize=options.optimize,
|
||||
overlap=options.overlap,
|
||||
updateFontNames=options.update_name_table,
|
||||
)
|
||||
|
||||
outfile = (
|
||||
@ -1443,9 +1420,3 @@ def main(args=None):
|
||||
outfile,
|
||||
)
|
||||
varfont.save(outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main())
|
5
Lib/fontTools/varLib/instancer/__main__.py
Normal file
5
Lib/fontTools/varLib/instancer/__main__.py
Normal file
@ -0,0 +1,5 @@
|
||||
import sys
|
||||
from fontTools.varLib.instancer import main
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
379
Lib/fontTools/varLib/instancer/names.py
Normal file
379
Lib/fontTools/varLib/instancer/names.py
Normal file
@ -0,0 +1,379 @@
|
||||
"""Helpers for instantiating name table records."""
|
||||
|
||||
from contextlib import contextmanager
|
||||
from copy import deepcopy
|
||||
from enum import IntEnum
|
||||
import re
|
||||
|
||||
|
||||
class NameID(IntEnum):
|
||||
FAMILY_NAME = 1
|
||||
SUBFAMILY_NAME = 2
|
||||
UNIQUE_FONT_IDENTIFIER = 3
|
||||
FULL_FONT_NAME = 4
|
||||
VERSION_STRING = 5
|
||||
POSTSCRIPT_NAME = 6
|
||||
TYPOGRAPHIC_FAMILY_NAME = 16
|
||||
TYPOGRAPHIC_SUBFAMILY_NAME = 17
|
||||
VARIATIONS_POSTSCRIPT_NAME_PREFIX = 25
|
||||
|
||||
|
||||
ELIDABLE_AXIS_VALUE_NAME = 2
|
||||
|
||||
|
||||
def getVariationNameIDs(varfont):
|
||||
used = []
|
||||
if "fvar" in varfont:
|
||||
fvar = varfont["fvar"]
|
||||
for axis in fvar.axes:
|
||||
used.append(axis.axisNameID)
|
||||
for instance in fvar.instances:
|
||||
used.append(instance.subfamilyNameID)
|
||||
if instance.postscriptNameID != 0xFFFF:
|
||||
used.append(instance.postscriptNameID)
|
||||
if "STAT" in varfont:
|
||||
stat = varfont["STAT"].table
|
||||
for axis in stat.DesignAxisRecord.Axis if stat.DesignAxisRecord else ():
|
||||
used.append(axis.AxisNameID)
|
||||
for value in stat.AxisValueArray.AxisValue if stat.AxisValueArray else ():
|
||||
used.append(value.ValueNameID)
|
||||
# nameIDs <= 255 are reserved by OT spec so we don't touch them
|
||||
return {nameID for nameID in used if nameID > 255}
|
||||
|
||||
|
||||
@contextmanager
|
||||
def pruningUnusedNames(varfont):
|
||||
from . import log
|
||||
|
||||
origNameIDs = getVariationNameIDs(varfont)
|
||||
|
||||
yield
|
||||
|
||||
log.info("Pruning name table")
|
||||
exclude = origNameIDs - getVariationNameIDs(varfont)
|
||||
varfont["name"].names[:] = [
|
||||
record for record in varfont["name"].names if record.nameID not in exclude
|
||||
]
|
||||
if "ltag" in varfont:
|
||||
# Drop the whole 'ltag' table if all the language-dependent Unicode name
|
||||
# records that reference it have been dropped.
|
||||
# TODO: Only prune unused ltag tags, renumerating langIDs accordingly.
|
||||
# Note ltag can also be used by feat or morx tables, so check those too.
|
||||
if not any(
|
||||
record
|
||||
for record in varfont["name"].names
|
||||
if record.platformID == 0 and record.langID != 0xFFFF
|
||||
):
|
||||
del varfont["ltag"]
|
||||
|
||||
|
||||
def updateNameTable(varfont, axisLimits):
|
||||
"""Update instatiated variable font's name table using STAT AxisValues.
|
||||
|
||||
Raises ValueError if the STAT table is missing or an Axis Value table is
|
||||
missing for requested axis locations.
|
||||
|
||||
First, collect all STAT AxisValues that match the new default axis locations
|
||||
(excluding "elided" ones); concatenate the strings in design axis order,
|
||||
while giving priority to "synthetic" values (Format 4), to form the
|
||||
typographic subfamily name associated with the new default instance.
|
||||
Finally, update all related records in the name table, making sure that
|
||||
legacy family/sub-family names conform to the the R/I/B/BI (Regular, Italic,
|
||||
Bold, Bold Italic) naming model.
|
||||
|
||||
Example: Updating a partial variable font:
|
||||
| >>> ttFont = TTFont("OpenSans[wdth,wght].ttf")
|
||||
| >>> updateNameTable(ttFont, {"wght": AxisRange(400, 900), "wdth": 75})
|
||||
|
||||
The name table records will be updated in the following manner:
|
||||
NameID 1 familyName: "Open Sans" --> "Open Sans Condensed"
|
||||
NameID 2 subFamilyName: "Regular" --> "Regular"
|
||||
NameID 3 Unique font identifier: "3.000;GOOG;OpenSans-Regular" --> \
|
||||
"3.000;GOOG;OpenSans-Condensed"
|
||||
NameID 4 Full font name: "Open Sans Regular" --> "Open Sans Condensed"
|
||||
NameID 6 PostScript name: "OpenSans-Regular" --> "OpenSans-Condensed"
|
||||
NameID 16 Typographic Family name: None --> "Open Sans"
|
||||
NameID 17 Typographic Subfamily name: None --> "Condensed"
|
||||
|
||||
References:
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/stat
|
||||
https://docs.microsoft.com/en-us/typography/opentype/spec/name#name-ids
|
||||
"""
|
||||
from . import AxisRange, axisValuesFromAxisLimits
|
||||
|
||||
if "STAT" not in varfont:
|
||||
raise ValueError("Cannot update name table since there is no STAT table.")
|
||||
stat = varfont["STAT"].table
|
||||
if not stat.AxisValueArray:
|
||||
raise ValueError("Cannot update name table since there are no STAT Axis Values")
|
||||
fvar = varfont["fvar"]
|
||||
|
||||
# The updated name table will reflect the new 'zero origin' of the font.
|
||||
# If we're instantiating a partial font, we will populate the unpinned
|
||||
# axes with their default axis values.
|
||||
fvarDefaults = {a.axisTag: a.defaultValue for a in fvar.axes}
|
||||
defaultAxisCoords = deepcopy(axisLimits)
|
||||
for axisTag, val in fvarDefaults.items():
|
||||
if axisTag not in defaultAxisCoords or isinstance(
|
||||
defaultAxisCoords[axisTag], AxisRange
|
||||
):
|
||||
defaultAxisCoords[axisTag] = val
|
||||
|
||||
axisValueTables = axisValuesFromAxisLimits(stat, defaultAxisCoords)
|
||||
checkAxisValuesExist(stat, axisValueTables, defaultAxisCoords)
|
||||
|
||||
# ignore "elidable" axis values, should be omitted in application font menus.
|
||||
axisValueTables = [
|
||||
v for v in axisValueTables if not v.Flags & ELIDABLE_AXIS_VALUE_NAME
|
||||
]
|
||||
axisValueTables = _sortAxisValues(axisValueTables)
|
||||
_updateNameRecords(varfont, axisValueTables)
|
||||
|
||||
|
||||
def checkAxisValuesExist(stat, axisValues, axisCoords):
|
||||
seen = set()
|
||||
designAxes = stat.DesignAxisRecord.Axis
|
||||
for axisValueTable in axisValues:
|
||||
axisValueFormat = axisValueTable.Format
|
||||
if axisValueTable.Format in (1, 2, 3):
|
||||
axisTag = designAxes[axisValueTable.AxisIndex].AxisTag
|
||||
if axisValueFormat == 2:
|
||||
axisValue = axisValueTable.NominalValue
|
||||
else:
|
||||
axisValue = axisValueTable.Value
|
||||
if axisTag in axisCoords and axisValue == axisCoords[axisTag]:
|
||||
seen.add(axisTag)
|
||||
elif axisValueTable.Format == 4:
|
||||
for rec in axisValueTable.AxisValueRecord:
|
||||
axisTag = designAxes[rec.AxisIndex].AxisTag
|
||||
if axisTag in axisCoords and rec.Value == axisCoords[axisTag]:
|
||||
seen.add(axisTag)
|
||||
|
||||
missingAxes = set(axisCoords) - seen
|
||||
if missingAxes:
|
||||
missing = ", ".join(f"'{i}={axisCoords[i]}'" for i in missingAxes)
|
||||
raise ValueError(f"Cannot find Axis Values [{missing}]")
|
||||
|
||||
|
||||
def _sortAxisValues(axisValues):
|
||||
# Sort by axis index, remove duplicates and ensure that format 4 AxisValues
|
||||
# are dominant.
|
||||
# The MS Spec states: "if a format 1, format 2 or format 3 table has a
|
||||
# (nominal) value used in a format 4 table that also has values for
|
||||
# other axes, the format 4 table, being the more specific match, is used",
|
||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/stat#axis-value-table-format-4
|
||||
results = []
|
||||
seenAxes = set()
|
||||
# Sort format 4 axes so the tables with the most AxisValueRecords are first
|
||||
format4 = sorted(
|
||||
[v for v in axisValues if v.Format == 4],
|
||||
key=lambda v: len(v.AxisValueRecord),
|
||||
reverse=True,
|
||||
)
|
||||
|
||||
for val in format4:
|
||||
axisIndexes = set(r.AxisIndex for r in val.AxisValueRecord)
|
||||
minIndex = min(axisIndexes)
|
||||
if not seenAxes & axisIndexes:
|
||||
seenAxes |= axisIndexes
|
||||
results.append((minIndex, val))
|
||||
|
||||
for val in axisValues:
|
||||
if val in format4:
|
||||
continue
|
||||
axisIndex = val.AxisIndex
|
||||
if axisIndex not in seenAxes:
|
||||
seenAxes.add(axisIndex)
|
||||
results.append((axisIndex, val))
|
||||
|
||||
return [axisValue for _, axisValue in sorted(results)]
|
||||
|
||||
|
||||
def _updateNameRecords(varfont, axisValues):
|
||||
# Update nametable based on the axisValues using the R/I/B/BI model.
|
||||
nametable = varfont["name"]
|
||||
stat = varfont["STAT"].table
|
||||
|
||||
axisValueNameIDs = [a.ValueNameID for a in axisValues]
|
||||
ribbiNameIDs = [n for n in axisValueNameIDs if _isRibbi(nametable, n)]
|
||||
nonRibbiNameIDs = [n for n in axisValueNameIDs if n not in ribbiNameIDs]
|
||||
elidedNameID = stat.ElidedFallbackNameID
|
||||
elidedNameIsRibbi = _isRibbi(nametable, elidedNameID)
|
||||
|
||||
getName = nametable.getName
|
||||
platforms = set((r.platformID, r.platEncID, r.langID) for r in nametable.names)
|
||||
for platform in platforms:
|
||||
if not all(getName(i, *platform) for i in (1, 2, elidedNameID)):
|
||||
# Since no family name and subfamily name records were found,
|
||||
# we cannot update this set of name Records.
|
||||
continue
|
||||
|
||||
subFamilyName = " ".join(
|
||||
getName(n, *platform).toUnicode() for n in ribbiNameIDs
|
||||
)
|
||||
if nonRibbiNameIDs:
|
||||
typoSubFamilyName = " ".join(
|
||||
getName(n, *platform).toUnicode() for n in axisValueNameIDs
|
||||
)
|
||||
else:
|
||||
typoSubFamilyName = None
|
||||
|
||||
# If neither subFamilyName and typographic SubFamilyName exist,
|
||||
# we will use the STAT's elidedFallbackName
|
||||
if not typoSubFamilyName and not subFamilyName:
|
||||
if elidedNameIsRibbi:
|
||||
subFamilyName = getName(elidedNameID, *platform).toUnicode()
|
||||
else:
|
||||
typoSubFamilyName = getName(elidedNameID, *platform).toUnicode()
|
||||
|
||||
familyNameSuffix = " ".join(
|
||||
getName(n, *platform).toUnicode() for n in nonRibbiNameIDs
|
||||
)
|
||||
|
||||
_updateNameTableStyleRecords(
|
||||
varfont,
|
||||
familyNameSuffix,
|
||||
subFamilyName,
|
||||
typoSubFamilyName,
|
||||
*platform,
|
||||
)
|
||||
|
||||
|
||||
def _isRibbi(nametable, nameID):
|
||||
englishRecord = nametable.getName(nameID, 3, 1, 0x409)
|
||||
return (
|
||||
True
|
||||
if englishRecord is not None
|
||||
and englishRecord.toUnicode() in ("Regular", "Italic", "Bold", "Bold Italic")
|
||||
else False
|
||||
)
|
||||
|
||||
|
||||
def _updateNameTableStyleRecords(
|
||||
varfont,
|
||||
familyNameSuffix,
|
||||
subFamilyName,
|
||||
typoSubFamilyName,
|
||||
platformID=3,
|
||||
platEncID=1,
|
||||
langID=0x409,
|
||||
):
|
||||
# TODO (Marc F) It may be nice to make this part a standalone
|
||||
# font renamer in the future.
|
||||
nametable = varfont["name"]
|
||||
platform = (platformID, platEncID, langID)
|
||||
|
||||
currentFamilyName = nametable.getName(
|
||||
NameID.TYPOGRAPHIC_FAMILY_NAME, *platform
|
||||
) or nametable.getName(NameID.FAMILY_NAME, *platform)
|
||||
|
||||
currentStyleName = nametable.getName(
|
||||
NameID.TYPOGRAPHIC_SUBFAMILY_NAME, *platform
|
||||
) or nametable.getName(NameID.SUBFAMILY_NAME, *platform)
|
||||
|
||||
if not all([currentFamilyName, currentStyleName]):
|
||||
raise ValueError(f"Missing required NameIDs 1 and 2 for platform {platform}")
|
||||
|
||||
currentFamilyName = currentFamilyName.toUnicode()
|
||||
currentStyleName = currentStyleName.toUnicode()
|
||||
|
||||
nameIDs = {
|
||||
NameID.FAMILY_NAME: currentFamilyName,
|
||||
NameID.SUBFAMILY_NAME: subFamilyName or "Regular",
|
||||
}
|
||||
if typoSubFamilyName:
|
||||
nameIDs[NameID.FAMILY_NAME] = f"{currentFamilyName} {familyNameSuffix}".strip()
|
||||
nameIDs[NameID.TYPOGRAPHIC_FAMILY_NAME] = currentFamilyName
|
||||
nameIDs[NameID.TYPOGRAPHIC_SUBFAMILY_NAME] = typoSubFamilyName
|
||||
else:
|
||||
# Remove previous Typographic Family and SubFamily names since they're
|
||||
# no longer required
|
||||
for nameID in (
|
||||
NameID.TYPOGRAPHIC_FAMILY_NAME,
|
||||
NameID.TYPOGRAPHIC_SUBFAMILY_NAME,
|
||||
):
|
||||
nametable.removeNames(nameID=nameID)
|
||||
|
||||
newFamilyName = (
|
||||
nameIDs.get(NameID.TYPOGRAPHIC_FAMILY_NAME) or nameIDs[NameID.FAMILY_NAME]
|
||||
)
|
||||
newStyleName = (
|
||||
nameIDs.get(NameID.TYPOGRAPHIC_SUBFAMILY_NAME) or nameIDs[NameID.SUBFAMILY_NAME]
|
||||
)
|
||||
|
||||
nameIDs[NameID.FULL_FONT_NAME] = f"{newFamilyName} {newStyleName}"
|
||||
nameIDs[NameID.POSTSCRIPT_NAME] = _updatePSNameRecord(
|
||||
varfont, newFamilyName, newStyleName, platform
|
||||
)
|
||||
|
||||
uniqueID = _updateUniqueIdNameRecord(varfont, nameIDs, platform)
|
||||
if uniqueID:
|
||||
nameIDs[NameID.UNIQUE_FONT_IDENTIFIER] = uniqueID
|
||||
|
||||
for nameID, string in nameIDs.items():
|
||||
assert string, nameID
|
||||
nametable.setName(string, nameID, *platform)
|
||||
|
||||
if "fvar" not in varfont:
|
||||
nametable.removeNames(NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX)
|
||||
|
||||
|
||||
def _updatePSNameRecord(varfont, familyName, styleName, platform):
|
||||
# Implementation based on Adobe Technical Note #5902 :
|
||||
# https://wwwimages2.adobe.com/content/dam/acom/en/devnet/font/pdfs/5902.AdobePSNameGeneration.pdf
|
||||
nametable = varfont["name"]
|
||||
|
||||
family_prefix = nametable.getName(
|
||||
NameID.VARIATIONS_POSTSCRIPT_NAME_PREFIX, *platform
|
||||
)
|
||||
if family_prefix:
|
||||
family_prefix = family_prefix.toUnicode()
|
||||
else:
|
||||
family_prefix = familyName
|
||||
|
||||
psName = f"{family_prefix}-{styleName}"
|
||||
# Remove any characters other than uppercase Latin letters, lowercase
|
||||
# Latin letters, digits and hyphens.
|
||||
psName = re.sub(r"[^A-Za-z0-9-]", r"", psName)
|
||||
|
||||
if len(psName) > 127:
|
||||
# Abbreviating the stylename so it fits within 127 characters whilst
|
||||
# conforming to every vendor's specification is too complex. Instead
|
||||
# we simply truncate the psname and add the required "..."
|
||||
return f"{psName[:124]}..."
|
||||
return psName
|
||||
|
||||
|
||||
def _updateUniqueIdNameRecord(varfont, nameIDs, platform):
|
||||
nametable = varfont["name"]
|
||||
currentRecord = nametable.getName(NameID.UNIQUE_FONT_IDENTIFIER, *platform)
|
||||
if not currentRecord:
|
||||
return None
|
||||
|
||||
# Check if full name and postscript name are a substring of currentRecord
|
||||
for nameID in (NameID.FULL_FONT_NAME, NameID.POSTSCRIPT_NAME):
|
||||
nameRecord = nametable.getName(nameID, *platform)
|
||||
if not nameRecord:
|
||||
continue
|
||||
if nameRecord.toUnicode() in currentRecord.toUnicode():
|
||||
return currentRecord.toUnicode().replace(
|
||||
nameRecord.toUnicode(), nameIDs[nameRecord.nameID]
|
||||
)
|
||||
|
||||
# Create a new string since we couldn't find any substrings.
|
||||
fontVersion = _fontVersion(varfont, platform)
|
||||
achVendID = varfont["OS/2"].achVendID
|
||||
# Remove non-ASCII characers and trailing spaces
|
||||
vendor = re.sub(r"[^\x00-\x7F]", "", achVendID).strip()
|
||||
psName = nameIDs[NameID.POSTSCRIPT_NAME]
|
||||
return f"{fontVersion};{vendor};{psName}"
|
||||
|
||||
|
||||
def _fontVersion(font, platform=(3, 1, 0x409)):
|
||||
nameRecord = font["name"].getName(NameID.VERSION_STRING, *platform)
|
||||
if nameRecord is None:
|
||||
return f'{font["head"].fontRevision:.3f}'
|
||||
# "Version 1.101; ttfautohint (v1.8.1.43-b0c9)" --> "1.101"
|
||||
# Also works fine with inputs "Version 1.101" or "1.101" etc
|
||||
versionNumber = nameRecord.toUnicode().split(";")[0]
|
||||
return versionNumber.lstrip("Version ").strip()
|
@ -138,7 +138,7 @@ def interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc):
|
||||
lsb_delta = 0
|
||||
else:
|
||||
lsb = boundsPen.bounds[0]
|
||||
lsb_delta = entry[1] - lsb
|
||||
lsb_delta = entry[1] - lsb
|
||||
|
||||
if lsb_delta or width_delta:
|
||||
if width_delta:
|
||||
|
@ -11,6 +11,7 @@ include Lib/fontTools/ttLib/tables/table_API_readme.txt
|
||||
|
||||
include *requirements.txt
|
||||
include tox.ini
|
||||
include mypy.ini
|
||||
include run-tests.sh
|
||||
|
||||
recursive-include Lib/fontTools py.typed
|
||||
@ -39,3 +40,5 @@ recursive-include Tests *.txt README
|
||||
recursive-include Tests *.lwfn *.pfa *.pfb
|
||||
recursive-include Tests *.xml *.designspace *.bin
|
||||
recursive-include Tests *.afm
|
||||
recursive-include Tests *.json
|
||||
recursive-include Tests *.ufoz
|
||||
|
97
NEWS.rst
97
NEWS.rst
@ -1,3 +1,100 @@
|
||||
4.21.1 (released 2021-02-26)
|
||||
----------------------------
|
||||
|
||||
- [pens] Reverted breaking change that turned ``AbstractPen`` and ``AbstractPointPen``
|
||||
into abstract base classes (#2164, #2198).
|
||||
|
||||
4.21.0 (released 2021-02-26)
|
||||
----------------------------
|
||||
|
||||
- [feaLib] Indent anchor statements in ``asFea()`` to make them more legible and
|
||||
diff-able (#2193).
|
||||
- [pens] Turn ``AbstractPen`` and ``AbstractPointPen`` into abstract base classes
|
||||
(#2164).
|
||||
- [feaLib] Added support for parsing and building ``STAT`` table from AFDKO feature
|
||||
files (#2039).
|
||||
- [instancer] Added option to update name table of generated instance using ``STAT``
|
||||
table's axis values (#2189).
|
||||
- [bezierTools] Added functions to compute bezier point-at-time, as well as line-line,
|
||||
curve-line and curve-curve intersections (#2192).
|
||||
|
||||
4.20.0 (released 2021-02-15)
|
||||
----------------------------
|
||||
|
||||
- [COLRv1] Added ``unbuildColrV1`` to deconstruct COLRv1 otTables to raw json-able
|
||||
data structure; it does the reverse of ``buildColrV1`` (#2171).
|
||||
- [feaLib] Allow ``sub X by NULL`` sequence to delete a glyph (#2170).
|
||||
- [arrayTools] Fixed ``Vector`` division (#2173).
|
||||
- [COLRv1] Define new ``PaintSweepGradient`` (#2172).
|
||||
- [otTables] Moved ``Paint.Format`` enum class outside of ``Paint`` class definition,
|
||||
now named ``PaintFormat``. It was clashing with paint instance ``Format`` attribute
|
||||
and thus was breaking lazy load of COLR table which relies on magic ``__getattr__``
|
||||
(#2175).
|
||||
- [COLRv1] Replace hand-coded builder functions with otData-driven dynamic
|
||||
implementation (#2181).
|
||||
- [COLRv1] Define additional static (non-variable) Paint formats (#2181).
|
||||
- [subset] Added support for subsetting COLR v1 and CPAL tables (#2174, #2177).
|
||||
- [fontBuilder] Allow ``setupFvar`` to optionally take ``designspaceLib.AxisDescriptor``
|
||||
objects. Added new ``setupAvar`` method. Support localised names for axes and
|
||||
named instances (#2185).
|
||||
|
||||
4.19.1 (released 2021-01-28)
|
||||
----------------------------
|
||||
|
||||
- [woff2] An initial off-curve point with an overlap flag now stays an off-curve
|
||||
point after compression.
|
||||
|
||||
4.19.0 (released 2021-01-25)
|
||||
----------------------------
|
||||
|
||||
- [codecs] Handle ``errors`` parameter different from 'strict' for the custom
|
||||
extended mac encodings (#2137, #2132).
|
||||
- [featureVars] Raise better error message when a script is missing the required
|
||||
default language system (#2154).
|
||||
- [COLRv1] Avoid abrupt change caused by rounding ``PaintRadialGradient.c0`` when
|
||||
the start circle almost touches the end circle's perimeter (#2148).
|
||||
- [COLRv1] Support building unlimited lists of paints as 255-ary trees of
|
||||
``PaintColrLayers`` tables (#2153).
|
||||
- [subset] Prune redundant format-12 cmap subtables when all non-BMP characters
|
||||
are dropped (#2146).
|
||||
- [basePen] Raise ``MissingComponentError`` instead of bare ``KeyError`` when a
|
||||
referenced component is missing (#2145).
|
||||
|
||||
4.18.2 (released 2020-12-16)
|
||||
----------------------------
|
||||
|
||||
- [COLRv1] Implemented ``PaintTranslate`` paint format (#2129).
|
||||
- [varLib.cff] Fixed unbound local variable error (#1787).
|
||||
- [otlLib] Don't crash when creating OpenType class definitions if some glyphs
|
||||
occur more than once (#2125).
|
||||
|
||||
4.18.1 (released 2020-12-09)
|
||||
----------------------------
|
||||
|
||||
- [colorLib] Speed optimization for ``LayerV1ListBuilder`` (#2119).
|
||||
- [mutator] Fixed missing tab in ``interpolate_cff2_metrics`` (0957dc7a).
|
||||
|
||||
4.18.0 (released 2020-12-04)
|
||||
----------------------------
|
||||
|
||||
- [COLRv1] Update to latest draft: added ``PaintRotate`` and ``PaintSkew`` (#2118).
|
||||
- [woff2] Support new ``brotlicffi`` bindings for PyPy (#2117).
|
||||
- [glifLib] Added ``expectContentsFile`` parameter to ``GlyphSet``, for use when
|
||||
reading existing UFOs, to comply with the specification stating that a
|
||||
``contents.plist`` file must exist in a glyph set (#2114).
|
||||
- [subset] Allow ``LangSys`` tags in ``--layout-scripts`` option (#2112). For example:
|
||||
``--layout-scripts=arab.dflt,arab.URD,latn``; this will keep ``DefaultLangSys``
|
||||
and ``URD`` language for ``arab`` script, and all languages for ``latn`` script.
|
||||
- [varLib.interpolatable] Allow UFOs to be checked; report open paths, non existant
|
||||
glyphs; add a ``--json`` option to produce a machine-readable list of
|
||||
incompatibilities
|
||||
- [pens] Added ``QuartzPen`` to create ``CGPath`` from glyph outlines on macOS.
|
||||
Requires pyobjc (#2107).
|
||||
- [feaLib] You can export ``FONTTOOLS_LOOKUP_DEBUGGING=1`` to enable feature file
|
||||
debugging info stored in ``Debg`` table (#2106).
|
||||
- [otlLib] Build more efficient format 1 and format 2 contextual lookups whenever
|
||||
possible (#2101).
|
||||
|
||||
4.17.1 (released 2020-11-16)
|
||||
----------------------------
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
|Travis Build Status| |Appveyor Build status| |Coverage Status| |PyPI| |Gitter Chat|
|
||||
|CI Build Status| |Coverage Status| |PyPI| |Gitter Chat|
|
||||
|
||||
What is this?
|
||||
~~~~~~~~~~~~~
|
||||
@ -240,10 +240,8 @@ Rights Reserved.
|
||||
|
||||
Have fun!
|
||||
|
||||
.. |Travis Build Status| image:: https://travis-ci.org/fonttools/fonttools.svg
|
||||
:target: https://travis-ci.org/fonttools/fonttools
|
||||
.. |Appveyor Build status| image:: https://ci.appveyor.com/api/projects/status/0f7fmee9as744sl7/branch/master?svg=true
|
||||
:target: https://ci.appveyor.com/project/fonttools/fonttools/branch/master
|
||||
.. |CI Build Status| image:: https://github.com/fonttools/fonttools/workflows/Test/badge.svg
|
||||
:target: https://github.com/fonttools/fonttools/actions?query=workflow%3ATest
|
||||
.. |Coverage Status| image:: https://codecov.io/gh/fonttools/fonttools/branch/master/graph/badge.svg
|
||||
:target: https://codecov.io/gh/fonttools/fonttools
|
||||
.. |PyPI| image:: https://img.shields.io/pypi/v/fonttools.svg
|
||||
|
File diff suppressed because it is too large
Load Diff
15
Tests/colorLib/table_builder_test.py
Normal file
15
Tests/colorLib/table_builder_test.py
Normal file
@ -0,0 +1,15 @@
|
||||
from fontTools.ttLib.tables import otTables # trigger setup to occur
|
||||
from fontTools.ttLib.tables.otConverters import UShort
|
||||
from fontTools.colorLib.table_builder import TableBuilder
|
||||
import pytest
|
||||
|
||||
|
||||
class WriteMe:
|
||||
value = None
|
||||
|
||||
|
||||
def test_intValue_otRound():
|
||||
dest = WriteMe()
|
||||
converter = UShort("value", None, None)
|
||||
TableBuilder()._convert(dest, "value", converter, 85.6)
|
||||
assert dest.value == 86, "Should have used otRound"
|
210
Tests/colorLib/unbuilder_test.py
Normal file
210
Tests/colorLib/unbuilder_test.py
Normal file
@ -0,0 +1,210 @@
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.colorLib.builder import buildColrV1
|
||||
from fontTools.colorLib.unbuilder import unbuildColrV1
|
||||
import pytest
|
||||
|
||||
|
||||
TEST_COLOR_GLYPHS = {
|
||||
"glyph00010": {
|
||||
"Format": int(ot.PaintFormat.PaintColrLayers),
|
||||
"Layers": [
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintSolid),
|
||||
"Color": {"PaletteIndex": 2, "Alpha": 0.5},
|
||||
},
|
||||
"Glyph": "glyph00011",
|
||||
},
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintVarLinearGradient),
|
||||
"ColorLine": {
|
||||
"Extend": "repeat",
|
||||
"ColorStop": [
|
||||
{
|
||||
"StopOffset": (0.0, 0),
|
||||
"Color": {"PaletteIndex": 3, "Alpha": (1.0, 0)},
|
||||
},
|
||||
{
|
||||
"StopOffset": (0.5, 0),
|
||||
"Color": {"PaletteIndex": 4, "Alpha": (1.0, 0)},
|
||||
},
|
||||
{
|
||||
"StopOffset": (1.0, 0),
|
||||
"Color": {"PaletteIndex": 5, "Alpha": (1.0, 0)},
|
||||
},
|
||||
],
|
||||
},
|
||||
"x0": (1, 0),
|
||||
"y0": (2, 0),
|
||||
"x1": (-3, 0),
|
||||
"y1": (-4, 0),
|
||||
"x2": (5, 0),
|
||||
"y2": (6, 0),
|
||||
},
|
||||
"Glyph": "glyph00012",
|
||||
},
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintVarTransform),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintRadialGradient),
|
||||
"ColorLine": {
|
||||
"Extend": "pad",
|
||||
"ColorStop": [
|
||||
{
|
||||
"StopOffset": 0,
|
||||
"Color": {"PaletteIndex": 6, "Alpha": 1.0},
|
||||
},
|
||||
{
|
||||
"StopOffset": 1.0,
|
||||
"Color": {"PaletteIndex": 7, "Alpha": 0.4},
|
||||
},
|
||||
],
|
||||
},
|
||||
"x0": 7,
|
||||
"y0": 8,
|
||||
"r0": 9,
|
||||
"x1": 10,
|
||||
"y1": 11,
|
||||
"r1": 12,
|
||||
},
|
||||
"Transform": {
|
||||
"xx": (-13.0, 0),
|
||||
"yx": (14.0, 0),
|
||||
"xy": (15.0, 0),
|
||||
"yy": (-17.0, 0),
|
||||
"dx": (18.0, 0),
|
||||
"dy": (19.0, 0),
|
||||
},
|
||||
},
|
||||
"Glyph": "glyph00013",
|
||||
},
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintVarTranslate),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintRotate),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintVarSkew),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintSolid),
|
||||
"Color": {"PaletteIndex": 2, "Alpha": 0.5},
|
||||
},
|
||||
"Glyph": "glyph00011",
|
||||
},
|
||||
"xSkewAngle": (-11.0, 0),
|
||||
"ySkewAngle": (5.0, 0),
|
||||
"centerX": (253.0, 0),
|
||||
"centerY": (254.0, 0),
|
||||
},
|
||||
"angle": 45.0,
|
||||
"centerX": 255.0,
|
||||
"centerY": 256.0,
|
||||
},
|
||||
"dx": (257.0, 0),
|
||||
"dy": (258.0, 0),
|
||||
},
|
||||
],
|
||||
},
|
||||
"glyph00014": {
|
||||
"Format": int(ot.PaintFormat.PaintComposite),
|
||||
"SourcePaint": {
|
||||
"Format": int(ot.PaintFormat.PaintColrGlyph),
|
||||
"Glyph": "glyph00010",
|
||||
},
|
||||
"CompositeMode": "src_over",
|
||||
"BackdropPaint": {
|
||||
"Format": int(ot.PaintFormat.PaintTransform),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintColrGlyph),
|
||||
"Glyph": "glyph00010",
|
||||
},
|
||||
"Transform": {
|
||||
"xx": 1.0,
|
||||
"yx": 0.0,
|
||||
"xy": 0.0,
|
||||
"yy": 1.0,
|
||||
"dx": 300.0,
|
||||
"dy": 0.0,
|
||||
},
|
||||
},
|
||||
},
|
||||
"glyph00015": {
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintSweepGradient),
|
||||
"ColorLine": {
|
||||
"Extend": "pad",
|
||||
"ColorStop": [
|
||||
{
|
||||
"StopOffset": 0.0,
|
||||
"Color": {"PaletteIndex": 3, "Alpha": 1.0},
|
||||
},
|
||||
{
|
||||
"StopOffset": 1.0,
|
||||
"Color": {"PaletteIndex": 5, "Alpha": 1.0},
|
||||
},
|
||||
],
|
||||
},
|
||||
"centerX": 259,
|
||||
"centerY": 300,
|
||||
"startAngle": 45.0,
|
||||
"endAngle": 135.0,
|
||||
},
|
||||
"Glyph": "glyph00011",
|
||||
},
|
||||
"glyph00016": {
|
||||
"Format": int(ot.PaintFormat.PaintColrLayers),
|
||||
"Layers": [
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintVarSolid),
|
||||
"Color": {"PaletteIndex": 2, "Alpha": (0.5, 0)},
|
||||
},
|
||||
"Glyph": "glyph00011",
|
||||
},
|
||||
{
|
||||
"Format": int(ot.PaintFormat.PaintGlyph),
|
||||
"Paint": {
|
||||
"Format": int(ot.PaintFormat.PaintVarLinearGradient),
|
||||
"ColorLine": {
|
||||
"Extend": "repeat",
|
||||
"ColorStop": [
|
||||
{
|
||||
"StopOffset": (0.0, 0),
|
||||
"Color": {"PaletteIndex": 3, "Alpha": (1.0, 0)},
|
||||
},
|
||||
{
|
||||
"StopOffset": (0.5, 0),
|
||||
"Color": {"PaletteIndex": 4, "Alpha": (1.0, 0)},
|
||||
},
|
||||
{
|
||||
"StopOffset": (1.0, 0),
|
||||
"Color": {"PaletteIndex": 5, "Alpha": (1.0, 0)},
|
||||
},
|
||||
],
|
||||
},
|
||||
"x0": (1, 0),
|
||||
"y0": (2, 0),
|
||||
"x1": (-3, 0),
|
||||
"y1": (-4, 0),
|
||||
"x2": (5, 0),
|
||||
"y2": (6, 0),
|
||||
},
|
||||
"Glyph": "glyph00012",
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def test_unbuildColrV1():
|
||||
layersV1, baseGlyphsV1 = buildColrV1(TEST_COLOR_GLYPHS)
|
||||
colorGlyphs = unbuildColrV1(layersV1, baseGlyphsV1)
|
||||
assert colorGlyphs == TEST_COLOR_GLYPHS
|
4
Tests/feaLib/STAT2.fea
Normal file
4
Tests/feaLib/STAT2.fea
Normal file
@ -0,0 +1,4 @@
|
||||
table STAT {
|
||||
ElidedFallbackName { name "Roman"; };
|
||||
DesignAxis zonk 0 { name "Zonkey"; };'
|
||||
} STAT;
|
@ -9,6 +9,7 @@ from fontTools.feaLib import ast
|
||||
from fontTools.feaLib.lexer import Lexer
|
||||
import difflib
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import tempfile
|
||||
@ -73,7 +74,7 @@ class BuilderTest(unittest.TestCase):
|
||||
LigatureSubtable AlternateSubtable MultipleSubstSubtable
|
||||
SingleSubstSubtable aalt_chain_contextual_subst AlternateChained
|
||||
MultipleLookupsPerGlyph MultipleLookupsPerGlyph2 GSUB_6_formats
|
||||
GSUB_5_formats
|
||||
GSUB_5_formats delete_glyph STAT_test STAT_test_elidedFallbackNameID
|
||||
""".split()
|
||||
|
||||
def __init__(self, methodName):
|
||||
@ -118,7 +119,7 @@ class BuilderTest(unittest.TestCase):
|
||||
def expect_ttx(self, font, expected_ttx, replace=None):
|
||||
path = self.temp_path(suffix=".ttx")
|
||||
font.saveXML(path, tables=['head', 'name', 'BASE', 'GDEF', 'GSUB',
|
||||
'GPOS', 'OS/2', 'hhea', 'vhea'])
|
||||
'GPOS', 'OS/2', 'STAT', 'hhea', 'vhea'])
|
||||
actual = self.read_ttx(path)
|
||||
expected = self.read_ttx(expected_ttx)
|
||||
if replace:
|
||||
@ -463,6 +464,201 @@ class BuilderTest(unittest.TestCase):
|
||||
"} test;"
|
||||
)
|
||||
|
||||
def test_STAT_elidedfallbackname_already_defined(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'ElidedFallbackName is already set.',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' ElidedFallbackNameID 256;'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_elidedfallbackname_set_twice(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'ElidedFallbackName is already set.',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' ElidedFallbackName { name "Italic"; };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_elidedfallbacknameID_already_defined(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'ElidedFallbackNameID is already set.',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackNameID 256;'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_elidedfallbacknameID_not_in_name_table(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'ElidedFallbackNameID 256 points to a nameID that does not '
|
||||
'exist in the "name" table',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 257 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackNameID 256;'
|
||||
' DesignAxis opsz 1 { name "Optical Size"; };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_design_axis_name(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'Expected "name"',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' DesignAxis opsz 0 { badtag "Optical Size"; };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_duplicate_design_axis_name(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'DesignAxis already defined for tag "opsz".',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' DesignAxis opsz 0 { name "Optical Size"; };'
|
||||
' DesignAxis opsz 1 { name "Optical Size"; };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_design_axis_duplicate_order(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
"DesignAxis already defined for axis number 0.",
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' DesignAxis opsz 0 { name "Optical Size"; };'
|
||||
' DesignAxis wdth 0 { name "Width"; };'
|
||||
' AxisValue {'
|
||||
' location opsz 8;'
|
||||
' location wdth 400;'
|
||||
' name "Caption";'
|
||||
' };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_undefined_tag(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'DesignAxis not defined for wdth.',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' DesignAxis opsz 0 { name "Optical Size"; };'
|
||||
' AxisValue { '
|
||||
' location wdth 125; '
|
||||
' name "Wide"; '
|
||||
' };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_axis_value_format4(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'Axis tag wdth already defined.',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' DesignAxis opsz 0 { name "Optical Size"; };'
|
||||
' DesignAxis wdth 1 { name "Width"; };'
|
||||
' DesignAxis wght 2 { name "Weight"; };'
|
||||
' AxisValue { '
|
||||
' location opsz 8; '
|
||||
' location wdth 125; '
|
||||
' location wdth 125; '
|
||||
' location wght 500; '
|
||||
' name "Caption Medium Wide"; '
|
||||
' };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_duplicate_axis_value_record(self):
|
||||
# Test for Duplicate AxisValueRecords even when the definition order
|
||||
# is different.
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'An AxisValueRecord with these values is already defined.',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; };'
|
||||
' DesignAxis opsz 0 { name "Optical Size"; };'
|
||||
' DesignAxis wdth 1 { name "Width"; };'
|
||||
' AxisValue {'
|
||||
' location opsz 8;'
|
||||
' location wdth 400;'
|
||||
' name "Caption";'
|
||||
' };'
|
||||
' AxisValue {'
|
||||
' location wdth 400;'
|
||||
' location opsz 8;'
|
||||
' name "Caption";'
|
||||
' };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_axis_value_missing_location(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'Expected "Axis location"',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; '
|
||||
'};'
|
||||
' DesignAxis opsz 0 { name "Optical Size"; };'
|
||||
' AxisValue { '
|
||||
' name "Wide"; '
|
||||
' };'
|
||||
'} STAT;')
|
||||
|
||||
def test_STAT_invalid_location_tag(self):
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'Tags cannot be longer than 4 characters',
|
||||
self.build,
|
||||
'table name {'
|
||||
' nameid 256 "Roman"; '
|
||||
'} name;'
|
||||
'table STAT {'
|
||||
' ElidedFallbackName { name "Roman"; '
|
||||
' name 3 1 0x0411 "ローマン"; }; '
|
||||
' DesignAxis width 0 { name "Width"; };'
|
||||
'} STAT;')
|
||||
|
||||
def test_extensions(self):
|
||||
class ast_BaseClass(ast.MarkClass):
|
||||
def asFea(self, indent=""):
|
||||
|
@ -6,7 +6,11 @@ markClass [cedilla] <anchor 222 22> @BOTTOM_MARKS;
|
||||
markClass [ogonek] <anchor 333 33> @SIDE_MARKS;
|
||||
|
||||
feature test {
|
||||
pos base a <anchor 11 1> mark @TOP_MARKS <anchor 12 -1> mark @BOTTOM_MARKS;
|
||||
pos base [b c] <anchor 22 -2> mark @BOTTOM_MARKS;
|
||||
pos base d <anchor 33 3> mark @SIDE_MARKS;
|
||||
pos base a
|
||||
<anchor 11 1> mark @TOP_MARKS
|
||||
<anchor 12 -1> mark @BOTTOM_MARKS;
|
||||
pos base [b c]
|
||||
<anchor 22 -2> mark @BOTTOM_MARKS;
|
||||
pos base d
|
||||
<anchor 33 3> mark @SIDE_MARKS;
|
||||
} test;
|
||||
|
@ -5,14 +5,29 @@ markClass [ogonek] <anchor 800 -10> @OGONEK;
|
||||
|
||||
feature test {
|
||||
|
||||
pos ligature [c_t s_t] <anchor 500 800> mark @TOP_MARKS <anchor 500 -200> mark @BOTTOM_MARKS
|
||||
ligComponent <anchor 1500 800> mark @TOP_MARKS <anchor 1500 -200> mark @BOTTOM_MARKS <anchor 1550 0> mark @OGONEK;
|
||||
pos ligature [c_t s_t]
|
||||
<anchor 500 800> mark @TOP_MARKS
|
||||
<anchor 500 -200> mark @BOTTOM_MARKS
|
||||
ligComponent
|
||||
<anchor 1500 800> mark @TOP_MARKS
|
||||
<anchor 1500 -200> mark @BOTTOM_MARKS
|
||||
<anchor 1550 0> mark @OGONEK;
|
||||
|
||||
pos ligature f_l <anchor 300 800> mark @TOP_MARKS <anchor 300 -200> mark @BOTTOM_MARKS
|
||||
ligComponent <anchor 600 800> mark @TOP_MARKS <anchor 600 -200> mark @BOTTOM_MARKS;
|
||||
pos ligature f_l
|
||||
<anchor 300 800> mark @TOP_MARKS
|
||||
<anchor 300 -200> mark @BOTTOM_MARKS
|
||||
ligComponent
|
||||
<anchor 600 800> mark @TOP_MARKS
|
||||
<anchor 600 -200> mark @BOTTOM_MARKS;
|
||||
|
||||
pos ligature [f_f_l] <anchor 300 800> mark @TOP_MARKS <anchor 300 -200> mark @BOTTOM_MARKS
|
||||
ligComponent <anchor 600 800> mark @TOP_MARKS <anchor 600 -200> mark @BOTTOM_MARKS
|
||||
ligComponent <anchor 900 800> mark @TOP_MARKS <anchor 900 -200> mark @BOTTOM_MARKS;
|
||||
pos ligature [f_f_l]
|
||||
<anchor 300 800> mark @TOP_MARKS
|
||||
<anchor 300 -200> mark @BOTTOM_MARKS
|
||||
ligComponent
|
||||
<anchor 600 800> mark @TOP_MARKS
|
||||
<anchor 600 -200> mark @BOTTOM_MARKS
|
||||
ligComponent
|
||||
<anchor 900 800> mark @TOP_MARKS
|
||||
<anchor 900 -200> mark @BOTTOM_MARKS;
|
||||
|
||||
} test;
|
||||
|
@ -5,6 +5,9 @@ markClass macron <anchor 2 2 contourpoint 22> @TOP_MARKS;
|
||||
markClass [cedilla] <anchor 3 3 contourpoint 33> @BOTTOM_MARKS;
|
||||
|
||||
feature test {
|
||||
pos mark [acute grave macron ogonek] <anchor 500 200> mark @TOP_MARKS <anchor 500 -80> mark @BOTTOM_MARKS;
|
||||
pos mark [dieresis caron] <anchor 500 200> mark @TOP_MARKS;
|
||||
pos mark [acute grave macron ogonek]
|
||||
<anchor 500 200> mark @TOP_MARKS
|
||||
<anchor 500 -80> mark @BOTTOM_MARKS;
|
||||
pos mark [dieresis caron]
|
||||
<anchor 500 200> mark @TOP_MARKS;
|
||||
} test;
|
||||
|
96
Tests/feaLib/data/STAT_bad.fea
Normal file
96
Tests/feaLib/data/STAT_bad.fea
Normal file
@ -0,0 +1,96 @@
|
||||
# bad fea file: Testing DesignAxis tag with incorrect label
|
||||
table name {
|
||||
nameid 25 "TestFont";
|
||||
} name;
|
||||
|
||||
|
||||
table STAT {
|
||||
|
||||
ElidedFallbackName { name "Roman"; };
|
||||
|
||||
DesignAxis opsz 0 { badtag "Optical Size"; }; #'badtag' instead of 'name' is incorrect
|
||||
DesignAxis wdth 1 { name "Width"; };
|
||||
DesignAxis wght 2 { name "Weight"; };
|
||||
DesignAxis ital 3 { name "Italic"; };
|
||||
|
||||
AxisValue {
|
||||
location opsz 8 5 9;
|
||||
location wdth 300 350 450;
|
||||
name "Caption";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location opsz 11 9 12;
|
||||
name "Text";
|
||||
flag OlderSiblingFontAttribute ElidableAxisValueName ;
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location opsz 16.7 12 24;
|
||||
name "Subhead";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location opsz 72 24 72;
|
||||
name "Display";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 80 80 89;
|
||||
name "Condensed";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 90 90 96;
|
||||
name "Semicondensed";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 100 97 101;
|
||||
name "Normal";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 125 102 125;
|
||||
name "Extended";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 300 300 349;
|
||||
name "Light";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 400 350 449;
|
||||
name "Regular";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 500 450 549;
|
||||
name "Medium";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 600 550 649;
|
||||
name "Semibold";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 700 650 749;
|
||||
name "Bold";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 900 750 900;
|
||||
name "Black";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location ital 0;
|
||||
name "Roman";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
|
||||
} STAT;
|
109
Tests/feaLib/data/STAT_test.fea
Normal file
109
Tests/feaLib/data/STAT_test.fea
Normal file
@ -0,0 +1,109 @@
|
||||
table name {
|
||||
nameid 25 "TestFont";
|
||||
} name;
|
||||
|
||||
|
||||
table STAT {
|
||||
|
||||
ElidedFallbackName {
|
||||
name "Roman";
|
||||
name 3 1 1041 "ローマン";
|
||||
};
|
||||
|
||||
DesignAxis opsz 0 {
|
||||
name "Optical Size";
|
||||
};
|
||||
|
||||
DesignAxis wdth 1 {
|
||||
name "Width";
|
||||
};
|
||||
|
||||
DesignAxis wght 2 {
|
||||
name "Weight";
|
||||
};
|
||||
|
||||
DesignAxis ital 3 {
|
||||
name "Italic";
|
||||
}; # here comment
|
||||
|
||||
AxisValue {
|
||||
location opsz 8; # comment here
|
||||
location wdth 400; # another comment
|
||||
name "Caption"; # more comments
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location opsz 11 9 12;
|
||||
name "Text";
|
||||
flag OlderSiblingFontAttribute ElidableAxisValueName;
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location opsz 16.7 12 24;
|
||||
name "Subhead";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location opsz 72 24 72;
|
||||
name "Display";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 80 80 89;
|
||||
name "Condensed";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 90 90 96;
|
||||
name "Semicondensed";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 100 97 101;
|
||||
name "Normal";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wdth 125 102 125;
|
||||
name "Extended";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 300 300 349;
|
||||
name "Light";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 400 350 449;
|
||||
name "Regular";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 500 450 549;
|
||||
name "Medium";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 600 550 649;
|
||||
name "Semibold";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 700 650 749;
|
||||
name "Bold";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location wght 900 750 900;
|
||||
name "Black";
|
||||
};
|
||||
|
||||
AxisValue {
|
||||
location ital 0;
|
||||
name "Roman";
|
||||
flag ElidableAxisValueName; # flag comment
|
||||
};
|
||||
|
||||
} STAT;
|
228
Tests/feaLib/data/STAT_test.ttx
Normal file
228
Tests/feaLib/data/STAT_test.ttx
Normal file
@ -0,0 +1,228 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.20">
|
||||
|
||||
<name>
|
||||
<namerecord nameID="25" platformID="3" platEncID="1" langID="0x409">
|
||||
TestFont
|
||||
</namerecord>
|
||||
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
|
||||
Roman
|
||||
</namerecord>
|
||||
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x411">
|
||||
ローマン
|
||||
</namerecord>
|
||||
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
|
||||
Optical Size
|
||||
</namerecord>
|
||||
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
|
||||
Text
|
||||
</namerecord>
|
||||
<namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
|
||||
Subhead
|
||||
</namerecord>
|
||||
<namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
|
||||
Display
|
||||
</namerecord>
|
||||
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
|
||||
Width
|
||||
</namerecord>
|
||||
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
|
||||
Condensed
|
||||
</namerecord>
|
||||
<namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
|
||||
Semicondensed
|
||||
</namerecord>
|
||||
<namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
|
||||
Normal
|
||||
</namerecord>
|
||||
<namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
|
||||
Extended
|
||||
</namerecord>
|
||||
<namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
|
||||
Weight
|
||||
</namerecord>
|
||||
<namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
|
||||
Light
|
||||
</namerecord>
|
||||
<namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
|
||||
Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
|
||||
Medium
|
||||
</namerecord>
|
||||
<namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
|
||||
Semibold
|
||||
</namerecord>
|
||||
<namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
|
||||
Bold
|
||||
</namerecord>
|
||||
<namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
|
||||
Black
|
||||
</namerecord>
|
||||
<namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
|
||||
Italic
|
||||
</namerecord>
|
||||
<namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
|
||||
Roman
|
||||
</namerecord>
|
||||
<namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
|
||||
Caption
|
||||
</namerecord>
|
||||
</name>
|
||||
|
||||
<STAT>
|
||||
<Version value="0x00010002"/>
|
||||
<DesignAxisRecordSize value="8"/>
|
||||
<!-- DesignAxisCount=4 -->
|
||||
<DesignAxisRecord>
|
||||
<Axis index="0">
|
||||
<AxisTag value="opsz"/>
|
||||
<AxisNameID value="257"/> <!-- Optical Size -->
|
||||
<AxisOrdering value="0"/>
|
||||
</Axis>
|
||||
<Axis index="1">
|
||||
<AxisTag value="wdth"/>
|
||||
<AxisNameID value="261"/> <!-- Width -->
|
||||
<AxisOrdering value="1"/>
|
||||
</Axis>
|
||||
<Axis index="2">
|
||||
<AxisTag value="wght"/>
|
||||
<AxisNameID value="266"/> <!-- Weight -->
|
||||
<AxisOrdering value="2"/>
|
||||
</Axis>
|
||||
<Axis index="3">
|
||||
<AxisTag value="ital"/>
|
||||
<AxisNameID value="273"/> <!-- Italic -->
|
||||
<AxisOrdering value="3"/>
|
||||
</Axis>
|
||||
</DesignAxisRecord>
|
||||
<!-- AxisValueCount=15 -->
|
||||
<AxisValueArray>
|
||||
<AxisValue index="0" Format="4">
|
||||
<!-- AxisCount=2 -->
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="275"/> <!-- Caption -->
|
||||
<AxisValueRecord index="0">
|
||||
<AxisIndex value="0"/>
|
||||
<Value value="8.0"/>
|
||||
</AxisValueRecord>
|
||||
<AxisValueRecord index="1">
|
||||
<AxisIndex value="1"/>
|
||||
<Value value="400.0"/>
|
||||
</AxisValueRecord>
|
||||
</AxisValue>
|
||||
<AxisValue index="1" Format="2">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="3"/> <!-- OlderSiblingFontAttribute ElidableAxisValueName -->
|
||||
<ValueNameID value="258"/> <!-- Text -->
|
||||
<NominalValue value="11.0"/>
|
||||
<RangeMinValue value="9.0"/>
|
||||
<RangeMaxValue value="12.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="2" Format="2">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="259"/> <!-- Subhead -->
|
||||
<NominalValue value="16.7"/>
|
||||
<RangeMinValue value="12.0"/>
|
||||
<RangeMaxValue value="24.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="3" Format="2">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="260"/> <!-- Display -->
|
||||
<NominalValue value="72.0"/>
|
||||
<RangeMinValue value="24.0"/>
|
||||
<RangeMaxValue value="72.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="4" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="262"/> <!-- Condensed -->
|
||||
<NominalValue value="80.0"/>
|
||||
<RangeMinValue value="80.0"/>
|
||||
<RangeMaxValue value="89.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="5" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="263"/> <!-- Semicondensed -->
|
||||
<NominalValue value="90.0"/>
|
||||
<RangeMinValue value="90.0"/>
|
||||
<RangeMaxValue value="96.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="6" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="264"/> <!-- Normal -->
|
||||
<NominalValue value="100.0"/>
|
||||
<RangeMinValue value="97.0"/>
|
||||
<RangeMaxValue value="101.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="7" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="265"/> <!-- Extended -->
|
||||
<NominalValue value="125.0"/>
|
||||
<RangeMinValue value="102.0"/>
|
||||
<RangeMaxValue value="125.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="8" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="267"/> <!-- Light -->
|
||||
<NominalValue value="300.0"/>
|
||||
<RangeMinValue value="300.0"/>
|
||||
<RangeMaxValue value="349.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="9" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="268"/> <!-- Regular -->
|
||||
<NominalValue value="400.0"/>
|
||||
<RangeMinValue value="350.0"/>
|
||||
<RangeMaxValue value="449.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="10" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="269"/> <!-- Medium -->
|
||||
<NominalValue value="500.0"/>
|
||||
<RangeMinValue value="450.0"/>
|
||||
<RangeMaxValue value="549.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="11" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="270"/> <!-- Semibold -->
|
||||
<NominalValue value="600.0"/>
|
||||
<RangeMinValue value="550.0"/>
|
||||
<RangeMaxValue value="649.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="12" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="271"/> <!-- Bold -->
|
||||
<NominalValue value="700.0"/>
|
||||
<RangeMinValue value="650.0"/>
|
||||
<RangeMaxValue value="749.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="13" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="272"/> <!-- Black -->
|
||||
<NominalValue value="900.0"/>
|
||||
<RangeMinValue value="750.0"/>
|
||||
<RangeMaxValue value="900.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="14" Format="1">
|
||||
<AxisIndex value="3"/>
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="274"/> <!-- Roman -->
|
||||
<Value value="0.0"/>
|
||||
</AxisValue>
|
||||
</AxisValueArray>
|
||||
<ElidedFallbackNameID value="256"/> <!-- Roman -->
|
||||
</STAT>
|
||||
|
||||
</ttFont>
|
84
Tests/feaLib/data/STAT_test_elidedFallbackNameID.fea
Normal file
84
Tests/feaLib/data/STAT_test_elidedFallbackNameID.fea
Normal file
@ -0,0 +1,84 @@
|
||||
table name {
|
||||
nameid 25 "TestFont";
|
||||
nameid 256 "Roman";
|
||||
} name;
|
||||
table STAT {
|
||||
ElidedFallbackNameID 256;
|
||||
DesignAxis opsz 0 {
|
||||
name "Optical Size";
|
||||
};
|
||||
DesignAxis wdth 1 {
|
||||
name "Width";
|
||||
};
|
||||
DesignAxis wght 2 {
|
||||
name "Weight";
|
||||
};
|
||||
DesignAxis ital 3 {
|
||||
name "Italic";
|
||||
}; # here comment
|
||||
AxisValue {
|
||||
location opsz 8; # comment here
|
||||
location wdth 400; # another comment
|
||||
name "Caption"; # more comments
|
||||
};
|
||||
AxisValue {
|
||||
location opsz 11 9 12;
|
||||
name "Text";
|
||||
flag OlderSiblingFontAttribute ElidableAxisValueName;
|
||||
};
|
||||
AxisValue {
|
||||
location opsz 16.7 12 24;
|
||||
name "Subhead";
|
||||
};
|
||||
AxisValue {
|
||||
location opsz 72 24 72;
|
||||
name "Display";
|
||||
};
|
||||
AxisValue {
|
||||
location wdth 80 80 89;
|
||||
name "Condensed";
|
||||
};
|
||||
AxisValue {
|
||||
location wdth 90 90 96;
|
||||
name "Semicondensed";
|
||||
};
|
||||
AxisValue {
|
||||
location wdth 100 97 101;
|
||||
name "Normal";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
AxisValue {
|
||||
location wdth 125 102 125;
|
||||
name "Extended";
|
||||
};
|
||||
AxisValue {
|
||||
location wght 300 300 349;
|
||||
name "Light";
|
||||
};
|
||||
AxisValue {
|
||||
location wght 400 350 449;
|
||||
name "Regular";
|
||||
flag ElidableAxisValueName;
|
||||
};
|
||||
AxisValue {
|
||||
location wght 500 450 549;
|
||||
name "Medium";
|
||||
};
|
||||
AxisValue {
|
||||
location wght 600 550 649;
|
||||
name "Semibold";
|
||||
};
|
||||
AxisValue {
|
||||
location wght 700 650 749;
|
||||
name "Bold";
|
||||
};
|
||||
AxisValue {
|
||||
location wght 900 750 900;
|
||||
name "Black";
|
||||
};
|
||||
AxisValue {
|
||||
location ital 0;
|
||||
name "Roman";
|
||||
flag ElidableAxisValueName; # flag comment
|
||||
};
|
||||
} STAT;
|
225
Tests/feaLib/data/STAT_test_elidedFallbackNameID.ttx
Normal file
225
Tests/feaLib/data/STAT_test_elidedFallbackNameID.ttx
Normal file
@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.20">
|
||||
|
||||
<name>
|
||||
<namerecord nameID="25" platformID="3" platEncID="1" langID="0x409">
|
||||
TestFont
|
||||
</namerecord>
|
||||
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
|
||||
Roman
|
||||
</namerecord>
|
||||
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
|
||||
Optical Size
|
||||
</namerecord>
|
||||
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
|
||||
Text
|
||||
</namerecord>
|
||||
<namerecord nameID="259" platformID="3" platEncID="1" langID="0x409">
|
||||
Subhead
|
||||
</namerecord>
|
||||
<namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
|
||||
Display
|
||||
</namerecord>
|
||||
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
|
||||
Width
|
||||
</namerecord>
|
||||
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
|
||||
Condensed
|
||||
</namerecord>
|
||||
<namerecord nameID="263" platformID="3" platEncID="1" langID="0x409">
|
||||
Semicondensed
|
||||
</namerecord>
|
||||
<namerecord nameID="264" platformID="3" platEncID="1" langID="0x409">
|
||||
Normal
|
||||
</namerecord>
|
||||
<namerecord nameID="265" platformID="3" platEncID="1" langID="0x409">
|
||||
Extended
|
||||
</namerecord>
|
||||
<namerecord nameID="266" platformID="3" platEncID="1" langID="0x409">
|
||||
Weight
|
||||
</namerecord>
|
||||
<namerecord nameID="267" platformID="3" platEncID="1" langID="0x409">
|
||||
Light
|
||||
</namerecord>
|
||||
<namerecord nameID="268" platformID="3" platEncID="1" langID="0x409">
|
||||
Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="269" platformID="3" platEncID="1" langID="0x409">
|
||||
Medium
|
||||
</namerecord>
|
||||
<namerecord nameID="270" platformID="3" platEncID="1" langID="0x409">
|
||||
Semibold
|
||||
</namerecord>
|
||||
<namerecord nameID="271" platformID="3" platEncID="1" langID="0x409">
|
||||
Bold
|
||||
</namerecord>
|
||||
<namerecord nameID="272" platformID="3" platEncID="1" langID="0x409">
|
||||
Black
|
||||
</namerecord>
|
||||
<namerecord nameID="273" platformID="3" platEncID="1" langID="0x409">
|
||||
Italic
|
||||
</namerecord>
|
||||
<namerecord nameID="274" platformID="3" platEncID="1" langID="0x409">
|
||||
Roman
|
||||
</namerecord>
|
||||
<namerecord nameID="275" platformID="3" platEncID="1" langID="0x409">
|
||||
Caption
|
||||
</namerecord>
|
||||
</name>
|
||||
|
||||
<STAT>
|
||||
<Version value="0x00010002"/>
|
||||
<DesignAxisRecordSize value="8"/>
|
||||
<!-- DesignAxisCount=4 -->
|
||||
<DesignAxisRecord>
|
||||
<Axis index="0">
|
||||
<AxisTag value="opsz"/>
|
||||
<AxisNameID value="257"/> <!-- Optical Size -->
|
||||
<AxisOrdering value="0"/>
|
||||
</Axis>
|
||||
<Axis index="1">
|
||||
<AxisTag value="wdth"/>
|
||||
<AxisNameID value="261"/> <!-- Width -->
|
||||
<AxisOrdering value="1"/>
|
||||
</Axis>
|
||||
<Axis index="2">
|
||||
<AxisTag value="wght"/>
|
||||
<AxisNameID value="266"/> <!-- Weight -->
|
||||
<AxisOrdering value="2"/>
|
||||
</Axis>
|
||||
<Axis index="3">
|
||||
<AxisTag value="ital"/>
|
||||
<AxisNameID value="273"/> <!-- Italic -->
|
||||
<AxisOrdering value="3"/>
|
||||
</Axis>
|
||||
</DesignAxisRecord>
|
||||
<!-- AxisValueCount=15 -->
|
||||
<AxisValueArray>
|
||||
<AxisValue index="0" Format="4">
|
||||
<!-- AxisCount=2 -->
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="275"/> <!-- Caption -->
|
||||
<AxisValueRecord index="0">
|
||||
<AxisIndex value="0"/>
|
||||
<Value value="8.0"/>
|
||||
</AxisValueRecord>
|
||||
<AxisValueRecord index="1">
|
||||
<AxisIndex value="1"/>
|
||||
<Value value="400.0"/>
|
||||
</AxisValueRecord>
|
||||
</AxisValue>
|
||||
<AxisValue index="1" Format="2">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="3"/> <!-- OlderSiblingFontAttribute ElidableAxisValueName -->
|
||||
<ValueNameID value="258"/> <!-- Text -->
|
||||
<NominalValue value="11.0"/>
|
||||
<RangeMinValue value="9.0"/>
|
||||
<RangeMaxValue value="12.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="2" Format="2">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="259"/> <!-- Subhead -->
|
||||
<NominalValue value="16.7"/>
|
||||
<RangeMinValue value="12.0"/>
|
||||
<RangeMaxValue value="24.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="3" Format="2">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="260"/> <!-- Display -->
|
||||
<NominalValue value="72.0"/>
|
||||
<RangeMinValue value="24.0"/>
|
||||
<RangeMaxValue value="72.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="4" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="262"/> <!-- Condensed -->
|
||||
<NominalValue value="80.0"/>
|
||||
<RangeMinValue value="80.0"/>
|
||||
<RangeMaxValue value="89.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="5" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="263"/> <!-- Semicondensed -->
|
||||
<NominalValue value="90.0"/>
|
||||
<RangeMinValue value="90.0"/>
|
||||
<RangeMaxValue value="96.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="6" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="264"/> <!-- Normal -->
|
||||
<NominalValue value="100.0"/>
|
||||
<RangeMinValue value="97.0"/>
|
||||
<RangeMaxValue value="101.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="7" Format="2">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="265"/> <!-- Extended -->
|
||||
<NominalValue value="125.0"/>
|
||||
<RangeMinValue value="102.0"/>
|
||||
<RangeMaxValue value="125.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="8" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="267"/> <!-- Light -->
|
||||
<NominalValue value="300.0"/>
|
||||
<RangeMinValue value="300.0"/>
|
||||
<RangeMaxValue value="349.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="9" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="268"/> <!-- Regular -->
|
||||
<NominalValue value="400.0"/>
|
||||
<RangeMinValue value="350.0"/>
|
||||
<RangeMaxValue value="449.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="10" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="269"/> <!-- Medium -->
|
||||
<NominalValue value="500.0"/>
|
||||
<RangeMinValue value="450.0"/>
|
||||
<RangeMaxValue value="549.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="11" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="270"/> <!-- Semibold -->
|
||||
<NominalValue value="600.0"/>
|
||||
<RangeMinValue value="550.0"/>
|
||||
<RangeMaxValue value="649.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="12" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="271"/> <!-- Bold -->
|
||||
<NominalValue value="700.0"/>
|
||||
<RangeMinValue value="650.0"/>
|
||||
<RangeMaxValue value="749.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="13" Format="2">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="0"/>
|
||||
<ValueNameID value="272"/> <!-- Black -->
|
||||
<NominalValue value="900.0"/>
|
||||
<RangeMinValue value="750.0"/>
|
||||
<RangeMaxValue value="900.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="14" Format="1">
|
||||
<AxisIndex value="3"/>
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="274"/> <!-- Roman -->
|
||||
<Value value="0.0"/>
|
||||
</AxisValue>
|
||||
</AxisValueArray>
|
||||
<ElidedFallbackNameID value="256"/> <!-- Roman -->
|
||||
</STAT>
|
||||
|
||||
</ttFont>
|
@ -2,10 +2,12 @@
|
||||
feature mark {
|
||||
lookup mark1 {
|
||||
markClass [acute] <anchor 150 -10> @TOP_MARKS;
|
||||
pos base [e] <anchor 250 450> mark @TOP_MARKS;
|
||||
pos base [e]
|
||||
<anchor 250 450> mark @TOP_MARKS;
|
||||
} mark1;
|
||||
lookup mark2 {
|
||||
markClass [acute] <anchor 150 -20> @TOP_MARKS_2;
|
||||
pos base [e] <anchor 250 450> mark @TOP_MARKS_2;
|
||||
pos base [e]
|
||||
<anchor 250 450> mark @TOP_MARKS_2;
|
||||
} mark2;
|
||||
} mark;
|
||||
|
3
Tests/feaLib/data/delete_glyph.fea
Normal file
3
Tests/feaLib/data/delete_glyph.fea
Normal file
@ -0,0 +1,3 @@
|
||||
feature test {
|
||||
sub a by NULL;
|
||||
} test;
|
43
Tests/feaLib/data/delete_glyph.ttx
Normal file
43
Tests/feaLib/data/delete_glyph.ttx
Normal file
@ -0,0 +1,43 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont>
|
||||
|
||||
<GSUB>
|
||||
<Version value="0x00010000"/>
|
||||
<ScriptList>
|
||||
<!-- ScriptCount=1 -->
|
||||
<ScriptRecord index="0">
|
||||
<ScriptTag value="DFLT"/>
|
||||
<Script>
|
||||
<DefaultLangSys>
|
||||
<ReqFeatureIndex value="65535"/>
|
||||
<!-- FeatureCount=1 -->
|
||||
<FeatureIndex index="0" value="0"/>
|
||||
</DefaultLangSys>
|
||||
<!-- LangSysCount=0 -->
|
||||
</Script>
|
||||
</ScriptRecord>
|
||||
</ScriptList>
|
||||
<FeatureList>
|
||||
<!-- FeatureCount=1 -->
|
||||
<FeatureRecord index="0">
|
||||
<FeatureTag value="test"/>
|
||||
<Feature>
|
||||
<!-- LookupCount=1 -->
|
||||
<LookupListIndex index="0" value="0"/>
|
||||
</Feature>
|
||||
</FeatureRecord>
|
||||
</FeatureList>
|
||||
<LookupList>
|
||||
<!-- LookupCount=1 -->
|
||||
<Lookup index="0">
|
||||
<LookupType value="2"/>
|
||||
<LookupFlag value="0"/>
|
||||
<!-- SubTableCount=1 -->
|
||||
<MultipleSubst index="0">
|
||||
<Substitution in="a" out=""/>
|
||||
</MultipleSubst>
|
||||
</Lookup>
|
||||
</LookupList>
|
||||
</GSUB>
|
||||
|
||||
</ttFont>
|
@ -9,7 +9,11 @@ markClass [dieresis umlaut] <anchor 300 -10> @TOP_MARKS;
|
||||
markClass [cedilla] <anchor 300 600> @BOTTOM_MARKS;
|
||||
|
||||
feature test {
|
||||
pos base [e o] <anchor 250 450> mark @TOP_MARKS <anchor 250 -12> mark @BOTTOM_MARKS;
|
||||
#test-fea2fea: pos base [a u] <anchor 265 450> mark @TOP_MARKS <anchor 250 -10> mark @BOTTOM_MARKS;
|
||||
position base [a u] <anchor 265 450> mark @TOP_MARKS <anchor 250-10> mark @BOTTOM_MARKS;
|
||||
pos base [e o]
|
||||
<anchor 250 450> mark @TOP_MARKS
|
||||
<anchor 250 -12> mark @BOTTOM_MARKS;
|
||||
#test-fea2fea: pos base [a u]
|
||||
position base [a u]
|
||||
<anchor 265 450> mark @TOP_MARKS
|
||||
<anchor 250 -10> mark @BOTTOM_MARKS;
|
||||
} test;
|
||||
|
@ -4,7 +4,10 @@ markClass sukun <anchor 261 488> @TOP_MARKS;
|
||||
markClass kasratan <anchor 346 -98> @BOTTOM_MARKS;
|
||||
|
||||
feature test {
|
||||
pos ligature lam_meem_jeem <anchor 625 1800> mark @TOP_MARKS # mark above lam
|
||||
ligComponent <anchor 376 -368> mark @BOTTOM_MARKS # mark below meem
|
||||
ligComponent <anchor NULL>; # jeem has no marks
|
||||
pos ligature lam_meem_jeem
|
||||
<anchor 625 1800> mark @TOP_MARKS # mark above lam
|
||||
ligComponent
|
||||
<anchor 376 -368> mark @BOTTOM_MARKS # mark below meem
|
||||
ligComponent
|
||||
<anchor NULL>; # jeem has no marks
|
||||
} test;
|
||||
|
@ -2,5 +2,6 @@ languagesystem DFLT dflt;
|
||||
|
||||
feature test {
|
||||
markClass damma <anchor 189 -103> @MARK_CLASS_1;
|
||||
pos mark hamza <anchor 221 301> mark @MARK_CLASS_1;
|
||||
pos mark hamza
|
||||
<anchor 221 301> mark @MARK_CLASS_1;
|
||||
} test;
|
||||
|
@ -12,8 +12,10 @@ lookup CNTXT_PAIR_POS {
|
||||
} CNTXT_PAIR_POS;
|
||||
|
||||
lookup CNTXT_MARK_TO_BASE {
|
||||
pos base o <anchor 250 450> mark @ALL_MARKS;
|
||||
pos base c <anchor 250 450> mark @ALL_MARKS;
|
||||
pos base o
|
||||
<anchor 250 450> mark @ALL_MARKS;
|
||||
pos base c
|
||||
<anchor 250 450> mark @ALL_MARKS;
|
||||
} CNTXT_MARK_TO_BASE;
|
||||
|
||||
feature test {
|
||||
|
@ -1280,6 +1280,76 @@ class ParserTest(unittest.TestCase):
|
||||
'"dflt" is not a valid script tag; use "DFLT" instead',
|
||||
self.parse, "feature test {script dflt;} test;")
|
||||
|
||||
def test_stat_design_axis(self): # STAT DesignAxis
|
||||
doc = self.parse('table STAT { DesignAxis opsz 0 '
|
||||
'{name "Optical Size";}; } STAT;')
|
||||
da = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(da, ast.STATDesignAxisStatement)
|
||||
self.assertEqual(da.tag, 'opsz')
|
||||
self.assertEqual(da.axisOrder, 0)
|
||||
self.assertEqual(da.names[0].string, 'Optical Size')
|
||||
|
||||
def test_stat_axis_value_format1(self): # STAT AxisValue
|
||||
doc = self.parse('table STAT { DesignAxis opsz 0 '
|
||||
'{name "Optical Size";}; '
|
||||
'AxisValue {location opsz 8; name "Caption";}; } '
|
||||
'STAT;')
|
||||
avr = doc.statements[0].statements[1]
|
||||
self.assertIsInstance(avr, ast.STATAxisValueStatement)
|
||||
self.assertEqual(avr.locations[0].tag, 'opsz')
|
||||
self.assertEqual(avr.locations[0].values[0], 8)
|
||||
self.assertEqual(avr.names[0].string, 'Caption')
|
||||
|
||||
def test_stat_axis_value_format2(self): # STAT AxisValue
|
||||
doc = self.parse('table STAT { DesignAxis opsz 0 '
|
||||
'{name "Optical Size";}; '
|
||||
'AxisValue {location opsz 8 6 10; name "Caption";}; } '
|
||||
'STAT;')
|
||||
avr = doc.statements[0].statements[1]
|
||||
self.assertIsInstance(avr, ast.STATAxisValueStatement)
|
||||
self.assertEqual(avr.locations[0].tag, 'opsz')
|
||||
self.assertEqual(avr.locations[0].values, [8, 6, 10])
|
||||
self.assertEqual(avr.names[0].string, 'Caption')
|
||||
|
||||
def test_stat_axis_value_format2_bad_range(self): # STAT AxisValue
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'Default value 5 is outside of specified range 6-10.',
|
||||
self.parse, 'table STAT { DesignAxis opsz 0 '
|
||||
'{name "Optical Size";}; '
|
||||
'AxisValue {location opsz 5 6 10; name "Caption";}; } '
|
||||
'STAT;')
|
||||
|
||||
def test_stat_axis_value_format4(self): # STAT AxisValue
|
||||
self.assertRaisesRegex(
|
||||
FeatureLibError,
|
||||
'Only one value is allowed in a Format 4 Axis Value Record, but 3 were found.',
|
||||
self.parse, 'table STAT { '
|
||||
'DesignAxis opsz 0 {name "Optical Size";}; '
|
||||
'DesignAxis wdth 0 {name "Width";}; '
|
||||
'AxisValue {'
|
||||
'location opsz 8 6 10; '
|
||||
'location wdth 400; '
|
||||
'name "Caption";}; } '
|
||||
'STAT;')
|
||||
|
||||
def test_stat_elidedfallbackname(self): # STAT ElidedFallbackName
|
||||
doc = self.parse('table STAT { ElidedFallbackName {name "Roman"; '
|
||||
'name 3 1 0x0411 "ローマン"; }; '
|
||||
'} STAT;')
|
||||
nameRecord = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(nameRecord, ast.ElidedFallbackName)
|
||||
self.assertEqual(nameRecord.names[0].string, 'Roman')
|
||||
self.assertEqual(nameRecord.names[1].string, 'ローマン')
|
||||
|
||||
def test_stat_elidedfallbacknameid(self): # STAT ElidedFallbackNameID
|
||||
doc = self.parse('table name { nameid 278 "Roman"; } name; '
|
||||
'table STAT { ElidedFallbackNameID 278; '
|
||||
'} STAT;')
|
||||
nameRecord = doc.statements[0].statements[0]
|
||||
self.assertIsInstance(nameRecord, ast.NameRecord)
|
||||
self.assertEqual(nameRecord.string, 'Roman')
|
||||
|
||||
def test_sub_single_format_a(self): # GSUB LookupType 1
|
||||
doc = self.parse("feature smcp {substitute a by a.sc;} smcp;")
|
||||
sub = doc.statements[0].statements[0]
|
||||
|
@ -141,9 +141,6 @@
|
||||
Test Axis
|
||||
</namerecord>
|
||||
<namerecord nameID="257" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
TotallyNormal
|
||||
</namerecord>
|
||||
<namerecord nameID="258" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
TotallyTested
|
||||
</namerecord>
|
||||
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
|
||||
@ -165,9 +162,6 @@
|
||||
Test Axis
|
||||
</namerecord>
|
||||
<namerecord nameID="257" platformID="3" platEncID="1" langID="0x409">
|
||||
TotallyNormal
|
||||
</namerecord>
|
||||
<namerecord nameID="258" platformID="3" platEncID="1" langID="0x409">
|
||||
TotallyTested
|
||||
</namerecord>
|
||||
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
|
||||
@ -290,12 +284,12 @@
|
||||
</Axis>
|
||||
|
||||
<!-- TotallyNormal -->
|
||||
<NamedInstance flags="0x0" subfamilyNameID="257">
|
||||
<NamedInstance flags="0x0" subfamilyNameID="2">
|
||||
<coord axis="TEST" value="0.0"/>
|
||||
</NamedInstance>
|
||||
|
||||
<!-- TotallyTested -->
|
||||
<NamedInstance flags="0x0" subfamilyNameID="258">
|
||||
<NamedInstance flags="0x0" subfamilyNameID="257">
|
||||
<coord axis="TEST" value="100.0"/>
|
||||
</NamedInstance>
|
||||
</fvar>
|
||||
|
@ -199,12 +199,9 @@
|
||||
Down
|
||||
</namerecord>
|
||||
<namerecord nameID="260" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
TotallyNormal
|
||||
</namerecord>
|
||||
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
Right Up
|
||||
</namerecord>
|
||||
<namerecord nameID="262" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
<namerecord nameID="261" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
Neutral
|
||||
</namerecord>
|
||||
<namerecord nameID="1" platformID="1" platEncID="0" langID="0x4" unicode="True">
|
||||
@ -235,12 +232,9 @@
|
||||
Down
|
||||
</namerecord>
|
||||
<namerecord nameID="260" platformID="3" platEncID="1" langID="0x409">
|
||||
TotallyNormal
|
||||
</namerecord>
|
||||
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
|
||||
Right Up
|
||||
</namerecord>
|
||||
<namerecord nameID="262" platformID="3" platEncID="1" langID="0x409">
|
||||
<namerecord nameID="261" platformID="3" platEncID="1" langID="0x409">
|
||||
Neutral
|
||||
</namerecord>
|
||||
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x413">
|
||||
@ -399,8 +393,8 @@
|
||||
<AxisValueArray>
|
||||
<AxisValue index="0" Format="1">
|
||||
<AxisIndex value="0"/>
|
||||
<Flags value="2"/>
|
||||
<ValueNameID value="262"/> <!-- Neutral -->
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="261"/> <!-- Neutral -->
|
||||
<Value value="0.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="1" Format="1">
|
||||
@ -411,8 +405,8 @@
|
||||
</AxisValue>
|
||||
<AxisValue index="2" Format="1">
|
||||
<AxisIndex value="1"/>
|
||||
<Flags value="2"/>
|
||||
<ValueNameID value="262"/> <!-- Neutral -->
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="261"/> <!-- Neutral -->
|
||||
<Value value="0.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="3" Format="1">
|
||||
@ -423,8 +417,8 @@
|
||||
</AxisValue>
|
||||
<AxisValue index="4" Format="1">
|
||||
<AxisIndex value="2"/>
|
||||
<Flags value="2"/>
|
||||
<ValueNameID value="262"/> <!-- Neutral -->
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="261"/> <!-- Neutral -->
|
||||
<Value value="0.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="5" Format="1">
|
||||
@ -435,8 +429,8 @@
|
||||
</AxisValue>
|
||||
<AxisValue index="6" Format="1">
|
||||
<AxisIndex value="3"/>
|
||||
<Flags value="2"/>
|
||||
<ValueNameID value="262"/> <!-- Neutral -->
|
||||
<Flags value="2"/> <!-- ElidableAxisValueName -->
|
||||
<ValueNameID value="261"/> <!-- Neutral -->
|
||||
<Value value="0.0"/>
|
||||
</AxisValue>
|
||||
<AxisValue index="7" Format="1">
|
||||
@ -492,7 +486,7 @@
|
||||
</Axis>
|
||||
|
||||
<!-- TotallyNormal -->
|
||||
<NamedInstance flags="0x0" subfamilyNameID="260">
|
||||
<NamedInstance flags="0x0" subfamilyNameID="2">
|
||||
<coord axis="LEFT" value="0.0"/>
|
||||
<coord axis="RGHT" value="0.0"/>
|
||||
<coord axis="UPPP" value="0.0"/>
|
||||
@ -500,7 +494,7 @@
|
||||
</NamedInstance>
|
||||
|
||||
<!-- Right Up -->
|
||||
<NamedInstance flags="0x0" subfamilyNameID="261">
|
||||
<NamedInstance flags="0x0" subfamilyNameID="260">
|
||||
<coord axis="LEFT" value="0.0"/>
|
||||
<coord axis="RGHT" value="100.0"/>
|
||||
<coord axis="UPPP" value="100.0"/>
|
||||
|
66
Tests/misc/vector_test.py
Normal file
66
Tests/misc/vector_test.py
Normal file
@ -0,0 +1,66 @@
|
||||
import math
|
||||
import pytest
|
||||
from fontTools.misc.arrayTools import Vector as ArrayVector
|
||||
from fontTools.misc.vector import Vector
|
||||
|
||||
|
||||
def test_Vector():
|
||||
v = Vector((100, 200))
|
||||
assert repr(v) == "Vector((100, 200))"
|
||||
assert v == Vector((100, 200))
|
||||
assert v == Vector([100, 200])
|
||||
assert v == (100, 200)
|
||||
assert (100, 200) == v
|
||||
assert v == [100, 200]
|
||||
assert [100, 200] == v
|
||||
assert v is Vector(v)
|
||||
assert v + 10 == (110, 210)
|
||||
assert 10 + v == (110, 210)
|
||||
assert v + Vector((1, 2)) == (101, 202)
|
||||
assert v - Vector((1, 2)) == (99, 198)
|
||||
assert v * 2 == (200, 400)
|
||||
assert 2 * v == (200, 400)
|
||||
assert v * 0.5 == (50, 100)
|
||||
assert v / 2 == (50, 100)
|
||||
assert 2 / v == (0.02, 0.01)
|
||||
v = Vector((3, 4))
|
||||
assert abs(v) == 5 # length
|
||||
assert v.length() == 5
|
||||
assert v.normalized() == Vector((0.6, 0.8))
|
||||
assert abs(Vector((1, 1, 1))) == math.sqrt(3)
|
||||
assert bool(Vector((0, 0, 1)))
|
||||
assert not bool(Vector((0, 0, 0)))
|
||||
v1 = Vector((2, 3))
|
||||
v2 = Vector((3, 4))
|
||||
assert v1.dot(v2) == 18
|
||||
v = Vector((2, 4))
|
||||
assert round(v / 3) == (1, 1)
|
||||
|
||||
|
||||
def test_deprecated():
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="fontTools.misc.arrayTools.Vector has been deprecated",
|
||||
):
|
||||
ArrayVector((1, 2))
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="the 'keep' argument has been deprecated",
|
||||
):
|
||||
Vector((1, 2), keep=True)
|
||||
v = Vector((1, 2))
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="the 'toInt' method has been deprecated",
|
||||
):
|
||||
v.toInt()
|
||||
with pytest.warns(
|
||||
DeprecationWarning,
|
||||
match="the 'values' attribute has been deprecated",
|
||||
):
|
||||
v.values
|
||||
with pytest.raises(
|
||||
AttributeError,
|
||||
match="the 'values' attribute has been deprecated",
|
||||
):
|
||||
v.values = [12, 23]
|
@ -2,7 +2,7 @@ import io
|
||||
import struct
|
||||
from fontTools.misc.fixedTools import floatToFixed
|
||||
from fontTools.misc.testTools import getXML
|
||||
from fontTools.otlLib import builder
|
||||
from fontTools.otlLib import builder, error
|
||||
from fontTools import ttLib
|
||||
from fontTools.ttLib.tables import otTables
|
||||
import pytest
|
||||
@ -1101,6 +1101,12 @@ class ClassDefBuilderTest(object):
|
||||
assert not b.canAdd({"d", "e", "f"})
|
||||
assert not b.canAdd({"f"})
|
||||
|
||||
def test_add_exception(self):
|
||||
b = builder.ClassDefBuilder(useClass0=True)
|
||||
b.add({"a", "b", "c"})
|
||||
with pytest.raises(error.OpenTypeLibError):
|
||||
b.add({"a", "d"})
|
||||
|
||||
|
||||
buildStatTable_test_data = [
|
||||
([
|
||||
@ -1132,7 +1138,7 @@ buildStatTable_test_data = [
|
||||
' </AxisValue>',
|
||||
' <AxisValue index="1" Format="1">',
|
||||
' <AxisIndex value="0"/>',
|
||||
' <Flags value="2"/>',
|
||||
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
||||
' <ValueNameID value="256"/> <!-- Regular -->',
|
||||
' <Value value="400.0"/>',
|
||||
' </AxisValue>',
|
||||
@ -1187,7 +1193,7 @@ buildStatTable_test_data = [
|
||||
' </AxisValue>',
|
||||
' <AxisValue index="1" Format="1">',
|
||||
' <AxisIndex value="0"/>',
|
||||
' <Flags value="2"/>',
|
||||
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
||||
' <ValueNameID value="258"/> <!-- Regular -->',
|
||||
' <Value value="400.0"/>',
|
||||
' </AxisValue>',
|
||||
@ -1205,7 +1211,7 @@ buildStatTable_test_data = [
|
||||
' </AxisValue>',
|
||||
' <AxisValue index="4" Format="1">',
|
||||
' <AxisIndex value="1"/>',
|
||||
' <Flags value="2"/>',
|
||||
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
||||
' <ValueNameID value="258"/> <!-- Regular -->',
|
||||
' <Value value="100.0"/>',
|
||||
' </AxisValue>',
|
||||
@ -1240,7 +1246,7 @@ buildStatTable_test_data = [
|
||||
' <AxisValueArray>',
|
||||
' <AxisValue index="0" Format="1">',
|
||||
' <AxisIndex value="0"/>',
|
||||
' <Flags value="2"/>',
|
||||
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
||||
' <ValueNameID value="257"/> <!-- Regular -->',
|
||||
' <Value value="400.0"/>',
|
||||
' </AxisValue>',
|
||||
@ -1285,7 +1291,7 @@ buildStatTable_test_data = [
|
||||
' </AxisValue>',
|
||||
' <AxisValue index="1" Format="2">',
|
||||
' <AxisIndex value="0"/>',
|
||||
' <Flags value="2"/>',
|
||||
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
||||
' <ValueNameID value="258"/> <!-- Text -->',
|
||||
' <NominalValue value="14.0"/>',
|
||||
' <RangeMinValue value="10.0"/>',
|
||||
@ -1348,7 +1354,7 @@ buildStatTable_test_data = [
|
||||
' </AxisValue>',
|
||||
' <AxisValue index="1" Format="1">',
|
||||
' <AxisIndex value="1"/>',
|
||||
' <Flags value="2"/>',
|
||||
' <Flags value="2"/> <!-- ElidableAxisValueName -->',
|
||||
' <ValueNameID value="258"/> <!-- Regular -->',
|
||||
' <Value value="100.0"/>',
|
||||
' </AxisValue>',
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.areaPen import AreaPen
|
||||
import unittest
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.basePen import \
|
||||
BasePen, decomposeSuperBezierSegment, decomposeQuadraticSegment
|
||||
AbstractPen, BasePen, decomposeSuperBezierSegment, decomposeQuadraticSegment
|
||||
from fontTools.pens.pointPen import AbstractPointPen
|
||||
from fontTools.misc.loggingTools import CapturingLogHandler
|
||||
import unittest
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.boundsPen import BoundsPen, ControlBoundsPen
|
||||
import unittest
|
||||
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
import unittest
|
||||
|
||||
try:
|
||||
@ -48,7 +47,7 @@ class CocoaPenTest(unittest.TestCase):
|
||||
"moveto 50.0 0.0 lineto 50.0 500.0 lineto 200.0 500.0 curveto 350.0 500.0 450.0 400.0 450.0 250.0 curveto 450.0 100.0 350.0 0.0 200.0 0.0 close ",
|
||||
cocoaPathToString(pen.path)
|
||||
)
|
||||
|
||||
|
||||
def test_empty(self):
|
||||
pen = CocoaPen(None)
|
||||
self.assertEqual("", cocoaPathToString(pen.path))
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.perimeterPen import PerimeterPen
|
||||
import unittest
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from io import StringIO
|
||||
from fontTools.pens.pointInsidePen import PointInsidePen
|
||||
import unittest
|
||||
|
||||
@ -72,16 +72,16 @@ class PointInsidePenTest(unittest.TestCase):
|
||||
|
||||
@staticmethod
|
||||
def render(draw_function, even_odd):
|
||||
result = BytesIO()
|
||||
result = StringIO()
|
||||
for y in range(5):
|
||||
for x in range(10):
|
||||
pen = PointInsidePen(None, (x + 0.5, y + 0.5), even_odd)
|
||||
draw_function(pen)
|
||||
if pen.getResult():
|
||||
result.write(b"*")
|
||||
result.write("*")
|
||||
else:
|
||||
result.write(b" ")
|
||||
return tounicode(result.getvalue())
|
||||
result.write(" ")
|
||||
return result.getvalue()
|
||||
|
||||
|
||||
def test_contour_no_solutions(self):
|
||||
|
@ -1,5 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.misc.loggingTools import CapturingLogHandler
|
||||
import unittest
|
||||
|
||||
from fontTools.pens.basePen import AbstractPen
|
||||
@ -43,7 +41,7 @@ def _reprKwargs(kwargs):
|
||||
items = []
|
||||
for key in sorted(kwargs):
|
||||
value = kwargs[key]
|
||||
if isinstance(value, basestring):
|
||||
if isinstance(value, str):
|
||||
items.append("%s='%s'" % (key, value))
|
||||
else:
|
||||
items.append("%s=%s" % (key, value))
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
import unittest
|
||||
|
||||
try:
|
||||
@ -68,7 +67,7 @@ class QuartzPenTest(unittest.TestCase):
|
||||
"moveto 50.0 0.0 lineto 50.0 500.0 lineto 200.0 500.0 curveto 350.0 500.0 450.0 400.0 450.0 250.0 curveto 450.0 100.0 350.0 0.0 200.0 0.0 close ",
|
||||
quartzPathToString(pen.path)
|
||||
)
|
||||
|
||||
|
||||
def test_empty(self):
|
||||
pen = QuartzPen(None)
|
||||
self.assertEqual("", quartzPathToString(pen.path))
|
||||
|
@ -1,4 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
from fontTools.pens.t2CharStringPen import T2CharStringPen
|
||||
import unittest
|
||||
|
||||
@ -7,16 +6,12 @@ class T2CharStringPenTest(unittest.TestCase):
|
||||
|
||||
def __init__(self, methodName):
|
||||
unittest.TestCase.__init__(self, methodName)
|
||||
# Python 3 renamed assertRaisesRegexp to assertRaisesRegex,
|
||||
# and fires deprecation warnings if a program uses the old name.
|
||||
if not hasattr(self, "assertRaisesRegex"):
|
||||
self.assertRaisesRegex = self.assertRaisesRegexp
|
||||
|
||||
def assertAlmostEqualProgram(self, expected, actual):
|
||||
self.assertEqual(len(expected), len(actual))
|
||||
for i1, i2 in zip(expected, actual):
|
||||
if isinstance(i1, basestring):
|
||||
self.assertIsInstance(i2, basestring)
|
||||
if isinstance(i1, str):
|
||||
self.assertIsInstance(i2, str)
|
||||
self.assertEqual(i1, i2)
|
||||
else:
|
||||
self.assertAlmostEqual(i1, i2)
|
||||
|
@ -1,5 +1,3 @@
|
||||
from fontTools.misc.py23 import *
|
||||
|
||||
import os
|
||||
import unittest
|
||||
import struct
|
||||
|
@ -14,7 +14,7 @@
|
||||
|
||||
from . import CUBIC_GLYPHS
|
||||
from fontTools.pens.pointPen import PointToSegmentPen, SegmentToPointPen
|
||||
from fontTools.misc.py23 import isclose
|
||||
from math import isclose
|
||||
import unittest
|
||||
|
||||
|
||||
|
14
Tests/subset/data/CmapSubsetTest.subset.ttx
Normal file
14
Tests/subset/data/CmapSubsetTest.subset.ttx
Normal file
@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
|
||||
|
||||
<cmap>
|
||||
<tableVersion version="0"/>
|
||||
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_4 platformID="3" platEncID="1" language="0">
|
||||
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
|
||||
</cmap_format_4>
|
||||
</cmap>
|
||||
|
||||
</ttFont>
|
225
Tests/subset/data/CmapSubsetTest.ttx
Normal file
225
Tests/subset/data/CmapSubsetTest.ttx
Normal file
@ -0,0 +1,225 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
|
||||
|
||||
<GlyphOrder>
|
||||
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||
<GlyphID id="0" name=".notdef"/>
|
||||
<GlyphID id="1" name="a"/>
|
||||
<GlyphID id="2" name="basket"/>
|
||||
</GlyphOrder>
|
||||
|
||||
<head>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="1.0"/>
|
||||
<fontRevision value="0.0"/>
|
||||
<checkSumAdjustment value="0xc643119c"/>
|
||||
<magicNumber value="0x5f0f3cf5"/>
|
||||
<flags value="00000000 00000011"/>
|
||||
<unitsPerEm value="1000"/>
|
||||
<created value="Tue Jan 12 16:39:39 2021"/>
|
||||
<modified value="Tue Jan 12 16:39:39 2021"/>
|
||||
<xMin value="50"/>
|
||||
<yMin value="-200"/>
|
||||
<xMax value="450"/>
|
||||
<yMax value="800"/>
|
||||
<macStyle value="00000000 00000000"/>
|
||||
<lowestRecPPEM value="6"/>
|
||||
<fontDirectionHint value="2"/>
|
||||
<indexToLocFormat value="0"/>
|
||||
<glyphDataFormat value="0"/>
|
||||
</head>
|
||||
|
||||
<hhea>
|
||||
<tableVersion value="0x00010000"/>
|
||||
<ascent value="1000"/>
|
||||
<descent value="-200"/>
|
||||
<lineGap value="0"/>
|
||||
<advanceWidthMax value="942"/>
|
||||
<minLeftSideBearing value="50"/>
|
||||
<minRightSideBearing value="50"/>
|
||||
<xMaxExtent value="450"/>
|
||||
<caretSlopeRise value="1"/>
|
||||
<caretSlopeRun value="0"/>
|
||||
<caretOffset value="0"/>
|
||||
<reserved0 value="0"/>
|
||||
<reserved1 value="0"/>
|
||||
<reserved2 value="0"/>
|
||||
<reserved3 value="0"/>
|
||||
<metricDataFormat value="0"/>
|
||||
<numberOfHMetrics value="3"/>
|
||||
</hhea>
|
||||
|
||||
<maxp>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="0x10000"/>
|
||||
<numGlyphs value="3"/>
|
||||
<maxPoints value="8"/>
|
||||
<maxContours value="2"/>
|
||||
<maxCompositePoints value="0"/>
|
||||
<maxCompositeContours value="0"/>
|
||||
<maxZones value="1"/>
|
||||
<maxTwilightPoints value="0"/>
|
||||
<maxStorage value="0"/>
|
||||
<maxFunctionDefs value="0"/>
|
||||
<maxInstructionDefs value="0"/>
|
||||
<maxStackElements value="0"/>
|
||||
<maxSizeOfInstructions value="0"/>
|
||||
<maxComponentElements value="0"/>
|
||||
<maxComponentDepth value="0"/>
|
||||
</maxp>
|
||||
|
||||
<OS_2>
|
||||
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
|
||||
will be recalculated by the compiler -->
|
||||
<version value="4"/>
|
||||
<xAvgCharWidth value="660"/>
|
||||
<usWeightClass value="400"/>
|
||||
<usWidthClass value="5"/>
|
||||
<fsType value="00000000 00000100"/>
|
||||
<ySubscriptXSize value="650"/>
|
||||
<ySubscriptYSize value="600"/>
|
||||
<ySubscriptXOffset value="0"/>
|
||||
<ySubscriptYOffset value="75"/>
|
||||
<ySuperscriptXSize value="650"/>
|
||||
<ySuperscriptYSize value="600"/>
|
||||
<ySuperscriptXOffset value="0"/>
|
||||
<ySuperscriptYOffset value="350"/>
|
||||
<yStrikeoutSize value="50"/>
|
||||
<yStrikeoutPosition value="300"/>
|
||||
<sFamilyClass value="0"/>
|
||||
<panose>
|
||||
<bFamilyType value="0"/>
|
||||
<bSerifStyle value="0"/>
|
||||
<bWeight value="0"/>
|
||||
<bProportion value="0"/>
|
||||
<bContrast value="0"/>
|
||||
<bStrokeVariation value="0"/>
|
||||
<bArmStyle value="0"/>
|
||||
<bLetterForm value="0"/>
|
||||
<bMidline value="0"/>
|
||||
<bXHeight value="0"/>
|
||||
</panose>
|
||||
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulUnicodeRange2 value="00000010 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
|
||||
<achVendID value="NONE"/>
|
||||
<fsSelection value="00000000 01000000"/>
|
||||
<usFirstCharIndex value="97"/>
|
||||
<usLastCharIndex value="65535"/>
|
||||
<sTypoAscender value="800"/>
|
||||
<sTypoDescender value="-200"/>
|
||||
<sTypoLineGap value="200"/>
|
||||
<usWinAscent value="1000"/>
|
||||
<usWinDescent value="200"/>
|
||||
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<sxHeight value="500"/>
|
||||
<sCapHeight value="700"/>
|
||||
<usDefaultChar value="0"/>
|
||||
<usBreakChar value="32"/>
|
||||
<usMaxContext value="0"/>
|
||||
</OS_2>
|
||||
|
||||
<hmtx>
|
||||
<mtx name=".notdef" width="500" lsb="50"/>
|
||||
<mtx name="a" width="538" lsb="0"/>
|
||||
<mtx name="basket" width="942" lsb="0"/>
|
||||
</hmtx>
|
||||
|
||||
<cmap>
|
||||
<tableVersion version="0"/>
|
||||
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="40" language="0" nGroups="2">
|
||||
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
|
||||
<map code="0x1f9fa" name="basket"/><!-- BASKET -->
|
||||
</cmap_format_12>
|
||||
<cmap_format_4 platformID="3" platEncID="1" language="0">
|
||||
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="40" language="0" nGroups="2">
|
||||
<map code="0x61" name="a"/><!-- LATIN SMALL LETTER A -->
|
||||
<map code="0x1f9fa" name="basket"/><!-- BASKET -->
|
||||
</cmap_format_12>
|
||||
</cmap>
|
||||
|
||||
<loca>
|
||||
<!-- The 'loca' table will be calculated by the compiler -->
|
||||
</loca>
|
||||
|
||||
<glyf>
|
||||
|
||||
<!-- The xMin, yMin, xMax and yMax values
|
||||
will be recalculated by the compiler. -->
|
||||
|
||||
<TTGlyph name=".notdef" xMin="50" yMin="-200" xMax="450" yMax="800">
|
||||
<contour>
|
||||
<pt x="50" y="-200" on="1"/>
|
||||
<pt x="50" y="800" on="1"/>
|
||||
<pt x="450" y="800" on="1"/>
|
||||
<pt x="450" y="-200" on="1"/>
|
||||
</contour>
|
||||
<contour>
|
||||
<pt x="100" y="-150" on="1"/>
|
||||
<pt x="400" y="-150" on="1"/>
|
||||
<pt x="400" y="750" on="1"/>
|
||||
<pt x="100" y="750" on="1"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
<TTGlyph name="a"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="basket"/><!-- contains no outline data -->
|
||||
|
||||
</glyf>
|
||||
|
||||
<name>
|
||||
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
|
||||
New Font
|
||||
</namerecord>
|
||||
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
|
||||
Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
|
||||
0.000;NONE;NewFont-Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
|
||||
New Font Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
|
||||
Version 0.000
|
||||
</namerecord>
|
||||
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
|
||||
NewFont-Regular
|
||||
</namerecord>
|
||||
</name>
|
||||
|
||||
<post>
|
||||
<formatType value="2.0"/>
|
||||
<italicAngle value="0.0"/>
|
||||
<underlinePosition value="-75"/>
|
||||
<underlineThickness value="50"/>
|
||||
<isFixedPitch value="0"/>
|
||||
<minMemType42 value="0"/>
|
||||
<maxMemType42 value="0"/>
|
||||
<minMemType1 value="0"/>
|
||||
<maxMemType1 value="0"/>
|
||||
<psNames>
|
||||
<!-- This file uses unique glyph names based on the information
|
||||
found in the 'post' table. Since these names might not be unique,
|
||||
we have to invent artificial names in case of clashes. In order to
|
||||
be able to retain the original information, we need a name to
|
||||
ps name mapping for those cases where they differ. That's what
|
||||
you see below.
|
||||
-->
|
||||
</psNames>
|
||||
<extraNames>
|
||||
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||
<psName name="basket"/>
|
||||
</extraNames>
|
||||
</post>
|
||||
|
||||
</ttFont>
|
@ -17,7 +17,7 @@
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="1.0"/>
|
||||
<fontRevision value="1.0"/>
|
||||
<checkSumAdjustment value="0xa69ed898"/>
|
||||
<checkSumAdjustment value="0xa6bcdc24"/>
|
||||
<magicNumber value="0x5f0f3cf5"/>
|
||||
<flags value="00000000 00001111"/>
|
||||
<unitsPerEm value="1000"/>
|
||||
@ -142,15 +142,9 @@
|
||||
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_12 platformID="0" platEncID="4" format="12" reserved="0" length="28" language="0" nGroups="1">
|
||||
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
|
||||
</cmap_format_12>
|
||||
<cmap_format_4 platformID="3" platEncID="1" language="0">
|
||||
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_12 platformID="3" platEncID="10" format="12" reserved="0" length="28" language="0" nGroups="1">
|
||||
<map code="0x2b" name="plus"/><!-- PLUS SIGN -->
|
||||
</cmap_format_12>
|
||||
</cmap>
|
||||
|
||||
<loca>
|
||||
|
@ -3,7 +3,9 @@ from fontTools.misc.py23 import *
|
||||
from fontTools.misc.testTools import getXML
|
||||
from fontTools import subset
|
||||
from fontTools.fontBuilder import FontBuilder
|
||||
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
||||
from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.ttLib.tables import otTables as ot
|
||||
from fontTools.misc.loggingTools import CapturingLogHandler
|
||||
import difflib
|
||||
import logging
|
||||
@ -753,6 +755,13 @@ class SubsetTest(unittest.TestCase):
|
||||
# check all glyphs are kept via GSUB closure, no changes expected
|
||||
self.expect_ttx(subsetfont, ttx)
|
||||
|
||||
def test_cmap_prune_format12(self):
|
||||
_, fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf")
|
||||
subsetpath = self.temp_path(".ttf")
|
||||
subset.main([fontpath, "--glyphs=a", "--output-file=%s" % subsetpath])
|
||||
subsetfont = TTFont(subsetpath)
|
||||
self.expect_ttx(subsetfont, self.getpath("CmapSubsetTest.subset.ttx"), ["cmap"])
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def featureVarsTestFont():
|
||||
@ -923,5 +932,256 @@ def test_subset_empty_glyf(tmp_path, ttf_path):
|
||||
assert all(loc == 0 for loc in loca)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def colrv1_path(tmp_path):
|
||||
base_glyph_names = ["uni%04X" % i for i in range(0xE000, 0xE000 + 10)]
|
||||
layer_glyph_names = ["glyph%05d" % i for i in range(10, 20)]
|
||||
glyph_order = [".notdef"] + base_glyph_names + layer_glyph_names
|
||||
|
||||
pen = TTGlyphPen(glyphSet=None)
|
||||
pen.moveTo((0, 0))
|
||||
pen.lineTo((0, 500))
|
||||
pen.lineTo((500, 500))
|
||||
pen.lineTo((500, 0))
|
||||
pen.closePath()
|
||||
glyph = pen.glyph()
|
||||
glyphs = {g: glyph for g in glyph_order}
|
||||
|
||||
fb = FontBuilder(unitsPerEm=1024, isTTF=True)
|
||||
fb.setupGlyphOrder(glyph_order)
|
||||
fb.setupCharacterMap({int(name[3:], 16): name for name in base_glyph_names})
|
||||
fb.setupGlyf(glyphs)
|
||||
fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order})
|
||||
fb.setupHorizontalHeader()
|
||||
fb.setupOS2()
|
||||
fb.setupPost()
|
||||
fb.setupNameTable({"familyName": "TestCOLRv1", "styleName": "Regular"})
|
||||
|
||||
fb.setupCOLR(
|
||||
{
|
||||
"uniE000": (
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
[
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintGlyph,
|
||||
"Paint": (ot.PaintFormat.PaintSolid, 0),
|
||||
"Glyph": "glyph00010",
|
||||
},
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintGlyph,
|
||||
"Paint": (ot.PaintFormat.PaintSolid, (2, 0.3)),
|
||||
"Glyph": "glyph00011",
|
||||
},
|
||||
],
|
||||
),
|
||||
"uniE001": (
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
[
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintTransform,
|
||||
"Paint": {
|
||||
"Format": ot.PaintFormat.PaintGlyph,
|
||||
"Paint": {
|
||||
"Format": ot.PaintFormat.PaintRadialGradient,
|
||||
"x0": 250,
|
||||
"y0": 250,
|
||||
"r0": 250,
|
||||
"x1": 200,
|
||||
"y1": 200,
|
||||
"r1": 0,
|
||||
"ColorLine": {
|
||||
"ColorStop": [(0.0, 1), (1.0, 2)],
|
||||
"Extend": "repeat",
|
||||
},
|
||||
},
|
||||
"Glyph": "glyph00012",
|
||||
},
|
||||
"Transform": (0.7071, 0.7071, -0.7071, 0.7071, 0, 0),
|
||||
},
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintGlyph,
|
||||
"Paint": (ot.PaintFormat.PaintSolid, (1, 0.5)),
|
||||
"Glyph": "glyph00013",
|
||||
},
|
||||
],
|
||||
),
|
||||
"uniE002": (
|
||||
ot.PaintFormat.PaintColrLayers,
|
||||
[
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintGlyph,
|
||||
"Paint": {
|
||||
"Format": ot.PaintFormat.PaintLinearGradient,
|
||||
"x0": 0,
|
||||
"y0": 0,
|
||||
"x1": 500,
|
||||
"y1": 500,
|
||||
"x2": -500,
|
||||
"y2": 500,
|
||||
"ColorLine": {"ColorStop": [(0.0, 1), (1.0, 2)]},
|
||||
},
|
||||
"Glyph": "glyph00014",
|
||||
},
|
||||
{
|
||||
"Format": ot.PaintFormat.PaintTransform,
|
||||
"Paint": {
|
||||
"Format": ot.PaintFormat.PaintGlyph,
|
||||
"Paint": (ot.PaintFormat.PaintSolid, 1),
|
||||
"Glyph": "glyph00015",
|
||||
},
|
||||
"Transform": (1, 0, 0, 1, 400, 400),
|
||||
},
|
||||
],
|
||||
),
|
||||
"uniE003": {
|
||||
"Format": ot.PaintFormat.PaintRotate,
|
||||
"Paint": {
|
||||
"Format": ot.PaintFormat.PaintColrGlyph,
|
||||
"Glyph": "uniE001",
|
||||
},
|
||||
"angle": 45,
|
||||
"centerX": 250,
|
||||
"centerY": 250,
|
||||
},
|
||||
"uniE004": [
|
||||
("glyph00016", 1),
|
||||
("glyph00017", 2),
|
||||
],
|
||||
},
|
||||
)
|
||||
fb.setupCPAL(
|
||||
[
|
||||
[
|
||||
(1.0, 0.0, 0.0, 1.0), # red
|
||||
(0.0, 1.0, 0.0, 1.0), # green
|
||||
(0.0, 0.0, 1.0, 1.0), # blue
|
||||
],
|
||||
],
|
||||
)
|
||||
|
||||
output_path = tmp_path / "TestCOLRv1.ttf"
|
||||
fb.save(output_path)
|
||||
|
||||
return output_path
|
||||
|
||||
|
||||
def test_subset_COLRv1_and_CPAL(colrv1_path):
|
||||
subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
|
||||
|
||||
subset.main(
|
||||
[
|
||||
str(colrv1_path),
|
||||
"--glyph-names",
|
||||
f"--output-file={subset_path}",
|
||||
"--unicodes=E002,E003,E004",
|
||||
]
|
||||
)
|
||||
subset_font = TTFont(subset_path)
|
||||
|
||||
glyph_set = set(subset_font.getGlyphOrder())
|
||||
|
||||
# uniE000 and its children are excluded from subset
|
||||
assert "uniE000" not in glyph_set
|
||||
assert "glyph00010" not in glyph_set
|
||||
assert "glyph00011" not in glyph_set
|
||||
|
||||
# uniE001 and children are pulled in indirectly as PaintColrGlyph by uniE003
|
||||
assert "uniE001" in glyph_set
|
||||
assert "glyph00012" in glyph_set
|
||||
assert "glyph00013" in glyph_set
|
||||
|
||||
assert "uniE002" in glyph_set
|
||||
assert "glyph00014" in glyph_set
|
||||
assert "glyph00015" in glyph_set
|
||||
|
||||
assert "uniE003" in glyph_set
|
||||
|
||||
assert "uniE004" in glyph_set
|
||||
assert "glyph00016" in glyph_set
|
||||
assert "glyph00017" in glyph_set
|
||||
|
||||
assert "COLR" in subset_font
|
||||
colr = subset_font["COLR"].table
|
||||
assert colr.Version == 1
|
||||
assert len(colr.BaseGlyphRecordArray.BaseGlyphRecord) == 1
|
||||
assert len(colr.BaseGlyphV1List.BaseGlyphV1Record) == 3 # was 4
|
||||
|
||||
base = colr.BaseGlyphV1List.BaseGlyphV1Record[0]
|
||||
assert base.BaseGlyph == "uniE001"
|
||||
layers = colr.LayerV1List.Paint[
|
||||
base.Paint.FirstLayerIndex: base.Paint.FirstLayerIndex + base.Paint.NumLayers
|
||||
]
|
||||
assert len(layers) == 2
|
||||
# check v1 palette indices were remapped
|
||||
assert layers[0].Paint.Paint.ColorLine.ColorStop[0].Color.PaletteIndex == 0
|
||||
assert layers[0].Paint.Paint.ColorLine.ColorStop[1].Color.PaletteIndex == 1
|
||||
assert layers[1].Paint.Color.PaletteIndex == 0
|
||||
|
||||
baseRecV0 = colr.BaseGlyphRecordArray.BaseGlyphRecord[0]
|
||||
assert baseRecV0.BaseGlyph == "uniE004"
|
||||
layersV0 = colr.LayerRecordArray.LayerRecord
|
||||
assert len(layersV0) == 2
|
||||
# check v0 palette indices were remapped
|
||||
assert layersV0[0].PaletteIndex == 0
|
||||
assert layersV0[1].PaletteIndex == 1
|
||||
|
||||
assert "CPAL" in subset_font
|
||||
cpal = subset_font["CPAL"]
|
||||
assert [
|
||||
tuple(v / 255 for v in (c.red, c.green, c.blue, c.alpha))
|
||||
for c in cpal.palettes[0]
|
||||
] == [
|
||||
# the first color 'red' was pruned
|
||||
(0.0, 1.0, 0.0, 1.0), # green
|
||||
(0.0, 0.0, 1.0, 1.0), # blue
|
||||
]
|
||||
|
||||
|
||||
def test_subset_COLRv1_and_CPAL_drop_empty(colrv1_path):
|
||||
subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
|
||||
|
||||
subset.main(
|
||||
[
|
||||
str(colrv1_path),
|
||||
"--glyph-names",
|
||||
f"--output-file={subset_path}",
|
||||
"--glyphs=glyph00010",
|
||||
]
|
||||
)
|
||||
subset_font = TTFont(subset_path)
|
||||
|
||||
glyph_set = set(subset_font.getGlyphOrder())
|
||||
|
||||
assert "glyph00010" in glyph_set
|
||||
assert "uniE000" not in glyph_set
|
||||
|
||||
assert "COLR" not in subset_font
|
||||
assert "CPAL" not in subset_font
|
||||
|
||||
|
||||
def test_subset_COLRv1_downgrade_version(colrv1_path):
|
||||
subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
|
||||
|
||||
subset.main(
|
||||
[
|
||||
str(colrv1_path),
|
||||
"--glyph-names",
|
||||
f"--output-file={subset_path}",
|
||||
"--unicodes=E004",
|
||||
]
|
||||
)
|
||||
subset_font = TTFont(subset_path)
|
||||
|
||||
assert set(subset_font.getGlyphOrder()) == {
|
||||
".notdef",
|
||||
"uniE004",
|
||||
"glyph00016",
|
||||
"glyph00017",
|
||||
}
|
||||
|
||||
assert "COLR" in subset_font
|
||||
assert subset_font["COLR"].version == 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(unittest.main())
|
||||
|
306
Tests/ttLib/data/woff2_overlap_offcurve_in.ttx
Normal file
306
Tests/ttLib/data/woff2_overlap_offcurve_in.ttx
Normal file
@ -0,0 +1,306 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ttFont sfntVersion="\x00\x01\x00\x00" ttLibVersion="4.18">
|
||||
|
||||
<GlyphOrder>
|
||||
<!-- The 'id' attribute is only for humans; it is ignored when parsed. -->
|
||||
<GlyphID id="0" name=".notdef"/>
|
||||
<GlyphID id="1" name="A"/>
|
||||
</GlyphOrder>
|
||||
|
||||
<head>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="1.0"/>
|
||||
<fontRevision value="1.0"/>
|
||||
<checkSumAdjustment value="0x9aec19bb"/>
|
||||
<magicNumber value="0x5f0f3cf5"/>
|
||||
<flags value="00000000 00000010"/>
|
||||
<unitsPerEm value="1000"/>
|
||||
<created value="Thu Jan 28 15:17:57 2021"/>
|
||||
<modified value="Thu Jan 28 15:52:10 2021"/>
|
||||
<xMin value="178"/>
|
||||
<yMin value="72"/>
|
||||
<xMax value="586"/>
|
||||
<yMax value="480"/>
|
||||
<macStyle value="00000000 00000000"/>
|
||||
<lowestRecPPEM value="6"/>
|
||||
<fontDirectionHint value="2"/>
|
||||
<indexToLocFormat value="0"/>
|
||||
<glyphDataFormat value="0"/>
|
||||
</head>
|
||||
|
||||
<hhea>
|
||||
<tableVersion value="0x00010000"/>
|
||||
<ascent value="750"/>
|
||||
<descent value="-250"/>
|
||||
<lineGap value="100"/>
|
||||
<advanceWidthMax value="639"/>
|
||||
<minLeftSideBearing value="178"/>
|
||||
<minRightSideBearing value="53"/>
|
||||
<xMaxExtent value="586"/>
|
||||
<caretSlopeRise value="1"/>
|
||||
<caretSlopeRun value="0"/>
|
||||
<caretOffset value="0"/>
|
||||
<reserved0 value="0"/>
|
||||
<reserved1 value="0"/>
|
||||
<reserved2 value="0"/>
|
||||
<reserved3 value="0"/>
|
||||
<metricDataFormat value="0"/>
|
||||
<numberOfHMetrics value="2"/>
|
||||
</hhea>
|
||||
|
||||
<maxp>
|
||||
<!-- Most of this table will be recalculated by the compiler -->
|
||||
<tableVersion value="0x10000"/>
|
||||
<numGlyphs value="2"/>
|
||||
<maxPoints value="20"/>
|
||||
<maxContours value="1"/>
|
||||
<maxCompositePoints value="0"/>
|
||||
<maxCompositeContours value="0"/>
|
||||
<maxZones value="1"/>
|
||||
<maxTwilightPoints value="0"/>
|
||||
<maxStorage value="0"/>
|
||||
<maxFunctionDefs value="0"/>
|
||||
<maxInstructionDefs value="0"/>
|
||||
<maxStackElements value="0"/>
|
||||
<maxSizeOfInstructions value="0"/>
|
||||
<maxComponentElements value="0"/>
|
||||
<maxComponentDepth value="0"/>
|
||||
</maxp>
|
||||
|
||||
<OS_2>
|
||||
<!-- The fields 'usFirstCharIndex' and 'usLastCharIndex'
|
||||
will be recalculated by the compiler -->
|
||||
<version value="4"/>
|
||||
<xAvgCharWidth value="445"/>
|
||||
<usWeightClass value="400"/>
|
||||
<usWidthClass value="5"/>
|
||||
<fsType value="00000000 00000100"/>
|
||||
<ySubscriptXSize value="650"/>
|
||||
<ySubscriptYSize value="600"/>
|
||||
<ySubscriptXOffset value="0"/>
|
||||
<ySubscriptYOffset value="75"/>
|
||||
<ySuperscriptXSize value="650"/>
|
||||
<ySuperscriptYSize value="600"/>
|
||||
<ySuperscriptXOffset value="0"/>
|
||||
<ySuperscriptYOffset value="350"/>
|
||||
<yStrikeoutSize value="50"/>
|
||||
<yStrikeoutPosition value="300"/>
|
||||
<sFamilyClass value="0"/>
|
||||
<panose>
|
||||
<bFamilyType value="0"/>
|
||||
<bSerifStyle value="0"/>
|
||||
<bWeight value="0"/>
|
||||
<bProportion value="0"/>
|
||||
<bContrast value="0"/>
|
||||
<bStrokeVariation value="0"/>
|
||||
<bArmStyle value="0"/>
|
||||
<bLetterForm value="0"/>
|
||||
<bMidline value="0"/>
|
||||
<bXHeight value="0"/>
|
||||
</panose>
|
||||
<ulUnicodeRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulUnicodeRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange3 value="00000000 00000000 00000000 00000000"/>
|
||||
<ulUnicodeRange4 value="00000000 00000000 00000000 00000000"/>
|
||||
<achVendID value="NONE"/>
|
||||
<fsSelection value="00000000 11000000"/>
|
||||
<usFirstCharIndex value="65"/>
|
||||
<usLastCharIndex value="65"/>
|
||||
<sTypoAscender value="750"/>
|
||||
<sTypoDescender value="-250"/>
|
||||
<sTypoLineGap value="100"/>
|
||||
<usWinAscent value="750"/>
|
||||
<usWinDescent value="250"/>
|
||||
<ulCodePageRange1 value="00000000 00000000 00000000 00000001"/>
|
||||
<ulCodePageRange2 value="00000000 00000000 00000000 00000000"/>
|
||||
<sxHeight value="500"/>
|
||||
<sCapHeight value="700"/>
|
||||
<usDefaultChar value="0"/>
|
||||
<usBreakChar value="32"/>
|
||||
<usMaxContext value="0"/>
|
||||
</OS_2>
|
||||
|
||||
<hmtx>
|
||||
<mtx name=".notdef" width="250" lsb="0"/>
|
||||
<mtx name="A" width="639" lsb="178"/>
|
||||
</hmtx>
|
||||
|
||||
<cmap>
|
||||
<tableVersion version="0"/>
|
||||
<cmap_format_4 platformID="0" platEncID="3" language="0">
|
||||
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
|
||||
</cmap_format_4>
|
||||
<cmap_format_4 platformID="3" platEncID="1" language="0">
|
||||
<map code="0x41" name="A"/><!-- LATIN CAPITAL LETTER A -->
|
||||
</cmap_format_4>
|
||||
</cmap>
|
||||
|
||||
<loca>
|
||||
<!-- The 'loca' table will be calculated by the compiler -->
|
||||
</loca>
|
||||
|
||||
<glyf>
|
||||
|
||||
<!-- The xMin, yMin, xMax and yMax values
|
||||
will be recalculated by the compiler. -->
|
||||
|
||||
<TTGlyph name=".notdef"/><!-- contains no outline data -->
|
||||
|
||||
<TTGlyph name="A" xMin="178" yMin="72" xMax="586" yMax="480">
|
||||
<contour>
|
||||
<pt x="382" y="72" on="0" overlap="1"/>
|
||||
<pt x="336" y="72" on="1"/>
|
||||
<pt x="261" y="101" on="0"/>
|
||||
<pt x="207" y="155" on="0"/>
|
||||
<pt x="178" y="230" on="0"/>
|
||||
<pt x="178" y="276" on="1"/>
|
||||
<pt x="178" y="322" on="0"/>
|
||||
<pt x="207" y="397" on="0"/>
|
||||
<pt x="261" y="451" on="0"/>
|
||||
<pt x="336" y="480" on="0"/>
|
||||
<pt x="382" y="480" on="1"/>
|
||||
<pt x="428" y="480" on="0"/>
|
||||
<pt x="503" y="451" on="0"/>
|
||||
<pt x="557" y="397" on="0"/>
|
||||
<pt x="586" y="322" on="0"/>
|
||||
<pt x="586" y="276" on="1"/>
|
||||
<pt x="586" y="230" on="0"/>
|
||||
<pt x="557" y="155" on="0"/>
|
||||
<pt x="503" y="101" on="0"/>
|
||||
<pt x="428" y="72" on="0"/>
|
||||
</contour>
|
||||
<instructions/>
|
||||
</TTGlyph>
|
||||
|
||||
</glyf>
|
||||
|
||||
<name>
|
||||
<namerecord nameID="256" platformID="1" platEncID="0" langID="0x0" unicode="True">
|
||||
Weight
|
||||
</namerecord>
|
||||
<namerecord nameID="1" platformID="3" platEncID="1" langID="0x409">
|
||||
Unnamed
|
||||
</namerecord>
|
||||
<namerecord nameID="2" platformID="3" platEncID="1" langID="0x409">
|
||||
Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="3" platformID="3" platEncID="1" langID="0x409">
|
||||
1.000;NONE;Unnamed-Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="4" platformID="3" platEncID="1" langID="0x409">
|
||||
Unnamed Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="5" platformID="3" platEncID="1" langID="0x409">
|
||||
Version 1.000
|
||||
</namerecord>
|
||||
<namerecord nameID="6" platformID="3" platEncID="1" langID="0x409">
|
||||
Unnamed-Regular
|
||||
</namerecord>
|
||||
<namerecord nameID="256" platformID="3" platEncID="1" langID="0x409">
|
||||
Weight
|
||||
</namerecord>
|
||||
</name>
|
||||
|
||||
<post>
|
||||
<formatType value="2.0"/>
|
||||
<italicAngle value="0.0"/>
|
||||
<underlinePosition value="-100"/>
|
||||
<underlineThickness value="50"/>
|
||||
<isFixedPitch value="0"/>
|
||||
<minMemType42 value="0"/>
|
||||
<maxMemType42 value="0"/>
|
||||
<minMemType1 value="0"/>
|
||||
<maxMemType1 value="0"/>
|
||||
<psNames>
|
||||
<!-- This file uses unique glyph names based on the information
|
||||
found in the 'post' table. Since these names might not be unique,
|
||||
we have to invent artificial names in case of clashes. In order to
|
||||
be able to retain the original information, we need a name to
|
||||
ps name mapping for those cases where they differ. That's what
|
||||
you see below.
|
||||
-->
|
||||
</psNames>
|
||||
<extraNames>
|
||||
<!-- following are the name that are not taken from the standard Mac glyph order -->
|
||||
</extraNames>
|
||||
</post>
|
||||
|
||||
<gasp>
|
||||
<gaspRange rangeMaxPPEM="65535" rangeGaspBehavior="15"/>
|
||||
</gasp>
|
||||
|
||||
<HVAR>
|
||||
<Version value="0x00010000"/>
|
||||
<VarStore Format="1">
|
||||
<Format value="1"/>
|
||||
<VarRegionList>
|
||||
<!-- RegionAxisCount=1 -->
|
||||
<!-- RegionCount=1 -->
|
||||
<Region index="0">
|
||||
<VarRegionAxis index="0">
|
||||
<StartCoord value="0.0"/>
|
||||
<PeakCoord value="1.0"/>
|
||||
<EndCoord value="1.0"/>
|
||||
</VarRegionAxis>
|
||||
</Region>
|
||||
</VarRegionList>
|
||||
<!-- VarDataCount=1 -->
|
||||
<VarData index="0">
|
||||
<!-- ItemCount=2 -->
|
||||
<NumShorts value="0"/>
|
||||
<!-- VarRegionCount=0 -->
|
||||
<Item index="0" value="[]"/>
|
||||
<Item index="1" value="[]"/>
|
||||
</VarData>
|
||||
</VarStore>
|
||||
</HVAR>
|
||||
|
||||
<STAT>
|
||||
<Version value="0x00010001"/>
|
||||
<DesignAxisRecordSize value="8"/>
|
||||
<!-- DesignAxisCount=1 -->
|
||||
<DesignAxisRecord>
|
||||
<Axis index="0">
|
||||
<AxisTag value="wght"/>
|
||||
<AxisNameID value="256"/> <!-- Weight -->
|
||||
<AxisOrdering value="0"/>
|
||||
</Axis>
|
||||
</DesignAxisRecord>
|
||||
<!-- AxisValueCount=0 -->
|
||||
<ElidedFallbackNameID value="2"/> <!-- Regular -->
|
||||
</STAT>
|
||||
|
||||
<fvar>
|
||||
|
||||
<!-- Weight -->
|
||||
<Axis>
|
||||
<AxisTag>wght</AxisTag>
|
||||
<Flags>0x0</Flags>
|
||||
<MinValue>400.0</MinValue>
|
||||
<DefaultValue>400.0</DefaultValue>
|
||||
<MaxValue>700.0</MaxValue>
|
||||
<AxisNameID>256</AxisNameID>
|
||||
</Axis>
|
||||
</fvar>
|
||||
|
||||
<gvar>
|
||||
<version value="1"/>
|
||||
<reserved value="0"/>
|
||||
<glyphVariations glyph="A">
|
||||
<tuple>
|
||||
<coord axis="wght" value="1.0"/>
|
||||
<delta pt="0" x="-64" y="-44"/>
|
||||
<delta pt="5" x="-138" y="30"/>
|
||||
<delta pt="7" x="-127" y="73"/>
|
||||
<delta pt="8" x="-108" y="92"/>
|
||||
<delta pt="10" x="-64" y="103"/>
|
||||
<delta pt="12" x="-21" y="92"/>
|
||||
<delta pt="13" x="-2" y="73"/>
|
||||
<delta pt="16" x="9" y="13"/>
|
||||
<delta pt="17" x="-2" y="-14"/>
|
||||
<delta pt="18" x="-21" y="-33"/>
|
||||
</tuple>
|
||||
</glyphVariations>
|
||||
</gvar>
|
||||
|
||||
</ttFont>
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user