transforms.py 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495
  1. from fontTools.misc.psCharStrings import (
  2. SimpleT2Decompiler,
  3. T2WidthExtractor,
  4. calcSubrBias,
  5. )
  6. def _uniq_sort(l):
  7. return sorted(set(l))
  8. class StopHintCountEvent(Exception):
  9. pass
  10. class _DesubroutinizingT2Decompiler(SimpleT2Decompiler):
  11. stop_hintcount_ops = (
  12. "op_hintmask",
  13. "op_cntrmask",
  14. "op_rmoveto",
  15. "op_hmoveto",
  16. "op_vmoveto",
  17. )
  18. def __init__(self, localSubrs, globalSubrs, private=None):
  19. SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
  20. def execute(self, charString):
  21. self.need_hintcount = True # until proven otherwise
  22. for op_name in self.stop_hintcount_ops:
  23. setattr(self, op_name, self.stop_hint_count)
  24. if hasattr(charString, "_desubroutinized"):
  25. # If a charstring has already been desubroutinized, we will still
  26. # need to execute it if we need to count hints in order to
  27. # compute the byte length for mask arguments, and haven't finished
  28. # counting hints pairs.
  29. if self.need_hintcount and self.callingStack:
  30. try:
  31. SimpleT2Decompiler.execute(self, charString)
  32. except StopHintCountEvent:
  33. del self.callingStack[-1]
  34. return
  35. charString._patches = []
  36. SimpleT2Decompiler.execute(self, charString)
  37. desubroutinized = charString.program[:]
  38. for idx, expansion in reversed(charString._patches):
  39. assert idx >= 2
  40. assert desubroutinized[idx - 1] in [
  41. "callsubr",
  42. "callgsubr",
  43. ], desubroutinized[idx - 1]
  44. assert type(desubroutinized[idx - 2]) == int
  45. if expansion[-1] == "return":
  46. expansion = expansion[:-1]
  47. desubroutinized[idx - 2 : idx] = expansion
  48. if not self.private.in_cff2:
  49. if "endchar" in desubroutinized:
  50. # Cut off after first endchar
  51. desubroutinized = desubroutinized[
  52. : desubroutinized.index("endchar") + 1
  53. ]
  54. charString._desubroutinized = desubroutinized
  55. del charString._patches
  56. def op_callsubr(self, index):
  57. subr = self.localSubrs[self.operandStack[-1] + self.localBias]
  58. SimpleT2Decompiler.op_callsubr(self, index)
  59. self.processSubr(index, subr)
  60. def op_callgsubr(self, index):
  61. subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
  62. SimpleT2Decompiler.op_callgsubr(self, index)
  63. self.processSubr(index, subr)
  64. def stop_hint_count(self, *args):
  65. self.need_hintcount = False
  66. for op_name in self.stop_hintcount_ops:
  67. setattr(self, op_name, None)
  68. cs = self.callingStack[-1]
  69. if hasattr(cs, "_desubroutinized"):
  70. raise StopHintCountEvent()
  71. def op_hintmask(self, index):
  72. SimpleT2Decompiler.op_hintmask(self, index)
  73. if self.need_hintcount:
  74. self.stop_hint_count()
  75. def processSubr(self, index, subr):
  76. cs = self.callingStack[-1]
  77. if not hasattr(cs, "_desubroutinized"):
  78. cs._patches.append((index, subr._desubroutinized))
  79. def desubroutinizeCharString(cs):
  80. """Desubroutinize a charstring in-place."""
  81. cs.decompile()
  82. subrs = getattr(cs.private, "Subrs", [])
  83. decompiler = _DesubroutinizingT2Decompiler(subrs, cs.globalSubrs, cs.private)
  84. decompiler.execute(cs)
  85. cs.program = cs._desubroutinized
  86. del cs._desubroutinized
  87. def desubroutinize(cff):
  88. for fontName in cff.fontNames:
  89. font = cff[fontName]
  90. cs = font.CharStrings
  91. for c in cs.values():
  92. desubroutinizeCharString(c)
  93. # Delete all the local subrs
  94. if hasattr(font, "FDArray"):
  95. for fd in font.FDArray:
  96. pd = fd.Private
  97. if hasattr(pd, "Subrs"):
  98. del pd.Subrs
  99. if "Subrs" in pd.rawDict:
  100. del pd.rawDict["Subrs"]
  101. else:
  102. pd = font.Private
  103. if hasattr(pd, "Subrs"):
  104. del pd.Subrs
  105. if "Subrs" in pd.rawDict:
  106. del pd.rawDict["Subrs"]
  107. # as well as the global subrs
  108. cff.GlobalSubrs.clear()
  109. class _MarkingT2Decompiler(SimpleT2Decompiler):
  110. def __init__(self, localSubrs, globalSubrs, private):
  111. SimpleT2Decompiler.__init__(self, localSubrs, globalSubrs, private)
  112. for subrs in [localSubrs, globalSubrs]:
  113. if subrs and not hasattr(subrs, "_used"):
  114. subrs._used = set()
  115. def op_callsubr(self, index):
  116. self.localSubrs._used.add(self.operandStack[-1] + self.localBias)
  117. SimpleT2Decompiler.op_callsubr(self, index)
  118. def op_callgsubr(self, index):
  119. self.globalSubrs._used.add(self.operandStack[-1] + self.globalBias)
  120. SimpleT2Decompiler.op_callgsubr(self, index)
  121. class _DehintingT2Decompiler(T2WidthExtractor):
  122. class Hints(object):
  123. def __init__(self):
  124. # Whether calling this charstring produces any hint stems
  125. # Note that if a charstring starts with hintmask, it will
  126. # have has_hint set to True, because it *might* produce an
  127. # implicit vstem if called under certain conditions.
  128. self.has_hint = False
  129. # Index to start at to drop all hints
  130. self.last_hint = 0
  131. # Index up to which we know more hints are possible.
  132. # Only relevant if status is 0 or 1.
  133. self.last_checked = 0
  134. # The status means:
  135. # 0: after dropping hints, this charstring is empty
  136. # 1: after dropping hints, there may be more hints
  137. # continuing after this, or there might be
  138. # other things. Not clear yet.
  139. # 2: no more hints possible after this charstring
  140. self.status = 0
  141. # Has hintmask instructions; not recursive
  142. self.has_hintmask = False
  143. # List of indices of calls to empty subroutines to remove.
  144. self.deletions = []
  145. pass
  146. def __init__(
  147. self, css, localSubrs, globalSubrs, nominalWidthX, defaultWidthX, private=None
  148. ):
  149. self._css = css
  150. T2WidthExtractor.__init__(
  151. self, localSubrs, globalSubrs, nominalWidthX, defaultWidthX
  152. )
  153. self.private = private
  154. def execute(self, charString):
  155. old_hints = charString._hints if hasattr(charString, "_hints") else None
  156. charString._hints = self.Hints()
  157. T2WidthExtractor.execute(self, charString)
  158. hints = charString._hints
  159. if hints.has_hint or hints.has_hintmask:
  160. self._css.add(charString)
  161. if hints.status != 2:
  162. # Check from last_check, make sure we didn't have any operators.
  163. for i in range(hints.last_checked, len(charString.program) - 1):
  164. if isinstance(charString.program[i], str):
  165. hints.status = 2
  166. break
  167. else:
  168. hints.status = 1 # There's *something* here
  169. hints.last_checked = len(charString.program)
  170. if old_hints:
  171. assert hints.__dict__ == old_hints.__dict__
  172. def op_callsubr(self, index):
  173. subr = self.localSubrs[self.operandStack[-1] + self.localBias]
  174. T2WidthExtractor.op_callsubr(self, index)
  175. self.processSubr(index, subr)
  176. def op_callgsubr(self, index):
  177. subr = self.globalSubrs[self.operandStack[-1] + self.globalBias]
  178. T2WidthExtractor.op_callgsubr(self, index)
  179. self.processSubr(index, subr)
  180. def op_hstem(self, index):
  181. T2WidthExtractor.op_hstem(self, index)
  182. self.processHint(index)
  183. def op_vstem(self, index):
  184. T2WidthExtractor.op_vstem(self, index)
  185. self.processHint(index)
  186. def op_hstemhm(self, index):
  187. T2WidthExtractor.op_hstemhm(self, index)
  188. self.processHint(index)
  189. def op_vstemhm(self, index):
  190. T2WidthExtractor.op_vstemhm(self, index)
  191. self.processHint(index)
  192. def op_hintmask(self, index):
  193. rv = T2WidthExtractor.op_hintmask(self, index)
  194. self.processHintmask(index)
  195. return rv
  196. def op_cntrmask(self, index):
  197. rv = T2WidthExtractor.op_cntrmask(self, index)
  198. self.processHintmask(index)
  199. return rv
  200. def processHintmask(self, index):
  201. cs = self.callingStack[-1]
  202. hints = cs._hints
  203. hints.has_hintmask = True
  204. if hints.status != 2:
  205. # Check from last_check, see if we may be an implicit vstem
  206. for i in range(hints.last_checked, index - 1):
  207. if isinstance(cs.program[i], str):
  208. hints.status = 2
  209. break
  210. else:
  211. # We are an implicit vstem
  212. hints.has_hint = True
  213. hints.last_hint = index + 1
  214. hints.status = 0
  215. hints.last_checked = index + 1
  216. def processHint(self, index):
  217. cs = self.callingStack[-1]
  218. hints = cs._hints
  219. hints.has_hint = True
  220. hints.last_hint = index
  221. hints.last_checked = index
  222. def processSubr(self, index, subr):
  223. cs = self.callingStack[-1]
  224. hints = cs._hints
  225. subr_hints = subr._hints
  226. # Check from last_check, make sure we didn't have
  227. # any operators.
  228. if hints.status != 2:
  229. for i in range(hints.last_checked, index - 1):
  230. if isinstance(cs.program[i], str):
  231. hints.status = 2
  232. break
  233. hints.last_checked = index
  234. if hints.status != 2:
  235. if subr_hints.has_hint:
  236. hints.has_hint = True
  237. # Decide where to chop off from
  238. if subr_hints.status == 0:
  239. hints.last_hint = index
  240. else:
  241. hints.last_hint = index - 2 # Leave the subr call in
  242. elif subr_hints.status == 0:
  243. hints.deletions.append(index)
  244. hints.status = max(hints.status, subr_hints.status)
  245. def _cs_subset_subroutines(charstring, subrs, gsubrs):
  246. p = charstring.program
  247. for i in range(1, len(p)):
  248. if p[i] == "callsubr":
  249. assert isinstance(p[i - 1], int)
  250. p[i - 1] = subrs._used.index(p[i - 1] + subrs._old_bias) - subrs._new_bias
  251. elif p[i] == "callgsubr":
  252. assert isinstance(p[i - 1], int)
  253. p[i - 1] = (
  254. gsubrs._used.index(p[i - 1] + gsubrs._old_bias) - gsubrs._new_bias
  255. )
  256. def _cs_drop_hints(charstring):
  257. hints = charstring._hints
  258. if hints.deletions:
  259. p = charstring.program
  260. for idx in reversed(hints.deletions):
  261. del p[idx - 2 : idx]
  262. if hints.has_hint:
  263. assert not hints.deletions or hints.last_hint <= hints.deletions[0]
  264. charstring.program = charstring.program[hints.last_hint :]
  265. if not charstring.program:
  266. # TODO CFF2 no need for endchar.
  267. charstring.program.append("endchar")
  268. if hasattr(charstring, "width"):
  269. # Insert width back if needed
  270. if charstring.width != charstring.private.defaultWidthX:
  271. # For CFF2 charstrings, this should never happen
  272. assert (
  273. charstring.private.defaultWidthX is not None
  274. ), "CFF2 CharStrings must not have an initial width value"
  275. charstring.program.insert(
  276. 0, charstring.width - charstring.private.nominalWidthX
  277. )
  278. if hints.has_hintmask:
  279. i = 0
  280. p = charstring.program
  281. while i < len(p):
  282. if p[i] in ["hintmask", "cntrmask"]:
  283. assert i + 1 <= len(p)
  284. del p[i : i + 2]
  285. continue
  286. i += 1
  287. assert len(charstring.program)
  288. del charstring._hints
  289. def remove_hints(cff, *, removeUnusedSubrs: bool = True):
  290. for fontname in cff.keys():
  291. font = cff[fontname]
  292. cs = font.CharStrings
  293. # This can be tricky, but doesn't have to. What we do is:
  294. #
  295. # - Run all used glyph charstrings and recurse into subroutines,
  296. # - For each charstring (including subroutines), if it has any
  297. # of the hint stem operators, we mark it as such.
  298. # Upon returning, for each charstring we note all the
  299. # subroutine calls it makes that (recursively) contain a stem,
  300. # - Dropping hinting then consists of the following two ops:
  301. # * Drop the piece of the program in each charstring before the
  302. # last call to a stem op or a stem-calling subroutine,
  303. # * Drop all hintmask operations.
  304. # - It's trickier... A hintmask right after hints and a few numbers
  305. # will act as an implicit vstemhm. As such, we track whether
  306. # we have seen any non-hint operators so far and do the right
  307. # thing, recursively... Good luck understanding that :(
  308. css = set()
  309. for c in cs.values():
  310. c.decompile()
  311. subrs = getattr(c.private, "Subrs", [])
  312. decompiler = _DehintingT2Decompiler(
  313. css,
  314. subrs,
  315. c.globalSubrs,
  316. c.private.nominalWidthX,
  317. c.private.defaultWidthX,
  318. c.private,
  319. )
  320. decompiler.execute(c)
  321. c.width = decompiler.width
  322. for charstring in css:
  323. _cs_drop_hints(charstring)
  324. del css
  325. # Drop font-wide hinting values
  326. all_privs = []
  327. if hasattr(font, "FDArray"):
  328. all_privs.extend(fd.Private for fd in font.FDArray)
  329. else:
  330. all_privs.append(font.Private)
  331. for priv in all_privs:
  332. for k in [
  333. "BlueValues",
  334. "OtherBlues",
  335. "FamilyBlues",
  336. "FamilyOtherBlues",
  337. "BlueScale",
  338. "BlueShift",
  339. "BlueFuzz",
  340. "StemSnapH",
  341. "StemSnapV",
  342. "StdHW",
  343. "StdVW",
  344. "ForceBold",
  345. "LanguageGroup",
  346. "ExpansionFactor",
  347. ]:
  348. if hasattr(priv, k):
  349. setattr(priv, k, None)
  350. if removeUnusedSubrs:
  351. remove_unused_subroutines(cff)
  352. def _pd_delete_empty_subrs(private_dict):
  353. if hasattr(private_dict, "Subrs") and not private_dict.Subrs:
  354. if "Subrs" in private_dict.rawDict:
  355. del private_dict.rawDict["Subrs"]
  356. del private_dict.Subrs
  357. def remove_unused_subroutines(cff):
  358. for fontname in cff.keys():
  359. font = cff[fontname]
  360. cs = font.CharStrings
  361. # Renumber subroutines to remove unused ones
  362. # Mark all used subroutines
  363. for c in cs.values():
  364. subrs = getattr(c.private, "Subrs", [])
  365. decompiler = _MarkingT2Decompiler(subrs, c.globalSubrs, c.private)
  366. decompiler.execute(c)
  367. all_subrs = [font.GlobalSubrs]
  368. if hasattr(font, "FDArray"):
  369. all_subrs.extend(
  370. fd.Private.Subrs
  371. for fd in font.FDArray
  372. if hasattr(fd.Private, "Subrs") and fd.Private.Subrs
  373. )
  374. elif hasattr(font.Private, "Subrs") and font.Private.Subrs:
  375. all_subrs.append(font.Private.Subrs)
  376. subrs = set(subrs) # Remove duplicates
  377. # Prepare
  378. for subrs in all_subrs:
  379. if not hasattr(subrs, "_used"):
  380. subrs._used = set()
  381. subrs._used = _uniq_sort(subrs._used)
  382. subrs._old_bias = calcSubrBias(subrs)
  383. subrs._new_bias = calcSubrBias(subrs._used)
  384. # Renumber glyph charstrings
  385. for c in cs.values():
  386. subrs = getattr(c.private, "Subrs", None)
  387. _cs_subset_subroutines(c, subrs, font.GlobalSubrs)
  388. # Renumber subroutines themselves
  389. for subrs in all_subrs:
  390. if subrs == font.GlobalSubrs:
  391. if not hasattr(font, "FDArray") and hasattr(font.Private, "Subrs"):
  392. local_subrs = font.Private.Subrs
  393. elif (
  394. hasattr(font, "FDArray")
  395. and len(font.FDArray) == 1
  396. and hasattr(font.FDArray[0].Private, "Subrs")
  397. ):
  398. # Technically we shouldn't do this. But I've run into fonts that do it.
  399. local_subrs = font.FDArray[0].Private.Subrs
  400. else:
  401. local_subrs = None
  402. else:
  403. local_subrs = subrs
  404. subrs.items = [subrs.items[i] for i in subrs._used]
  405. if hasattr(subrs, "file"):
  406. del subrs.file
  407. if hasattr(subrs, "offsets"):
  408. del subrs.offsets
  409. for subr in subrs.items:
  410. _cs_subset_subroutines(subr, local_subrs, font.GlobalSubrs)
  411. # Delete local SubrsIndex if empty
  412. if hasattr(font, "FDArray"):
  413. for fd in font.FDArray:
  414. _pd_delete_empty_subrs(fd.Private)
  415. else:
  416. _pd_delete_empty_subrs(font.Private)
  417. # Cleanup
  418. for subrs in all_subrs:
  419. del subrs._used, subrs._old_bias, subrs._new_bias