CFF2ToCFF.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203
  1. """CFF2 to CFF converter."""
  2. from fontTools.ttLib import TTFont, newTable
  3. from fontTools.misc.cliTools import makeOutputFileName
  4. from fontTools.cffLib import (
  5. TopDictIndex,
  6. buildOrder,
  7. buildDefaults,
  8. topDictOperators,
  9. privateDictOperators,
  10. )
  11. from .width import optimizeWidths
  12. from collections import defaultdict
  13. import logging
  14. __all__ = ["convertCFF2ToCFF", "main"]
  15. log = logging.getLogger("fontTools.cffLib")
  16. def _convertCFF2ToCFF(cff, otFont):
  17. """Converts this object from CFF2 format to CFF format. This conversion
  18. is done 'in-place'. The conversion cannot be reversed.
  19. The CFF2 font cannot be variable. (TODO Accept those and convert to the
  20. default instance?)
  21. This assumes a decompiled CFF table. (i.e. that the object has been
  22. filled via :meth:`decompile` and e.g. not loaded from XML.)"""
  23. cff.major = 1
  24. topDictData = TopDictIndex(None)
  25. for item in cff.topDictIndex:
  26. # Iterate over, such that all are decompiled
  27. item.cff2GetGlyphOrder = None
  28. topDictData.append(item)
  29. cff.topDictIndex = topDictData
  30. topDict = topDictData[0]
  31. if hasattr(topDict, "VarStore"):
  32. raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
  33. opOrder = buildOrder(topDictOperators)
  34. topDict.order = opOrder
  35. for key in topDict.rawDict.keys():
  36. if key not in opOrder:
  37. del topDict.rawDict[key]
  38. if hasattr(topDict, key):
  39. delattr(topDict, key)
  40. fdArray = topDict.FDArray
  41. charStrings = topDict.CharStrings
  42. defaults = buildDefaults(privateDictOperators)
  43. order = buildOrder(privateDictOperators)
  44. for fd in fdArray:
  45. fd.setCFF2(False)
  46. privateDict = fd.Private
  47. privateDict.order = order
  48. for key in order:
  49. if key not in privateDict.rawDict and key in defaults:
  50. privateDict.rawDict[key] = defaults[key]
  51. for key in privateDict.rawDict.keys():
  52. if key not in order:
  53. del privateDict.rawDict[key]
  54. if hasattr(privateDict, key):
  55. delattr(privateDict, key)
  56. for cs in charStrings.values():
  57. cs.decompile()
  58. cs.program.append("endchar")
  59. for subrSets in [cff.GlobalSubrs] + [
  60. getattr(fd.Private, "Subrs", []) for fd in fdArray
  61. ]:
  62. for cs in subrSets:
  63. cs.program.append("return")
  64. # Add (optimal) width to CharStrings that need it.
  65. widths = defaultdict(list)
  66. metrics = otFont["hmtx"].metrics
  67. for glyphName in charStrings.keys():
  68. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  69. if fdIndex == None:
  70. fdIndex = 0
  71. widths[fdIndex].append(metrics[glyphName][0])
  72. for fdIndex, widthList in widths.items():
  73. bestDefault, bestNominal = optimizeWidths(widthList)
  74. private = fdArray[fdIndex].Private
  75. private.defaultWidthX = bestDefault
  76. private.nominalWidthX = bestNominal
  77. for glyphName in charStrings.keys():
  78. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  79. if fdIndex == None:
  80. fdIndex = 0
  81. private = fdArray[fdIndex].Private
  82. width = metrics[glyphName][0]
  83. if width != private.defaultWidthX:
  84. cs.program.insert(0, width - private.nominalWidthX)
  85. mapping = {
  86. name: ("cid" + str(n) if n else ".notdef")
  87. for n, name in enumerate(topDict.charset)
  88. }
  89. topDict.charset = [
  90. "cid" + str(n) if n else ".notdef" for n in range(len(topDict.charset))
  91. ]
  92. charStrings.charStrings = {
  93. mapping[name]: v for name, v in charStrings.charStrings.items()
  94. }
  95. # I'm not sure why the following is *not* necessary. And it breaks
  96. # the output if I add it.
  97. # topDict.ROS = ("Adobe", "Identity", 0)
  98. def convertCFF2ToCFF(font, *, updatePostTable=True):
  99. cff = font["CFF2"].cff
  100. _convertCFF2ToCFF(cff, font)
  101. del font["CFF2"]
  102. table = font["CFF "] = newTable("CFF ")
  103. table.cff = cff
  104. if updatePostTable and "post" in font:
  105. # Only version supported for fonts with CFF table is 0x00030000 not 0x20000
  106. post = font["post"]
  107. if post.formatType == 2.0:
  108. post.formatType = 3.0
  109. def main(args=None):
  110. """Convert CFF OTF font to CFF2 OTF font"""
  111. if args is None:
  112. import sys
  113. args = sys.argv[1:]
  114. import argparse
  115. parser = argparse.ArgumentParser(
  116. "fonttools cffLib.CFFToCFF2",
  117. description="Upgrade a CFF font to CFF2.",
  118. )
  119. parser.add_argument(
  120. "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
  121. )
  122. parser.add_argument(
  123. "-o",
  124. "--output",
  125. metavar="OUTPUT.ttf",
  126. default=None,
  127. help="Output instance OTF file (default: INPUT-CFF2.ttf).",
  128. )
  129. parser.add_argument(
  130. "--no-recalc-timestamp",
  131. dest="recalc_timestamp",
  132. action="store_false",
  133. help="Don't set the output font's timestamp to the current time.",
  134. )
  135. loggingGroup = parser.add_mutually_exclusive_group(required=False)
  136. loggingGroup.add_argument(
  137. "-v", "--verbose", action="store_true", help="Run more verbosely."
  138. )
  139. loggingGroup.add_argument(
  140. "-q", "--quiet", action="store_true", help="Turn verbosity off."
  141. )
  142. options = parser.parse_args(args)
  143. from fontTools import configLogger
  144. configLogger(
  145. level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
  146. )
  147. import os
  148. infile = options.input
  149. if not os.path.isfile(infile):
  150. parser.error("No such file '{}'".format(infile))
  151. outfile = (
  152. makeOutputFileName(infile, overWrite=True, suffix="-CFF")
  153. if not options.output
  154. else options.output
  155. )
  156. font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
  157. convertCFF2ToCFF(font)
  158. log.info(
  159. "Saving %s",
  160. outfile,
  161. )
  162. font.save(outfile)
  163. if __name__ == "__main__":
  164. import sys
  165. sys.exit(main(sys.argv[1:]))