2019-12-12 12:29:08 +00:00
|
|
|
import io
|
2022-04-19 15:26:27 +01:00
|
|
|
import fontTools.ttLib.tables.otBase
|
2022-04-05 18:34:30 +01:00
|
|
|
from fontTools.misc.testTools import getXML, stripVariableItemsFromTTX
|
2021-08-20 00:45:43 +02:00
|
|
|
from fontTools.misc.textTools import tobytes, tostr
|
2015-11-13 17:40:17 -08:00
|
|
|
from fontTools import subset
|
2019-12-12 12:29:08 +00:00
|
|
|
from fontTools.fontBuilder import FontBuilder
|
2021-02-01 16:35:32 +00:00
|
|
|
from fontTools.pens.ttGlyphPen import TTGlyphPen
|
2016-09-26 19:13:56 +01:00
|
|
|
from fontTools.ttLib import TTFont, newTable
|
2021-02-01 16:35:32 +00:00
|
|
|
from fontTools.ttLib.tables import otTables as ot
|
2016-10-04 14:31:30 +01:00
|
|
|
from fontTools.misc.loggingTools import CapturingLogHandler
|
2021-11-12 18:59:09 +00:00
|
|
|
from fontTools.subset.svg import etree
|
2015-11-13 17:40:17 -08:00
|
|
|
import difflib
|
2016-05-11 17:39:37 -07:00
|
|
|
import logging
|
2015-11-13 17:40:17 -08:00
|
|
|
import os
|
|
|
|
import shutil
|
|
|
|
import sys
|
|
|
|
import tempfile
|
|
|
|
import unittest
|
2020-02-12 14:20:36 +00:00
|
|
|
import pathlib
|
|
|
|
import pytest
|
2015-11-13 17:40:17 -08:00
|
|
|
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
class SubsetTest:
|
|
|
|
@classmethod
|
|
|
|
def setup_class(cls):
|
|
|
|
cls.tempdir = None
|
|
|
|
cls.num_tempfiles = 0
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
def teardown_class(cls):
|
|
|
|
if cls.tempdir:
|
2022-04-22 11:46:07 +01:00
|
|
|
shutil.rmtree(cls.tempdir, ignore_errors=True)
|
2015-11-13 17:40:17 -08:00
|
|
|
|
|
|
|
@staticmethod
|
2023-01-18 16:42:43 -07:00
|
|
|
def getpath(*testfile):
|
2015-11-13 17:40:17 -08:00
|
|
|
path, _ = os.path.split(__file__)
|
2023-01-18 16:42:43 -07:00
|
|
|
return os.path.join(path, "data", *testfile)
|
2015-11-13 17:40:17 -08:00
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
@classmethod
|
|
|
|
def temp_path(cls, suffix):
|
|
|
|
if not cls.tempdir:
|
|
|
|
cls.tempdir = tempfile.mkdtemp()
|
|
|
|
cls.num_tempfiles += 1
|
|
|
|
return os.path.join(cls.tempdir, "tmp%d%s" % (cls.num_tempfiles, suffix))
|
2015-11-13 17:40:17 -08:00
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
@staticmethod
|
|
|
|
def read_ttx(path):
|
2022-04-05 18:34:30 +01:00
|
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
|
|
ttx = f.read()
|
|
|
|
# don't care whether TTF or OTF, thus strip sfntVersion as well
|
|
|
|
return stripVariableItemsFromTTX(ttx, sfntVersion=True).splitlines(True)
|
2015-11-13 17:40:17 -08:00
|
|
|
|
2020-05-11 18:32:32 +01:00
|
|
|
def expect_ttx(self, font, expected_ttx, tables=None):
|
2015-11-13 17:40:17 -08:00
|
|
|
path = self.temp_path(suffix=".ttx")
|
2016-02-01 13:39:39 +00:00
|
|
|
font.saveXML(path, tables=tables)
|
2015-11-13 17:40:17 -08:00
|
|
|
actual = self.read_ttx(path)
|
|
|
|
expected = self.read_ttx(expected_ttx)
|
|
|
|
if actual != expected:
|
|
|
|
for line in difflib.unified_diff(
|
2017-01-17 17:39:41 +00:00
|
|
|
expected, actual, fromfile=expected_ttx, tofile=path
|
|
|
|
):
|
2015-11-13 17:40:17 -08:00
|
|
|
sys.stdout.write(line)
|
2022-04-19 12:03:50 +01:00
|
|
|
pytest.fail("TTX output is different from expected")
|
2015-11-13 17:40:17 -08:00
|
|
|
|
|
|
|
def compile_font(self, path, suffix):
|
|
|
|
savepath = self.temp_path(suffix=suffix)
|
|
|
|
font = TTFont(recalcBBoxes=False, recalcTimestamp=False)
|
2016-02-01 13:39:39 +00:00
|
|
|
font.importXML(path)
|
2015-11-13 17:40:17 -08:00
|
|
|
font.save(savepath, reorderTables=None)
|
2022-04-19 12:40:10 +01:00
|
|
|
return savepath
|
2015-11-13 17:40:17 -08:00
|
|
|
|
|
|
|
# -----
|
|
|
|
# Tests
|
|
|
|
# -----
|
|
|
|
|
2020-11-20 23:31:09 +02:00
|
|
|
def test_layout_scripts(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("layout_scripts.ttx"), ".otf")
|
2020-11-20 23:31:09 +02:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2020-11-20 23:31:09 +02:00
|
|
|
fontpath,
|
|
|
|
"--glyphs=*",
|
|
|
|
"--layout-features=*",
|
|
|
|
"--layout-scripts=latn,arab.URD,arab.dflt",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2020-11-20 23:31:09 +02:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_layout_scripts.ttx"), ["GPOS", "GSUB"]
|
|
|
|
)
|
|
|
|
|
2015-11-13 17:40:17 -08:00
|
|
|
def test_no_notdef_outline_otf(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
|
2015-11-13 17:40:17 -08:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2015-11-13 17:40:17 -08:00
|
|
|
fontpath,
|
|
|
|
"--no-notdef-outline",
|
|
|
|
"--gids=0",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2015-11-13 17:40:17 -08:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_no_notdef_outline_otf.ttx"), ["CFF "]
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_no_notdef_outline_cid(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestCID-Regular.ttx"), ".otf")
|
2015-11-13 17:40:17 -08:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2015-11-13 17:40:17 -08:00
|
|
|
fontpath,
|
|
|
|
"--no-notdef-outline",
|
|
|
|
"--gids=0",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2015-11-13 17:40:17 -08:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_no_notdef_outline_cid.ttx"), ["CFF "]
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_no_notdef_outline_ttf(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2015-11-13 17:40:17 -08:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2015-11-13 17:40:17 -08:00
|
|
|
fontpath,
|
|
|
|
"--no-notdef-outline",
|
|
|
|
"--gids=0",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2015-11-13 17:40:17 -08:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_no_notdef_outline_ttf.ttx"),
|
|
|
|
["glyf", "hmtx"],
|
|
|
|
)
|
|
|
|
|
2017-08-31 12:07:07 +02:00
|
|
|
def test_subset_ankr(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf")
|
2017-08-31 12:07:07 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_ankr.ttx"), ["ankr"])
|
|
|
|
|
|
|
|
def test_subset_ankr_remove(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestANKR.ttx"), ".ttf")
|
2017-08-31 12:07:07 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=two", "--output-file=%s" % subsetpath])
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "ankr" not in TTFont(subsetpath)
|
2017-08-31 12:07:07 +02:00
|
|
|
|
2017-08-25 22:01:14 +02:00
|
|
|
def test_subset_bsln_format_0(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestBSLN-0.ttx"), ".ttf")
|
2017-08-25 22:01:14 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_bsln_0.ttx"), ["bsln"])
|
|
|
|
|
|
|
|
def test_subset_bsln_format_0_from_format_1(self):
|
|
|
|
# TestBSLN-1 defines the ideographic baseline to be the font's default,
|
|
|
|
# and specifies that glyphs {.notdef, zero, one, two} use the roman
|
|
|
|
# baseline instead of the default ideographic baseline. As we request
|
|
|
|
# a subsetted font with {zero, one} and the implicit .notdef, all
|
|
|
|
# glyphs in the resulting font use the Roman baseline. In this case,
|
|
|
|
# we expect a format 0 'bsln' table because it is the most compact.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf")
|
2017-08-25 22:01:14 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--unicodes=U+0030-0031", "--output-file=%s" % subsetpath]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_bsln_0.ttx"), ["bsln"])
|
|
|
|
|
|
|
|
def test_subset_bsln_format_1(self):
|
|
|
|
# TestBSLN-1 defines the ideographic baseline to be the font's default,
|
|
|
|
# and specifies that glyphs {.notdef, zero, one, two} use the roman
|
|
|
|
# baseline instead of the default ideographic baseline. We request
|
|
|
|
# a subset where the majority of glyphs use the roman baseline,
|
|
|
|
# but one single glyph (uni2EA2) is ideographic. In the resulting
|
|
|
|
# subsetted font, we expect a format 1 'bsln' table whose default
|
|
|
|
# is Roman, but with an override that uses the ideographic baseline
|
|
|
|
# for uni2EA2.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestBSLN-1.ttx"), ".ttf")
|
2017-08-25 22:01:14 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--unicodes=U+0030-0031,U+2EA2", "--output-file=%s" % subsetpath]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_bsln_1.ttx"), ["bsln"])
|
|
|
|
|
|
|
|
def test_subset_bsln_format_2(self):
|
|
|
|
# The 'bsln' table in TestBSLN-2 refers to control points in glyph 'P'
|
|
|
|
# for defining its baselines. Therefore, the subsetted font should
|
|
|
|
# include this glyph even though it is not requested explicitly.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestBSLN-2.ttx"), ".ttf")
|
2017-08-25 22:01:14 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_bsln_2.ttx"), ["bsln"])
|
|
|
|
|
|
|
|
def test_subset_bsln_format_2_from_format_3(self):
|
|
|
|
# TestBSLN-3 defines the ideographic baseline to be the font's default,
|
|
|
|
# and specifies that glyphs {.notdef, zero, one, two, P} use the roman
|
|
|
|
# baseline instead of the default ideographic baseline. As we request
|
|
|
|
# a subsetted font with zero and the implicit .notdef and P for
|
|
|
|
# baseline measurement, all glyphs in the resulting font use the Roman
|
|
|
|
# baseline. In this case, we expect a format 2 'bsln' table because it
|
|
|
|
# is the most compact encoding.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf")
|
2017-08-25 22:01:14 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=U+0030", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_bsln_2.ttx"), ["bsln"])
|
|
|
|
|
|
|
|
def test_subset_bsln_format_3(self):
|
|
|
|
# TestBSLN-3 defines the ideographic baseline to be the font's default,
|
|
|
|
# and specifies that glyphs {.notdef, zero, one, two} use the roman
|
|
|
|
# baseline instead of the default ideographic baseline. We request
|
|
|
|
# a subset where the majority of glyphs use the roman baseline,
|
|
|
|
# but one single glyph (uni2EA2) is ideographic. In the resulting
|
|
|
|
# subsetted font, we expect a format 1 'bsln' table whose default
|
|
|
|
# is Roman, but with an override that uses the ideographic baseline
|
|
|
|
# for uni2EA2.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestBSLN-3.ttx"), ".ttf")
|
2017-08-25 22:01:14 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--unicodes=U+0030-0031,U+2EA2", "--output-file=%s" % subsetpath]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_bsln_3.ttx"), ["bsln"])
|
|
|
|
|
2015-11-07 14:09:00 +04:00
|
|
|
def test_subset_clr(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestCLR-Regular.ttx"), ".ttf")
|
2015-11-07 14:09:00 +04:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=smileface", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_keep_colr.ttx"),
|
|
|
|
["GlyphOrder", "hmtx", "glyf", "COLR", "CPAL"],
|
|
|
|
)
|
|
|
|
|
2016-03-31 16:23:24 +02:00
|
|
|
def test_subset_gvar(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf")
|
2016-03-31 16:23:24 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--unicodes=U+002B,U+2212", "--output-file=%s" % subsetpath]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_keep_gvar.ttx"),
|
|
|
|
["GlyphOrder", "avar", "fvar", "gvar", "name"],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_subset_gvar_notdef_outline(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestGVAR.ttx"), ".ttf")
|
2016-03-31 16:23:24 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2016-03-31 16:23:24 +02:00
|
|
|
fontpath,
|
|
|
|
"--unicodes=U+0030",
|
|
|
|
"--notdef_outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2016-03-31 16:23:24 +02:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_keep_gvar_notdef_outline.ttx"),
|
|
|
|
["GlyphOrder", "avar", "fvar", "gvar", "name"],
|
|
|
|
)
|
|
|
|
|
2017-08-25 15:35:33 +02:00
|
|
|
def test_subset_lcar_remove(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf")
|
2017-08-25 15:35:33 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "lcar" not in subsetfont
|
2017-08-25 15:35:33 +02:00
|
|
|
|
|
|
|
def test_subset_lcar_format_0(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestLCAR-0.ttx"), ".ttf")
|
2017-08-25 15:35:33 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=U+FB01", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_lcar_0.ttx"), ["lcar"])
|
|
|
|
|
|
|
|
def test_subset_lcar_format_1(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestLCAR-1.ttx"), ".ttf")
|
2017-08-25 15:35:33 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=U+FB01", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_lcar_1.ttx"), ["lcar"])
|
|
|
|
|
2016-01-23 14:10:32 +04:00
|
|
|
def test_subset_math(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestMATH-Regular.ttx"), ".ttf")
|
2016-01-23 14:10:32 +04:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2016-01-23 14:10:32 +04:00
|
|
|
fontpath,
|
|
|
|
"--unicodes=U+0041,U+0028,U+0302,U+1D400,U+1D435",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2016-01-23 14:10:32 +04:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_keep_math.ttx"),
|
|
|
|
["GlyphOrder", "CFF ", "MATH", "hmtx"],
|
|
|
|
)
|
|
|
|
|
2019-06-09 02:59:53 +02:00
|
|
|
def test_subset_math_partial(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("test_math_partial.ttx"), ".ttf")
|
2019-06-09 02:59:53 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--text=A", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_math_partial.ttx"), ["MATH"])
|
|
|
|
|
2017-08-22 20:37:44 +02:00
|
|
|
def test_subset_opbd_remove(self):
|
|
|
|
# In the test font, only the glyphs 'A' and 'zero' have an entry in
|
|
|
|
# the Optical Bounds table. When subsetting, we do not request any
|
|
|
|
# of those glyphs. Therefore, the produced subsetted font should
|
|
|
|
# not contain an 'opbd' table.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf")
|
2017-08-22 20:37:44 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=one", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "opbd" not in subsetfont
|
2017-08-22 20:37:44 +02:00
|
|
|
|
|
|
|
def test_subset_opbd_format_0(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestOPBD-0.ttx"), ".ttf")
|
2017-08-22 20:37:44 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_opbd_0.ttx"), ["opbd"])
|
|
|
|
|
|
|
|
def test_subset_opbd_format_1(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestOPBD-1.ttx"), ".ttf")
|
2017-08-22 20:37:44 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--glyphs=A", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_opbd_1.ttx"), ["opbd"])
|
|
|
|
|
2017-08-22 00:55:17 +02:00
|
|
|
def test_subset_prop_remove_default_zero(self):
|
|
|
|
# If all glyphs have an AAT glyph property with value 0,
|
|
|
|
# the "prop" table should be removed from the subsetted font.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
|
2017-08-22 00:55:17 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=U+0041", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "prop" not in subsetfont
|
2017-08-22 00:55:17 +02:00
|
|
|
|
|
|
|
def test_subset_prop_0(self):
|
|
|
|
# If all glyphs share the same AAT glyph properties, the "prop" table
|
|
|
|
# in the subsetted font should use format 0.
|
|
|
|
#
|
|
|
|
# Unless the shared value is zero, in which case the subsetted font
|
|
|
|
# should have no "prop" table at all. But that case has already been
|
|
|
|
# tested above in test_subset_prop_remove_default_zero().
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
|
2017-08-22 00:55:17 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-08-22 00:55:17 +02:00
|
|
|
fontpath,
|
|
|
|
"--unicodes=U+0030-0032",
|
|
|
|
"--no-notdef-glyph",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-08-22 00:55:17 +02:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_prop_0.ttx"), ["prop"])
|
|
|
|
|
|
|
|
def test_subset_prop_1(self):
|
|
|
|
# If not all glyphs share the same AAT glyph properties, the subsetted
|
|
|
|
# font should contain a "prop" table in format 1. To save space, the
|
|
|
|
# DefaultProperties should be set to the most frequent value.
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestPROP.ttx"), ".ttf")
|
2017-08-22 00:55:17 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-08-22 00:55:17 +02:00
|
|
|
fontpath,
|
|
|
|
"--unicodes=U+0030-0032",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-08-22 00:55:17 +02:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_prop_1.ttx"), ["prop"])
|
|
|
|
|
2015-12-07 11:13:44 +01:00
|
|
|
def test_options(self):
|
2019-03-06 16:47:16 +01:00
|
|
|
# https://github.com/fonttools/fonttools/issues/413
|
2015-12-07 11:13:44 +01:00
|
|
|
opt1 = subset.Options()
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "Xyz-" not in opt1.layout_features
|
2015-12-07 11:13:44 +01:00
|
|
|
opt2 = subset.Options()
|
|
|
|
opt2.layout_features.append("Xyz-")
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "Xyz-" in opt2.layout_features
|
|
|
|
assert "Xyz-" not in opt1.layout_features
|
2015-12-07 11:13:44 +01:00
|
|
|
|
2015-12-09 13:05:16 -08:00
|
|
|
def test_google_color(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf")
|
2015-12-09 13:05:16 -08:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "CBDT" in subsetfont
|
|
|
|
assert "CBLC" in subsetfont
|
|
|
|
assert "x" in subsetfont["CBDT"].strikeData[0]
|
|
|
|
assert "y" not in subsetfont["CBDT"].strikeData[0]
|
2015-12-09 13:05:16 -08:00
|
|
|
|
|
|
|
def test_google_color_all(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("google_color.ttx"), ".ttf")
|
2015-12-09 13:05:16 -08:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert "x" in subsetfont["CBDT"].strikeData[0]
|
|
|
|
assert "y" in subsetfont["CBDT"].strikeData[0]
|
2015-11-13 17:40:17 -08:00
|
|
|
|
2019-08-01 01:33:10 +02:00
|
|
|
def test_sbix(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("sbix.ttx"), ".ttf")
|
2019-08-01 01:33:10 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--gids=0,1", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_sbix.ttx"), ["sbix"])
|
|
|
|
|
2023-01-18 16:42:43 -07:00
|
|
|
def test_varComposite(self):
|
|
|
|
fontpath = self.getpath("..", "..", "ttLib", "data", "varc-ac00-ac01.ttf")
|
|
|
|
origfont = TTFont(fontpath)
|
|
|
|
assert len(origfont.getGlyphOrder()) == 6
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=ac00", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
assert len(subsetfont.getGlyphOrder()) == 4
|
|
|
|
subset.main([fontpath, "--unicodes=ac01", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
assert len(subsetfont.getGlyphOrder()) == 5
|
|
|
|
|
2016-05-11 17:39:37 -07:00
|
|
|
def test_timing_publishes_parts(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2016-05-11 17:39:37 -07:00
|
|
|
|
|
|
|
options = subset.Options()
|
|
|
|
options.timing = True
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(text="ABC")
|
|
|
|
font = TTFont(fontpath)
|
|
|
|
with CapturingLogHandler("fontTools.subset.timer", logging.DEBUG) as captor:
|
|
|
|
subsetter.subset(font)
|
2018-07-24 12:17:47 +01:00
|
|
|
logs = captor.records
|
2016-05-11 17:39:37 -07:00
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert len(logs) > 5
|
|
|
|
assert len(logs) == len(
|
|
|
|
[l for l in logs if "msg" in l.args and "time" in l.args]
|
|
|
|
)
|
2016-05-11 17:39:37 -07:00
|
|
|
# Look for a few things we know should happen
|
2022-04-19 12:03:50 +01:00
|
|
|
assert filter(lambda l: l.args["msg"] == "load 'cmap'", logs)
|
|
|
|
assert filter(lambda l: l.args["msg"] == "subset 'cmap'", logs)
|
|
|
|
assert filter(lambda l: l.args["msg"] == "subset 'glyf'", logs)
|
2016-05-11 17:39:37 -07:00
|
|
|
|
2016-09-26 19:13:56 +01:00
|
|
|
def test_passthrough_tables(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2016-09-26 19:13:56 +01:00
|
|
|
font = TTFont(fontpath)
|
|
|
|
unknown_tag = "ZZZZ"
|
|
|
|
unknown_table = newTable(unknown_tag)
|
|
|
|
unknown_table.data = b"\0" * 10
|
|
|
|
font[unknown_tag] = unknown_table
|
|
|
|
font.save(fontpath)
|
|
|
|
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
|
|
|
|
# tables we can't subset are dropped by default
|
2022-04-19 12:03:50 +01:00
|
|
|
assert unknown_tag not in subsetfont
|
2016-09-26 19:13:56 +01:00
|
|
|
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--passthrough-tables", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
|
|
|
|
# unknown tables are kept if --passthrough-tables option is passed
|
2022-04-19 12:03:50 +01:00
|
|
|
assert unknown_tag in subsetfont
|
2016-09-26 19:13:56 +01:00
|
|
|
|
2016-11-29 15:45:07 +00:00
|
|
|
def test_non_BMP_text_arg_input(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(
|
2016-11-29 15:45:07 +00:00
|
|
|
self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf"
|
|
|
|
)
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
text = tostr("A\U0001F6D2", encoding="utf-8")
|
|
|
|
|
|
|
|
subset.main([fontpath, "--text=%s" % text, "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert subsetfont["maxp"].numGlyphs == 3
|
|
|
|
assert subsetfont.getGlyphOrder() == [".notdef", "A", "u1F6D2"]
|
2016-11-29 15:45:07 +00:00
|
|
|
|
|
|
|
def test_non_BMP_text_file_input(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(
|
2016-11-29 15:45:07 +00:00
|
|
|
self.getpath("TestTTF-Regular_non_BMP_char.ttx"), ".ttf"
|
|
|
|
)
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
text = tobytes("A\U0001F6D2", encoding="utf-8")
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False) as tmp:
|
|
|
|
tmp.write(text)
|
|
|
|
|
|
|
|
try:
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--text-file=%s" % tmp.name, "--output-file=%s" % subsetpath]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
finally:
|
|
|
|
os.remove(tmp.name)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert subsetfont["maxp"].numGlyphs == 3
|
|
|
|
assert subsetfont.getGlyphOrder() == [".notdef", "A", "u1F6D2"]
|
2016-11-29 15:45:07 +00:00
|
|
|
|
2017-01-14 14:27:30 +00:00
|
|
|
def test_no_hinting_CFF(self):
|
|
|
|
ttxpath = self.getpath("Lobster.subset.ttx")
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2017-01-14 14:27:30 +00:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-01-14 14:27:30 +00:00
|
|
|
fontpath,
|
|
|
|
"--no-hinting",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"*",
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-01-14 14:27:30 +00:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, self.getpath("expect_no_hinting_CFF.ttx"), ["CFF "])
|
|
|
|
|
|
|
|
def test_desubroutinize_CFF(self):
|
|
|
|
ttxpath = self.getpath("Lobster.subset.ttx")
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2017-01-14 14:27:30 +00:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-01-14 14:27:30 +00:00
|
|
|
fontpath,
|
|
|
|
"--desubroutinize",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"*",
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-01-14 14:27:30 +00:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_desubroutinize_CFF.ttx"), ["CFF "]
|
|
|
|
)
|
|
|
|
|
[subset CFF] Fix de-subroutinizing bug when subroutines contain hints (#1499)
* [subset CFF] Fix bug in de-subroutinizing when subroutines contain hints, issue 1493
The code was skipping executing a subroutine if it had already been desubroutinized. The initial set of vstemhm and hstemhm operators and values may be in a subroutine. If a charstring is being executed which calls such subroutines, they still need to be executed in order to count the number of hint values seen, so that the byte length of the hintmask can be calculated.
I fixed this bug by executing subroutines even if they have already been desubroutinized, as long as (we don't know yet if we are doing hintmasks) or ( we do need a hintmask, but have not yet seen it).
Clean up code per Cosimo's suggestions:
In arg list for stop_hint_count(), use *args to accept unused argument, rather than a dummy positional argument.
Change stop_hintcount_ops to a from a global variable to a class variable in _DesubroutinizingT2Decompiler.
Remove un-needed 'return' at line 387
Remove duplicate assignment of cs at line 437
Add patch for the bug where AttributeError is encountered when remove_hints is run after desubroutinize: remove lines deleting the GlobalSubrs for each FontDict. This always needed to be done only once, and is now in any case done in cff.GlobalSubrs.clear(), at the end of the desubroutinize() function.
Changed test case subset_test.py::'test_no_hinting_desubroutinize_CFF' to reference a font with a non-empty GlobalSubr, in order to trigger AttributeError traceback.
2019-02-18 01:43:27 -08:00
|
|
|
def test_desubroutinize_hinted_subrs_CFF(self):
|
|
|
|
ttxpath = self.getpath("test_hinted_subrs_CFF.ttx")
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
[subset CFF] Fix de-subroutinizing bug when subroutines contain hints (#1499)
* [subset CFF] Fix bug in de-subroutinizing when subroutines contain hints, issue 1493
The code was skipping executing a subroutine if it had already been desubroutinized. The initial set of vstemhm and hstemhm operators and values may be in a subroutine. If a charstring is being executed which calls such subroutines, they still need to be executed in order to count the number of hint values seen, so that the byte length of the hintmask can be calculated.
I fixed this bug by executing subroutines even if they have already been desubroutinized, as long as (we don't know yet if we are doing hintmasks) or ( we do need a hintmask, but have not yet seen it).
Clean up code per Cosimo's suggestions:
In arg list for stop_hint_count(), use *args to accept unused argument, rather than a dummy positional argument.
Change stop_hintcount_ops to a from a global variable to a class variable in _DesubroutinizingT2Decompiler.
Remove un-needed 'return' at line 387
Remove duplicate assignment of cs at line 437
Add patch for the bug where AttributeError is encountered when remove_hints is run after desubroutinize: remove lines deleting the GlobalSubrs for each FontDict. This always needed to be done only once, and is now in any case done in cff.GlobalSubrs.clear(), at the end of the desubroutinize() function.
Changed test case subset_test.py::'test_no_hinting_desubroutinize_CFF' to reference a font with a non-empty GlobalSubr, in order to trigger AttributeError traceback.
2019-02-18 01:43:27 -08:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
[subset CFF] Fix de-subroutinizing bug when subroutines contain hints (#1499)
* [subset CFF] Fix bug in de-subroutinizing when subroutines contain hints, issue 1493
The code was skipping executing a subroutine if it had already been desubroutinized. The initial set of vstemhm and hstemhm operators and values may be in a subroutine. If a charstring is being executed which calls such subroutines, they still need to be executed in order to count the number of hint values seen, so that the byte length of the hintmask can be calculated.
I fixed this bug by executing subroutines even if they have already been desubroutinized, as long as (we don't know yet if we are doing hintmasks) or ( we do need a hintmask, but have not yet seen it).
Clean up code per Cosimo's suggestions:
In arg list for stop_hint_count(), use *args to accept unused argument, rather than a dummy positional argument.
Change stop_hintcount_ops to a from a global variable to a class variable in _DesubroutinizingT2Decompiler.
Remove un-needed 'return' at line 387
Remove duplicate assignment of cs at line 437
Add patch for the bug where AttributeError is encountered when remove_hints is run after desubroutinize: remove lines deleting the GlobalSubrs for each FontDict. This always needed to be done only once, and is now in any case done in cff.GlobalSubrs.clear(), at the end of the desubroutinize() function.
Changed test case subset_test.py::'test_no_hinting_desubroutinize_CFF' to reference a font with a non-empty GlobalSubr, in order to trigger AttributeError traceback.
2019-02-18 01:43:27 -08:00
|
|
|
fontpath,
|
|
|
|
"--desubroutinize",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"*",
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
[subset CFF] Fix de-subroutinizing bug when subroutines contain hints (#1499)
* [subset CFF] Fix bug in de-subroutinizing when subroutines contain hints, issue 1493
The code was skipping executing a subroutine if it had already been desubroutinized. The initial set of vstemhm and hstemhm operators and values may be in a subroutine. If a charstring is being executed which calls such subroutines, they still need to be executed in order to count the number of hint values seen, so that the byte length of the hintmask can be calculated.
I fixed this bug by executing subroutines even if they have already been desubroutinized, as long as (we don't know yet if we are doing hintmasks) or ( we do need a hintmask, but have not yet seen it).
Clean up code per Cosimo's suggestions:
In arg list for stop_hint_count(), use *args to accept unused argument, rather than a dummy positional argument.
Change stop_hintcount_ops to a from a global variable to a class variable in _DesubroutinizingT2Decompiler.
Remove un-needed 'return' at line 387
Remove duplicate assignment of cs at line 437
Add patch for the bug where AttributeError is encountered when remove_hints is run after desubroutinize: remove lines deleting the GlobalSubrs for each FontDict. This always needed to be done only once, and is now in any case done in cff.GlobalSubrs.clear(), at the end of the desubroutinize() function.
Changed test case subset_test.py::'test_no_hinting_desubroutinize_CFF' to reference a font with a non-empty GlobalSubr, in order to trigger AttributeError traceback.
2019-02-18 01:43:27 -08:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("test_hinted_subrs_CFF.desub.ttx"), ["CFF "]
|
|
|
|
)
|
|
|
|
|
2019-05-03 01:14:09 +02:00
|
|
|
def test_desubroutinize_cntrmask_CFF(self):
|
|
|
|
ttxpath = self.getpath("test_cntrmask_CFF.ttx")
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2019-05-03 01:14:09 +02:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2019-05-03 01:14:09 +02:00
|
|
|
fontpath,
|
|
|
|
"--desubroutinize",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"*",
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2019-05-03 01:14:09 +02:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("test_cntrmask_CFF.desub.ttx"), ["CFF "]
|
|
|
|
)
|
|
|
|
|
2017-01-14 14:27:30 +00:00
|
|
|
def test_no_hinting_desubroutinize_CFF(self):
|
[subset CFF] Fix de-subroutinizing bug when subroutines contain hints (#1499)
* [subset CFF] Fix bug in de-subroutinizing when subroutines contain hints, issue 1493
The code was skipping executing a subroutine if it had already been desubroutinized. The initial set of vstemhm and hstemhm operators and values may be in a subroutine. If a charstring is being executed which calls such subroutines, they still need to be executed in order to count the number of hint values seen, so that the byte length of the hintmask can be calculated.
I fixed this bug by executing subroutines even if they have already been desubroutinized, as long as (we don't know yet if we are doing hintmasks) or ( we do need a hintmask, but have not yet seen it).
Clean up code per Cosimo's suggestions:
In arg list for stop_hint_count(), use *args to accept unused argument, rather than a dummy positional argument.
Change stop_hintcount_ops to a from a global variable to a class variable in _DesubroutinizingT2Decompiler.
Remove un-needed 'return' at line 387
Remove duplicate assignment of cs at line 437
Add patch for the bug where AttributeError is encountered when remove_hints is run after desubroutinize: remove lines deleting the GlobalSubrs for each FontDict. This always needed to be done only once, and is now in any case done in cff.GlobalSubrs.clear(), at the end of the desubroutinize() function.
Changed test case subset_test.py::'test_no_hinting_desubroutinize_CFF' to reference a font with a non-empty GlobalSubr, in order to trigger AttributeError traceback.
2019-02-18 01:43:27 -08:00
|
|
|
ttxpath = self.getpath("test_hinted_subrs_CFF.ttx")
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2017-01-14 14:27:30 +00:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-01-14 14:27:30 +00:00
|
|
|
fontpath,
|
|
|
|
"--no-hinting",
|
|
|
|
"--desubroutinize",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"*",
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-01-14 14:27:30 +00:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_no_hinting_desubroutinize_CFF.ttx"),
|
|
|
|
["CFF "],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_no_hinting_TTF(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2017-01-14 14:27:30 +00:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-01-14 14:27:30 +00:00
|
|
|
fontpath,
|
|
|
|
"--no-hinting",
|
|
|
|
"--notdef-outline",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"*",
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-01-14 14:27:30 +00:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_no_hinting_TTF.ttx"), ["glyf", "maxp"]
|
|
|
|
)
|
|
|
|
for tag in subset.Options().hinting_tables:
|
2022-04-19 12:03:50 +01:00
|
|
|
assert tag not in subsetfont
|
2017-01-14 14:27:30 +00:00
|
|
|
|
2017-02-16 12:43:56 +09:00
|
|
|
def test_notdef_width_cid(self):
|
|
|
|
# https://github.com/fonttools/fonttools/pull/845
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("NotdefWidthCID-Regular.ttx"), ".otf")
|
2017-02-16 12:43:56 +09:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2017-02-16 12:43:56 +09:00
|
|
|
fontpath,
|
|
|
|
"--no-notdef-outline",
|
|
|
|
"--gids=0,1",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2017-02-16 12:43:56 +09:00
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_notdef_width_cid.ttx"), ["CFF "]
|
|
|
|
)
|
|
|
|
|
2019-05-17 16:19:44 +02:00
|
|
|
def test_recalc_bounds_ttf(self):
|
|
|
|
ttxpath = self.getpath("TestTTF-Regular.ttx")
|
|
|
|
font = TTFont()
|
|
|
|
font.importXML(ttxpath)
|
|
|
|
head = font["head"]
|
|
|
|
bounds = [head.xMin, head.yMin, head.xMax, head.yMax]
|
|
|
|
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".ttf")
|
2019-05-17 16:19:44 +02:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
|
|
|
|
# by default, the subsetter does not recalculate the bounding box
|
|
|
|
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
|
|
|
|
head = TTFont(subsetpath)["head"]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
|
2019-05-17 16:19:44 +02:00
|
|
|
|
|
|
|
subset.main([fontpath, "--recalc-bounds", "--output-file=%s" % subsetpath, "*"])
|
|
|
|
head = TTFont(subsetpath)["head"]
|
|
|
|
bounds = [132, 304, 365, 567]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
|
2019-05-17 16:19:44 +02:00
|
|
|
|
|
|
|
def test_recalc_bounds_otf(self):
|
|
|
|
ttxpath = self.getpath("TestOTF-Regular.ttx")
|
|
|
|
font = TTFont()
|
|
|
|
font.importXML(ttxpath)
|
|
|
|
head = font["head"]
|
|
|
|
bounds = [head.xMin, head.yMin, head.xMax, head.yMax]
|
|
|
|
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2019-05-17 16:19:44 +02:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
|
|
|
|
# by default, the subsetter does not recalculate the bounding box
|
|
|
|
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
|
|
|
|
head = TTFont(subsetpath)["head"]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
|
2019-05-17 16:19:44 +02:00
|
|
|
|
|
|
|
subset.main([fontpath, "--recalc-bounds", "--output-file=%s" % subsetpath, "*"])
|
|
|
|
head = TTFont(subsetpath)["head"]
|
|
|
|
bounds = [132, 304, 365, 567]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert bounds == [head.xMin, head.yMin, head.xMax, head.yMax]
|
2019-05-17 16:19:44 +02:00
|
|
|
|
2017-02-21 13:45:50 +01:00
|
|
|
def test_recalc_timestamp_ttf(self):
|
2017-02-19 20:12:22 -08:00
|
|
|
ttxpath = self.getpath("TestTTF-Regular.ttx")
|
|
|
|
font = TTFont()
|
|
|
|
font.importXML(ttxpath)
|
|
|
|
modified = font["head"].modified
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".ttf")
|
2017-02-19 20:12:22 -08:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
|
|
|
|
# by default, the subsetter does not recalculate the modified timestamp
|
|
|
|
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
|
2022-04-19 12:03:50 +01:00
|
|
|
assert modified == TTFont(subsetpath)["head"].modified
|
2017-02-19 20:12:22 -08:00
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"]
|
|
|
|
)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert modified < TTFont(subsetpath)["head"].modified
|
2017-02-19 20:12:22 -08:00
|
|
|
|
2017-02-21 13:45:50 +01:00
|
|
|
def test_recalc_timestamp_otf(self):
|
|
|
|
ttxpath = self.getpath("TestOTF-Regular.ttx")
|
|
|
|
font = TTFont()
|
|
|
|
font.importXML(ttxpath)
|
|
|
|
modified = font["head"].modified
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2017-02-21 13:45:50 +01:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
|
|
|
|
# by default, the subsetter does not recalculate the modified timestamp
|
|
|
|
subset.main([fontpath, "--output-file=%s" % subsetpath, "*"])
|
2022-04-19 12:03:50 +01:00
|
|
|
assert modified == TTFont(subsetpath)["head"].modified
|
2017-02-21 13:45:50 +01:00
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--recalc-timestamp", "--output-file=%s" % subsetpath, "*"]
|
|
|
|
)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert modified < TTFont(subsetpath)["head"].modified
|
2017-02-21 13:45:50 +01:00
|
|
|
|
2019-04-23 16:52:15 -07:00
|
|
|
def test_recalc_max_context(self):
|
|
|
|
ttxpath = self.getpath("Lobster.subset.ttx")
|
|
|
|
font = TTFont()
|
|
|
|
font.importXML(ttxpath)
|
|
|
|
max_context = font["OS/2"].usMaxContext
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttxpath, ".otf")
|
2019-04-23 16:52:15 -07:00
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
|
|
|
|
# by default, the subsetter does not recalculate the usMaxContext
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--drop-tables+=GSUB,GPOS", "--output-file=%s" % subsetpath]
|
|
|
|
)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert max_context == TTFont(subsetpath)["OS/2"].usMaxContext
|
2019-04-23 16:52:15 -07:00
|
|
|
|
|
|
|
subset.main(
|
2022-12-13 11:26:36 +00:00
|
|
|
[
|
2019-04-23 16:52:15 -07:00
|
|
|
fontpath,
|
|
|
|
"--recalc-max-context",
|
|
|
|
"--drop-tables+=GSUB,GPOS",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2022-12-13 11:26:36 +00:00
|
|
|
]
|
2019-04-23 16:52:15 -07:00
|
|
|
)
|
2022-04-19 12:03:50 +01:00
|
|
|
assert 0 == TTFont(subsetpath)["OS/2"].usMaxContext
|
2019-04-23 16:52:15 -07:00
|
|
|
|
2019-01-15 14:07:54 +00:00
|
|
|
def test_retain_gids_ttf(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2019-01-15 11:31:16 +00:00
|
|
|
font = TTFont(fontpath)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert font["hmtx"]["A"] == (500, 132)
|
|
|
|
assert font["hmtx"]["B"] == (400, 132)
|
2019-01-15 11:31:16 +00:00
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert font["glyf"]["A"].numberOfContours > 0
|
|
|
|
assert font["glyf"]["B"].numberOfContours > 0
|
2019-01-15 11:31:16 +00:00
|
|
|
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
fontpath,
|
|
|
|
"--retain-gids",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"--glyph-names",
|
2019-05-15 13:33:10 -07:00
|
|
|
"B",
|
2019-01-15 11:31:16 +00:00
|
|
|
]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert subsetfont.getGlyphOrder() == font.getGlyphOrder()[0:3]
|
2019-01-15 11:31:16 +00:00
|
|
|
|
|
|
|
hmtx = subsetfont["hmtx"]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert hmtx["A"] == (0, 0)
|
|
|
|
assert hmtx["B"] == (400, 132)
|
2019-01-15 11:31:16 +00:00
|
|
|
|
|
|
|
glyf = subsetfont["glyf"]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert glyf["A"].numberOfContours == 0
|
|
|
|
assert glyf["B"].numberOfContours > 0
|
2019-01-15 11:31:16 +00:00
|
|
|
|
2019-01-16 16:02:07 +00:00
|
|
|
def test_retain_gids_cff(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestOTF-Regular.ttx"), ".otf")
|
2019-01-15 14:07:54 +00:00
|
|
|
font = TTFont(fontpath)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert font["hmtx"]["A"] == (500, 132)
|
|
|
|
assert font["hmtx"]["B"] == (400, 132)
|
|
|
|
assert font["hmtx"]["C"] == (500, 0)
|
2019-01-15 14:07:54 +00:00
|
|
|
|
|
|
|
font["CFF "].cff[0].decompileAllCharStrings()
|
|
|
|
cs = font["CFF "].cff[0].CharStrings
|
2022-04-19 12:03:50 +01:00
|
|
|
assert len(cs["A"].program) > 0
|
|
|
|
assert len(cs["B"].program) > 0
|
|
|
|
assert len(cs["C"].program) > 0
|
2019-01-15 14:07:54 +00:00
|
|
|
|
2019-01-16 16:02:07 +00:00
|
|
|
subsetpath = self.temp_path(".otf")
|
2019-01-15 14:07:54 +00:00
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
fontpath,
|
|
|
|
"--retain-gids",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
"--glyph-names",
|
2019-05-15 13:33:10 -07:00
|
|
|
"B",
|
2019-01-15 14:07:54 +00:00
|
|
|
]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert subsetfont.getGlyphOrder() == font.getGlyphOrder()[0:3]
|
2019-01-15 14:07:54 +00:00
|
|
|
|
|
|
|
hmtx = subsetfont["hmtx"]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert hmtx["A"] == (0, 0)
|
|
|
|
assert hmtx["B"] == (400, 132)
|
2019-01-15 14:07:54 +00:00
|
|
|
|
|
|
|
subsetfont["CFF "].cff[0].decompileAllCharStrings()
|
|
|
|
cs = subsetfont["CFF "].cff[0].CharStrings
|
2019-05-15 13:33:10 -07:00
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert cs["A"].program == ["endchar"]
|
|
|
|
assert len(cs["B"].program) > 0
|
2019-01-16 16:02:07 +00:00
|
|
|
|
|
|
|
def test_retain_gids_cff2(self):
|
2019-05-01 16:01:43 -07:00
|
|
|
ttx_path = self.getpath(
|
|
|
|
"../../varLib/data/master_ttx_varfont_otf/TestCFF2VF.ttx"
|
|
|
|
)
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttx_path, ".otf")
|
|
|
|
font = TTFont(fontpath)
|
2019-01-16 16:02:07 +00:00
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert font["hmtx"]["A"] == (600, 31)
|
|
|
|
assert font["hmtx"]["T"] == (600, 41)
|
2019-01-16 16:02:07 +00:00
|
|
|
|
|
|
|
font["CFF2"].cff[0].decompileAllCharStrings()
|
|
|
|
cs = font["CFF2"].cff[0].CharStrings
|
2022-04-19 12:03:50 +01:00
|
|
|
assert len(cs["A"].program) > 0
|
|
|
|
assert len(cs["T"].program) > 0
|
2019-01-16 16:02:07 +00:00
|
|
|
|
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
fontpath,
|
|
|
|
"--retain-gids",
|
|
|
|
"--output-file=%s" % subsetpath,
|
2019-05-15 13:33:10 -07:00
|
|
|
"T",
|
2019-01-16 16:02:07 +00:00
|
|
|
]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert len(subsetfont.getGlyphOrder()) == len(font.getGlyphOrder()[0:3])
|
2019-01-16 16:02:07 +00:00
|
|
|
|
|
|
|
hmtx = subsetfont["hmtx"]
|
2022-04-19 12:03:50 +01:00
|
|
|
assert hmtx["glyph00001"] == (0, 0)
|
|
|
|
assert hmtx["T"] == (600, 41)
|
2019-01-16 16:02:07 +00:00
|
|
|
|
|
|
|
subsetfont["CFF2"].cff[0].decompileAllCharStrings()
|
|
|
|
cs = subsetfont["CFF2"].cff[0].CharStrings
|
2022-04-19 12:03:50 +01:00
|
|
|
assert cs["glyph00001"].program == []
|
|
|
|
assert len(cs["T"].program) > 0
|
2019-01-15 14:07:54 +00:00
|
|
|
|
2019-04-23 17:03:04 -07:00
|
|
|
def test_HVAR_VVAR(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
|
2019-04-23 17:03:04 -07:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--text=BD", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_HVVAR.ttx"),
|
|
|
|
["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"],
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_HVAR_VVAR_retain_gids(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestHVVAR.ttx"), ".ttf")
|
2019-04-23 17:03:04 -07:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main(
|
|
|
|
[fontpath, "--text=BD", "--retain-gids", "--output-file=%s" % subsetpath]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("expect_HVVAR_retain_gids.ttx"),
|
|
|
|
["GlyphOrder", "HVAR", "VVAR", "avar", "fvar"],
|
|
|
|
)
|
|
|
|
|
2023-05-24 16:26:38 +01:00
|
|
|
def test_subset_flavor_woff(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2019-06-19 11:31:40 +01:00
|
|
|
woff_path = self.temp_path(".woff")
|
2023-05-24 16:26:38 +01:00
|
|
|
|
2019-06-19 11:31:40 +01:00
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
fontpath,
|
|
|
|
"*",
|
|
|
|
"--flavor=woff",
|
|
|
|
"--output-file=%s" % woff_path,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
woff = TTFont(woff_path)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert woff.flavor == "woff"
|
2019-06-19 11:31:40 +01:00
|
|
|
|
2023-05-24 16:26:38 +01:00
|
|
|
def test_subset_flavor_woff2(self):
|
|
|
|
# skip if brotli is not importable, required for woff2
|
|
|
|
pytest.importorskip("brotli")
|
|
|
|
|
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2019-06-19 11:31:40 +01:00
|
|
|
woff2_path = self.temp_path(".woff2")
|
2023-05-24 16:26:38 +01:00
|
|
|
|
2019-06-19 11:31:40 +01:00
|
|
|
subset.main(
|
|
|
|
[
|
2023-05-24 16:26:38 +01:00
|
|
|
fontpath,
|
2019-06-19 11:31:40 +01:00
|
|
|
"*",
|
|
|
|
"--flavor=woff2",
|
|
|
|
"--output-file=%s" % woff2_path,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
woff2 = TTFont(woff2_path)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert woff2.flavor == "woff2"
|
2019-06-19 11:31:40 +01:00
|
|
|
|
2023-05-24 16:26:38 +01:00
|
|
|
def test_subset_flavor_none(self):
|
|
|
|
fontpath = self.compile_font(self.getpath("TestTTF-Regular.ttx"), ".ttf")
|
2019-06-19 11:31:40 +01:00
|
|
|
ttf_path = self.temp_path(".ttf")
|
2023-05-24 16:26:38 +01:00
|
|
|
|
2019-06-19 11:31:40 +01:00
|
|
|
subset.main(
|
|
|
|
[
|
2023-05-24 16:26:38 +01:00
|
|
|
fontpath,
|
2019-06-19 11:31:40 +01:00
|
|
|
"*",
|
|
|
|
"--output-file=%s" % ttf_path,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
ttf = TTFont(ttf_path)
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
assert ttf.flavor is None
|
2019-06-19 11:31:40 +01:00
|
|
|
|
2020-05-11 18:32:32 +01:00
|
|
|
def test_subset_context_subst_format_3(self):
|
|
|
|
# https://github.com/fonttools/fonttools/issues/1879
|
|
|
|
# Test font contains 'calt' feature with Format 3 ContextSubst lookup subtables
|
|
|
|
ttx = self.getpath("TestContextSubstFormat3.ttx")
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(ttx, ".ttf")
|
2020-05-11 18:32:32 +01:00
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "--unicodes=*", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
# check all glyphs are kept via GSUB closure, no changes expected
|
|
|
|
self.expect_ttx(subsetfont, ttx)
|
|
|
|
|
2021-01-12 17:21:22 +00:00
|
|
|
def test_cmap_prune_format12(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("CmapSubsetTest.ttx"), ".ttf")
|
2021-01-12 17:21:22 +00:00
|
|
|
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"])
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
@pytest.mark.parametrize("text, n", [("!", 1), ("#", 2)])
|
|
|
|
def test_GPOS_PairPos_Format2_useClass0(self, text, n):
|
2021-03-23 16:00:36 +00:00
|
|
|
# Check two things related to class 0 ('every other glyph'):
|
|
|
|
# 1) that it's reused for ClassDef1 when it becomes empty as the subset glyphset
|
|
|
|
# is intersected with the table's Coverage
|
|
|
|
# 2) that it is never reused for ClassDef2 even when it happens to become empty
|
|
|
|
# because of the subset glyphset. In this case, we don't keep a PairPosClass2
|
|
|
|
# subtable if only ClassDef2's class0 survived subsetting.
|
|
|
|
# The test font (from Harfbuzz test suite) is constructed to trigger these two
|
|
|
|
# situations depending on the input subset --text.
|
|
|
|
# https://github.com/fonttools/fonttools/pull/2221
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(
|
2021-03-23 16:00:36 +00:00
|
|
|
self.getpath("GPOS_PairPos_Format2_PR_2221.ttx"), ".ttf"
|
|
|
|
)
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
|
2022-04-19 12:03:50 +01:00
|
|
|
expected_ttx = self.getpath(
|
|
|
|
f"GPOS_PairPos_Format2_ClassDef{n}_useClass0.subset.ttx"
|
|
|
|
)
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
fontpath,
|
|
|
|
f"--text='{text}'",
|
|
|
|
"--layout-features+=test",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(subsetfont, expected_ttx, ["GPOS"])
|
2021-03-23 16:00:36 +00:00
|
|
|
|
2021-05-19 20:05:36 +01:00
|
|
|
def test_GPOS_SinglePos_prune_post_subset_no_value(self):
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(
|
2021-05-19 20:05:36 +01:00
|
|
|
self.getpath("GPOS_SinglePos_no_value_issue_2312.ttx"), ".ttf"
|
|
|
|
)
|
|
|
|
subsetpath = self.temp_path(".ttf")
|
|
|
|
subset.main([fontpath, "*", "--glyph-names", "--output-file=%s" % subsetpath])
|
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont,
|
|
|
|
self.getpath("GPOS_SinglePos_no_value_issue_2312.subset.ttx"),
|
|
|
|
["GlyphOrder", "GPOS"],
|
|
|
|
)
|
|
|
|
|
2022-04-19 15:26:27 +01:00
|
|
|
@pytest.mark.parametrize(
|
2022-04-20 12:59:58 +01:00
|
|
|
"installed, enabled, ok",
|
2022-04-19 15:26:27 +01:00
|
|
|
[
|
2022-04-21 18:11:20 +01:00
|
|
|
pytest.param(True, None, True, id="installed-auto-ok"),
|
2022-04-22 15:31:34 +01:00
|
|
|
pytest.param(True, None, False, id="installed-auto-fail"),
|
2022-04-20 12:59:58 +01:00
|
|
|
pytest.param(True, True, True, id="installed-enabled-ok"),
|
|
|
|
pytest.param(True, True, False, id="installed-enabled-fail"),
|
|
|
|
pytest.param(True, False, True, id="installed-disabled"),
|
|
|
|
pytest.param(False, True, True, id="not_installed-enabled"),
|
|
|
|
pytest.param(False, False, True, id="not_installed-disabled"),
|
2022-04-19 15:26:27 +01:00
|
|
|
],
|
|
|
|
)
|
2022-04-20 12:59:58 +01:00
|
|
|
def test_harfbuzz_repacker(self, caplog, monkeypatch, installed, enabled, ok):
|
2022-04-19 15:26:27 +01:00
|
|
|
# Use a mock to test the pure-python serializer is used when uharfbuzz
|
|
|
|
# returns an error or is not installed
|
|
|
|
have_uharfbuzz = fontTools.ttLib.tables.otBase.have_uharfbuzz
|
|
|
|
if installed:
|
|
|
|
if not have_uharfbuzz:
|
|
|
|
pytest.skip("uharfbuzz is not installed")
|
|
|
|
if not ok:
|
2022-08-15 12:10:13 +02:00
|
|
|
# pretend hb.repack/repack_with_tag return an error
|
2022-04-19 15:26:27 +01:00
|
|
|
import uharfbuzz as hb
|
|
|
|
|
|
|
|
def mock_repack(data, obj_list):
|
|
|
|
raise hb.RepackerError("mocking")
|
|
|
|
|
|
|
|
monkeypatch.setattr(hb, "repack", mock_repack)
|
2022-08-15 12:10:13 +02:00
|
|
|
|
|
|
|
if hasattr(hb, "repack_with_tag"): # uharfbuzz >= 0.30.0
|
|
|
|
|
|
|
|
def mock_repack_with_tag(tag, data, obj_list):
|
|
|
|
raise hb.RepackerError("mocking")
|
|
|
|
|
|
|
|
monkeypatch.setattr(hb, "repack_with_tag", mock_repack_with_tag)
|
2022-04-19 15:26:27 +01:00
|
|
|
else:
|
|
|
|
if have_uharfbuzz:
|
|
|
|
# pretend uharfbuzz is not installed
|
|
|
|
monkeypatch.setattr(
|
|
|
|
fontTools.ttLib.tables.otBase, "have_uharfbuzz", False
|
|
|
|
)
|
|
|
|
|
2022-04-19 12:40:10 +01:00
|
|
|
fontpath = self.compile_font(self.getpath("harfbuzz_repacker.ttx"), ".otf")
|
2022-03-17 13:59:23 -07:00
|
|
|
subsetpath = self.temp_path(".otf")
|
2022-04-20 12:59:58 +01:00
|
|
|
args = [
|
|
|
|
fontpath,
|
|
|
|
"--unicodes=0x53a9",
|
|
|
|
"--layout-features=*",
|
|
|
|
f"--output-file={subsetpath}",
|
|
|
|
]
|
2022-04-21 18:11:20 +01:00
|
|
|
if enabled is True:
|
|
|
|
args.append("--harfbuzz-repacker")
|
|
|
|
elif enabled is False:
|
2022-04-20 12:59:58 +01:00
|
|
|
args.append("--no-harfbuzz-repacker")
|
2022-04-21 18:11:20 +01:00
|
|
|
# elif enabled is None: ... is the default
|
|
|
|
|
2022-04-22 19:05:54 +01:00
|
|
|
if enabled is True and not installed:
|
|
|
|
# raise if enabled but not installed
|
|
|
|
with pytest.raises(ImportError, match="uharfbuzz"):
|
|
|
|
subset.main(args)
|
|
|
|
return
|
2022-04-20 12:59:58 +01:00
|
|
|
|
|
|
|
with caplog.at_level(logging.DEBUG, "fontTools.ttLib.tables.otBase"):
|
|
|
|
subset.main(args)
|
|
|
|
|
2022-04-19 15:26:27 +01:00
|
|
|
subsetfont = TTFont(subsetpath)
|
|
|
|
# both hb.repack and pure-python serializer compile to the same ttx
|
|
|
|
self.expect_ttx(
|
|
|
|
subsetfont, self.getpath("expect_harfbuzz_repacker.ttx"), ["GSUB"]
|
|
|
|
)
|
2022-03-17 13:59:23 -07:00
|
|
|
|
2022-04-21 18:11:20 +01:00
|
|
|
if enabled or enabled is None:
|
2022-04-20 12:59:58 +01:00
|
|
|
if installed:
|
|
|
|
assert "serializing 'GSUB' with hb.repack" in caplog.text
|
2022-04-21 18:11:20 +01:00
|
|
|
|
|
|
|
if enabled is None and not installed:
|
|
|
|
assert (
|
|
|
|
"uharfbuzz not found, compiling 'GSUB' with pure-python serializer"
|
|
|
|
) in caplog.text
|
|
|
|
|
|
|
|
if enabled is False:
|
2022-04-20 12:59:58 +01:00
|
|
|
assert (
|
|
|
|
"hb.repack disabled, compiling 'GSUB' with pure-python serializer"
|
|
|
|
) in caplog.text
|
|
|
|
|
2022-04-19 15:26:27 +01:00
|
|
|
# test we emit a log.error if hb.repack fails (and we don't if successful)
|
|
|
|
assert (
|
|
|
|
(
|
2022-06-27 18:13:52 +00:00
|
|
|
"hb.repack failed to serialize 'GSUB', attempting fonttools resolutions "
|
|
|
|
"; the error message was: RepackerError: mocking"
|
2022-04-19 15:26:27 +01:00
|
|
|
)
|
|
|
|
in caplog.text
|
|
|
|
) ^ ok
|
2022-03-17 13:59:23 -07:00
|
|
|
|
2023-10-19 11:20:48 +01:00
|
|
|
def test_retain_east_asian_spacing_features(self):
|
|
|
|
# This test font contains halt and vhal features, check that
|
|
|
|
# they are retained by default after subsetting.
|
|
|
|
ttx_path = self.getpath("NotoSansCJKjp-Regular.subset.ttx")
|
|
|
|
ttx = pathlib.Path(ttx_path).read_text()
|
|
|
|
assert 'FeatureTag value="halt"' in ttx
|
|
|
|
assert 'FeatureTag value="vhal"' in ttx
|
|
|
|
|
|
|
|
fontpath = self.compile_font(ttx_path, ".otf")
|
|
|
|
subsetpath = self.temp_path(".otf")
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
fontpath,
|
|
|
|
"--unicodes=*",
|
|
|
|
"--output-file=%s" % subsetpath,
|
|
|
|
]
|
|
|
|
)
|
|
|
|
# subset output is the same as the input
|
|
|
|
self.expect_ttx(TTFont(subsetpath), ttx_path)
|
|
|
|
|
2016-05-11 17:39:37 -07:00
|
|
|
|
2020-04-25 18:40:19 +01:00
|
|
|
@pytest.fixture
|
|
|
|
def featureVarsTestFont():
|
2019-12-12 12:29:08 +00:00
|
|
|
fb = FontBuilder(unitsPerEm=100)
|
|
|
|
fb.setupGlyphOrder([".notdef", "f", "f_f", "dollar", "dollar.rvrn"])
|
|
|
|
fb.setupCharacterMap({ord("f"): "f", ord("$"): "dollar"})
|
|
|
|
fb.setupNameTable({"familyName": "TestFeatureVars", "styleName": "Regular"})
|
|
|
|
fb.setupPost()
|
|
|
|
fb.setupFvar(axes=[("wght", 100, 400, 900, "Weight")], instances=[])
|
|
|
|
fb.addOpenTypeFeatures(
|
|
|
|
"""\
|
|
|
|
feature dlig {
|
|
|
|
sub f f by f_f;
|
|
|
|
} dlig;
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
fb.addFeatureVariations(
|
|
|
|
[([{"wght": (0.20886, 1.0)}], {"dollar": "dollar.rvrn"})], featureTag="rvrn"
|
|
|
|
)
|
|
|
|
buf = io.BytesIO()
|
|
|
|
fb.save(buf)
|
|
|
|
buf.seek(0)
|
|
|
|
|
2020-04-25 18:40:19 +01:00
|
|
|
return TTFont(buf)
|
|
|
|
|
|
|
|
|
|
|
|
def test_subset_feature_variations_keep_all(featureVarsTestFont):
|
|
|
|
font = featureVarsTestFont
|
2019-12-12 12:29:08 +00:00
|
|
|
|
|
|
|
options = subset.Options()
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(unicodes=[ord("f"), ord("$")])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
featureTags = {r.FeatureTag for r in font["GSUB"].table.FeatureList.FeatureRecord}
|
|
|
|
# 'dlig' is discretionary so it is dropped by default
|
|
|
|
assert "dlig" not in featureTags
|
|
|
|
assert "f_f" not in font.getGlyphOrder()
|
|
|
|
# 'rvrn' is required so it is kept by default
|
|
|
|
assert "rvrn" in featureTags
|
|
|
|
assert "dollar.rvrn" in font.getGlyphOrder()
|
|
|
|
|
|
|
|
|
2020-04-25 18:40:19 +01:00
|
|
|
def test_subset_feature_variations_drop_all(featureVarsTestFont):
|
|
|
|
font = featureVarsTestFont
|
|
|
|
|
|
|
|
options = subset.Options()
|
|
|
|
options.layout_features.remove("rvrn") # drop 'rvrn'
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(unicodes=[ord("f"), ord("$")])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
featureTags = {r.FeatureTag for r in font["GSUB"].table.FeatureList.FeatureRecord}
|
|
|
|
glyphs = set(font.getGlyphOrder())
|
|
|
|
|
|
|
|
assert "rvrn" not in featureTags
|
|
|
|
assert glyphs == {".notdef", "f", "dollar"}
|
|
|
|
# all FeatureVariationRecords were dropped
|
2020-04-28 18:30:50 +01:00
|
|
|
assert font["GSUB"].table.FeatureVariations is None
|
|
|
|
assert font["GSUB"].table.Version == 0x00010000
|
2020-04-25 18:40:19 +01:00
|
|
|
|
|
|
|
|
|
|
|
# TODO test_subset_feature_variations_drop_from_end_empty_records
|
|
|
|
# https://github.com/fonttools/fonttools/issues/1881#issuecomment-619415044
|
|
|
|
|
|
|
|
|
2022-04-27 15:58:29 +01:00
|
|
|
@pytest.fixture
|
|
|
|
def singlepos2_font():
|
2020-01-16 18:30:07 -08:00
|
|
|
fb = FontBuilder(unitsPerEm=1000)
|
|
|
|
fb.setupGlyphOrder([".notdef", "a", "b", "c"])
|
|
|
|
fb.setupCharacterMap({ord("a"): "a", ord("b"): "b", ord("c"): "c"})
|
|
|
|
fb.setupNameTable({"familyName": "TestSingePosFormat", "styleName": "Regular"})
|
|
|
|
fb.setupPost()
|
|
|
|
fb.addOpenTypeFeatures(
|
|
|
|
"""
|
|
|
|
feature kern {
|
|
|
|
pos a -50;
|
|
|
|
pos b -40;
|
|
|
|
pos c -50;
|
|
|
|
} kern;
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
fb.save(buf)
|
|
|
|
buf.seek(0)
|
|
|
|
|
2022-04-27 15:58:29 +01:00
|
|
|
return TTFont(buf)
|
|
|
|
|
2020-01-16 18:30:07 -08:00
|
|
|
|
2022-04-27 15:58:29 +01:00
|
|
|
def test_subset_single_pos_format(singlepos2_font):
|
|
|
|
font = singlepos2_font
|
2020-01-16 18:30:07 -08:00
|
|
|
# The input font has a SinglePos Format 2 subtable where each glyph has
|
|
|
|
# different ValueRecords
|
|
|
|
assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [
|
|
|
|
"<Lookup>",
|
|
|
|
' <LookupType value="1"/>',
|
|
|
|
' <LookupFlag value="0"/>',
|
|
|
|
" <!-- SubTableCount=1 -->",
|
|
|
|
' <SinglePos index="0" Format="2">',
|
2021-03-23 11:11:27 -07:00
|
|
|
" <Coverage>",
|
2020-01-16 18:30:07 -08:00
|
|
|
' <Glyph value="a"/>',
|
|
|
|
' <Glyph value="b"/>',
|
|
|
|
' <Glyph value="c"/>',
|
|
|
|
" </Coverage>",
|
|
|
|
' <ValueFormat value="4"/>',
|
|
|
|
" <!-- ValueCount=3 -->",
|
|
|
|
' <Value index="0" XAdvance="-50"/>',
|
|
|
|
' <Value index="1" XAdvance="-40"/>',
|
|
|
|
' <Value index="2" XAdvance="-50"/>',
|
|
|
|
" </SinglePos>",
|
|
|
|
"</Lookup>",
|
|
|
|
]
|
|
|
|
|
|
|
|
options = subset.Options()
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(unicodes=[ord("a"), ord("c")])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
# All the subsetted glyphs from the original SinglePos Format2 subtable
|
|
|
|
# now have the same ValueRecord, so we use a more compact Format 1 subtable.
|
|
|
|
assert getXML(font["GPOS"].table.LookupList.Lookup[0].toXML, font) == [
|
|
|
|
"<Lookup>",
|
|
|
|
' <LookupType value="1"/>',
|
|
|
|
' <LookupFlag value="0"/>',
|
|
|
|
" <!-- SubTableCount=1 -->",
|
|
|
|
' <SinglePos index="0" Format="1">',
|
2021-03-23 11:11:27 -07:00
|
|
|
" <Coverage>",
|
2020-01-16 18:30:07 -08:00
|
|
|
' <Glyph value="a"/>',
|
|
|
|
' <Glyph value="c"/>',
|
|
|
|
" </Coverage>",
|
|
|
|
' <ValueFormat value="4"/>',
|
|
|
|
' <Value XAdvance="-50"/>',
|
|
|
|
" </SinglePos>",
|
|
|
|
"</Lookup>",
|
|
|
|
]
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2022-04-27 15:58:29 +01:00
|
|
|
def test_subset_single_pos_format2_all_None(singlepos2_font):
|
|
|
|
# https://github.com/fonttools/fonttools/issues/2602
|
|
|
|
font = singlepos2_font
|
|
|
|
gpos = font["GPOS"].table
|
|
|
|
subtable = gpos.LookupList.Lookup[0].SubTable[0]
|
|
|
|
assert subtable.Format == 2
|
|
|
|
# Hack a SinglePosFormat2 with ValueFormat = 0; our own buildSinglePos
|
|
|
|
# never makes these as a SinglePosFormat1 is more compact, but they can
|
|
|
|
# be found in the wild.
|
|
|
|
subtable.Value = [None] * subtable.ValueCount
|
|
|
|
subtable.ValueFormat = 0
|
|
|
|
|
|
|
|
assert getXML(subtable.toXML, font) == [
|
|
|
|
'<SinglePos Format="2">',
|
|
|
|
" <Coverage>",
|
|
|
|
' <Glyph value="a"/>',
|
|
|
|
' <Glyph value="b"/>',
|
|
|
|
' <Glyph value="c"/>',
|
|
|
|
" </Coverage>",
|
|
|
|
' <ValueFormat value="0"/>',
|
|
|
|
" <!-- ValueCount=3 -->",
|
|
|
|
"</SinglePos>",
|
|
|
|
]
|
|
|
|
|
|
|
|
options = subset.Options()
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(unicodes=[ord("a"), ord("c")])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
# Check it was downgraded to Format1 after subsetting
|
|
|
|
assert getXML(font["GPOS"].table.LookupList.Lookup[0].SubTable[0].toXML, font) == [
|
|
|
|
'<SinglePos Format="1">',
|
|
|
|
" <Coverage>",
|
|
|
|
' <Glyph value="a"/>',
|
|
|
|
' <Glyph value="c"/>',
|
|
|
|
" </Coverage>",
|
|
|
|
' <ValueFormat value="0"/>',
|
|
|
|
"</SinglePos>",
|
|
|
|
]
|
|
|
|
|
2020-01-16 18:30:07 -08:00
|
|
|
|
2020-02-12 14:20:36 +00:00
|
|
|
@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)
|
|
|
|
|
|
|
|
|
2021-02-01 16:35:32 +00:00
|
|
|
@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(
|
|
|
|
{
|
2021-02-15 11:03:02 +00:00
|
|
|
"uniE000": (
|
|
|
|
ot.PaintFormat.PaintColrLayers,
|
|
|
|
[
|
|
|
|
{
|
|
|
|
"Format": ot.PaintFormat.PaintGlyph,
|
|
|
|
"Paint": (ot.PaintFormat.PaintSolid, 0),
|
|
|
|
"Glyph": "glyph00010",
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
{
|
|
|
|
"Format": ot.PaintFormat.PaintGlyph,
|
2021-07-22 12:56:22 +01:00
|
|
|
"Paint": (ot.PaintFormat.PaintSolid, 2, 0.3),
|
2021-02-15 11:03:02 +00:00
|
|
|
"Glyph": "glyph00011",
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
"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",
|
|
|
|
},
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
"Glyph": "glyph00012",
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
"Transform": (0.7071, 0.7071, -0.7071, 0.7071, 0, 0),
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
{
|
|
|
|
"Format": ot.PaintFormat.PaintGlyph,
|
2021-07-22 12:56:22 +01:00
|
|
|
"Paint": (ot.PaintFormat.PaintSolid, 1, 0.5),
|
2021-02-15 11:03:02 +00:00
|
|
|
"Glyph": "glyph00013",
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
],
|
|
|
|
),
|
|
|
|
"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",
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
{
|
|
|
|
"Format": ot.PaintFormat.PaintTransform,
|
|
|
|
"Paint": {
|
|
|
|
"Format": ot.PaintFormat.PaintGlyph,
|
|
|
|
"Paint": (ot.PaintFormat.PaintSolid, 1),
|
|
|
|
"Glyph": "glyph00015",
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
"Transform": (1, 0, 0, 1, 400, 400),
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-02-15 11:03:02 +00:00
|
|
|
],
|
|
|
|
),
|
2021-02-01 16:35:32 +00:00
|
|
|
"uniE003": {
|
2021-06-28 18:38:47 +01:00
|
|
|
"Format": ot.PaintFormat.PaintRotateAroundCenter,
|
2021-02-15 11:03:02 +00:00
|
|
|
"Paint": {
|
|
|
|
"Format": ot.PaintFormat.PaintColrGlyph,
|
|
|
|
"Glyph": "uniE001",
|
|
|
|
},
|
2021-02-01 16:35:32 +00:00
|
|
|
"angle": 45,
|
|
|
|
"centerX": 250,
|
|
|
|
"centerY": 250,
|
|
|
|
},
|
2021-02-15 11:58:56 +00:00
|
|
|
"uniE004": [
|
|
|
|
("glyph00016", 1),
|
2021-04-06 19:43:45 +01:00
|
|
|
("glyph00017", 0xFFFF), # special palette index for foreground text
|
|
|
|
("glyph00018", 2),
|
2021-02-15 11:58:56 +00:00
|
|
|
],
|
2021-02-01 16:35:32 +00:00
|
|
|
},
|
2021-08-18 09:42:10 +02:00
|
|
|
clipBoxes={
|
|
|
|
"uniE000": (0, 0, 200, 300),
|
|
|
|
"uniE001": (0, 0, 500, 500),
|
2023-01-26 18:02:14 +00:00
|
|
|
"uniE002": (-50, -50, 400, 400),
|
|
|
|
"uniE003": (-50, -50, 400, 400),
|
2021-08-18 09:42:10 +02:00
|
|
|
},
|
2021-02-01 16:35:32 +00:00
|
|
|
)
|
|
|
|
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
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2022-09-30 12:10:56 +01:00
|
|
|
@pytest.fixture
|
|
|
|
def colrv1_cpalv1_path(colrv1_path):
|
|
|
|
# upgrade CPAL from v0 to v1 by adding labels
|
|
|
|
font = TTFont(colrv1_path)
|
|
|
|
fb = FontBuilder(font=font)
|
|
|
|
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
|
|
|
|
],
|
|
|
|
],
|
|
|
|
paletteLabels=["test palette"],
|
|
|
|
paletteEntryLabels=["first color", "second color", "third color"],
|
|
|
|
)
|
2022-09-30 15:41:14 +01:00
|
|
|
|
|
|
|
output_path = colrv1_path.parent / "TestCOLRv1CPALv1.ttf"
|
|
|
|
fb.save(output_path)
|
|
|
|
|
|
|
|
return output_path
|
|
|
|
|
2022-12-13 11:26:36 +00:00
|
|
|
|
2022-09-30 15:41:14 +01:00
|
|
|
@pytest.fixture
|
|
|
|
def colrv1_cpalv1_share_nameID_path(colrv1_path):
|
|
|
|
font = TTFont(colrv1_path)
|
|
|
|
fb = FontBuilder(font=font)
|
|
|
|
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
|
|
|
|
],
|
|
|
|
],
|
|
|
|
paletteLabels=["test palette"],
|
|
|
|
paletteEntryLabels=["first color", "second color", "third color"],
|
|
|
|
)
|
|
|
|
|
|
|
|
# Set the name ID of the first color to use nameID 1 = familyName = "TestCOLRv1"
|
|
|
|
fb.font["CPAL"].paletteEntryLabels[0] = 1
|
|
|
|
|
2022-09-30 12:10:56 +01:00
|
|
|
output_path = colrv1_path.parent / "TestCOLRv1CPALv1.ttf"
|
|
|
|
fb.save(output_path)
|
|
|
|
|
|
|
|
return output_path
|
2022-09-30 15:41:14 +01:00
|
|
|
|
2021-02-01 16:35:32 +00:00
|
|
|
|
2021-02-11 17:49:27 +00:00
|
|
|
def test_subset_COLRv1_and_CPAL(colrv1_path):
|
2021-02-01 16:35:32 +00:00
|
|
|
subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
|
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(colrv1_path),
|
|
|
|
"--glyph-names",
|
|
|
|
f"--output-file={subset_path}",
|
2021-02-15 11:58:56 +00:00
|
|
|
"--unicodes=E002,E003,E004",
|
2021-02-01 16:35:32 +00:00
|
|
|
]
|
|
|
|
)
|
|
|
|
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
|
|
|
|
|
2021-02-15 11:58:56 +00:00
|
|
|
assert "uniE004" in glyph_set
|
|
|
|
assert "glyph00016" in glyph_set
|
|
|
|
assert "glyph00017" in glyph_set
|
2021-04-06 19:43:45 +01:00
|
|
|
assert "glyph00018" in glyph_set
|
2021-02-15 11:58:56 +00:00
|
|
|
|
2021-02-11 17:49:27 +00:00
|
|
|
assert "COLR" in subset_font
|
|
|
|
colr = subset_font["COLR"].table
|
2021-02-15 11:58:56 +00:00
|
|
|
assert colr.Version == 1
|
|
|
|
assert len(colr.BaseGlyphRecordArray.BaseGlyphRecord) == 1
|
2021-06-28 16:13:47 +01:00
|
|
|
assert len(colr.BaseGlyphList.BaseGlyphPaintRecord) == 3 # was 4
|
2021-02-11 17:49:27 +00:00
|
|
|
|
2021-06-28 16:13:47 +01:00
|
|
|
base = colr.BaseGlyphList.BaseGlyphPaintRecord[0]
|
2021-02-11 17:49:27 +00:00
|
|
|
assert base.BaseGlyph == "uniE001"
|
2021-06-28 16:13:47 +01:00
|
|
|
layers = colr.LayerList.Paint[
|
2021-02-11 17:49:27 +00:00
|
|
|
base.Paint.FirstLayerIndex : base.Paint.FirstLayerIndex + base.Paint.NumLayers
|
|
|
|
]
|
|
|
|
assert len(layers) == 2
|
2021-02-15 11:58:56 +00:00
|
|
|
# check v1 palette indices were remapped
|
2021-07-22 12:56:22 +01:00
|
|
|
assert layers[0].Paint.Paint.ColorLine.ColorStop[0].PaletteIndex == 0
|
|
|
|
assert layers[0].Paint.Paint.ColorLine.ColorStop[1].PaletteIndex == 1
|
|
|
|
assert layers[1].Paint.PaletteIndex == 0
|
2021-02-11 17:49:27 +00:00
|
|
|
|
2021-02-15 11:58:56 +00:00
|
|
|
baseRecV0 = colr.BaseGlyphRecordArray.BaseGlyphRecord[0]
|
|
|
|
assert baseRecV0.BaseGlyph == "uniE004"
|
|
|
|
layersV0 = colr.LayerRecordArray.LayerRecord
|
2021-04-06 19:43:45 +01:00
|
|
|
assert len(layersV0) == 3
|
|
|
|
# check v0 palette indices were remapped (except for 0xFFFF)
|
2021-02-15 11:58:56 +00:00
|
|
|
assert layersV0[0].PaletteIndex == 0
|
2021-04-06 19:43:45 +01:00
|
|
|
assert layersV0[1].PaletteIndex == 0xFFFF
|
|
|
|
assert layersV0[2].PaletteIndex == 1
|
2021-02-15 11:58:56 +00:00
|
|
|
|
2021-08-18 09:42:10 +02:00
|
|
|
clipBoxes = colr.ClipList.clips
|
|
|
|
assert {"uniE001", "uniE002", "uniE003"} == set(clipBoxes)
|
2023-01-26 18:02:14 +00:00
|
|
|
assert clipBoxes["uniE002"] == clipBoxes["uniE003"]
|
2021-08-18 09:42:10 +02:00
|
|
|
|
2021-02-11 17:49:27 +00:00
|
|
|
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
|
|
|
|
]
|
|
|
|
|
2022-09-30 15:41:14 +01:00
|
|
|
|
2022-09-30 12:10:56 +01:00
|
|
|
def test_subset_COLRv1_and_CPALv1(colrv1_cpalv1_path):
|
|
|
|
subset_path = colrv1_cpalv1_path.parent / (colrv1_cpalv1_path.name + ".subset")
|
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(colrv1_cpalv1_path),
|
|
|
|
"--glyph-names",
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
"--unicodes=E002,E003,E004",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
|
|
|
|
|
|
|
assert "CPAL" in subset_font
|
|
|
|
cpal = subset_font["CPAL"]
|
|
|
|
name_table = subset_font["name"]
|
|
|
|
assert [
|
|
|
|
name_table.getDebugName(name_id) for name_id in cpal.paletteEntryLabels
|
|
|
|
] == [
|
|
|
|
# "first color", # The first color was pruned
|
|
|
|
"second color",
|
|
|
|
"third color",
|
|
|
|
]
|
2022-09-30 15:41:14 +01:00
|
|
|
# check that the "first color" name is dropped from name table
|
|
|
|
font = TTFont(colrv1_cpalv1_path)
|
2022-09-30 15:59:11 +01:00
|
|
|
|
|
|
|
first_color_nameID = None
|
2022-09-30 15:41:14 +01:00
|
|
|
for n in font["name"].names:
|
|
|
|
if n.toUnicode() == "first color":
|
|
|
|
first_color_nameID = n.nameID
|
|
|
|
break
|
|
|
|
assert first_color_nameID is not None
|
|
|
|
assert all(n.nameID != first_color_nameID for n in name_table.names)
|
|
|
|
|
|
|
|
|
|
|
|
def test_subset_COLRv1_and_CPALv1_keep_nameID(colrv1_cpalv1_path):
|
|
|
|
subset_path = colrv1_cpalv1_path.parent / (colrv1_cpalv1_path.name + ".subset")
|
|
|
|
|
|
|
|
# figure out the name ID of first color so we can keep it
|
|
|
|
font = TTFont(colrv1_cpalv1_path)
|
|
|
|
|
2022-09-30 15:59:11 +01:00
|
|
|
first_color_nameID = None
|
2022-09-30 15:41:14 +01:00
|
|
|
for n in font["name"].names:
|
|
|
|
if n.toUnicode() == "first color":
|
|
|
|
first_color_nameID = n.nameID
|
|
|
|
break
|
|
|
|
assert first_color_nameID is not None
|
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(colrv1_cpalv1_path),
|
|
|
|
"--glyph-names",
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
"--unicodes=E002,E003,E004",
|
|
|
|
f"--name-IDs={first_color_nameID}",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
|
|
|
|
|
|
|
assert "CPAL" in subset_font
|
|
|
|
cpal = subset_font["CPAL"]
|
|
|
|
name_table = subset_font["name"]
|
|
|
|
assert [
|
|
|
|
name_table.getDebugName(name_id) for name_id in cpal.paletteEntryLabels
|
|
|
|
] == [
|
|
|
|
# "first color", # The first color was pruned
|
|
|
|
"second color",
|
|
|
|
"third color",
|
|
|
|
]
|
|
|
|
|
|
|
|
# Check that the name ID is kept
|
|
|
|
assert any(n.nameID == first_color_nameID for n in name_table.names)
|
|
|
|
|
|
|
|
|
|
|
|
def test_subset_COLRv1_and_CPALv1_share_nameID(colrv1_cpalv1_share_nameID_path):
|
|
|
|
subset_path = colrv1_cpalv1_share_nameID_path.parent / (
|
|
|
|
colrv1_cpalv1_share_nameID_path.name + ".subset"
|
|
|
|
)
|
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(colrv1_cpalv1_share_nameID_path),
|
|
|
|
"--glyph-names",
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
"--unicodes=E002,E003,E004",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
|
|
|
|
|
|
|
assert "CPAL" in subset_font
|
|
|
|
cpal = subset_font["CPAL"]
|
|
|
|
name_table = subset_font["name"]
|
|
|
|
assert [
|
|
|
|
name_table.getDebugName(name_id) for name_id in cpal.paletteEntryLabels
|
|
|
|
] == [
|
|
|
|
# "first color", # The first color was pruned
|
|
|
|
"second color",
|
|
|
|
"third color",
|
|
|
|
]
|
|
|
|
|
|
|
|
# Check that the name ID 1 is kept
|
|
|
|
assert any(n.nameID == 1 for n in name_table.names)
|
2022-09-30 12:10:56 +01:00
|
|
|
|
2021-02-11 17:49:27 +00:00
|
|
|
|
|
|
|
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
|
|
|
|
|
2021-02-01 16:35:32 +00:00
|
|
|
|
2021-02-15 11:58:56 +00:00
|
|
|
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",
|
2021-04-06 19:43:45 +01:00
|
|
|
"glyph00018",
|
2021-02-15 11:58:56 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
assert "COLR" in subset_font
|
|
|
|
assert subset_font["COLR"].version == 0
|
|
|
|
|
|
|
|
|
2021-05-14 10:43:15 +01:00
|
|
|
def test_subset_COLRv1_drop_all_v0_glyphs(colrv1_path):
|
|
|
|
subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
|
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(colrv1_path),
|
|
|
|
"--glyph-names",
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
"--unicodes=E003",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
|
|
|
|
|
|
|
assert set(subset_font.getGlyphOrder()) == {
|
|
|
|
".notdef",
|
|
|
|
"uniE001",
|
|
|
|
"uniE003",
|
|
|
|
"glyph00012",
|
|
|
|
"glyph00013",
|
|
|
|
}
|
|
|
|
|
|
|
|
assert "COLR" in subset_font
|
|
|
|
colr = subset_font["COLR"]
|
|
|
|
assert colr.version == 1
|
|
|
|
assert colr.table.BaseGlyphRecordCount == 0
|
|
|
|
assert colr.table.BaseGlyphRecordArray is None
|
|
|
|
assert colr.table.LayerRecordArray is None
|
|
|
|
assert colr.table.LayerRecordCount is 0
|
|
|
|
|
|
|
|
|
2021-10-11 16:43:13 +02:00
|
|
|
def test_subset_COLRv1_no_ClipList(colrv1_path):
|
|
|
|
font = TTFont(colrv1_path)
|
|
|
|
font["COLR"].table.ClipList = None # empty ClipList
|
|
|
|
font.save(colrv1_path)
|
|
|
|
|
|
|
|
subset_path = colrv1_path.parent / (colrv1_path.name + ".subset")
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(colrv1_path),
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
"--unicodes=*",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
|
|
|
assert subset_font["COLR"].table.ClipList is None
|
|
|
|
|
|
|
|
|
2021-05-27 13:01:18 +01:00
|
|
|
def test_subset_keep_size_drop_empty_stylistic_set():
|
|
|
|
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
|
|
|
|
glyph_order = [".notdef", "a", "b", "b.ss01"]
|
|
|
|
fb.setupGlyphOrder(glyph_order)
|
|
|
|
fb.setupGlyf({g: TTGlyphPen(None).glyph() for g in glyph_order})
|
|
|
|
fb.setupCharacterMap({ord("a"): "a", ord("b"): "b"})
|
|
|
|
fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order})
|
|
|
|
fb.setupHorizontalHeader()
|
|
|
|
fb.setupOS2()
|
|
|
|
fb.setupPost()
|
|
|
|
fb.setupNameTable({"familyName": "TestKeepSizeFeature", "styleName": "Regular"})
|
|
|
|
fb.addOpenTypeFeatures(
|
|
|
|
"""
|
|
|
|
feature size {
|
|
|
|
parameters 10.0 0;
|
|
|
|
} size;
|
|
|
|
feature ss01 {
|
|
|
|
featureNames {
|
|
|
|
name "Alternate b";
|
|
|
|
};
|
|
|
|
sub b by b.ss01;
|
|
|
|
} ss01;
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
fb.save(buf)
|
|
|
|
buf.seek(0)
|
|
|
|
|
|
|
|
font = TTFont(buf)
|
|
|
|
|
|
|
|
gpos_features = font["GPOS"].table.FeatureList.FeatureRecord
|
|
|
|
assert gpos_features[0].FeatureTag == "size"
|
|
|
|
assert isinstance(gpos_features[0].Feature.FeatureParams, ot.FeatureParamsSize)
|
|
|
|
assert gpos_features[0].Feature.LookupCount == 0
|
|
|
|
gsub_features = font["GSUB"].table.FeatureList.FeatureRecord
|
|
|
|
assert gsub_features[0].FeatureTag == "ss01"
|
|
|
|
assert isinstance(
|
|
|
|
gsub_features[0].Feature.FeatureParams, ot.FeatureParamsStylisticSet
|
|
|
|
)
|
|
|
|
|
|
|
|
options = subset.Options(layout_features=["*"])
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(unicodes=[ord("a")])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
# empty size feature was kept
|
|
|
|
gpos_features = font["GPOS"].table.FeatureList.FeatureRecord
|
|
|
|
assert gpos_features[0].FeatureTag == "size"
|
|
|
|
assert isinstance(gpos_features[0].Feature.FeatureParams, ot.FeatureParamsSize)
|
|
|
|
assert gpos_features[0].Feature.LookupCount == 0
|
|
|
|
# empty ss01 feature was dropped
|
|
|
|
assert font["GSUB"].table.FeatureList.FeatureCount == 0
|
|
|
|
|
|
|
|
|
2021-11-12 18:59:09 +00:00
|
|
|
@pytest.mark.skipif(etree is not None, reason="lxml is installed")
|
|
|
|
def test_subset_svg_missing_lxml(ttf_path):
|
|
|
|
# add dummy SVG table and confirm we raise ImportError upon trying to subset it
|
|
|
|
font = TTFont(ttf_path)
|
|
|
|
font["SVG "] = newTable("SVG ")
|
|
|
|
font["SVG "].docList = [('<svg><g id="glyph1"/></svg>', 1, 1)]
|
|
|
|
font.save(ttf_path)
|
|
|
|
|
2022-07-18 12:17:33 +02:00
|
|
|
with pytest.raises(ImportError):
|
2021-11-12 18:59:09 +00:00
|
|
|
subset.main([str(ttf_path), "--gids=0,1"])
|
|
|
|
|
|
|
|
|
2021-12-02 18:01:24 +00:00
|
|
|
def test_subset_COLR_glyph_closure(tmp_path):
|
2021-12-02 18:18:09 +00:00
|
|
|
# https://github.com/fonttools/fonttools/issues/2461
|
2021-12-02 18:01:24 +00:00
|
|
|
font = TTFont()
|
|
|
|
ttx = pathlib.Path(__file__).parent / "data" / "BungeeColor-Regular.ttx"
|
|
|
|
font.importXML(ttx)
|
2021-12-02 18:18:09 +00:00
|
|
|
|
|
|
|
color_layers = font["COLR"].ColorLayers
|
|
|
|
assert ".notdef" in color_layers
|
|
|
|
assert "Agrave" in color_layers
|
|
|
|
assert "grave" in color_layers
|
|
|
|
|
2021-12-02 18:01:24 +00:00
|
|
|
font_path = tmp_path / "BungeeColor-Regular.ttf"
|
|
|
|
subset_path = font_path.with_suffix(".subset.ttf)")
|
|
|
|
font.save(font_path)
|
|
|
|
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(font_path),
|
|
|
|
"--glyph-names",
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
"--glyphs=Agrave",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
2021-12-02 18:18:09 +00:00
|
|
|
|
|
|
|
glyph_order = subset_font.getGlyphOrder()
|
|
|
|
|
|
|
|
assert glyph_order == [
|
|
|
|
".notdef", # '.notdef' is always included automatically
|
|
|
|
"A",
|
2021-12-03 11:16:59 +00:00
|
|
|
"grave",
|
2021-12-02 18:18:09 +00:00
|
|
|
"Agrave",
|
|
|
|
".notdef.alt001",
|
|
|
|
".notdef.alt002",
|
|
|
|
"A.alt002",
|
|
|
|
"Agrave.alt001",
|
|
|
|
"Agrave.alt002",
|
|
|
|
"grave.alt002",
|
|
|
|
]
|
|
|
|
|
|
|
|
color_layers = subset_font["COLR"].ColorLayers
|
|
|
|
assert ".notdef" in color_layers
|
|
|
|
assert "Agrave" in color_layers
|
2021-12-03 11:16:59 +00:00
|
|
|
# Agrave 'glyf' uses grave. It should be retained in 'glyf' but NOT in
|
|
|
|
# COLR when we subset down to Agrave.
|
2021-12-02 18:18:09 +00:00
|
|
|
assert "grave" not in color_layers
|
2021-12-02 18:01:24 +00:00
|
|
|
|
|
|
|
|
2023-03-01 16:28:51 +00:00
|
|
|
def test_subset_recalc_xAvgCharWidth(ttf_path):
|
|
|
|
# Note that the font in in the *ttLib*/data/TestTTF-Regular.ttx file,
|
|
|
|
# not this subset/data folder.
|
|
|
|
font = TTFont(ttf_path)
|
|
|
|
xAvgCharWidth_before = font["OS/2"].xAvgCharWidth
|
|
|
|
|
|
|
|
subset_path = ttf_path.with_suffix(".subset.ttf")
|
|
|
|
subset.main(
|
|
|
|
[
|
|
|
|
str(ttf_path),
|
|
|
|
f"--output-file={subset_path}",
|
|
|
|
# Keep only the ellipsis, which is very wide, that ought to bump up the average
|
|
|
|
"--glyphs=ellipsis",
|
|
|
|
"--recalc-average-width",
|
|
|
|
"--no-prune-unicode-ranges",
|
|
|
|
]
|
|
|
|
)
|
|
|
|
subset_font = TTFont(subset_path)
|
|
|
|
xAvgCharWidth_after = subset_font["OS/2"].xAvgCharWidth
|
|
|
|
|
|
|
|
# Check that the value gets updated
|
|
|
|
assert xAvgCharWidth_after != xAvgCharWidth_before
|
|
|
|
|
|
|
|
# Check that the value gets updated to the actual new value
|
|
|
|
subset_font["OS/2"].recalcAvgCharWidth(subset_font)
|
|
|
|
assert xAvgCharWidth_after == subset_font["OS/2"].xAvgCharWidth
|
|
|
|
|
|
|
|
|
2015-11-13 17:40:17 -08:00
|
|
|
if __name__ == "__main__":
|
2017-01-11 13:05:35 +00:00
|
|
|
sys.exit(unittest.main())
|
2023-05-22 14:30:15 +03:00
|
|
|
|
|
|
|
|
|
|
|
def test_subset_prune_gdef_markglyphsetsdef():
|
|
|
|
# GDEF_MarkGlyphSetsDef
|
|
|
|
fb = FontBuilder(unitsPerEm=1000, isTTF=True)
|
|
|
|
glyph_order = [
|
|
|
|
".notdef",
|
|
|
|
"A",
|
|
|
|
"Aacute",
|
|
|
|
"Acircumflex",
|
|
|
|
"Adieresis",
|
|
|
|
"a",
|
|
|
|
"aacute",
|
|
|
|
"acircumflex",
|
|
|
|
"adieresis",
|
|
|
|
"dieresiscomb",
|
|
|
|
"acutecomb",
|
|
|
|
"circumflexcomb",
|
|
|
|
]
|
|
|
|
fb.setupGlyphOrder(glyph_order)
|
|
|
|
fb.setupGlyf({g: TTGlyphPen(None).glyph() for g in glyph_order})
|
|
|
|
fb.setupHorizontalMetrics({g: (500, 0) for g in glyph_order})
|
|
|
|
fb.setupHorizontalHeader()
|
|
|
|
fb.setupPost()
|
|
|
|
fb.setupNameTable(
|
|
|
|
{"familyName": "TestGDEFMarkGlyphSetsDef", "styleName": "Regular"}
|
|
|
|
)
|
|
|
|
fb.addOpenTypeFeatures(
|
|
|
|
"""
|
|
|
|
feature ccmp {
|
|
|
|
lookup ccmp_1 {
|
|
|
|
lookupflag UseMarkFilteringSet [acutecomb];
|
|
|
|
sub a acutecomb by aacute;
|
|
|
|
sub A acutecomb by Aacute;
|
|
|
|
} ccmp_1;
|
|
|
|
lookup ccmp_2 {
|
|
|
|
lookupflag UseMarkFilteringSet [circumflexcomb];
|
|
|
|
sub a circumflexcomb by acircumflex;
|
|
|
|
sub A circumflexcomb by Acircumflex;
|
|
|
|
} ccmp_2;
|
|
|
|
lookup ccmp_3 {
|
|
|
|
lookupflag UseMarkFilteringSet [dieresiscomb];
|
|
|
|
sub a dieresiscomb by adieresis;
|
|
|
|
sub A dieresiscomb by Adieresis;
|
|
|
|
sub A acutecomb by Aacute;
|
|
|
|
} ccmp_3;
|
|
|
|
} ccmp;
|
|
|
|
"""
|
|
|
|
)
|
|
|
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
fb.save(buf)
|
|
|
|
buf.seek(0)
|
|
|
|
|
|
|
|
font = TTFont(buf)
|
|
|
|
|
|
|
|
features = font["GSUB"].table.FeatureList.FeatureRecord
|
|
|
|
assert features[0].FeatureTag == "ccmp"
|
|
|
|
lookups = font["GSUB"].table.LookupList.Lookup
|
|
|
|
assert lookups[0].LookupFlag == 16
|
|
|
|
assert lookups[0].MarkFilteringSet == 0
|
|
|
|
assert lookups[1].LookupFlag == 16
|
|
|
|
assert lookups[1].MarkFilteringSet == 1
|
|
|
|
assert lookups[2].LookupFlag == 16
|
|
|
|
assert lookups[2].MarkFilteringSet == 2
|
|
|
|
marksets = font["GDEF"].table.MarkGlyphSetsDef.Coverage
|
|
|
|
assert marksets[0].glyphs == ["acutecomb"]
|
|
|
|
assert marksets[1].glyphs == ["circumflexcomb"]
|
|
|
|
assert marksets[2].glyphs == ["dieresiscomb"]
|
|
|
|
|
|
|
|
options = subset.Options(layout_features=["*"])
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(glyphs=["A", "a", "acutecomb", "dieresiscomb"])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
features = font["GSUB"].table.FeatureList.FeatureRecord
|
|
|
|
assert features[0].FeatureTag == "ccmp"
|
|
|
|
lookups = font["GSUB"].table.LookupList.Lookup
|
|
|
|
assert lookups[0].LookupFlag == 16
|
|
|
|
assert lookups[0].MarkFilteringSet == 0
|
|
|
|
assert lookups[1].LookupFlag == 16
|
|
|
|
assert lookups[1].MarkFilteringSet == 1
|
|
|
|
marksets = font["GDEF"].table.MarkGlyphSetsDef.Coverage
|
|
|
|
assert marksets[0].glyphs == ["acutecomb"]
|
|
|
|
assert marksets[1].glyphs == ["dieresiscomb"]
|
|
|
|
|
|
|
|
buf = io.BytesIO()
|
|
|
|
fb.save(buf)
|
|
|
|
buf.seek(0)
|
|
|
|
|
|
|
|
font = TTFont(buf)
|
|
|
|
|
|
|
|
options = subset.Options(layout_features=["*"], layout_closure=False)
|
|
|
|
subsetter = subset.Subsetter(options)
|
|
|
|
subsetter.populate(glyphs=["A", "acutecomb", "Aacute"])
|
|
|
|
subsetter.subset(font)
|
|
|
|
|
|
|
|
features = font["GSUB"].table.FeatureList.FeatureRecord
|
|
|
|
assert features[0].FeatureTag == "ccmp"
|
|
|
|
lookups = font["GSUB"].table.LookupList.Lookup
|
|
|
|
assert lookups[0].LookupFlag == 16
|
|
|
|
assert lookups[0].MarkFilteringSet == 0
|
|
|
|
assert lookups[1].LookupFlag == 0
|
|
|
|
assert lookups[1].MarkFilteringSet == None
|
|
|
|
marksets = font["GDEF"].table.MarkGlyphSetsDef.Coverage
|
|
|
|
assert marksets[0].glyphs == ["acutecomb"]
|