CFF2ToCFF.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258
  1. """CFF2 to CFF converter."""
  2. from fontTools.ttLib import TTFont, newTable
  3. from fontTools.misc.cliTools import makeOutputFileName
  4. from fontTools.misc.psCharStrings import T2StackUseExtractor
  5. from fontTools.cffLib import (
  6. TopDictIndex,
  7. buildOrder,
  8. buildDefaults,
  9. topDictOperators,
  10. privateDictOperators,
  11. FDSelect,
  12. )
  13. from .transforms import desubroutinizeCharString
  14. from .specializer import specializeProgram
  15. from .width import optimizeWidths
  16. from collections import defaultdict
  17. import logging
  18. __all__ = ["convertCFF2ToCFF", "main"]
  19. log = logging.getLogger("fontTools.cffLib")
  20. def _convertCFF2ToCFF(cff, otFont):
  21. """Converts this object from CFF2 format to CFF format. This conversion
  22. is done 'in-place'. The conversion cannot be reversed.
  23. The CFF2 font cannot be variable. (TODO Accept those and convert to the
  24. default instance?)
  25. This assumes a decompiled CFF2 table. (i.e. that the object has been
  26. filled via :meth:`decompile` and e.g. not loaded from XML.)"""
  27. cff.major = 1
  28. topDictData = TopDictIndex(None)
  29. for item in cff.topDictIndex:
  30. # Iterate over, such that all are decompiled
  31. item.cff2GetGlyphOrder = None
  32. topDictData.append(item)
  33. cff.topDictIndex = topDictData
  34. topDict = topDictData[0]
  35. if hasattr(topDict, "VarStore"):
  36. raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
  37. opOrder = buildOrder(topDictOperators)
  38. topDict.order = opOrder
  39. for key in topDict.rawDict.keys():
  40. if key not in opOrder:
  41. del topDict.rawDict[key]
  42. if hasattr(topDict, key):
  43. delattr(topDict, key)
  44. charStrings = topDict.CharStrings
  45. fdArray = topDict.FDArray
  46. if not hasattr(topDict, "FDSelect"):
  47. # FDSelect is optional in CFF2, but required in CFF.
  48. fdSelect = topDict.FDSelect = FDSelect()
  49. fdSelect.gidArray = [0] * len(charStrings.charStrings)
  50. defaults = buildDefaults(privateDictOperators)
  51. order = buildOrder(privateDictOperators)
  52. for fd in fdArray:
  53. fd.setCFF2(False)
  54. privateDict = fd.Private
  55. privateDict.order = order
  56. for key in order:
  57. if key not in privateDict.rawDict and key in defaults:
  58. privateDict.rawDict[key] = defaults[key]
  59. for key in privateDict.rawDict.keys():
  60. if key not in order:
  61. del privateDict.rawDict[key]
  62. if hasattr(privateDict, key):
  63. delattr(privateDict, key)
  64. # Add ending operators
  65. for cs in charStrings.values():
  66. cs.decompile()
  67. cs.program.append("endchar")
  68. for subrSets in [cff.GlobalSubrs] + [
  69. getattr(fd.Private, "Subrs", []) for fd in fdArray
  70. ]:
  71. for cs in subrSets:
  72. cs.program.append("return")
  73. # Add (optimal) width to CharStrings that need it.
  74. widths = defaultdict(list)
  75. metrics = otFont["hmtx"].metrics
  76. for glyphName in charStrings.keys():
  77. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  78. if fdIndex == None:
  79. fdIndex = 0
  80. widths[fdIndex].append(metrics[glyphName][0])
  81. for fdIndex, widthList in widths.items():
  82. bestDefault, bestNominal = optimizeWidths(widthList)
  83. private = fdArray[fdIndex].Private
  84. private.defaultWidthX = bestDefault
  85. private.nominalWidthX = bestNominal
  86. for glyphName in charStrings.keys():
  87. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  88. if fdIndex == None:
  89. fdIndex = 0
  90. private = fdArray[fdIndex].Private
  91. width = metrics[glyphName][0]
  92. if width != private.defaultWidthX:
  93. cs.program.insert(0, width - private.nominalWidthX)
  94. # Handle stack use since stack-depth is lower in CFF than in CFF2.
  95. for glyphName in charStrings.keys():
  96. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  97. if fdIndex is None:
  98. fdIndex = 0
  99. private = fdArray[fdIndex].Private
  100. extractor = T2StackUseExtractor(
  101. getattr(private, "Subrs", []), cff.GlobalSubrs, private=private
  102. )
  103. stackUse = extractor.execute(cs)
  104. if stackUse > 48: # CFF stack depth is 48
  105. desubroutinizeCharString(cs)
  106. cs.program = specializeProgram(cs.program)
  107. # Unused subroutines are still in CFF2 (ie. lacking 'return' operator)
  108. # because they were not decompiled when we added the 'return'.
  109. # Moreover, some used subroutines may have become unused after the
  110. # stack-use fixup. So we remove all unused subroutines now.
  111. cff.remove_unused_subroutines()
  112. mapping = {
  113. name: ("cid" + str(n).zfill(5) if n else ".notdef")
  114. for n, name in enumerate(topDict.charset)
  115. }
  116. topDict.charset = [
  117. "cid" + str(n).zfill(5) if n else ".notdef" for n in range(len(topDict.charset))
  118. ]
  119. charStrings.charStrings = {
  120. mapping[name]: v for name, v in charStrings.charStrings.items()
  121. }
  122. topDict.ROS = ("Adobe", "Identity", 0)
  123. def convertCFF2ToCFF(font, *, updatePostTable=True):
  124. if "CFF2" not in font:
  125. raise ValueError("Input font does not contain a CFF2 table.")
  126. cff = font["CFF2"].cff
  127. _convertCFF2ToCFF(cff, font)
  128. del font["CFF2"]
  129. table = font["CFF "] = newTable("CFF ")
  130. table.cff = cff
  131. if updatePostTable and "post" in font:
  132. # Only version supported for fonts with CFF table is 0x00030000 not 0x20000
  133. post = font["post"]
  134. if post.formatType == 2.0:
  135. post.formatType = 3.0
  136. def main(args=None):
  137. """Convert CFF2 OTF font to CFF OTF font"""
  138. if args is None:
  139. import sys
  140. args = sys.argv[1:]
  141. import argparse
  142. parser = argparse.ArgumentParser(
  143. "fonttools cffLib.CFF2ToCFF",
  144. description="Convert a non-variable CFF2 font to CFF.",
  145. )
  146. parser.add_argument(
  147. "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
  148. )
  149. parser.add_argument(
  150. "-o",
  151. "--output",
  152. metavar="OUTPUT.ttf",
  153. default=None,
  154. help="Output instance OTF file (default: INPUT-CFF2.ttf).",
  155. )
  156. parser.add_argument(
  157. "--no-recalc-timestamp",
  158. dest="recalc_timestamp",
  159. action="store_false",
  160. help="Don't set the output font's timestamp to the current time.",
  161. )
  162. parser.add_argument(
  163. "--remove-overlaps",
  164. action="store_true",
  165. help="Merge overlapping contours and components. Requires skia-pathops",
  166. )
  167. parser.add_argument(
  168. "--ignore-overlap-errors",
  169. action="store_true",
  170. help="Don't crash if the remove-overlaps operation fails for some glyphs.",
  171. )
  172. loggingGroup = parser.add_mutually_exclusive_group(required=False)
  173. loggingGroup.add_argument(
  174. "-v", "--verbose", action="store_true", help="Run more verbosely."
  175. )
  176. loggingGroup.add_argument(
  177. "-q", "--quiet", action="store_true", help="Turn verbosity off."
  178. )
  179. options = parser.parse_args(args)
  180. from fontTools import configLogger
  181. configLogger(
  182. level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
  183. )
  184. import os
  185. infile = options.input
  186. if not os.path.isfile(infile):
  187. parser.error("No such file '{}'".format(infile))
  188. outfile = (
  189. makeOutputFileName(infile, overWrite=True, suffix="-CFF")
  190. if not options.output
  191. else options.output
  192. )
  193. font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
  194. convertCFF2ToCFF(font)
  195. if options.remove_overlaps:
  196. from fontTools.ttLib.removeOverlaps import removeOverlaps
  197. from io import BytesIO
  198. log.debug("Removing overlaps")
  199. stream = BytesIO()
  200. font.save(stream)
  201. stream.seek(0)
  202. font = TTFont(stream, recalcTimestamp=False, recalcBBoxes=False)
  203. removeOverlaps(
  204. font,
  205. ignoreErrors=options.ignore_overlap_errors,
  206. )
  207. log.info(
  208. "Saving %s",
  209. outfile,
  210. )
  211. font.save(outfile)
  212. if __name__ == "__main__":
  213. import sys
  214. sys.exit(main(sys.argv[1:]))