Skip to content

Commit 93067d2

Browse files
authored
Merge pull request #5 from anthrotype/desubroutinize
add function to desubroutinize
2 parents 3945694 + fdd316e commit 93067d2

3 files changed

Lines changed: 105 additions & 6 deletions

File tree

src/cffsubr/__init__.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from fontTools import ttLib
1717

1818

19-
__all__ = ["subroutinize", "Error"]
19+
__all__ = ["subroutinize", "desubroutinize", "has_subroutines", "Error"]
2020

2121

2222
try:
@@ -117,15 +117,18 @@ def _tx_subroutinize(data: bytes, output_format: str = CFFTableTag.CFF) -> bytes
117117
return output_data
118118

119119

120-
def _sniff_cff_table_format(otf: ttLib.TTFont) -> Optional[CFFTableTag]:
121-
return next(
120+
def _sniff_cff_table_format(otf: ttLib.TTFont) -> CFFTableTag:
121+
cff_tag = next(
122122
(
123123
CFFTableTag(tag)
124124
for tag in otf.keys()
125125
if tag in CFFTableTag.__members__.values()
126126
),
127127
None,
128128
)
129+
if not cff_tag:
130+
raise Error("Invalid OTF: no 'CFF ' or 'CFF2' tables found")
131+
return cff_tag
129132

130133

131134
def subroutinize(
@@ -158,8 +161,6 @@ def subroutinize(
158161
or if subroutinization process fails.
159162
"""
160163
input_format = _sniff_cff_table_format(otf)
161-
if not input_format:
162-
raise Error("Invalid OTF: no 'CFF ' or 'CFF2' tables found")
163164

164165
if cff_version is None:
165166
output_format = input_format
@@ -197,3 +198,49 @@ def subroutinize(
197198
post_table.glyphOrder = glyphOrder
198199

199200
return otf
201+
202+
203+
def has_subroutines(otf: ttLib.TTFont) -> bool:
204+
"""Return True if the font's CFF or CFF2 table contains any subroutines."""
205+
table_tag = _sniff_cff_table_format(otf)
206+
top_dict = otf[table_tag].cff.topDictIndex[0]
207+
all_subrs = [top_dict.GlobalSubrs]
208+
if hasattr(top_dict, "FDArray"):
209+
all_subrs.extend(
210+
fd.Private.Subrs for fd in top_dict.FDArray if hasattr(fd.Private, "Subrs")
211+
)
212+
elif hasattr(top_dict.Private, "Subrs"):
213+
all_subrs.append(top_dict.Private.Subrs)
214+
return any(all_subrs)
215+
216+
217+
def desubroutinize(otf: ttLib.TTFont, inplace=True) -> ttLib.TTFont:
218+
"""Remove all subroutines from the font.
219+
220+
Args:
221+
otf (ttLib.TTFont): the input font object.
222+
inplace (bool): whether to create a copy or modify the input font. By default
223+
the input font is modified.
224+
225+
Returns:
226+
The modified font containing the desubroutinized CFF or CFF2 table.
227+
This will be a different TTFont object if inplace=False.
228+
229+
Raises:
230+
cffsubr.Error if the font doesn't contain 'CFF ' or 'CFF2' table,
231+
or if desubroutinization process fails.
232+
"""
233+
# the 'desubroutinize' method is dynamically added to the CFF table class
234+
# as a side-effect of importing the fontTools.subset.cff module...
235+
from fontTools.subset import cff as _
236+
237+
if not inplace:
238+
otf = copy.deepcopy(otf)
239+
240+
table_tag = _sniff_cff_table_format(otf)
241+
try:
242+
otf[table_tag].desubroutinize()
243+
except Exception as e:
244+
raise Error("Desubroutinization failed") from e
245+
246+
return otf

src/cffsubr/__main__.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def main(args=None):
3939
action="store_false",
4040
help="whether to drop postscript glyph names when converting from CFF to CFF2.",
4141
)
42+
parser.add_argument(
43+
"-d",
44+
"--desubroutinize",
45+
action="store_true",
46+
help="Don't subroutinize, instead remove all subroutines (in any).",
47+
)
4248
options = parser.parse_args(args)
4349

4450
if options.inplace:
@@ -47,7 +53,10 @@ def main(args=None):
4753
options.output_file = sys.stdout.buffer
4854

4955
with ttLib.TTFont(options.input_file, lazy=True) as font:
50-
cffsubr.subroutinize(font, options.cff_version, options.keep_glyph_names)
56+
if options.desubroutinize:
57+
cffsubr.desubroutinize(font)
58+
else:
59+
cffsubr.subroutinize(font, options.cff_version, options.keep_glyph_names)
5160
font.save(options.output_file)
5261

5362

tests/cffsubr_test.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,3 +99,46 @@ def test_drop_glyph_names(self):
9999
font2 = ttLib.TTFont(buf)
100100

101101
assert font2.getGlyphOrder() != glyph_order
102+
103+
104+
@pytest.mark.parametrize(
105+
"testfile, table_tag",
106+
[
107+
("SourceSansPro-Regular.subset.ttx", "CFF "),
108+
("SourceSansVariable-Roman.subset.ttx", "CFF2"),
109+
],
110+
)
111+
def test_sniff_cff_table_format(testfile, table_tag):
112+
font = load_test_font(testfile)
113+
114+
assert cffsubr._sniff_cff_table_format(font) == table_tag
115+
116+
117+
def test_sniff_cff_table_format_invalid():
118+
with pytest.raises(cffsubr.Error, match="Invalid OTF"):
119+
cffsubr._sniff_cff_table_format(ttLib.TTFont())
120+
121+
122+
@pytest.mark.parametrize(
123+
"testfile",
124+
["SourceSansPro-Regular.subset.ttx", "SourceSansVariable-Roman.subset.ttx"],
125+
)
126+
def test_has_subroutines(testfile):
127+
font = load_test_font(testfile)
128+
129+
assert not cffsubr.has_subroutines(font)
130+
assert cffsubr.has_subroutines(cffsubr.subroutinize(font))
131+
132+
133+
@pytest.mark.parametrize(
134+
"testfile",
135+
["SourceSansPro-Regular.subset.ttx", "SourceSansVariable-Roman.subset.ttx"],
136+
)
137+
def test_desubroutinize(testfile):
138+
font = load_test_font(testfile)
139+
cffsubr.subroutinize(font)
140+
141+
font2 = cffsubr.desubroutinize(font, inplace=False)
142+
143+
assert cffsubr.has_subroutines(font)
144+
assert not cffsubr.has_subroutines(font2)

0 commit comments

Comments
 (0)