Merge remote-tracking branch 'origin/master' into partial-instancer
This commit is contained in:
commit
1726a4a1fc
@ -5,6 +5,6 @@ from fontTools.misc.loggingTools import configLogger
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
version = __version__ = "3.43.1.dev0"
|
||||
version = __version__ = "3.43.2.dev0"
|
||||
|
||||
__all__ = ["version", "log", "configLogger"]
|
||||
|
@ -174,7 +174,7 @@ class WOFF2Writer(SFNTWriter):
|
||||
self.file = file
|
||||
self.numTables = numTables
|
||||
self.sfntVersion = Tag(sfntVersion)
|
||||
self.flavorData = flavorData or WOFF2FlavorData()
|
||||
self.flavorData = WOFF2FlavorData(data=flavorData)
|
||||
|
||||
self.directoryFormat = woff2DirectoryFormat
|
||||
self.directorySize = woff2DirectorySize
|
||||
@ -1128,24 +1128,37 @@ class WOFF2FlavorData(WOFFFlavorData):
|
||||
|
||||
Flavor = 'woff2'
|
||||
|
||||
def __init__(self, reader=None, transformedTables=None):
|
||||
def __init__(self, reader=None, data=None, transformedTables=None):
|
||||
"""Data class that holds the WOFF2 header major/minor version, any
|
||||
metadata or private data (as bytes strings), and the set of
|
||||
table tags that have transformations applied (if reader is not None),
|
||||
or will have once the WOFF2 font is compiled.
|
||||
|
||||
Args:
|
||||
reader: an SFNTReader (or subclass) object to read flavor data from.
|
||||
data: another WOFFFlavorData object to initialise data from.
|
||||
transformedTables: set of strings containing table tags to be transformed.
|
||||
|
||||
Raises:
|
||||
ImportError if the brotli module is not installed.
|
||||
|
||||
NOTE: The 'reader' argument, on the one hand, and the 'data' and
|
||||
'transformedTables' arguments, on the other hand, are mutually exclusive.
|
||||
"""
|
||||
if not haveBrotli:
|
||||
raise ImportError("No module named brotli")
|
||||
|
||||
if reader is not None and transformedTables is not None:
|
||||
if reader is not None:
|
||||
if data is not None:
|
||||
raise TypeError(
|
||||
"'reader' and 'data' arguments are mutually exclusive"
|
||||
)
|
||||
if transformedTables is not None:
|
||||
raise TypeError(
|
||||
"'reader' and 'transformedTables' arguments are mutually exclusive"
|
||||
)
|
||||
|
||||
if transformedTables is None:
|
||||
transformedTables = woff2TransformedTableTags
|
||||
else:
|
||||
if (
|
||||
if transformedTables is not None and (
|
||||
"glyf" in transformedTables and "loca" not in transformedTables
|
||||
or "loca" in transformedTables and "glyf" not in transformedTables
|
||||
):
|
||||
@ -1164,19 +1177,29 @@ class WOFF2FlavorData(WOFFFlavorData):
|
||||
reader.file.seek(reader.metaOffset)
|
||||
rawData = reader.file.read(reader.metaLength)
|
||||
assert len(rawData) == reader.metaLength
|
||||
data = brotli.decompress(rawData)
|
||||
assert len(data) == reader.metaOrigLength
|
||||
self.metaData = data
|
||||
metaData = brotli.decompress(rawData)
|
||||
assert len(metaData) == reader.metaOrigLength
|
||||
self.metaData = metaData
|
||||
if reader.privLength:
|
||||
reader.file.seek(reader.privOffset)
|
||||
data = reader.file.read(reader.privLength)
|
||||
assert len(data) == reader.privLength
|
||||
self.privData = data
|
||||
privData = reader.file.read(reader.privLength)
|
||||
assert len(privData) == reader.privLength
|
||||
self.privData = privData
|
||||
transformedTables = [
|
||||
tag
|
||||
for tag, entry in reader.tables.items()
|
||||
if entry.transformed
|
||||
]
|
||||
elif data:
|
||||
self.majorVersion = data.majorVersion
|
||||
self.majorVersion = data.minorVersion
|
||||
self.metaData = data.metaData
|
||||
self.privData = data.privData
|
||||
if transformedTables is None and hasattr(data, "transformedTables"):
|
||||
transformedTables = data.transformedTables
|
||||
|
||||
if transformedTables is None:
|
||||
transformedTables = woff2TransformedTableTags
|
||||
|
||||
self.transformedTables = set(transformedTables)
|
||||
|
||||
@ -1354,7 +1377,9 @@ def compress(input_file, output_file, transform_tables=None):
|
||||
font.flavor = "woff2"
|
||||
|
||||
if transform_tables is not None:
|
||||
font.flavorData = WOFF2FlavorData(transformedTables=transform_tables)
|
||||
font.flavorData = WOFF2FlavorData(
|
||||
data=font.flavorData, transformedTables=transform_tables
|
||||
)
|
||||
|
||||
font.save(output_file, reorderTables=False)
|
||||
|
||||
|
@ -767,6 +767,45 @@ def load_designspace(designspace):
|
||||
)
|
||||
|
||||
|
||||
# https://docs.microsoft.com/en-us/typography/opentype/spec/os2#uswidthclass
|
||||
WDTH_VALUE_TO_OS2_WIDTH_CLASS = {
|
||||
50: 1,
|
||||
62.5: 2,
|
||||
75: 3,
|
||||
87.5: 4,
|
||||
100: 5,
|
||||
112.5: 6,
|
||||
125: 7,
|
||||
150: 8,
|
||||
200: 9,
|
||||
}
|
||||
|
||||
|
||||
def set_default_weight_width_slant(font, location):
|
||||
if "OS/2" in font:
|
||||
if "wght" in location:
|
||||
weight_class = otRound(max(1, min(location["wght"], 1000)))
|
||||
if font["OS/2"].usWeightClass != weight_class:
|
||||
log.info("Setting OS/2.usWidthClass = %s", weight_class)
|
||||
font["OS/2"].usWeightClass = weight_class
|
||||
|
||||
if "wdth" in location:
|
||||
# map 'wdth' axis (50..200) to OS/2.usWidthClass (1..9), rounding to closest
|
||||
widthValue = min(max(location["wdth"], 50), 200)
|
||||
widthClass = otRound(
|
||||
models.piecewiseLinearMap(widthValue, WDTH_VALUE_TO_OS2_WIDTH_CLASS)
|
||||
)
|
||||
if font["OS/2"].usWidthClass != widthClass:
|
||||
log.info("Setting OS/2.usWidthClass = %s", widthClass)
|
||||
font["OS/2"].usWidthClass = widthClass
|
||||
|
||||
if "slnt" in location and "post" in font:
|
||||
italicAngle = max(-90, min(location["slnt"], 90))
|
||||
if font["post"].italicAngle != italicAngle:
|
||||
log.info("Setting post.italicAngle = %s", italicAngle)
|
||||
font["post"].italicAngle = italicAngle
|
||||
|
||||
|
||||
def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
|
||||
"""
|
||||
Build variation font from a designspace file.
|
||||
@ -840,6 +879,10 @@ def build(designspace, master_finder=lambda s:s, exclude=[], optimize=True):
|
||||
post.extraNames = []
|
||||
post.mapping = {}
|
||||
|
||||
set_default_weight_width_slant(
|
||||
vf, location={axis.axisTag: axis.defaultValue for axis in vf["fvar"].axes}
|
||||
)
|
||||
|
||||
for tag in exclude:
|
||||
if tag in vf:
|
||||
del vf[tag]
|
||||
|
6
NEWS.rst
6
NEWS.rst
@ -1,3 +1,9 @@
|
||||
3.43.1 (released 2019-06-19)
|
||||
----------------------------
|
||||
|
||||
- [subset] Fixed regression when passing ``--flavor=woff2`` option with an input font
|
||||
that was already compressed as WOFF 1.0 (#1650).
|
||||
|
||||
3.43.0 (released 2019-06-18)
|
||||
----------------------------
|
||||
|
||||
|
BIN
Tests/subset/data/Lobster.subset.otf
Normal file
BIN
Tests/subset/data/Lobster.subset.otf
Normal file
Binary file not shown.
@ -678,6 +678,48 @@ class SubsetTest(unittest.TestCase):
|
||||
subsetfont = TTFont(subsetpath)
|
||||
self.expect_ttx(subsetfont, self.getpath("expect_HVVAR_retain_gids.ttx"), ["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"])
|
||||
|
||||
def test_subset_flavor(self):
|
||||
_, fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
||||
font = TTFont(fontpath)
|
||||
|
||||
woff_path = self.temp_path(".woff")
|
||||
subset.main(
|
||||
[
|
||||
fontpath,
|
||||
"*",
|
||||
"--flavor=woff",
|
||||
"--output-file=%s" % woff_path,
|
||||
]
|
||||
)
|
||||
woff = TTFont(woff_path)
|
||||
|
||||
self.assertEqual(woff.flavor, "woff")
|
||||
|
||||
woff2_path = self.temp_path(".woff2")
|
||||
subset.main(
|
||||
[
|
||||
woff_path,
|
||||
"*",
|
||||
"--flavor=woff2",
|
||||
"--output-file=%s" % woff2_path,
|
||||
]
|
||||
)
|
||||
woff2 = TTFont(woff2_path)
|
||||
|
||||
self.assertEqual(woff2.flavor, "woff2")
|
||||
|
||||
ttf_path = self.temp_path(".ttf")
|
||||
subset.main(
|
||||
[
|
||||
woff2_path,
|
||||
"*",
|
||||
"--output-file=%s" % ttf_path,
|
||||
]
|
||||
)
|
||||
ttf = TTFont(ttf_path)
|
||||
|
||||
self.assertEqual(ttf.flavor, None)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(unittest.main())
|
||||
|
@ -386,15 +386,18 @@ class WOFF2FlavorDataTest(unittest.TestCase):
|
||||
self.assertEqual(flavorData.minorVersion, 1)
|
||||
|
||||
def test_mutually_exclusive_args(self):
|
||||
msg = "arguments are mutually exclusive"
|
||||
reader = DummyReader(self.file)
|
||||
with self.assertRaisesRegex(TypeError, "arguments are mutually exclusive"):
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
WOFF2FlavorData(reader, transformedTables={"hmtx"})
|
||||
with self.assertRaisesRegex(TypeError, msg):
|
||||
WOFF2FlavorData(reader, data=WOFF2FlavorData())
|
||||
|
||||
def test_transformTables_default(self):
|
||||
def test_transformedTables_default(self):
|
||||
flavorData = WOFF2FlavorData()
|
||||
self.assertEqual(flavorData.transformedTables, set(woff2TransformedTableTags))
|
||||
|
||||
def test_transformTables_invalid(self):
|
||||
def test_transformedTables_invalid(self):
|
||||
msg = r"'glyf' and 'loca' must be transformed \(or not\) together"
|
||||
|
||||
with self.assertRaisesRegex(ValueError, msg):
|
||||
@ -1258,6 +1261,24 @@ class MainTest(object):
|
||||
|
||||
assert (tmpdir / "TestOTF-Regular.woff2").check(file=True)
|
||||
|
||||
def test_recompress_woff2_keeps_flavorData(self, tmpdir):
|
||||
woff2_font = ttLib.TTFont(BytesIO(TT_WOFF2.getvalue()))
|
||||
woff2_font.flavorData.privData = b"FOOBAR"
|
||||
woff2_file = tmpdir / "TestTTF-Regular.woff2"
|
||||
woff2_font.save(str(woff2_file))
|
||||
|
||||
assert woff2_font.flavorData.transformedTables == {"glyf", "loca"}
|
||||
|
||||
woff2.main(["compress", "--hmtx-transform", str(woff2_file)])
|
||||
|
||||
output_file = tmpdir / "TestTTF-Regular#1.woff2"
|
||||
assert output_file.check(file=True)
|
||||
|
||||
new_woff2_font = ttLib.TTFont(str(output_file))
|
||||
|
||||
assert new_woff2_font.flavorData.transformedTables == {"glyf", "loca", "hmtx"}
|
||||
assert new_woff2_font.flavorData.privData == b"FOOBAR"
|
||||
|
||||
def test_decompress_ttf(self, tmpdir):
|
||||
input_file = tmpdir / "TestTTF-Regular.woff2"
|
||||
input_file.write_binary(TT_WOFF2.getvalue())
|
||||
|
@ -55,7 +55,7 @@
|
||||
will be recalculated by the compiler -->
|
||||
<version value="4"/>
|
||||
<xAvgCharWidth value="506"/>
|
||||
<usWeightClass value="400"/>
|
||||
<usWeightClass value="368"/>
|
||||
<usWidthClass value="5"/>
|
||||
<fsType value="00000000 00000100"/>
|
||||
<ySubscriptXSize value="650"/>
|
||||
|
@ -55,7 +55,7 @@
|
||||
will be recalculated by the compiler -->
|
||||
<version value="4"/>
|
||||
<xAvgCharWidth value="580"/>
|
||||
<usWeightClass value="400"/>
|
||||
<usWeightClass value="350"/>
|
||||
<usWidthClass value="5"/>
|
||||
<fsType value="00000000 00000100"/>
|
||||
<ySubscriptXSize value="650"/>
|
||||
|
@ -4,6 +4,7 @@ from fontTools.ttLib import TTFont, newTable
|
||||
from fontTools.varLib import build
|
||||
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
|
||||
from fontTools.designspaceLib import (
|
||||
DesignSpaceDocumentError, DesignSpaceDocument, SourceDescriptor,
|
||||
)
|
||||
@ -661,5 +662,81 @@ def _extract_flat_kerning(font, pairpos_table):
|
||||
return extracted_kerning
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ttFont():
|
||||
f = TTFont()
|
||||
f["OS/2"] = newTable("OS/2")
|
||||
f["OS/2"].usWeightClass = 400
|
||||
f["OS/2"].usWidthClass = 100
|
||||
f["post"] = newTable("post")
|
||||
f["post"].italicAngle = 0
|
||||
return f
|
||||
|
||||
|
||||
class SetDefaultWeightWidthSlantTest(object):
|
||||
@pytest.mark.parametrize(
|
||||
"location, expected",
|
||||
[
|
||||
({"wght": 0}, 1),
|
||||
({"wght": 1}, 1),
|
||||
({"wght": 100}, 100),
|
||||
({"wght": 1000}, 1000),
|
||||
({"wght": 1001}, 1000),
|
||||
],
|
||||
)
|
||||
def test_wght(self, ttFont, location, expected):
|
||||
set_default_weight_width_slant(ttFont, location)
|
||||
|
||||
assert ttFont["OS/2"].usWeightClass == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location, expected",
|
||||
[
|
||||
({"wdth": 0}, 1),
|
||||
({"wdth": 56}, 1),
|
||||
({"wdth": 57}, 2),
|
||||
({"wdth": 62.5}, 2),
|
||||
({"wdth": 75}, 3),
|
||||
({"wdth": 87.5}, 4),
|
||||
({"wdth": 100}, 5),
|
||||
({"wdth": 112.5}, 6),
|
||||
({"wdth": 125}, 7),
|
||||
({"wdth": 150}, 8),
|
||||
({"wdth": 200}, 9),
|
||||
({"wdth": 201}, 9),
|
||||
({"wdth": 1000}, 9),
|
||||
],
|
||||
)
|
||||
def test_wdth(self, ttFont, location, expected):
|
||||
set_default_weight_width_slant(ttFont, location)
|
||||
|
||||
assert ttFont["OS/2"].usWidthClass == expected
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"location, expected",
|
||||
[
|
||||
({"slnt": -91}, -90),
|
||||
({"slnt": -90}, -90),
|
||||
({"slnt": 0}, 0),
|
||||
({"slnt": 11.5}, 11.5),
|
||||
({"slnt": 90}, 90),
|
||||
({"slnt": 91}, 90),
|
||||
],
|
||||
)
|
||||
def test_slnt(self, ttFont, location, expected):
|
||||
set_default_weight_width_slant(ttFont, location)
|
||||
|
||||
assert ttFont["post"].italicAngle == expected
|
||||
|
||||
def test_all(self, ttFont):
|
||||
set_default_weight_width_slant(
|
||||
ttFont, {"wght": 500, "wdth": 150, "slnt": -12.0}
|
||||
)
|
||||
|
||||
assert ttFont["OS/2"].usWeightClass == 500
|
||||
assert ttFont["OS/2"].usWidthClass == 8
|
||||
assert ttFont["post"].italicAngle == -12.0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(unittest.main())
|
||||
|
@ -1,5 +1,5 @@
|
||||
[bumpversion]
|
||||
current_version = 3.43.1.dev0
|
||||
current_version = 3.43.2.dev0
|
||||
commit = True
|
||||
tag = False
|
||||
tag_name = {new_version}
|
||||
|
Loading…
x
Reference in New Issue
Block a user