CFF2ToCFF.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  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, isCFF2=True)
  25. for item in cff.topDictIndex:
  26. # Iterate over, such that all are decompiled
  27. topDictData.append(item)
  28. cff.topDictIndex = topDictData
  29. topDict = topDictData[0]
  30. if hasattr(topDict, "VarStore"):
  31. raise ValueError("Variable CFF2 font cannot be converted to CFF format.")
  32. opOrder = buildOrder(topDictOperators)
  33. topDict.order = opOrder
  34. for key in topDict.rawDict.keys():
  35. if key not in opOrder:
  36. del topDict.rawDict[key]
  37. if hasattr(topDict, key):
  38. delattr(topDict, key)
  39. fdArray = topDict.FDArray
  40. charStrings = topDict.CharStrings
  41. defaults = buildDefaults(privateDictOperators)
  42. order = buildOrder(privateDictOperators)
  43. for fd in fdArray:
  44. fd.setCFF2(False)
  45. privateDict = fd.Private
  46. privateDict.order = order
  47. for key in order:
  48. if key not in privateDict.rawDict and key in defaults:
  49. privateDict.rawDict[key] = defaults[key]
  50. for key in privateDict.rawDict.keys():
  51. if key not in order:
  52. del privateDict.rawDict[key]
  53. if hasattr(privateDict, key):
  54. delattr(privateDict, key)
  55. for cs in charStrings.values():
  56. cs.decompile()
  57. cs.program.append("endchar")
  58. for subrSets in [cff.GlobalSubrs] + [
  59. getattr(fd.Private, "Subrs", []) for fd in fdArray
  60. ]:
  61. for cs in subrSets:
  62. cs.program.append("return")
  63. # Add (optimal) width to CharStrings that need it.
  64. widths = defaultdict(list)
  65. metrics = otFont["hmtx"].metrics
  66. for glyphName in charStrings.keys():
  67. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  68. if fdIndex == None:
  69. fdIndex = 0
  70. widths[fdIndex].append(metrics[glyphName][0])
  71. for fdIndex, widthList in widths.items():
  72. bestDefault, bestNominal = optimizeWidths(widthList)
  73. private = fdArray[fdIndex].Private
  74. private.defaultWidthX = bestDefault
  75. private.nominalWidthX = bestNominal
  76. for glyphName in charStrings.keys():
  77. cs, fdIndex = charStrings.getItemAndSelector(glyphName)
  78. if fdIndex == None:
  79. fdIndex = 0
  80. private = fdArray[fdIndex].Private
  81. width = metrics[glyphName][0]
  82. if width != private.defaultWidthX:
  83. cs.program.insert(0, width - private.nominalWidthX)
  84. def convertCFF2ToCFF(font, *, updatePostTable=True):
  85. cff = font["CFF2"].cff
  86. _convertCFF2ToCFF(cff, font)
  87. del font["CFF2"]
  88. table = font["CFF "] = newTable("CFF ")
  89. table.cff = cff
  90. if updatePostTable and "post" in font:
  91. # Only version supported for fonts with CFF table is 0x00030000 not 0x20000
  92. post = font["post"]
  93. if post.formatType == 2.0:
  94. post.formatType = 3.0
  95. def main(args=None):
  96. """Convert CFF OTF font to CFF2 OTF font"""
  97. if args is None:
  98. import sys
  99. args = sys.argv[1:]
  100. import argparse
  101. parser = argparse.ArgumentParser(
  102. "fonttools cffLib.CFFToCFF2",
  103. description="Upgrade a CFF font to CFF2.",
  104. )
  105. parser.add_argument(
  106. "input", metavar="INPUT.ttf", help="Input OTF file with CFF table."
  107. )
  108. parser.add_argument(
  109. "-o",
  110. "--output",
  111. metavar="OUTPUT.ttf",
  112. default=None,
  113. help="Output instance OTF file (default: INPUT-CFF2.ttf).",
  114. )
  115. parser.add_argument(
  116. "--no-recalc-timestamp",
  117. dest="recalc_timestamp",
  118. action="store_false",
  119. help="Don't set the output font's timestamp to the current time.",
  120. )
  121. loggingGroup = parser.add_mutually_exclusive_group(required=False)
  122. loggingGroup.add_argument(
  123. "-v", "--verbose", action="store_true", help="Run more verbosely."
  124. )
  125. loggingGroup.add_argument(
  126. "-q", "--quiet", action="store_true", help="Turn verbosity off."
  127. )
  128. options = parser.parse_args(args)
  129. from fontTools import configLogger
  130. configLogger(
  131. level=("DEBUG" if options.verbose else "ERROR" if options.quiet else "INFO")
  132. )
  133. import os
  134. infile = options.input
  135. if not os.path.isfile(infile):
  136. parser.error("No such file '{}'".format(infile))
  137. outfile = (
  138. makeOutputFileName(infile, overWrite=True, suffix="-CFF")
  139. if not options.output
  140. else options.output
  141. )
  142. font = TTFont(infile, recalcTimestamp=options.recalc_timestamp, recalcBBoxes=False)
  143. convertCFF2ToCFF(font)
  144. log.info(
  145. "Saving %s",
  146. outfile,
  147. )
  148. font.save(outfile)
  149. if __name__ == "__main__":
  150. import sys
  151. sys.exit(main(sys.argv[1:]))