cff.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536
  1. from fontTools.misc import psCharStrings
  2. from fontTools import ttLib
  3. from fontTools.pens.basePen import NullPen
  4. from fontTools.misc.roundTools import otRound
  5. from fontTools.misc.loggingTools import deprecateFunction
  6. from fontTools.subset.util import _add_method, _uniq_sort
  7. class _ClosureGlyphsT2Decompiler(psCharStrings.SimpleT2Decompiler):
  8. def __init__(self, components, localSubrs, globalSubrs):
  9. psCharStrings.SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs)
  10. self.components = components
  11. def op_endchar(self, index):
  12. args = self.popall()
  13. if len(args) >= 4:
  14. from fontTools.encodings.StandardEncoding import StandardEncoding
  15. # endchar can do seac accent bulding; The T2 spec says it's deprecated,
  16. # but recent software that shall remain nameless does output it.
  17. adx, ady, bchar, achar = args[-4:]
  18. baseGlyph = StandardEncoding[bchar]
  19. accentGlyph = StandardEncoding[achar]
  20. self.components.add(baseGlyph)
  21. self.components.add(accentGlyph)
  22. @_add_method(ttLib.getTableClass("CFF "))
  23. def closure_glyphs(self, s):
  24. cff = self.cff
  25. assert len(cff) == 1
  26. font = cff[cff.keys()[0]]
  27. glyphSet = font.CharStrings
  28. decompose = s.glyphs
  29. while decompose:
  30. components = set()
  31. for g in decompose:
  32. if g not in glyphSet:
  33. continue
  34. gl = glyphSet[g]
  35. subrs = getattr(gl.private, "Subrs", [])
  36. decompiler = _ClosureGlyphsT2Decompiler(components, subrs, gl.globalSubrs)
  37. decompiler.execute(gl)
  38. components -= s.glyphs
  39. s.glyphs.update(components)
  40. decompose = components
  41. def _empty_charstring(font, glyphName, isCFF2, ignoreWidth=False):
  42. c, fdSelectIndex = font.CharStrings.getItemAndSelector(glyphName)
  43. if isCFF2 or ignoreWidth:
  44. # CFF2 charstrings have no widths nor 'endchar' operators
  45. c.setProgram([] if isCFF2 else ["endchar"])
  46. else:
  47. if hasattr(font, "FDArray") and font.FDArray is not None:
  48. private = font.FDArray[fdSelectIndex].Private
  49. else:
  50. private = font.Private
  51. dfltWdX = private.defaultWidthX
  52. nmnlWdX = private.nominalWidthX
  53. pen = NullPen()
  54. c.draw(pen) # this will set the charstring's width
  55. if c.width != dfltWdX:
  56. c.program = [c.width - nmnlWdX, "endchar"]
  57. else:
  58. c.program = ["endchar"]
  59. @_add_method(ttLib.getTableClass("CFF "))
  60. def prune_pre_subset(self, font, options):
  61. cff = self.cff
  62. # CFF table must have one font only
  63. cff.fontNames = cff.fontNames[:1]
  64. if options.notdef_glyph and not options.notdef_outline:
  65. isCFF2 = cff.major > 1
  66. for fontname in cff.keys():
  67. font = cff[fontname]
  68. _empty_charstring(font, ".notdef", isCFF2=isCFF2)
  69. # Clear useless Encoding
  70. for fontname in cff.keys():
  71. font = cff[fontname]
  72. # https://github.com/fonttools/fonttools/issues/620
  73. font.Encoding = "StandardEncoding"
  74. return True # bool(cff.fontNames)
  75. @_add_method(ttLib.getTableClass("CFF "))
  76. def subset_glyphs(self, s):
  77. cff = self.cff
  78. for fontname in cff.keys():
  79. font = cff[fontname]
  80. cs = font.CharStrings
  81. glyphs = s.glyphs.union(s.glyphs_emptied)
  82. # Load all glyphs
  83. for g in font.charset:
  84. if g not in glyphs:
  85. continue
  86. c, _ = cs.getItemAndSelector(g)
  87. if cs.charStringsAreIndexed:
  88. indices = [i for i, g in enumerate(font.charset) if g in glyphs]
  89. csi = cs.charStringsIndex
  90. csi.items = [csi.items[i] for i in indices]
  91. del csi.file, csi.offsets
  92. if hasattr(font, "FDSelect"):
  93. sel = font.FDSelect
  94. sel.format = None
  95. sel.gidArray = [sel.gidArray[i] for i in indices]
  96. newCharStrings = {}
  97. for indicesIdx, charsetIdx in enumerate(indices):
  98. g = font.charset[charsetIdx]
  99. if g in cs.charStrings:
  100. newCharStrings[g] = indicesIdx
  101. cs.charStrings = newCharStrings
  102. else:
  103. cs.charStrings = {g: v for g, v in cs.charStrings.items() if g in glyphs}
  104. font.charset = [g for g in font.charset if g in glyphs]
  105. font.numGlyphs = len(font.charset)
  106. if s.options.retain_gids:
  107. isCFF2 = cff.major > 1
  108. for g in s.glyphs_emptied:
  109. _empty_charstring(font, g, isCFF2=isCFF2, ignoreWidth=True)
  110. return True # any(cff[fontname].numGlyphs for fontname in cff.keys())
  111. @_add_method(psCharStrings.T2CharString)
  112. def subset_subroutines(self, subrs, gsubrs):
  113. p = self.program
  114. for i in range(1, len(p)):
  115. if p[i] == "callsubr":
  116. assert isinstance(p[i - 1], int)
  117. p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
  118. elif p[i] == "callgsubr":
  119. assert isinstance(p[i - 1], int)
  120. p[i - 1] = (
  121. gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
  122. )
  123. @_add_method(psCharStrings.T2CharString)
  124. def drop_hints(self):
  125. hints = self._hints
  126. if hints.deletions:
  127. p = self.program
  128. for idx in reversed(hints.deletions):
  129. del p[idx - 2 : idx]
  130. if hints.has_hint:
  131. assert not hints.deletions or hints.last_hint <= hints.deletions[0]
  132. self.program = self.program[hints.last_hint :]
  133. if not self.program:
  134. # TODO CFF2 no need for endchar.
  135. self.program.append("endchar")
  136. if hasattr(self, "width"):
  137. # Insert width back if needed
  138. if self.width != self.private.defaultWidthX:
  139. # For CFF2 charstrings, this should never happen
  140. assert (
  141. self.private.defaultWidthX is not None
  142. ), "CFF2 CharStrings must not have an initial width value"
  143. self.program.insert(0, self.width - self.private.nominalWidthX)
  144. if hints.has_hintmask:
  145. i = 0
  146. p = self.program
  147. while i < len(p):
  148. if p[i] in ["hintmask", "cntrmask"]:
  149. assert i + 1 <= len(p)
  150. del p[i : i + 2]
  151. continue
  152. i += 1
  153. assert len(self.program)
  154. del self._hints
  155. class _MarkingT2Decompiler(psCharStrings.SimpleT2Decompiler):
  156. def __init__(self, localSubrs, globalSubrs, private):
  157. psCharStrings.SimpleT2Decompiler.__init__(
  158. self, localSubrs, globalSubrs, private
  159. )
  160. for subrs in [localSubrs, globalSubrs]:
  161. if subrs and not hasattr(subrs, "_used"):
  162. subrs._used = set()
  163. def op_callsubr(self, index):
  164. self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
  165. psCharStrings.SimpleT2Decompiler.op_callsubr(self, index)
  166. def op_callgsubr(self, index):
  167. self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
  168. psCharStrings.SimpleT2Decompiler.op_callgsubr(self, index)
  169. class _DehintingT2Decompiler(psCharStrings.T2WidthExtractor):
  170. class Hints(object):
  171. def __init__(self):
  172. # Whether calling this charstring produces any hint stems
  173. # Note that if a charstring starts with hintmask, it will
  174. # have has_hint set to True, because it *might* produce an
  175. # implicit vstem if called under certain conditions.
  176. self.has_hint = False
  177. # Index to start at to drop all hints
  178. self.last_hint = 0
  179. # Index up to which we know more hints are possible.
  180. # Only relevant if status is 0 or 1.
  181. self.last_checked = 0
  182. # The status means:
  183. # 0: after dropping hints, this charstring is empty
  184. # 1: after dropping hints, there may be more hints
  185. # continuing after this, or there might be
  186. # other things. Not clear yet.
  187. # 2: no more hints possible after this charstring
  188. self.status = 0
  189. # Has hintmask instructions; not recursive
  190. self.has_hintmask = False
  191. # List of indices of calls to empty subroutines to remove.
  192. self.deletions = []
  193. pass
  194. def __init__(
  195. self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
  196. ):
  197. self._css = css
  198. psCharStrings.T2WidthExtractor.__init__(
  199. self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
  200. )
  201. self.private = private
  202. def execute(self, charString):
  203. old_hints = charString._hints if hasattr(charString, "_hints") else None
  204. charString._hints = self.Hints()
  205. psCharStrings.T2WidthExtractor.execute(self, charString)
  206. hints = charString._hints
  207. if hints.has_hint or hints.has_hintmask:
  208. self._css.add(charString)
  209. if hints.status != 2:
  210. # Check from last_check, make sure we didn't have any operators.
  211. for i in range(hints.last_checked, len(charString.program) - 1):
  212. if isinstance(charString.program[i], str):
  213. hints.status = 2
  214. break
  215. else:
  216. hints.status = 1 # There's *something* here
  217. hints.last_checked = len(charString.program)
  218. if old_hints:
  219. assert hints.__dict__ == old_hints.__dict__
  220. def op_callsubr(self, index):
  221. subr = self.localSubrs[self.operandStack[-1] + self.localBias]
  222. psCharStrings.T2WidthExtractor.op_callsubr(self, index)
  223. self.processSubr(index, subr)
  224. def op_callgsubr(self, index):
  225. subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
  226. psCharStrings.T2WidthExtractor.op_callgsubr(self, index)
  227. self.processSubr(index, subr)
  228. def op_hstem(self, index):
  229. psCharStrings.T2WidthExtractor.op_hstem(self, index)
  230. self.processHint(index)
  231. def op_vstem(self, index):
  232. psCharStrings.T2WidthExtractor.op_vstem(self, index)
  233. self.processHint(index)
  234. def op_hstemhm(self, index):
  235. psCharStrings.T2WidthExtractor.op_hstemhm(self, index)
  236. self.processHint(index)
  237. def op_vstemhm(self, index):
  238. psCharStrings.T2WidthExtractor.op_vstemhm(self, index)
  239. self.processHint(index)
  240. def op_hintmask(self, index):
  241. rv = psCharStrings.T2WidthExtractor.op_hintmask(self, index)
  242. self.processHintmask(index)
  243. return rv
  244. def op_cntrmask(self, index):
  245. rv = psCharStrings.T2WidthExtractor.op_cntrmask(self, index)
  246. self.processHintmask(index)
  247. return rv
  248. def processHintmask(self, index):
  249. cs = self.callingStack[-1]
  250. hints = cs._hints
  251. hints.has_hintmask = True
  252. if hints.status != 2:
  253. # Check from last_check, see if we may be an implicit vstem
  254. for i in range(hints.last_checked, index - 1):
  255. if isinstance(cs.program[i], str):
  256. hints.status = 2
  257. break
  258. else:
  259. # We are an implicit vstem
  260. hints.has_hint = True
  261. hints.last_hint = index + 1
  262. hints.status = 0
  263. hints.last_checked = index + 1
  264. def processHint(self, index):
  265. cs = self.callingStack[-1]
  266. hints = cs._hints
  267. hints.has_hint = True
  268. hints.last_hint = index
  269. hints.last_checked = index
  270. def processSubr(self, index, subr):
  271. cs = self.callingStack[-1]
  272. hints = cs._hints
  273. subr_hints = subr._hints
  274. # Check from last_check, make sure we didn't have
  275. # any operators.
  276. if hints.status != 2:
  277. for i in range(hints.last_checked, index - 1):
  278. if isinstance(cs.program[i], str):
  279. hints.status = 2
  280. break
  281. hints.last_checked = index
  282. if hints.status != 2:
  283. if subr_hints.has_hint:
  284. hints.has_hint = True
  285. # Decide where to chop off from
  286. if subr_hints.status == 0:
  287. hints.last_hint = index
  288. else:
  289. hints.last_hint = index - 2 # Leave the subr call in
  290. elif subr_hints.status == 0:
  291. hints.deletions.append(index)
  292. hints.status = max(hints.status, subr_hints.status)
  293. @_add_method(ttLib.getTableClass("CFF "))
  294. def prune_post_subset(self, ttfFont, options):
  295. cff = self.cff
  296. for fontname in cff.keys():
  297. font = cff[fontname]
  298. cs = font.CharStrings
  299. # Drop unused FontDictionaries
  300. if hasattr(font, "FDSelect"):
  301. sel = font.FDSelect
  302. indices = _uniq_sort(sel.gidArray)
  303. sel.gidArray = [indices.index(ss) for ss in sel.gidArray]
  304. arr = font.FDArray
  305. arr.items = [arr[i] for i in indices]
  306. del arr.file, arr.offsets
  307. # Desubroutinize if asked for
  308. if options.desubroutinize:
  309. cff.desubroutinize()
  310. # Drop hints if not needed
  311. if not options.hinting:
  312. self.remove_hints()
  313. elif not options.desubroutinize:
  314. self.remove_unused_subroutines()
  315. return True
  316. def _delete_empty_subrs(private_dict):
  317. if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
  318. if "Subrs" in private_dict.rawDict:
  319. del private_dict.rawDict["Subrs"]
  320. del private_dict.Subrs
  321. @deprecateFunction(
  322. "use 'CFFFontSet.desubroutinize()' instead", category=DeprecationWarning
  323. )
  324. @_add_method(ttLib.getTableClass("CFF "))
  325. def desubroutinize(self):
  326. self.cff.desubroutinize()
  327. @_add_method(ttLib.getTableClass("CFF "))
  328. def remove_hints(self):
  329. cff = self.cff
  330. for fontname in cff.keys():
  331. font = cff[fontname]
  332. cs = font.CharStrings
  333. # This can be tricky, but doesn't have to. What we do is:
  334. #
  335. # - Run all used glyph charstrings and recurse into subroutines,
  336. # - For each charstring (including subroutines), if it has any
  337. # of the hint stem operators, we mark it as such.
  338. # Upon returning, for each charstring we note all the
  339. # subroutine calls it makes that (recursively) contain a stem,
  340. # - Dropping hinting then consists of the following two ops:
  341. # * Drop the piece of the program in each charstring before the
  342. # last call to a stem op or a stem-calling subroutine,
  343. # * Drop all hintmask operations.
  344. # - It's trickier... A hintmask right after hints and a few numbers
  345. # will act as an implicit vstemhm. As such, we track whether
  346. # we have seen any non-hint operators so far and do the right
  347. # thing, recursively... Good luck understanding that :(
  348. css = set()
  349. for g in font.charset:
  350. c, _ = cs.getItemAndSelector(g)
  351. c.decompile()
  352. subrs = getattr(c.private, "Subrs", [])
  353. decompiler = _DehintingT2Decompiler(
  354. css,
  355. subrs,
  356. c.globalSubrs,
  357. c.private.nominalWidthX,
  358. c.private.defaultWidthX,
  359. c.private,
  360. )
  361. decompiler.execute(c)
  362. c.width = decompiler.width
  363. for charstring in css:
  364. charstring.drop_hints()
  365. del css
  366. # Drop font-wide hinting values
  367. all_privs = []
  368. if hasattr(font, "FDArray"):
  369. all_privs.extend(fd.Private for fd in font.FDArray)
  370. else:
  371. all_privs.append(font.Private)
  372. for priv in all_privs:
  373. for k in [
  374. "BlueValues",
  375. "OtherBlues",
  376. "FamilyBlues",
  377. "FamilyOtherBlues",
  378. "BlueScale",
  379. "BlueShift",
  380. "BlueFuzz",
  381. "StemSnapH",
  382. "StemSnapV",
  383. "StdHW",
  384. "StdVW",
  385. "ForceBold",
  386. "LanguageGroup",
  387. "ExpansionFactor",
  388. ]:
  389. if hasattr(priv, k):
  390. setattr(priv, k, None)
  391. self.remove_unused_subroutines()
  392. @_add_method(ttLib.getTableClass("CFF "))
  393. def remove_unused_subroutines(self):
  394. cff = self.cff
  395. for fontname in cff.keys():
  396. font = cff[fontname]
  397. cs = font.CharStrings
  398. # Renumber subroutines to remove unused ones
  399. # Mark all used subroutines
  400. for g in font.charset:
  401. c, _ = cs.getItemAndSelector(g)
  402. subrs = getattr(c.private, "Subrs", [])
  403. decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
  404. decompiler.execute(c)
  405. all_subrs = [font.GlobalSubrs]
  406. if hasattr(font, "FDArray"):
  407. all_subrs.extend(
  408. fd.Private.Subrs
  409. for fd in font.FDArray
  410. if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
  411. )
  412. elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
  413. all_subrs.append(font.Private.Subrs)
  414. subrs = set(subrs) # Remove duplicates
  415. # Prepare
  416. for subrs in all_subrs:
  417. if not hasattr(subrs, "_used"):
  418. subrs._used = set()
  419. subrs._used = _uniq_sort(subrs._used)
  420. subrs._old_bias = psCharStrings.calcSubrBias(subrs)
  421. subrs._new_bias = psCharStrings.calcSubrBias(subrs._used)
  422. # Renumber glyph charstrings
  423. for g in font.charset:
  424. c, _ = cs.getItemAndSelector(g)
  425. subrs = getattr(c.private, "Subrs", None)
  426. c.subset_subroutines(subrs, font.GlobalSubrs)
  427. # Renumber subroutines themselves
  428. for subrs in all_subrs:
  429. if subrs == font.GlobalSubrs:
  430. if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
  431. local_subrs = font.Private.Subrs
  432. else:
  433. local_subrs = None
  434. else:
  435. local_subrs = subrs
  436. subrs.items = [subrs.items[i] for i in subrs._used]
  437. if hasattr(subrs, "file"):
  438. del subrs.file
  439. if hasattr(subrs, "offsets"):
  440. del subrs.offsets
  441. for subr in subrs.items:
  442. subr.subset_subroutines(local_subrs, font.GlobalSubrs)
  443. # Delete local SubrsIndex if empty
  444. if hasattr(font, "FDArray"):
  445. for fd in font.FDArray:
  446. _delete_empty_subrs(fd.Private)
  447. else:
  448. _delete_empty_subrs(font.Private)
  449. # Cleanup
  450. for subrs in all_subrs:
  451. del subrs._used, subrs._old_bias, subrs._new_bias