transforms.py 17 KB

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