CFFToCFF2.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. """CFF to CFF2 converter."""
  2. from fontTools.ttLib import TTFont, newTable
  3. from fontTools.misc.cliTools import makeOutputFileName
  4. from fontTools.misc.psCharStrings import T2WidthExtractor
  5. from fontTools.cffLib import (
  6. TopDictIndex,
  7. FDArrayIndex,
  8. FontDict,
  9. buildOrder,
  10. topDictOperators,
  11. privateDictOperators,
  12. topDictOperators2,
  13. privateDictOperators2,
  14. )
  15. from io import BytesIO
  16. import logging
  17. __all__ = ["convertCFFToCFF2", "main"]
  18. log = logging.getLogger("fontTools.cffLib")
  19. class _NominalWidthUsedError(Exception):
  20. def __add__(self, other):
  21. raise self
  22. def __radd__(self, other):
  23. raise self
  24. def _convertCFFToCFF2(cff, otFont):
  25. """Converts this object from CFF format to CFF2 format. This conversion
  26. is done 'in-place'. The conversion cannot be reversed.
  27. This assumes a decompiled CFF table. (i.e. that the object has been
  28. filled via :meth:`decompile` and e.g. not loaded from XML.)"""
  29. # Clean up T2CharStrings
  30. topDict = cff.topDictIndex[0]
  31. fdArray = topDict.FDArray if hasattr(topDict, "FDArray") else None
  32. charStrings = topDict.CharStrings
  33. globalSubrs = cff.GlobalSubrs
  34. localSubrs = (
  35. [getattr(fd.Private, "Subrs", []) for fd in fdArray]
  36. if fdArray
  37. else (
  38. [topDict.Private.Subrs]
  39. if hasattr(topDict, "Private") and hasattr(topDict.Private, "Subrs")
  40. else []
  41. )
  42. )
  43. for glyphName in charStrings.keys():
  44. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  45. cs.decompile()
  46. # Clean up subroutines first
  47. for subrs in [globalSubrs] + localSubrs:
  48. for subr in subrs:
  49. program = subr.program
  50. i = j = len(program)
  51. try:
  52. i = program.index("return")
  53. except ValueError:
  54. pass
  55. try:
  56. j = program.index("endchar")
  57. except ValueError:
  58. pass
  59. program[min(i, j) :] = []
  60. # Clean up glyph charstrings
  61. removeUnusedSubrs = False
  62. nominalWidthXError = _NominalWidthUsedError()
  63. for glyphName in charStrings.keys():
  64. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  65. program = cs.program
  66. thisLocalSubrs = (
  67. localSubrs[fdIndex]
  68. if fdIndex is not None
  69. else (
  70. getattr(topDict.Private, "Subrs", [])
  71. if hasattr(topDict, "Private")
  72. else []
  73. )
  74. )
  75. # Intentionally use custom type for nominalWidthX, such that any
  76. # CharString that has an explicit width encoded will throw back to us.
  77. extractor = T2WidthExtractor(
  78. thisLocalSubrs,
  79. globalSubrs,
  80. nominalWidthXError,
  81. 0,
  82. )
  83. try:
  84. extractor.execute(cs)
  85. except _NominalWidthUsedError:
  86. # Program has explicit width. We want to drop it, but can't
  87. # just pop the first number since it may be a subroutine call.
  88. # Instead, when seeing that, we embed the subroutine and recurse.
  89. # If this ever happened, we later prune unused subroutines.
  90. while len(program) >= 2 and program[1] in ["callsubr", "callgsubr"]:
  91. removeUnusedSubrs = True
  92. subrNumber = program.pop(0)
  93. assert isinstance(subrNumber, int), subrNumber
  94. op = program.pop(0)
  95. bias = extractor.localBias if op == "callsubr" else extractor.globalBias
  96. subrNumber += bias
  97. subrSet = thisLocalSubrs if op == "callsubr" else globalSubrs
  98. subrProgram = subrSet[subrNumber].program
  99. program[:0] = subrProgram
  100. # Now pop the actual width
  101. assert len(program) >= 1, program
  102. program.pop(0)
  103. if program and program[-1] == "endchar":
  104. program.pop()
  105. if removeUnusedSubrs:
  106. cff.remove_unused_subroutines()
  107. # Upconvert TopDict
  108. cff.major = 2
  109. cff2GetGlyphOrder = cff.otFont.getGlyphOrder
  110. topDictData = TopDictIndex(None, cff2GetGlyphOrder)
  111. for item in cff.topDictIndex:
  112. # Iterate over, such that all are decompiled
  113. topDictData.append(item)
  114. cff.topDictIndex = topDictData
  115. topDict = topDictData[0]
  116. if hasattr(topDict, "Private"):
  117. privateDict = topDict.Private
  118. else:
  119. privateDict = None
  120. opOrder = buildOrder(topDictOperators2)
  121. topDict.order = opOrder
  122. topDict.cff2GetGlyphOrder = cff2GetGlyphOrder
  123. if not hasattr(topDict, "FDArray"):
  124. fdArray = topDict.FDArray = FDArrayIndex()
  125. fdArray.strings = None
  126. fdArray.GlobalSubrs = topDict.GlobalSubrs
  127. topDict.GlobalSubrs.fdArray = fdArray
  128. charStrings = topDict.CharStrings
  129. if charStrings.charStringsAreIndexed:
  130. charStrings.charStringsIndex.fdArray = fdArray
  131. else:
  132. charStrings.fdArray = fdArray
  133. fontDict = FontDict()
  134. fontDict.setCFF2(True)
  135. fdArray.append(fontDict)
  136. fontDict.Private = privateDict
  137. privateOpOrder = buildOrder(privateDictOperators2)
  138. if privateDict is not None:
  139. for entry in privateDictOperators:
  140. key = entry[1]
  141. if key not in privateOpOrder:
  142. if key in privateDict.rawDict:
  143. # print "Removing private dict", key
  144. del privateDict.rawDict[key]
  145. if hasattr(privateDict, key):
  146. delattr(privateDict, key)
  147. # print "Removing privateDict attr", key
  148. else:
  149. # clean up the PrivateDicts in the fdArray
  150. fdArray = topDict.FDArray
  151. privateOpOrder = buildOrder(privateDictOperators2)
  152. for fontDict in fdArray:
  153. fontDict.setCFF2(True)
  154. for key in list(fontDict.rawDict.keys()):
  155. if key not in fontDict.order:
  156. del fontDict.rawDict[key]
  157. if hasattr(fontDict, key):
  158. delattr(fontDict, key)
  159. privateDict = fontDict.Private
  160. for entry in privateDictOperators:
  161. key = entry[1]
  162. if key not in privateOpOrder:
  163. if key in list(privateDict.rawDict.keys()):
  164. # print "Removing private dict", key
  165. del privateDict.rawDict[key]
  166. if hasattr(privateDict, key):
  167. delattr(privateDict, key)
  168. # print "Removing privateDict attr", key
  169. # Now delete up the deprecated topDict operators from CFF 1.0
  170. for entry in topDictOperators:
  171. key = entry[1]
  172. # We seem to need to keep the charset operator for now,
  173. # or we fail to compile with some fonts, like AdditionFont.otf.
  174. # I don't know which kind of CFF font those are. But keeping
  175. # charset seems to work. It will be removed when we save and
  176. # read the font again.
  177. #
  178. # AdditionFont.otf has <Encoding name="StandardEncoding"/>.
  179. if key == "charset":
  180. continue
  181. if key not in opOrder:
  182. if key in topDict.rawDict:
  183. del topDict.rawDict[key]
  184. if hasattr(topDict, key):
  185. delattr(topDict, key)
  186. # TODO(behdad): What does the following comment even mean? Both CFF and CFF2
  187. # use the same T2Charstring class. I *think* what it means is that the CharStrings
  188. # were loaded for CFF1, and we need to reload them for CFF2 to set varstore, etc
  189. # on them. At least that's what I understand. It's probably safe to remove this
  190. # and just set vstore where needed.
  191. #
  192. # See comment above about charset as well.
  193. # At this point, the Subrs and Charstrings are all still T2Charstring class
  194. # easiest to fix this by compiling, then decompiling again
  195. file = BytesIO()
  196. cff.compile(file, otFont, isCFF2=True)
  197. file.seek(0)
  198. cff.decompile(file, otFont, isCFF2=True)
  199. def convertCFFToCFF2(font):
  200. cff = font["CFF "].cff
  201. del font["CFF "]
  202. _convertCFFToCFF2(cff, font)
  203. table = font["CFF2"] = newTable("CFF2")
  204. table.cff = cff
  205. def main(args=None):
  206. """Convert CFF OTF font to CFF2 OTF font"""
  207. if args is None:
  208. import sys
  209. args = sys.argv[1:]
  210. import argparse
  211. parser = argparse.ArgumentParser(
  212. "fonttools cffLib.CFFToCFF2",
  213. description="Upgrade a CFF font to CFF2.",
  214. )
  215. parser.add_argument(
  216. "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
  217. )
  218. parser.add_argument(
  219. "-o",
  220. "--output",
  221. metavar="OUTPUT.ttf",
  222. default=None,
  223. help="Output instance OTF file (default: INPUT-CFF2.ttf).",
  224. )
  225. parser.add_argument(
  226. "--no-recalc-timestamp",
  227. dest="recalc_timestamp",
  228. action="store_false",
  229. help="Don't set the output font's timestamp to the current time.",
  230. )
  231. loggingGroup = parser.add_mutually_exclusive_group(required=False)
  232. loggingGroup.add_argument(
  233. "-v", "--verbose", action="store_true", help="Run more verbosely."
  234. )
  235. loggingGroup.add_argument(
  236. "-q", "--quiet", action="store_true", help="Turn verbosity off."
  237. )
  238. options = parser.parse_args(args)
  239. from fontTools import configLogger
  240. configLogger(
  241. level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
  242. )
  243. import os
  244. infile = options.input
  245. if not os.path.isfile(infile):
  246. parser.error("No such file '{}'".format(infile))
  247. outfile = (
  248. makeOutputFileName(infile, overWrite=True, suffix="-CFF2")
  249. if not options.output
  250. else options.output
  251. )
  252. font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
  253. convertCFFToCFF2(font)
  254. log.info(
  255. "Saving %s",
  256. outfile,
  257. )
  258. font.save(outfile)
  259. if __name__ == "__main__":
  260. import sys
  261. sys.exit(main(sys.argv[1:]))