Merge remote-tracking branch 'origin/master' into otdata-colr

This commit is contained in:
Cosimo Lupo 2020-03-02 10:59:55 +00:00
commit d659e055b2
No known key found for this signature in database
GPG Key ID: 20D4A261E4A0E642
39 changed files with 1067 additions and 172 deletions

View File

@ -1,5 +1,5 @@
language: python
python: 3.5
python: 3.6
env:
global:
@ -16,9 +16,6 @@ branches:
matrix:
fast_finish: true
exclude:
# Exclude the default Python 3.6 build
- python: 3.6
include:
- python: 3.6
env:
@ -52,6 +49,11 @@ cache:
- directories:
- $HOME/.pyenv_cache
addons:
apt:
packages:
- language-pack-de
before_install:
- source ./.travis/before_install.sh

View File

@ -4,6 +4,6 @@ from fontTools.misc.loggingTools import configLogger
log = logging.getLogger(__name__)
version = __version__ = "4.3.1.dev0"
version = __version__ = "4.4.2.dev0"
__all__ = ["version", "log", "configLogger"]

View File

View File

@ -0,0 +1,147 @@
import enum
from typing import Dict, Iterable, List, Optional, Tuple, Union
from fontTools.ttLib.tables.C_O_L_R_ import LayerRecord, table_C_O_L_R_
from fontTools.ttLib.tables.C_P_A_L_ import Color, table_C_P_A_L_
from fontTools.ttLib.tables._n_a_m_e import table__n_a_m_e
from .errors import ColorLibError
def buildCOLR(colorLayers: Dict[str, List[Tuple[str, int]]]) -> table_C_O_L_R_:
"""Build COLR table from color layers mapping.
Args:
colorLayers: : map of base glyph names to lists of (layer glyph names,
palette indices) tuples.
Return:
A new COLRv0 table.
"""
colorLayerLists = {}
for baseGlyphName, layers in colorLayers.items():
colorLayerLists[baseGlyphName] = [
LayerRecord(layerGlyphName, colorID) for layerGlyphName, colorID in layers
]
colr = table_C_O_L_R_()
colr.version = 0
colr.ColorLayers = colorLayerLists
return colr
class ColorPaletteType(enum.IntFlag):
USABLE_WITH_LIGHT_BACKGROUND = 0x0001
USABLE_WITH_DARK_BACKGROUND = 0x0002
@classmethod
def _missing_(cls, value):
# enforce reserved bits
if isinstance(value, int) and (value < 0 or value & 0xFFFC != 0):
raise ValueError(f"{value} is not a valid {cls.__name__}")
return super()._missing_(value)
# None, 'abc' or {'en': 'abc', 'de': 'xyz'}
_OptionalLocalizedString = Union[None, str, Dict[str, str]]
def buildPaletteLabels(
labels: List[_OptionalLocalizedString], nameTable: table__n_a_m_e
) -> List[Optional[int]]:
return [
nameTable.addMultilingualName(l, mac=False)
if isinstance(l, dict)
else table_C_P_A_L_.NO_NAME_ID
if l is None
else nameTable.addMultilingualName({"en": l}, mac=False)
for l in labels
]
def buildCPAL(
palettes: List[List[Tuple[float, float, float, float]]],
paletteTypes: Optional[List[ColorPaletteType]] = None,
paletteLabels: Optional[List[_OptionalLocalizedString]] = None,
paletteEntryLabels: Optional[List[_OptionalLocalizedString]] = None,
nameTable: Optional[table__n_a_m_e] = None,
) -> table_C_P_A_L_:
"""Build CPAL table from list of color palettes.
Args:
palettes: list of lists of colors encoded as tuples of (R, G, B, A) floats
in the range [0..1].
paletteTypes: optional list of ColorPaletteType, one for each palette.
paletteLabels: optional list of palette labels. Each lable can be either:
None (no label), a string (for for default English labels), or a
localized string (as a dict keyed with BCP47 language codes).
paletteEntryLabels: optional list of palette entry labels, one for each
palette entry (see paletteLabels).
nameTable: optional name table where to store palette and palette entry
labels. Required if either paletteLabels or paletteEntryLabels is set.
Return:
A new CPAL v0 or v1 table, if custom palette types or labels are specified.
"""
if len({len(p) for p in palettes}) != 1:
raise ColorLibError("color palettes have different lengths")
if (paletteLabels or paletteEntryLabels) and not nameTable:
raise TypeError(
"nameTable is required if palette or palette entries have labels"
)
cpal = table_C_P_A_L_()
cpal.numPaletteEntries = len(palettes[0])
cpal.palettes = []
for i, palette in enumerate(palettes):
colors = []
for j, color in enumerate(palette):
if not isinstance(color, tuple) or len(color) != 4:
raise ColorLibError(
f"In palette[{i}][{j}]: expected (R, G, B, A) tuple, got {color!r}"
)
if any(v > 1 or v < 0 for v in color):
raise ColorLibError(
f"palette[{i}][{j}] has invalid out-of-range [0..1] color: {color!r}"
)
# input colors are RGBA, CPAL encodes them as BGRA
red, green, blue, alpha = color
colors.append(Color(*(round(v * 255) for v in (blue, green, red, alpha))))
cpal.palettes.append(colors)
if any(v is not None for v in (paletteTypes, paletteLabels, paletteEntryLabels)):
cpal.version = 1
if paletteTypes is not None:
if len(paletteTypes) != len(palettes):
raise ColorLibError(
f"Expected {len(palettes)} paletteTypes, got {len(paletteTypes)}"
)
cpal.paletteTypes = [ColorPaletteType(t).value for t in paletteTypes]
else:
cpal.paletteTypes = [table_C_P_A_L_.DEFAULT_PALETTE_TYPE] * len(palettes)
if paletteLabels is not None:
if len(paletteLabels) != len(palettes):
raise ColorLibError(
f"Expected {len(palettes)} paletteLabels, got {len(paletteLabels)}"
)
cpal.paletteLabels = buildPaletteLabels(paletteLabels, nameTable)
else:
cpal.paletteLabels = [table_C_P_A_L_.NO_NAME_ID] * len(palettes)
if paletteEntryLabels is not None:
if len(paletteEntryLabels) != cpal.numPaletteEntries:
raise ColorLibError(
f"Expected {cpal.numPaletteEntries} paletteEntryLabels, "
f"got {len(paletteEntryLabels)}"
)
cpal.paletteEntryLabels = buildPaletteLabels(paletteEntryLabels, nameTable)
else:
cpal.paletteEntryLabels = [
table_C_P_A_L_.NO_NAME_ID
] * cpal.numPaletteEntries
else:
cpal.version = 0
return cpal

View File

@ -0,0 +1,3 @@
class ColorLibError(Exception):
pass

View File

@ -9,10 +9,18 @@ class FeatureLibError(Exception):
message = Exception.__str__(self)
if self.location:
path, line, column = self.location
return "%s:%d:%d: %s" % (path, line, column, message)
return f"{path}:{line}:{column}: {message}"
else:
return message
class IncludedFeaNotFound(FeatureLibError):
pass
def __str__(self):
assert self.location is not None
message = (
"The following feature file should be included but cannot be found: "
f"{Exception.__str__(self)}"
)
path, line, column = self.location
return f"{path}:{line}:{column}: {message}"

View File

@ -19,6 +19,14 @@ class Parser(object):
def __init__(self, featurefile, glyphNames=(), followIncludes=True,
**kwargs):
"""Initializes a Parser object.
Note: the `glyphNames` iterable serves a double role to help distinguish
glyph names from ranges in the presence of hyphens and to ensure that glyph
names referenced in a feature file are actually part of a font's glyph set.
If the iterable is left empty, no glyph name in glyph set checking takes
place.
"""
if "glyphMap" in kwargs:
from fontTools.misc.loggingTools import deprecateArgument
deprecateArgument("glyphMap", "use 'glyphNames' (iterable) instead")
@ -268,6 +276,7 @@ class Parser(object):
if (accept_glyphname and
self.next_token_type_ in (Lexer.NAME, Lexer.CID)):
glyph = self.expect_glyph_()
self.check_glyph_name_in_glyph_set(glyph)
return self.ast.GlyphName(glyph, location=self.cur_token_location_)
if self.next_token_type_ is Lexer.GLYPHCLASS:
self.advance_lexer_()
@ -292,6 +301,7 @@ class Parser(object):
location = self.cur_token_location_
if '-' in glyph and glyph not in self.glyphNames_:
start, limit = self.split_glyph_range_(glyph, location)
self.check_glyph_name_in_glyph_set(start, limit)
glyphs.add_range(
start, limit,
self.make_glyph_range_(location, start, limit))
@ -299,10 +309,12 @@ class Parser(object):
start = glyph
self.expect_symbol_("-")
limit = self.expect_glyph_()
self.check_glyph_name_in_glyph_set(start, limit)
glyphs.add_range(
start, limit,
self.make_glyph_range_(location, start, limit))
else:
self.check_glyph_name_in_glyph_set(glyph)
glyphs.append(glyph)
elif self.next_token_type_ is Lexer.CID:
glyph = self.expect_glyph_()
@ -311,11 +323,17 @@ class Parser(object):
range_start = self.cur_token_
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}",
)
glyphs.add_cid_range(range_start, range_end,
self.make_cid_range_(range_location,
range_start, range_end))
else:
glyphs.append("cid%05d" % self.cur_token_)
glyph_name = f"cid{self.cur_token_:05d}"
self.check_glyph_name_in_glyph_set(glyph_name)
glyphs.append(glyph_name)
elif self.next_token_type_ is Lexer.GLYPHCLASS:
self.advance_lexer_()
gc = self.glyphclasses_.resolve(self.cur_token_)
@ -1509,6 +1527,21 @@ class Parser(object):
raise FeatureLibError("Expected a glyph name or CID",
self.cur_token_location_)
def check_glyph_name_in_glyph_set(self, *names):
"""Raises if glyph name (just `start`) or glyph names of a
range (`start` and `end`) are not in the glyph set.
If no glyph set is present, does nothing.
"""
if self.glyphNames_:
missing = [name for name in names if name not in self.glyphNames_]
if missing:
raise FeatureLibError(
"The following glyph names are referenced but are missing from the "
f"glyph set: {', '.join(missing)}",
self.cur_token_location_
)
def expect_markClass_reference_(self):
name = self.expect_class_name_()
mc = self.glyphclasses_.resolve(name)

View File

@ -506,6 +506,7 @@ class FontBuilder(object):
fontSet = CFFFontSet()
fontSet.major = 1
fontSet.minor = 0
fontSet.otFont = self.font
fontSet.fontNames = [psName]
fontSet.topDictIndex = TopDictIndex()
@ -520,6 +521,7 @@ class FontBuilder(object):
topDict = TopDict()
topDict.charset = self.font.getGlyphOrder()
topDict.Private = private
topDict.GlobalSubrs = fontSet.GlobalSubrs
for key, value in fontInfo.items():
setattr(topDict, key, value)
if "FontMatrix" not in fontInfo:
@ -768,6 +770,39 @@ class FontBuilder(object):
self.font, conditionalSubstitutions, featureTag=featureTag
)
def setupCOLR(self, colorLayers):
"""Build new COLR table using color layers dictionary.
Cf. `fontTools.colorLib.builder.buildCOLR`.
"""
from fontTools.colorLib.builder import buildCOLR
self.font["COLR"] = buildCOLR(colorLayers)
def setupCPAL(
self,
palettes,
paletteTypes=None,
paletteLabels=None,
paletteEntryLabels=None,
):
"""Build new CPAL table using list of palettes.
Optionally build CPAL v1 table using paletteTypes, paletteLabels and
paletteEntryLabels.
Cf. `fontTools.colorLib.builder.buildCPAL`.
"""
from fontTools.colorLib.builder import buildCPAL
self.font["CPAL"] = buildCPAL(
palettes,
paletteTypes=paletteTypes,
paletteLabels=paletteLabels,
paletteEntryLabels=paletteEntryLabels,
nameTable=self.font.get("name")
)
def buildCmapSubTable(cmapping, format, platformID, platEncID):
subTable = cmap_classes[format](format)

View File

@ -4,6 +4,7 @@
from fontTools.misc.py23 import *
import os
import time
from datetime import datetime, timezone
import calendar
@ -44,7 +45,12 @@ def timestampToString(value):
return asctime(time.gmtime(max(0, value + epoch_diff)))
def timestampFromString(value):
return calendar.timegm(time.strptime(value)) - epoch_diff
wkday, mnth = value[:7].split()
t = datetime.strptime(value[7:], ' %d %H:%M:%S %Y')
t = t.replace(month=MONTHNAMES.index(mnth), tzinfo=timezone.utc)
wkday_idx = DAYNAMES.index(wkday)
assert t.weekday() == wkday_idx, '"' + value + '" has inconsistent weekday'
return int(t.timestamp()) - epoch_diff
def timestampNow():
# https://reproducible-builds.org/specs/source-date-epoch/

View File

@ -1,6 +1,6 @@
from fontTools.misc.py23 import *
from array import array
from fontTools.misc.fixedTools import MAX_F2DOT14, otRound
from fontTools.misc.fixedTools import MAX_F2DOT14, otRound, floatToFixedToFloat
from fontTools.pens.basePen import LoggingPen
from fontTools.pens.transformPen import TransformPen
from fontTools.ttLib.tables import ttProgram
@ -119,7 +119,11 @@ class TTGlyphPen(LoggingPen):
component = GlyphComponent()
component.glyphName = glyphName
component.x, component.y = (otRound(v) for v in transformation[4:])
transformation = transformation[:4]
# quantize floats to F2Dot14 so we get same values as when decompiled
# from a binary glyf table
transformation = tuple(
floatToFixedToFloat(v, 14) for v in transformation[:4]
)
if transformation != (1, 0, 0, 1):
if (self.handleOverflowingTransforms and
any(MAX_F2DOT14 < s <= 2 for s in transformation)):

View File

@ -13,6 +13,9 @@ import sys
class table_C_P_A_L_(DefaultTable.DefaultTable):
NO_NAME_ID = 0xFFFF
DEFAULT_PALETTE_TYPE = 0
def __init__(self, tag=None):
DefaultTable.DefaultTable.__init__(self, tag)
self.palettes = []
@ -45,24 +48,25 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
offsetToPaletteEntryLabelArray) = (
struct.unpack(">LLL", data[pos:pos+12]))
self.paletteTypes = self._decompileUInt32Array(
data, offsetToPaletteTypeArray, numPalettes)
data, offsetToPaletteTypeArray, numPalettes,
default=self.DEFAULT_PALETTE_TYPE)
self.paletteLabels = self._decompileUInt16Array(
data, offsetToPaletteLabelArray, numPalettes)
data, offsetToPaletteLabelArray, numPalettes, default=self.NO_NAME_ID)
self.paletteEntryLabels = self._decompileUInt16Array(
data, offsetToPaletteEntryLabelArray,
self.numPaletteEntries)
self.numPaletteEntries, default=self.NO_NAME_ID)
def _decompileUInt16Array(self, data, offset, numElements):
def _decompileUInt16Array(self, data, offset, numElements, default=0):
if offset == 0:
return [0] * numElements
return [default] * numElements
result = array.array("H", data[offset : offset + 2 * numElements])
if sys.byteorder != "big": result.byteswap()
assert len(result) == numElements, result
return result.tolist()
def _decompileUInt32Array(self, data, offset, numElements):
def _decompileUInt32Array(self, data, offset, numElements, default=0):
if offset == 0:
return [0] * numElements
return [default] * numElements
result = array.array("I", data[offset : offset + 4 * numElements])
if sys.byteorder != "big": result.byteswap()
assert len(result) == numElements, result
@ -136,7 +140,7 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
return result
def _compilePaletteLabels(self):
if self.version == 0 or not any(self.paletteLabels):
if self.version == 0 or all(l == self.NO_NAME_ID for l in self.paletteLabels):
return b''
assert len(self.paletteLabels) == len(self.palettes)
result = bytesjoin([struct.pack(">H", label)
@ -145,7 +149,7 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
return result
def _compilePaletteEntryLabels(self):
if self.version == 0 or not any(self.paletteEntryLabels):
if self.version == 0 or all(l == self.NO_NAME_ID for l in self.paletteEntryLabels):
return b''
assert len(self.paletteEntryLabels) == self.numPaletteEntries
result = bytesjoin([struct.pack(">H", label)
@ -165,15 +169,15 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
writer.newline()
for index, palette in enumerate(self.palettes):
attrs = {"index": index}
paletteType = paletteTypes.get(index)
paletteLabel = paletteLabels.get(index)
if self.version > 0 and paletteLabel is not None:
paletteType = paletteTypes.get(index, self.DEFAULT_PALETTE_TYPE)
paletteLabel = paletteLabels.get(index, self.NO_NAME_ID)
if self.version > 0 and paletteLabel != self.NO_NAME_ID:
attrs["label"] = paletteLabel
if self.version > 0 and paletteType is not None:
if self.version > 0 and paletteType != self.DEFAULT_PALETTE_TYPE:
attrs["type"] = paletteType
writer.begintag("palette", **attrs)
writer.newline()
if (self.version > 0 and paletteLabel and
if (self.version > 0 and paletteLabel != self.NO_NAME_ID and
ttFont and "name" in ttFont):
name = ttFont["name"].getDebugName(paletteLabel)
if name is not None:
@ -184,11 +188,11 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
color.toXML(writer, ttFont, cindex)
writer.endtag("palette")
writer.newline()
if self.version > 0 and any(self.paletteEntryLabels):
if self.version > 0 and not all(l == self.NO_NAME_ID for l in self.paletteEntryLabels):
writer.begintag("paletteEntryLabels")
writer.newline()
for index, label in enumerate(self.paletteEntryLabels):
if label:
if label != self.NO_NAME_ID:
writer.simpletag("label", index=index, value=label)
if (self.version > 0 and label and ttFont and "name" in ttFont):
name = ttFont["name"].getDebugName(label)
@ -200,8 +204,8 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
def fromXML(self, name, attrs, content, ttFont):
if name == "palette":
self.paletteLabels.append(int(attrs.get("label", "0")))
self.paletteTypes.append(int(attrs.get("type", "0")))
self.paletteLabels.append(int(attrs.get("label", self.NO_NAME_ID)))
self.paletteTypes.append(int(attrs.get("type", self.DEFAULT_PALETTE_TYPE)))
palette = []
for element in content:
if isinstance(element, basestring):
@ -221,13 +225,13 @@ class table_C_P_A_L_(DefaultTable.DefaultTable):
nameID = safeEval(elementAttr["value"])
colorLabels[labelIndex] = nameID
self.paletteEntryLabels = [
colorLabels.get(i, 0)
colorLabels.get(i, self.NO_NAME_ID)
for i in range(self.numPaletteEntries)]
elif "value" in attrs:
value = safeEval(attrs["value"])
setattr(self, name, value)
if name == "numPaletteEntries":
self.paletteEntryLabels = [0] * self.numPaletteEntries
self.paletteEntryLabels = [self.NO_NAME_ID] * self.numPaletteEntries
class Color(namedtuple("Color", "blue green red alpha")):

View File

@ -122,6 +122,12 @@ class table__g_l_y_f(DefaultTable.DefaultTable):
ttFont['loca'].set(locations)
if 'maxp' in ttFont:
ttFont['maxp'].numGlyphs = len(self.glyphs)
if not data:
# As a special case when all glyph in the font are empty, add a zero byte
# to the table, so that OTS doesnt reject it, and to make the table work
# on Windows as well.
# See https://github.com/khaledhosny/ots/issues/52
data = b"\0"
return data
def toXML(self, writer, ttFont, splitGlyphs=False):
@ -1006,33 +1012,37 @@ class Glyph(object):
coordinates, endPts, flags = g.getCoordinates(glyfTable)
except RecursionError:
raise ttLib.TTLibError("glyph '%s' contains a recursive component reference" % compo.glyphName)
coordinates = GlyphCoordinates(coordinates)
if hasattr(compo, "firstPt"):
# move according to two reference points
# component uses two reference points: we apply the transform _before_
# computing the offset between the points
if hasattr(compo, "transform"):
coordinates.transform(compo.transform)
x1,y1 = allCoords[compo.firstPt]
x2,y2 = coordinates[compo.secondPt]
move = x1-x2, y1-y2
else:
move = compo.x, compo.y
coordinates = GlyphCoordinates(coordinates)
if not hasattr(compo, "transform"):
coordinates.translate(move)
else:
apple_way = compo.flags & SCALED_COMPONENT_OFFSET
ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
assert not (apple_way and ms_way)
if not (apple_way or ms_way):
scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file
else:
scale_component_offset = apple_way
if scale_component_offset:
# the Apple way: first move, then scale (ie. scale the component offset)
# component uses XY offsets
move = compo.x, compo.y
if not hasattr(compo, "transform"):
coordinates.translate(move)
coordinates.transform(compo.transform)
else:
# the MS way: first scale, then move
coordinates.transform(compo.transform)
coordinates.translate(move)
apple_way = compo.flags & SCALED_COMPONENT_OFFSET
ms_way = compo.flags & UNSCALED_COMPONENT_OFFSET
assert not (apple_way and ms_way)
if not (apple_way or ms_way):
scale_component_offset = SCALE_COMPONENT_OFFSET_DEFAULT # see top of this file
else:
scale_component_offset = apple_way
if scale_component_offset:
# the Apple way: first move, then scale (ie. scale the component offset)
coordinates.translate(move)
coordinates.transform(compo.transform)
else:
# the MS way: first scale, then move
coordinates.transform(compo.transform)
coordinates.translate(move)
offset = len(allCoords)
allEndPts.extend(e + offset for e in endPts)
allCoords.extend(coordinates)

View File

@ -41,7 +41,7 @@ class table__h_e_a_d(DefaultTable.DefaultTable):
if rest:
# this is quite illegal, but there seem to be fonts out there that do this
log.warning("extra bytes at the end of 'head' table")
assert rest == "\0\0"
assert rest == b"\0\0"
# For timestamp fields, ignore the top four bytes. Some fonts have
# bogus values there. Since till 2038 those bytes only can be zero,

View File

@ -226,7 +226,11 @@ class WOFF2Writer(SFNTWriter):
# See:
# https://github.com/khaledhosny/ots/issues/60
# https://github.com/google/woff2/issues/15
if isTrueType and "glyf" in self.flavorData.transformedTables:
if (
isTrueType
and "glyf" in self.flavorData.transformedTables
and "glyf" in self.tables
):
self._normaliseGlyfAndLoca(padding=4)
self._setHeadTransformFlag()

View File

@ -2146,18 +2146,14 @@ def convertFontInfoValueForAttributeFromVersion2ToVersion3(attr, value):
"""
if attr in _ufo2To3FloatToInt:
try:
v = int(round(value))
value = round(value)
except (ValueError, TypeError):
raise UFOLibError("Could not convert value for %s." % attr)
if v != value:
value = v
if attr in _ufo2To3NonNegativeInt:
try:
v = int(abs(value))
value = int(abs(value))
except (ValueError, TypeError):
raise UFOLibError("Could not convert value for %s." % attr)
if v != value:
value = v
elif attr in _ufo2To3NonNegativeIntOrFloat:
try:
v = float(abs(value))

View File

@ -39,13 +39,11 @@ import os.path
import logging
from copy import deepcopy
from pprint import pformat
from .errors import VarLibError, VarLibValidationError
log = logging.getLogger("fontTools.varLib")
class VarLibError(Exception):
pass
#
# Creation routines
#
@ -81,7 +79,12 @@ def _add_fvar(font, axes, instances):
coordinates = instance.location
if "en" not in instance.localisedStyleName:
assert instance.styleName
if not instance.styleName:
raise VarLibValidationError(
f"Instance at location '{coordinates}' must have a default English "
"style name ('stylename' attribute on the instance element or a "
"stylename element with an 'xml:lang=\"en\"' attribute)."
)
localisedStyleName = dict(instance.localisedStyleName)
localisedStyleName["en"] = tounicode(instance.styleName)
else:
@ -137,20 +140,32 @@ def _add_avar(font, axes):
# Current avar requirements. We don't have to enforce
# these on the designer and can deduce some ourselves,
# but for now just enforce them.
assert axis.minimum == min(keys)
assert axis.maximum == max(keys)
assert axis.default in keys
# No duplicates
assert len(set(keys)) == len(keys), (
f"{axis.tag} axis: All axis mapping input='...' "
"values must be unique, but we found duplicates."
)
assert len(set(vals)) == len(vals), (
f"{axis.tag} axis: All axis mapping output='...' "
"values must be unique, but we found duplicates."
)
if axis.minimum != min(keys):
raise VarLibValidationError(
f"Axis '{axis.name}': there must be a mapping for the axis minimum "
f"value {axis.minimum} and it must be the lowest input mapping value."
)
if axis.maximum != max(keys):
raise VarLibValidationError(
f"Axis '{axis.name}': there must be a mapping for the axis maximum "
f"value {axis.maximum} and it must be the highest input mapping value."
)
if axis.default not in keys:
raise VarLibValidationError(
f"Axis '{axis.name}': there must be a mapping for the axis default "
f"value {axis.default}."
)
# No duplicate input values (output values can be >= their preceeding value).
if len(set(keys)) != len(keys):
raise VarLibValidationError(
f"Axis '{axis.name}': All axis mapping input='...' values must be "
"unique, but we found duplicates."
)
# Ascending values
assert sorted(vals) == vals
if sorted(vals) != vals:
raise VarLibValidationError(
f"Axis '{axis.name}': mapping output values must be in ascending order."
)
keys_triple = (axis.minimum, axis.default, axis.maximum)
vals_triple = tuple(axis.map_forward(v) for v in keys_triple)
@ -214,8 +229,8 @@ def _add_stat(font, axes):
def _add_gvar(font, masterModel, master_ttfs, tolerance=0.5, optimize=True):
assert tolerance >= 0
if tolerance < 0:
raise ValueError("`tolerance` must be a positive number.")
log.info("Generating gvar")
assert "gvar" not in font
@ -703,9 +718,10 @@ def load_designspace(designspace):
masters = ds.sources
if not masters:
raise VarLibError("no sources found in .designspace")
raise VarLibValidationError("Designspace must have at least one source.")
instances = ds.instances
# TODO: Use fontTools.designspaceLib.tagForAxisName instead.
standard_axis_map = OrderedDict([
('weight', ('wght', {'en': u'Weight'})),
('width', ('wdth', {'en': u'Width'})),
@ -715,11 +731,15 @@ def load_designspace(designspace):
])
# Setup axes
if not ds.axes:
raise VarLibValidationError(f"Designspace must have at least one axis.")
axes = OrderedDict()
for axis in ds.axes:
for axis_index, axis in enumerate(ds.axes):
axis_name = axis.name
if not axis_name:
assert axis.tag is not None
if not axis.tag:
raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
axis_name = axis.name = axis.tag
if axis_name in standard_axis_map:
@ -728,7 +748,8 @@ def load_designspace(designspace):
if not axis.labelNames:
axis.labelNames.update(standard_axis_map[axis_name][1])
else:
assert axis.tag is not None
if not axis.tag:
raise VarLibValidationError(f"Axis at index {axis_index} needs a tag.")
if not axis.labelNames:
axis.labelNames["en"] = tounicode(axis_name)
@ -739,14 +760,28 @@ def load_designspace(designspace):
for obj in masters+instances:
obj_name = obj.name or obj.styleName or ''
loc = obj.location
if loc is None:
raise VarLibValidationError(
f"Source or instance '{obj_name}' has no location."
)
for axis_name in loc.keys():
assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name)
if axis_name not in axes:
raise VarLibValidationError(
f"Location axis '{axis_name}' unknown for '{obj_name}'."
)
for axis_name,axis in axes.items():
if axis_name not in loc:
loc[axis_name] = axis.default
# NOTE: `axis.default` is always user-space, but `obj.location` always design-space.
loc[axis_name] = axis.map_forward(axis.default)
else:
v = axis.map_backward(loc[axis_name])
assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum)
if not (axis.minimum <= v <= axis.maximum):
raise VarLibValidationError(
f"Source or instance '{obj_name}' has out-of-range location "
f"for axis '{axis_name}': is mapped to {v} but must be in "
f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all "
"values are in user-space)."
)
# Normalize master locations
@ -767,9 +802,15 @@ def load_designspace(designspace):
base_idx = None
for i,m in enumerate(normalized_master_locs):
if all(v == 0 for v in m.values()):
assert base_idx is None
if base_idx is not None:
raise VarLibValidationError(
"More than one base master found in Designspace."
)
base_idx = i
assert base_idx is not None, "Base master not found; no master at default location?"
if base_idx is None:
raise VarLibValidationError(
"Base master not found; no master at default location?"
)
log.info("Index of base master: %s", base_idx)
return _DesignSpaceData(
@ -926,7 +967,7 @@ def _open_font(path, master_finder=lambda s: s):
elif tp in ("TTF", "OTF", "WOFF", "WOFF2"):
font = TTFont(master_path)
else:
raise VarLibError("Invalid master path: %r" % master_path)
raise VarLibValidationError("Invalid master path: %r" % master_path)
return font
@ -946,10 +987,10 @@ def load_masters(designspace, master_finder=lambda s: s):
# If a SourceDescriptor has a layer name, demand that the compiled TTFont
# be supplied by the caller. This spares us from modifying MasterFinder.
if master.layerName and master.font is None:
raise AttributeError(
"Designspace source '%s' specified a layer name but lacks the "
"required TTFont object in the 'font' attribute."
% (master.name or "<Unknown>")
raise VarLibValidationError(
f"Designspace source '{master.name or '<Unknown>'}' specified a "
"layer name but lacks the required TTFont object in the 'font' "
"attribute."
)
return designspace.loadSourceFonts(_open_font, master_finder=master_finder)

View File

@ -1,5 +1,4 @@
from collections import namedtuple
import os
from fontTools.cffLib import (
maxStackLimit,
TopDictIndex,
@ -21,6 +20,13 @@ from fontTools.varLib.models import allEqual
from fontTools.misc.psCharStrings import T2CharString, T2OutlineExtractor
from fontTools.pens.t2CharStringPen import T2CharStringPen, t2c_round
from .errors import VarLibCFFDictMergeError, VarLibCFFPointTypeMergeError, VarLibMergeError
# Backwards compatibility
MergeDictError = VarLibCFFDictMergeError
MergeTypeError = VarLibCFFPointTypeMergeError
def addCFFVarStore(varFont, varModel, varDataList, masterSupports):
fvarTable = varFont['fvar']
@ -126,16 +132,6 @@ def convertCFFtoCFF2(varFont):
del varFont['CFF ']
class MergeDictError(TypeError):
def __init__(self, key, value, values):
error_msg = ["For the Private Dict key '{}', ".format(key),
"the default font value list:",
"\t{}".format(value),
"had a different number of values than a region font:"]
error_msg += ["\t{}".format(region_value) for region_value in values]
error_msg = os.linesep.join(error_msg)
def conv_to_int(num):
if isinstance(num, float) and num.is_integer():
return int(num)
@ -219,7 +215,7 @@ def merge_PrivateDicts(top_dicts, vsindex_dict, var_model, fd_map):
try:
values = zip(*values)
except IndexError:
raise MergeDictError(key, value, values)
raise VarLibCFFDictMergeError(key, value, values)
"""
Row 0 contains the first value from each master.
Convert each row from absolute values to relative
@ -426,21 +422,6 @@ def merge_charstrings(glyphOrder, num_masters, top_dicts, masterModel):
return cvData
class MergeTypeError(TypeError):
def __init__(self, point_type, pt_index, m_index, default_type, glyphName):
self.error_msg = [
"In glyph '{gname}' "
"'{point_type}' at point index {pt_index} in master "
"index {m_index} differs from the default font point "
"type '{default_type}'"
"".format(
gname=glyphName,
point_type=point_type, pt_index=pt_index,
m_index=m_index, default_type=default_type)
][0]
super(MergeTypeError, self).__init__(self.error_msg)
def makeRoundNumberFunc(tolerance):
if tolerance < 0:
raise ValueError("Rounding tolerance must be positive")
@ -547,7 +528,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
else:
cmd = self._commands[self.pt_index]
if cmd[0] != point_type:
raise MergeTypeError(
raise VarLibCFFPointTypeMergeError(
point_type,
self.pt_index, len(cmd[1]),
cmd[0], self.glyphName)
@ -560,7 +541,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
else:
cmd = self._commands[self.pt_index]
if cmd[0] != hint_type:
raise MergeTypeError(hint_type, self.pt_index, len(cmd[1]),
raise VarLibCFFPointTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
cmd[0], self.glyphName)
cmd[1].append(args)
self.pt_index += 1
@ -576,7 +557,7 @@ class CFF2CharStringMergePen(T2CharStringPen):
else:
cmd = self._commands[self.pt_index]
if cmd[0] != hint_type:
raise MergeTypeError(hint_type, self.pt_index, len(cmd[1]),
raise VarLibCFFPointTypeMergeError(hint_type, self.pt_index, len(cmd[1]),
cmd[0], self.glyphName)
self.pt_index += 1
cmd = self._commands[self.pt_index]
@ -646,8 +627,8 @@ class CFF2CharStringMergePen(T2CharStringPen):
# second has only args.
if lastOp in ['hintmask', 'cntrmask']:
coord = list(cmd[1])
assert allEqual(coord), (
"hintmask values cannot differ between source fonts.")
if not allEqual(coord):
raise VarLibMergeError("Hintmask values cannot differ between source fonts.")
cmd[1] = [coord[0][0]]
else:
coords = cmd[1]

View File

@ -0,0 +1,39 @@
class VarLibError(Exception):
"""Base exception for the varLib module."""
class VarLibValidationError(VarLibError):
"""Raised when input data is invalid from varLib's point of view."""
class VarLibMergeError(VarLibError):
"""Raised when input data cannot be merged into a variable font."""
class VarLibCFFDictMergeError(VarLibMergeError):
"""Raised when a CFF PrivateDict cannot be merged."""
def __init__(self, key, value, values):
error_msg = (
f"For the Private Dict key '{key}', the default font value list:"
f"\n\t{value}\nhad a different number of values than a region font:"
)
for region_value in values:
error_msg += f"\n\t{region_value}"
self.args = (error_msg,)
class VarLibCFFPointTypeMergeError(VarLibMergeError):
"""Raised when a CFF glyph cannot be merged."""
def __init__(self, point_type, pt_index, m_index, default_type, glyph_name):
error_msg = (
f"Glyph '{glyph_name}': '{point_type}' at point index {pt_index} in "
f"master index {m_index} differs from the default font point type "
f"'{default_type}'"
)
self.args = (error_msg,)
class VariationModelError(VarLibError):
"""Raised when a variation model is faulty."""

View File

@ -10,6 +10,8 @@ from fontTools.ttLib.tables import otTables as ot
from fontTools.otlLib.builder import buildLookup, buildSingleSubstSubtable
from collections import OrderedDict
from .errors import VarLibValidationError
def addFeatureVariations(font, conditionalSubstitutions, featureTag='rvrn'):
"""Add conditional substitutions to a Variable Font.
@ -312,7 +314,10 @@ def addFeatureVariationsRaw(font, conditionalSubstitutions, featureTag='rvrn'):
for conditionSet, substitutions in conditionalSubstitutions:
conditionTable = []
for axisTag, (minValue, maxValue) in sorted(conditionSet.items()):
assert minValue < maxValue
if minValue > maxValue:
raise VarLibValidationError(
"A condition set has a minimum value above the maximum value."
)
ct = buildConditionTable(axisIndices[axisTag], minValue, maxValue)
conditionTable.append(ct)

View File

@ -14,6 +14,8 @@ from fontTools.varLib.varStore import VarStoreInstancer
from functools import reduce
from fontTools.otlLib.builder import buildSinglePos
from .errors import VarLibMergeError
class Merger(object):
@ -66,8 +68,8 @@ class Merger(object):
if hasattr(item, "ensureDecompiled"):
item.ensureDecompiled()
keys = sorted(vars(out).keys())
assert all(keys == sorted(vars(v).keys()) for v in lst), \
(keys, [sorted(vars(v).keys()) for v in lst])
if not all(keys == sorted(vars(v).keys()) for v in lst):
raise VarLibMergeError((keys, [sorted(vars(v).keys()) for v in lst]))
mergers = self.mergersFor(out)
defaultMerger = mergers.get('*', self.__class__.mergeThings)
try:
@ -82,7 +84,8 @@ class Merger(object):
raise
def mergeLists(self, out, lst):
assert allEqualTo(out, lst, len), (len(out), [len(v) for v in lst])
if not allEqualTo(out, lst, len):
raise VarLibMergeError((len(out), [len(v) for v in lst]))
for i,(value,values) in enumerate(zip(out, zip(*lst))):
try:
self.mergeThings(value, values)
@ -92,7 +95,8 @@ class Merger(object):
def mergeThings(self, out, lst):
try:
assert allEqualTo(out, lst, type), (out, lst)
if not allEqualTo(out, lst, type):
raise VarLibMergeError((out, lst))
mergerFunc = self.mergersFor(out).get(None, None)
if mergerFunc is not None:
mergerFunc(self, out, lst)
@ -101,7 +105,8 @@ class Merger(object):
elif isinstance(out, list):
self.mergeLists(out, lst)
else:
assert allEqualTo(out, lst), (out, lst)
if not allEqualTo(out, lst):
raise VarLibMergeError((out, lst))
except Exception as e:
e.args = e.args + (type(out).__name__,)
raise
@ -122,7 +127,8 @@ class AligningMerger(Merger):
@AligningMerger.merger(ot.GDEF, "GlyphClassDef")
def merge(merger, self, lst):
if self is None:
assert allNone(lst), (lst)
if not allNone(lst):
raise VarLibMergeError(lst)
return
lst = [l.classDefs for l in lst]
@ -134,7 +140,8 @@ def merge(merger, self, lst):
allKeys.update(*[l.keys() for l in lst])
for k in allKeys:
allValues = nonNone(l.get(k) for l in lst)
assert allEqual(allValues), allValues
if not allEqual(allValues):
raise VarLibMergeError(allValues)
if not allValues:
self[k] = None
else:
@ -170,7 +177,8 @@ def _merge_GlyphOrders(font, lst, values_lst=None, default=None):
sortKey = font.getReverseGlyphMap().__getitem__
order = sorted(combined, key=sortKey)
# Make sure all input glyphsets were in proper order
assert all(sorted(vs, key=sortKey) == vs for vs in lst), "glyph orders are not consistent across masters"
if not all(sorted(vs, key=sortKey) == vs for vs in lst):
raise VarLibMergeError("Glyph order inconsistent across masters.")
del combined
paddedValues = None
@ -197,7 +205,10 @@ def _Lookup_SinglePos_get_effective_value(subtables, glyph):
elif self.Format == 2:
return self.Value[self.Coverage.glyphs.index(glyph)]
else:
assert 0
raise VarLibMergeError(
"Cannot retrieve effective value for SinglePos lookup, unsupported "
f"format {self.Format}."
)
return None
def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph):
@ -219,13 +230,17 @@ def _Lookup_PairPos_get_effective_value_pair(subtables, firstGlyph, secondGlyph)
klass2 = self.ClassDef2.classDefs.get(secondGlyph, 0)
return self.Class1Record[klass1].Class2Record[klass2]
else:
assert 0
raise VarLibMergeError(
"Cannot retrieve effective value pair for PairPos lookup, unsupported "
f"format {self.Format}."
)
return None
@AligningMerger.merger(ot.SinglePos)
def merge(merger, self, lst):
self.ValueFormat = valueFormat = reduce(int.__or__, [l.ValueFormat for l in lst], 0)
assert len(lst) == 1 or (valueFormat & ~0xF == 0), valueFormat
if not (len(lst) == 1 or (valueFormat & ~0xF == 0)):
raise VarLibMergeError(f"SinglePos format {valueFormat} is unsupported.")
# If all have same coverage table and all are format 1,
coverageGlyphs = self.Coverage.glyphs
@ -511,7 +526,9 @@ def merge(merger, self, lst):
elif self.Format == 2:
_PairPosFormat2_merge(self, lst, merger)
else:
assert False
raise VarLibMergeError(
f"Cannot merge PairPos lookup, unsupported format {self.Format}."
)
del merger.valueFormat1, merger.valueFormat2
@ -576,7 +593,8 @@ def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
# failures in that case will probably signify mistakes in the
# input masters.
assert allEqual(allClasses), allClasses
if not allEqual(allClasses):
raise VarLibMergeError(allClasses)
if not allClasses:
rec = None
else:
@ -625,19 +643,31 @@ def _MarkBasePosFormat1_merge(self, lst, merger, Mark='Mark', Base='Base'):
@AligningMerger.merger(ot.MarkBasePos)
def merge(merger, self, lst):
assert allEqualTo(self.Format, (l.Format for l in lst))
if not allEqualTo(self.Format, (l.Format for l in lst)):
raise VarLibMergeError(
f"MarkBasePos formats inconsistent across masters, "
f"expected {self.Format} but got {[l.Format for l in lst]}."
)
if self.Format == 1:
_MarkBasePosFormat1_merge(self, lst, merger)
else:
assert False
raise VarLibMergeError(
f"Cannot merge MarkBasePos lookup, unsupported format {self.Format}."
)
@AligningMerger.merger(ot.MarkMarkPos)
def merge(merger, self, lst):
assert allEqualTo(self.Format, (l.Format for l in lst))
if not allEqualTo(self.Format, (l.Format for l in lst)):
raise VarLibMergeError(
f"MarkMarkPos formats inconsistent across masters, "
f"expected {self.Format} but got {[l.Format for l in lst]}."
)
if self.Format == 1:
_MarkBasePosFormat1_merge(self, lst, merger, 'Mark1', 'Mark2')
else:
assert False
raise VarLibMergeError(
f"Cannot merge MarkMarkPos lookup, unsupported format {self.Format}."
)
def _PairSet_flatten(lst, font):
@ -766,8 +796,16 @@ def merge(merger, self, lst):
if not sts:
continue
if sts[0].__class__.__name__.startswith('Extension'):
assert allEqual([st.__class__ for st in sts])
assert allEqual([st.ExtensionLookupType for st in sts])
if not allEqual([st.__class__ for st in sts]):
raise VarLibMergeError(
"Use of extensions inconsistent between masters: "
f"{[st.__class__.__name__ for st in sts]}."
)
if not allEqual([st.ExtensionLookupType for st in sts]):
raise VarLibMergeError(
"Extension lookup type differs between masters: "
f"{[st.ExtensionLookupType for st in sts]}."
)
l.LookupType = sts[0].ExtensionLookupType
new_sts = [st.ExtSubTable for st in sts]
del sts[:]
@ -995,7 +1033,8 @@ class VariationMerger(AligningMerger):
masterModel = None
if None in lst:
if allNone(lst):
assert out is None, (out, lst)
if out is not None:
raise VarLibMergeError((out, lst))
return
masterModel = self.model
model, lst = masterModel.getSubModel(lst)
@ -1015,7 +1054,8 @@ def buildVarDevTable(store_builder, master_values):
@VariationMerger.merger(ot.CaretValue)
def merge(merger, self, lst):
assert self.Format == 1
if self.Format != 1:
raise VarLibMergeError(f"CaretValue format {self.Format} unsupported.")
self.Coordinate, DeviceTable = buildVarDevTable(merger.store_builder, [a.Coordinate for a in lst])
if DeviceTable:
self.Format = 3
@ -1023,7 +1063,8 @@ def merge(merger, self, lst):
@VariationMerger.merger(ot.Anchor)
def merge(merger, self, lst):
assert self.Format == 1
if self.Format != 1:
raise VarLibMergeError(f"Anchor format {self.Format} unsupported.")
self.XCoordinate, XDeviceTable = buildVarDevTable(merger.store_builder, [a.XCoordinate for a in lst])
self.YCoordinate, YDeviceTable = buildVarDevTable(merger.store_builder, [a.YCoordinate for a in lst])
if XDeviceTable or YDeviceTable:

View File

@ -5,6 +5,8 @@ __all__ = ['nonNone', 'allNone', 'allEqual', 'allEqualTo', 'subList',
'supportScalar',
'VariationModel']
from .errors import VariationModelError
def nonNone(lst):
return [l for l in lst if l is not None]
@ -43,7 +45,11 @@ def normalizeValue(v, triple):
0.5
"""
lower, default, upper = triple
assert lower <= default <= upper, "invalid axis values: %3.3f, %3.3f %3.3f"%(lower, default, upper)
if not (lower <= default <= upper):
raise ValueError(
f"Invalid axis values, must be minimum, default, maximum: "
f"{lower:3.3f}, {default:3.3f}, {upper:3.3f}"
)
v = max(min(v, upper), lower)
if v == default:
v = 0.
@ -192,7 +198,7 @@ class VariationModel(object):
def __init__(self, locations, axisOrder=None):
if len(set(tuple(sorted(l.items())) for l in locations)) != len(locations):
raise ValueError("locations must be unique")
raise VariationModelError("Locations must be unique.")
self.origLocations = locations
self.axisOrder = axisOrder if axisOrder is not None else []
@ -220,7 +226,8 @@ class VariationModel(object):
@staticmethod
def getMasterLocationsSortKeyFunc(locations, axisOrder=[]):
assert {} in locations, "Base master not found."
if {} not in locations:
raise VariationModelError("Base master not found.")
axisPoints = {}
for loc in locations:
if len(loc) != 1:

View File

@ -1,3 +1,36 @@
4.4.1 (released 2020-02-26)
---------------------------
- [woff2] Skip normalizing ``glyf`` and ``loca`` tables if these are missing from
a font (e.g. in NotoColorEmoji using ``CBDT/CBLC`` tables).
- [timeTools] Use non-localized date parsing in ``timestampFromString``, to fix
error when non-English ``LC_TIME`` locale is set (#1838, #1839).
- [fontBuilder] Make sure the CFF table generated by fontBuilder can be used by varLib
without having to compile and decompile the table first. This was breaking in
converting the CFF table to CFF2 due to some unset attributes (#1836).
4.4.0 (released 2020-02-18)
---------------------------
- [colorLib] Added ``fontTools.colorLib.builder`` module, initially with ``buildCOLR``
and ``buildCPAL`` public functions. More color font formats will follow (#1827).
- [fontBuilder] Added ``setupCOLR`` and ``setupCPAL`` methods (#1826).
- [ttGlyphPen] Quantize ``GlyphComponent.transform`` floats to ``F2Dot14`` to fix
round-trip issue when computing bounding boxes of transformed components (#1830).
- [glyf] If a component uses reference points (``firstPt`` and ``secondPt``) for
alignment (instead of X and Y offsets), compute the effective translation offset
*after* having applied any transform (#1831).
- [glyf] When all glyphs have zero contours, compile ``glyf`` table data as a single
null byte in order to pass validation by OTS and Windows (#1829).
- [feaLib] Parsing feature code now ensures that referenced glyph names are part of
the known glyph set, unless a glyph set was not provided.
- [varLib] When filling in the default axis value for a missing location of a source or
instance, correctly map the value forward.
- [varLib] The avar table can now contain mapping output values that are greater than
OR EQUAL to the preceeding value, as the avar specification allows this.
- [varLib] The errors of the module are now ordered hierarchically below VarLibError.
See #1821.
4.3.0 (released 2020-02-03)
---------------------------

View File

View File

@ -0,0 +1,187 @@
from fontTools.ttLib import newTable
from fontTools.colorLib import builder
from fontTools.colorLib.errors import ColorLibError
import pytest
def test_buildCOLR_v0():
color_layer_lists = {
"a": [("a.color0", 0), ("a.color1", 1)],
"b": [("b.color1", 1), ("b.color0", 0)],
}
colr = builder.buildCOLR(color_layer_lists)
assert colr.tableTag == "COLR"
assert colr.version == 0
assert colr.ColorLayers["a"][0].name == "a.color0"
assert colr.ColorLayers["a"][0].colorID == 0
assert colr.ColorLayers["a"][1].name == "a.color1"
assert colr.ColorLayers["a"][1].colorID == 1
assert colr.ColorLayers["b"][0].name == "b.color1"
assert colr.ColorLayers["b"][0].colorID == 1
assert colr.ColorLayers["b"][1].name == "b.color0"
assert colr.ColorLayers["b"][1].colorID == 0
def test_buildCPAL_v0():
palettes = [
[(0.68, 0.20, 0.32, 1.0), (0.45, 0.68, 0.21, 1.0)],
[(0.68, 0.20, 0.32, 0.6), (0.45, 0.68, 0.21, 0.6)],
[(0.68, 0.20, 0.32, 0.3), (0.45, 0.68, 0.21, 0.3)],
]
cpal = builder.buildCPAL(palettes)
assert cpal.tableTag == "CPAL"
assert cpal.version == 0
assert cpal.numPaletteEntries == 2
assert len(cpal.palettes) == 3
assert [tuple(c) for c in cpal.palettes[0]] == [
(82, 51, 173, 255),
(54, 173, 115, 255),
]
assert [tuple(c) for c in cpal.palettes[1]] == [
(82, 51, 173, 153),
(54, 173, 115, 153),
]
assert [tuple(c) for c in cpal.palettes[2]] == [
(82, 51, 173, 76),
(54, 173, 115, 76),
]
def test_buildCPAL_palettes_different_lengths():
with pytest.raises(ColorLibError, match="have different lengths"):
builder.buildCPAL([[(1, 1, 1, 1)], [(0, 0, 0, 1), (0.5, 0.5, 0.5, 1)]])
def test_buildPaletteLabels():
name_table = newTable("name")
name_table.names = []
name_ids = builder.buildPaletteLabels(
[None, "hi", {"en": "hello", "de": "hallo"}], name_table
)
assert name_ids == [0xFFFF, 256, 257]
assert len(name_table.names) == 3
assert str(name_table.names[0]) == "hi"
assert name_table.names[0].nameID == 256
assert str(name_table.names[1]) == "hallo"
assert name_table.names[1].nameID == 257
assert str(name_table.names[2]) == "hello"
assert name_table.names[2].nameID == 257
def test_build_CPAL_v1_types_no_labels():
palettes = [
[(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)],
[(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)],
[(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)],
]
paletteTypes = [
builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND,
builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND,
builder.ColorPaletteType.USABLE_WITH_LIGHT_BACKGROUND
| builder.ColorPaletteType.USABLE_WITH_DARK_BACKGROUND,
]
cpal = builder.buildCPAL(palettes, paletteTypes=paletteTypes)
assert cpal.tableTag == "CPAL"
assert cpal.version == 1
assert cpal.numPaletteEntries == 2
assert len(cpal.palettes) == 3
assert cpal.paletteTypes == paletteTypes
assert cpal.paletteLabels == [cpal.NO_NAME_ID] * len(palettes)
assert cpal.paletteEntryLabels == [cpal.NO_NAME_ID] * cpal.numPaletteEntries
def test_build_CPAL_v1_labels():
palettes = [
[(0.1, 0.2, 0.3, 1.0), (0.4, 0.5, 0.6, 1.0)],
[(0.1, 0.2, 0.3, 0.6), (0.4, 0.5, 0.6, 0.6)],
[(0.1, 0.2, 0.3, 0.3), (0.4, 0.5, 0.6, 0.3)],
]
paletteLabels = ["First", {"en": "Second", "it": "Seconda"}, None]
paletteEntryLabels = ["Foo", "Bar"]
with pytest.raises(TypeError, match="nameTable is required"):
builder.buildCPAL(palettes, paletteLabels=paletteLabels)
with pytest.raises(TypeError, match="nameTable is required"):
builder.buildCPAL(palettes, paletteEntryLabels=paletteEntryLabels)
name_table = newTable("name")
name_table.names = []
cpal = builder.buildCPAL(
palettes,
paletteLabels=paletteLabels,
paletteEntryLabels=paletteEntryLabels,
nameTable=name_table,
)
assert cpal.tableTag == "CPAL"
assert cpal.version == 1
assert cpal.numPaletteEntries == 2
assert len(cpal.palettes) == 3
assert cpal.paletteTypes == [cpal.DEFAULT_PALETTE_TYPE] * len(palettes)
assert cpal.paletteLabels == [256, 257, cpal.NO_NAME_ID]
assert cpal.paletteEntryLabels == [258, 259]
assert name_table.getDebugName(256) == "First"
assert name_table.getDebugName(257) == "Second"
assert name_table.getDebugName(258) == "Foo"
assert name_table.getDebugName(259) == "Bar"
def test_invalid_ColorPaletteType():
with pytest.raises(ValueError, match="not a valid ColorPaletteType"):
builder.ColorPaletteType(-1)
with pytest.raises(ValueError, match="not a valid ColorPaletteType"):
builder.ColorPaletteType(4)
with pytest.raises(ValueError, match="not a valid ColorPaletteType"):
builder.ColorPaletteType("abc")
def test_buildCPAL_v1_invalid_args_length():
with pytest.raises(ColorLibError, match="Expected 2 paletteTypes, got 1"):
builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, 1, 1)]], paletteTypes=[1])
with pytest.raises(ColorLibError, match="Expected 2 paletteLabels, got 1"):
builder.buildCPAL(
[[(0, 0, 0, 0)], [(1, 1, 1, 1)]],
paletteLabels=["foo"],
nameTable=newTable("name"),
)
with pytest.raises(ColorLibError, match="Expected 1 paletteEntryLabels, got 0"):
cpal = builder.buildCPAL(
[[(0, 0, 0, 0)], [(1, 1, 1, 1)]],
paletteEntryLabels=[],
nameTable=newTable("name"),
)
def test_buildCPAL_invalid_color():
with pytest.raises(
ColorLibError,
match=r"In palette\[0\]\[1\]: expected \(R, G, B, A\) tuple, got \(1, 1, 1\)",
):
builder.buildCPAL([[(1, 1, 1, 1), (1, 1, 1)]])
with pytest.raises(
ColorLibError,
match=(
r"palette\[1\]\[0\] has invalid out-of-range "
r"\[0..1\] color: \(1, 1, -1, 2\)"
),
):
builder.buildCPAL([[(0, 0, 0, 0)], [(1, 1, -1, 2)]])

View File

@ -41,7 +41,7 @@ def makeTTFont():
a_n_d T_h T_h.swash germandbls ydieresis yacute breve
grave acute dieresis macron circumflex cedilla umlaut ogonek caron
damma hamza sukun kasratan lam_meem_jeem noon.final noon.initial
by feature lookup sub table
by feature lookup sub table uni0327 uni0328 e.fina
""".split()
font = TTFont()
font.setGlyphOrder(glyphs)

View File

@ -178,7 +178,9 @@ class IncludingLexerTest(unittest.TestCase):
def test_include_missing_file(self):
lexer = IncludingLexer(self.getpath("include/includemissingfile.fea"))
self.assertRaisesRegex(IncludedFeaNotFound,
"includemissingfile.fea:1:8: missingfile.fea",
"includemissingfile.fea:1:8: The following feature file "
"should be included but cannot be found: "
"missingfile.fea",
lambda: list(lexer))
def test_featurefilepath_None(self):
@ -223,7 +225,7 @@ class IncludingLexerTest(unittest.TestCase):
# an in-memory stream, so it will use the current working
# directory to resolve relative include statements
lexer = IncludingLexer(UnicodeIO("include(included.fea);"))
files = set(loc[0] for _, _, loc in lexer)
files = set(os.path.realpath(loc[0]) for _, _, loc in lexer)
expected = os.path.realpath(included.name)
self.assertIn(expected, files)
finally:

View File

@ -39,6 +39,14 @@ GLYPHNAMES = ("""
n.sc o.sc p.sc q.sc r.sc s.sc t.sc u.sc v.sc w.sc x.sc y.sc z.sc
a.swash b.swash x.swash y.swash z.swash
foobar foo.09 foo.1234 foo.9876
one two five six acute grave dieresis umlaut cedilla ogonek macron
a_f_f_i o_f_f_i f_i f_f_i one.fitted one.oldstyle a.1 a.2 a.3 c_t
PRE SUF FIX BACK TRACK LOOK AHEAD ampersand ampersand.1 ampersand.2
cid00001 cid00002 cid00003 cid00004 cid00005 cid00006 cid00007
cid12345 cid78987 cid00999 cid01000 cid01001 cid00998 cid00995
cid00111 cid00222
comma endash emdash figuredash damma hamza
c_d d.alt n.end s.end f_f
""").split() + ["foo.%d" % i for i in range(1, 200)]
@ -260,6 +268,12 @@ class ParserTest(unittest.TestCase):
FeatureLibError, "Font revision numbers must be positive",
self.parse, "table head {FontRevision -17.2;} head;")
def test_strict_glyph_name_check(self):
self.parse("@bad = [a b ccc];", glyphNames=("a", "b", "ccc"))
with self.assertRaisesRegex(FeatureLibError, "missing from the glyph set: ccc"):
self.parse("@bad = [a b ccc];", glyphNames=("a", "b"))
def test_glyphclass(self):
[gc] = self.parse("@dash = [endash emdash figuredash];").statements
self.assertEqual(gc.name, "dash")

View File

@ -255,6 +255,19 @@ def test_build_cff2(tmpdir):
_verifyOutput(outPath)
def test_build_cff_to_cff2(tmpdir):
fb, _, _ = _setupFontBuilder(False, 1000)
pen = T2CharStringPen(600, None)
drawTestGlyph(pen)
charString = pen.getCharString()
charStrings = {".notdef": charString, "A": charString, "a": charString, ".null": charString}
fb.setupCFF("TestFont", {}, charStrings, {})
from fontTools.varLib.cff import convertCFFtoCFF2
convertCFFtoCFF2(fb.font)
def test_setupNameTable_no_mac():
fb, _, nameStrings = _setupFontBuilder(True)
fb.setupNameTable(nameStrings, mac=False)

View File

@ -1,7 +1,8 @@
from fontTools.misc.py23 import *
from fontTools.misc.timeTools import asctime, timestampNow, epoch_diff
from fontTools.misc.timeTools import asctime, timestampNow, timestampToString, timestampFromString, epoch_diff
import os
import time
import locale
import pytest
@ -21,3 +22,17 @@ def test_source_date_epoch():
del os.environ["SOURCE_DATE_EPOCH"]
assert timestampNow() + epoch_diff != 150687315
# test for issue #1838
def test_date_parsing_with_locale():
l = locale.getlocale(locale.LC_TIME)
try:
locale.setlocale(locale.LC_TIME, 'de_DE.utf8')
except locale.Error:
pytest.skip("Locale de_DE not available")
try:
assert timestampFromString(timestampToString(timestampNow()))
finally:
locale.setlocale(locale.LC_TIME, l)

View File

@ -264,6 +264,33 @@ class TTGlyphPenTest(TestCase):
compositeGlyph.recalcBounds(glyphSet)
self.assertGlyphBoundsEqual(compositeGlyph, (-86, 0, 282, 1))
def test_scaled_component_bounds(self):
glyphSet = {}
pen = TTGlyphPen(glyphSet)
pen.moveTo((-231, 939))
pen.lineTo((-55, 939))
pen.lineTo((-55, 745))
pen.lineTo((-231, 745))
pen.closePath()
glyphSet["gravecomb"] = gravecomb = pen.glyph()
pen = TTGlyphPen(glyphSet)
pen.moveTo((-278, 939))
pen.lineTo((8, 939))
pen.lineTo((8, 745))
pen.lineTo((-278, 745))
pen.closePath()
glyphSet["circumflexcomb"] = circumflexcomb = pen.glyph()
pen = TTGlyphPen(glyphSet)
pen.addComponent("circumflexcomb", (1, 0, 0, 1, 0, 0))
pen.addComponent("gravecomb", (0.9, 0, 0, 0.9, 198, 180))
glyphSet["uni0302_uni0300"] = uni0302_uni0300 = pen.glyph()
uni0302_uni0300.recalcBounds(glyphSet)
self.assertGlyphBoundsEqual(uni0302_uni0300, (-278, 745, 148, 1025))
class _TestGlyph(object):
def __init__(self, glyph):

View File

@ -12,6 +12,8 @@ import shutil
import sys
import tempfile
import unittest
import pathlib
import pytest
class SubsetTest(unittest.TestCase):
@ -835,5 +837,40 @@ def test_subset_single_pos_format():
]
@pytest.fixture
def ttf_path(tmp_path):
# $(dirname $0)/../ttLib/data
ttLib_data = pathlib.Path(__file__).parent.parent / "ttLib" / "data"
font = TTFont()
font.importXML(ttLib_data / "TestTTF-Regular.ttx")
font_path = tmp_path / "TestTTF-Regular.ttf"
font.save(font_path)
return font_path
def test_subset_empty_glyf(tmp_path, ttf_path):
subset_path = tmp_path / (ttf_path.name + ".subset")
# only keep empty .notdef and space glyph, resulting in an empty glyf table
subset.main(
[
str(ttf_path),
"--no-notdef-outline",
"--glyph-names",
f"--output-file={subset_path}",
"--glyphs=.notdef space",
]
)
subset_font = TTFont(subset_path)
assert subset_font.getGlyphOrder() == [".notdef", "space"]
assert subset_font.reader['glyf'] == b"\x00"
glyf = subset_font["glyf"]
assert all(glyf[g].numberOfContours == 0 for g in subset_font.getGlyphOrder())
loca = subset_font["loca"]
assert all(loc == 0 for loc in loca)
if __name__ == "__main__":
sys.exit(unittest.main())

View File

@ -66,9 +66,6 @@ class CPALTest(unittest.TestCase):
self.assertEqual(cpal.numPaletteEntries, 2)
self.assertEqual(repr(cpal.palettes),
'[[#000000FF, #66CCFFFF], [#000000FF, #800000FF]]')
self.assertEqual(cpal.paletteLabels, [0, 0])
self.assertEqual(cpal.paletteTypes, [0, 0])
self.assertEqual(cpal.paletteEntryLabels, [0, 0])
def test_decompile_v0_sharingColors(self):
cpal = newTable('CPAL')
@ -80,9 +77,6 @@ class CPALTest(unittest.TestCase):
'[#223344FF, #99887711, #55555555]',
'[#223344FF, #99887711, #FFFFFFFF]',
'[#223344FF, #99887711, #55555555]'])
self.assertEqual(cpal.paletteLabels, [0, 0, 0, 0])
self.assertEqual(cpal.paletteTypes, [0, 0, 0, 0])
self.assertEqual(cpal.paletteEntryLabels, [0, 0, 0])
def test_decompile_v1_noLabelsNoTypes(self):
cpal = newTable('CPAL')
@ -92,9 +86,10 @@ class CPALTest(unittest.TestCase):
self.assertEqual([repr(p) for p in cpal.palettes], [
'[#CAFECAFE, #22110033, #66554477]', # RGBA
'[#59413127, #42424242, #13330037]'])
self.assertEqual(cpal.paletteLabels, [0, 0])
self.assertEqual(cpal.paletteLabels, [cpal.NO_NAME_ID] * len(cpal.palettes))
self.assertEqual(cpal.paletteTypes, [0, 0])
self.assertEqual(cpal.paletteEntryLabels, [0, 0, 0])
self.assertEqual(cpal.paletteEntryLabels,
[cpal.NO_NAME_ID] * cpal.numPaletteEntries)
def test_decompile_v1(self):
cpal = newTable('CPAL')
@ -194,9 +189,6 @@ class CPALTest(unittest.TestCase):
self.assertEqual(cpal.version, 0)
self.assertEqual(cpal.numPaletteEntries, 2)
self.assertEqual(repr(cpal.palettes), '[[#12345678, #FEDCBA98]]')
self.assertEqual(cpal.paletteLabels, [0])
self.assertEqual(cpal.paletteTypes, [0])
self.assertEqual(cpal.paletteEntryLabels, [0, 0])
def test_fromXML_v1(self):
cpal = newTable('CPAL')
@ -218,7 +210,8 @@ class CPALTest(unittest.TestCase):
'[[#12345678, #FEDCBA98, #CAFECAFE]]')
self.assertEqual(cpal.paletteLabels, [259])
self.assertEqual(cpal.paletteTypes, [2])
self.assertEqual(cpal.paletteEntryLabels, [0, 262, 0])
self.assertEqual(cpal.paletteEntryLabels,
[cpal.NO_NAME_ID, 262, cpal.NO_NAME_ID])
if __name__ == "__main__":

View File

@ -6,9 +6,12 @@ from fontTools.pens.recordingPen import RecordingPen, RecordingPointPen
from fontTools.pens.pointPen import PointToSegmentPen
from fontTools.ttLib import TTFont, newTable, TTLibError
from fontTools.ttLib.tables._g_l_y_f import (
Glyph,
GlyphCoordinates,
GlyphComponent,
ARGS_ARE_XY_VALUES,
SCALED_COMPONENT_OFFSET,
UNSCALED_COMPONENT_OFFSET,
WE_HAVE_A_SCALE,
WE_HAVE_A_TWO_BY_TWO,
WE_HAVE_AN_X_AND_Y_SCALE,
@ -190,7 +193,7 @@ def strip_ttLibVersion(string):
return re.sub(' ttLibVersion=".*"', '', string)
class glyfTableTest(unittest.TestCase):
class GlyfTableTest(unittest.TestCase):
def __init__(self, methodName):
unittest.TestCase.__init__(self, methodName)
@ -338,6 +341,136 @@ class glyfTableTest(unittest.TestCase):
glyfTable["glyph00003"].drawPoints(PointToSegmentPen(pen2), glyfTable)
self.assertEqual(pen1.value, pen2.value)
def test_compile_empty_table(self):
font = TTFont(sfntVersion="\x00\x01\x00\x00")
font.importXML(GLYF_TTX)
glyfTable = font['glyf']
# set all glyphs to zero contours
glyfTable.glyphs = {glyphName: Glyph() for glyphName in font.getGlyphOrder()}
glyfData = glyfTable.compile(font)
self.assertEqual(glyfData, b"\x00")
self.assertEqual(list(font["loca"]), [0] * (font["maxp"].numGlyphs+1))
def test_decompile_empty_table(self):
font = TTFont()
glyphNames = [".notdef", "space"]
font.setGlyphOrder(glyphNames)
font["loca"] = newTable("loca")
font["loca"].locations = [0] * (len(glyphNames) + 1)
font["glyf"] = newTable("glyf")
font["glyf"].decompile(b"\x00", font)
self.assertEqual(len(font["glyf"]), 2)
self.assertEqual(font["glyf"][".notdef"].numberOfContours, 0)
self.assertEqual(font["glyf"]["space"].numberOfContours, 0)
class GlyphTest:
def test_getCoordinates(self):
glyphSet = {}
pen = TTGlyphPen(glyphSet)
pen.moveTo((0, 0))
pen.lineTo((100, 0))
pen.lineTo((100, 100))
pen.lineTo((0, 100))
pen.closePath()
# simple contour glyph
glyphSet["a"] = a = pen.glyph()
assert a.getCoordinates(glyphSet) == (
GlyphCoordinates([(0, 0), (100, 0), (100, 100), (0, 100)]),
[3],
array.array("B", [1, 1, 1, 1]),
)
# composite glyph with only XY offset
pen = TTGlyphPen(glyphSet)
pen.addComponent("a", (1, 0, 0, 1, 10, 20))
glyphSet["b"] = b = pen.glyph()
assert b.getCoordinates(glyphSet) == (
GlyphCoordinates([(10, 20), (110, 20), (110, 120), (10, 120)]),
[3],
array.array("B", [1, 1, 1, 1]),
)
# composite glyph with a scale (and referencing another composite glyph)
pen = TTGlyphPen(glyphSet)
pen.addComponent("b", (0.5, 0, 0, 0.5, 0, 0))
glyphSet["c"] = c = pen.glyph()
assert c.getCoordinates(glyphSet) == (
GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]),
[3],
array.array("B", [1, 1, 1, 1]),
)
# composite glyph with unscaled offset (MS-style)
pen = TTGlyphPen(glyphSet)
pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20))
glyphSet["d"] = d = pen.glyph()
d.components[0].flags |= UNSCALED_COMPONENT_OFFSET
assert d.getCoordinates(glyphSet) == (
GlyphCoordinates([(10, 20), (60, 20), (60, 70), (10, 70)]),
[3],
array.array("B", [1, 1, 1, 1]),
)
# composite glyph with a scaled offset (Apple-style)
pen = TTGlyphPen(glyphSet)
pen.addComponent("a", (0.5, 0, 0, 0.5, 10, 20))
glyphSet["e"] = e = pen.glyph()
e.components[0].flags |= SCALED_COMPONENT_OFFSET
assert e.getCoordinates(glyphSet) == (
GlyphCoordinates([(5, 10), (55, 10), (55, 60), (5, 60)]),
[3],
array.array("B", [1, 1, 1, 1]),
)
# composite glyph where the 2nd and 3rd components use anchor points
pen = TTGlyphPen(glyphSet)
pen.addComponent("a", (1, 0, 0, 1, 0, 0))
glyphSet["f"] = f = pen.glyph()
comp1 = GlyphComponent()
comp1.glyphName = "a"
# aling the new component's pt 0 to pt 2 of contour points added so far
comp1.firstPt = 2
comp1.secondPt = 0
comp1.flags = 0
f.components.append(comp1)
comp2 = GlyphComponent()
comp2.glyphName = "a"
# aling the new component's pt 0 to pt 6 of contour points added so far
comp2.firstPt = 6
comp2.secondPt = 0
comp2.transform = [[0.707107, 0.707107], [-0.707107, 0.707107]] # rotate 45 deg
comp2.flags = WE_HAVE_A_TWO_BY_TWO
f.components.append(comp2)
coords, end_pts, flags = f.getCoordinates(glyphSet)
assert end_pts == [3, 7, 11]
assert flags == array.array("B", [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1])
assert list(sum(coords, ())) == pytest.approx(
[
0, 0,
100, 0,
100, 100,
0, 100,
100, 100,
200, 100,
200, 200,
100, 200,
200, 200,
270.7107, 270.7107,
200.0, 341.4214,
129.2893, 270.7107,
]
)
class GlyphComponentTest:
@ -456,6 +589,29 @@ class GlyphComponentTest:
):
assert value == pytest.approx(expected)
def test_toXML_reference_points(self):
comp = GlyphComponent()
comp.glyphName = "a"
comp.flags = 0
comp.firstPt = 1
comp.secondPt = 2
assert getXML(comp.toXML) == [
'<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>'
]
def test_fromXML_reference_points(self):
comp = GlyphComponent()
for name, attrs, content in parseXML(
['<component glyphName="a" firstPt="1" secondPt="2" flags="0x0"/>']
):
comp.fromXML(name, attrs, content, ttFont=None)
assert comp.glyphName == "a"
assert comp.flags == 0
assert (comp.firstPt, comp.secondPt) == (1, 2)
assert not hasattr(comp, "transform")
if __name__ == "__main__":
import sys

View File

@ -1199,6 +1199,24 @@ class WOFF2RoundtripTest(object):
assert tmp.getvalue() == tmp2.getvalue()
assert ttFont2.reader.flavorData.transformedTables == {"hmtx"}
def test_roundtrip_no_glyf_and_loca_tables(self):
ttx = os.path.join(
os.path.dirname(current_dir), "subset", "data", "google_color.ttx"
)
ttFont = ttLib.TTFont()
ttFont.importXML(ttx)
assert "glyf" not in ttFont
assert "loca" not in ttFont
ttFont.flavor = "woff2"
tmp = BytesIO()
ttFont.save(tmp)
tmp2, ttFont2 = self.roundtrip(tmp)
assert tmp.getvalue() == tmp2.getvalue()
assert ttFont.flavor == "woff2"
class MainTest(object):

View File

@ -0,0 +1,26 @@
<?xml version='1.0' encoding='utf-8'?>
<designspace format="4.0">
<axes>
<axis default="100" maximum="900" minimum="100" name="weight" tag="wght">
<map input="500" output="105"/>
<map input="300" output="57"/>
<map input="900" output="158"/>
<map input="100" output="0"/>
</axis>
<axis default="50" maximum="100" minimum="0" name="width" tag="wdth" />
</axes>
<sources>
<source filename="A.ufo">
<location>
<dimension name="weight" xvalue="0" />
<dimension name="width" xvalue="50" />
</location>
</source>
</sources>
<instances>
<instance filename="C.ufo" familyname="C" stylename="CCC">
<location>
</location>
</instance>
</instances>
</designspace>

View File

@ -1,6 +1,6 @@
from fontTools.misc.py23 import *
from fontTools.varLib.models import (
normalizeLocation, supportScalar, VariationModel)
normalizeLocation, supportScalar, VariationModel, VariationModelError)
import pytest
@ -145,7 +145,7 @@ class VariationModelTest(object):
assert model.deltaWeights == deltaWeights
def test_init_duplicate_locations(self):
with pytest.raises(ValueError, match="locations must be unique"):
with pytest.raises(VariationModelError, match="Locations must be unique."):
VariationModel(
[
{"foo": 0.0, "bar": 0.0},

View File

@ -1,6 +1,7 @@
from fontTools.misc.py23 import *
from fontTools.ttLib import TTFont, newTable
from fontTools.varLib import build
from fontTools.varLib import build, load_designspace
from fontTools.varLib.errors import VarLibValidationError
from fontTools.varLib.mutator import instantiateVariableFont
from fontTools.varLib import main as varLib_main, load_masters
from fontTools.varLib import set_default_weight_width_slant
@ -728,6 +729,13 @@ class BuildTest(unittest.TestCase):
("B", "D"): 40,
}
def test_designspace_fill_in_location(self):
ds_path = self.get_test_input("VarLibLocationTest.designspace")
ds = DesignSpaceDocument.fromfile(ds_path)
ds_loaded = load_designspace(ds)
assert ds_loaded.instances[0].location == {"weight": 0, "width": 50}
def test_load_masters_layerName_without_required_font():
ds = DesignSpaceDocument()
@ -737,7 +745,7 @@ def test_load_masters_layerName_without_required_font():
ds.addSource(s)
with pytest.raises(
AttributeError,
VarLibValidationError,
match="specified a layer name but lacks the required TTFont object",
):
load_masters(ds)

View File

@ -1,5 +1,5 @@
[bumpversion]
current_version = 4.3.1.dev0
current_version = 4.4.2.dev0
commit = True
tag = False
tag_name = {new_version}

View File

@ -345,7 +345,7 @@ def find_data_files(manpath="share/man"):
setup(
name="fonttools",
version="4.3.1.dev0",
version="4.4.2.dev0",
description="Tools to manipulate font files",
author="Just van Rossum",
author_email="just@letterror.com",