| 10 | | def makeOutlineOTF(font, path, glyphOrder=None): |
|---|
| 11 | | otf = TTFont(sfntVersion="OTTO") |
|---|
| 12 | | # populate default values |
|---|
| 13 | | _setupTable_head(otf, font) |
|---|
| 14 | | _setupTable_hhea(otf, font) |
|---|
| 15 | | _setupTable_hmtx(otf, font) |
|---|
| 16 | | _setupTable_name(otf, font) |
|---|
| 17 | | _setupTable_maxp(otf, font) |
|---|
| 18 | | _setupTable_cmap(otf, font) |
|---|
| 19 | | _setupTable_OS2(otf, font) |
|---|
| 20 | | _setupTable_post(otf, font) |
|---|
| 21 | | _setupTable_CFF(otf, font) |
|---|
| 22 | | # populate the outlines |
|---|
| 23 | | if glyphOrder is None: |
|---|
| 24 | | glyphOrder = sorted(font.keys()) |
|---|
| 25 | | _populate_glyphs(otf, font, glyphOrder) |
|---|
| 26 | | # write the file |
|---|
| 27 | | otf.save(path) |
|---|
| 28 | | # discard the object |
|---|
| 29 | | otf.close() |
|---|
| 30 | | |
|---|
| 31 | | def _setupTable_head(otf, font): |
|---|
| 32 | | otf["head"] = head = newTable("head") |
|---|
| 33 | | head.checkSumAdjustment = 0 # XXX this is a guess |
|---|
| 34 | | head.tableVersion = 1.0 |
|---|
| 35 | | head.fontRevision = 1.0 |
|---|
| 36 | | head.magicNumber = 0x5F0F3CF5 |
|---|
| 37 | | head.flags = 0 # XXX this is a guess |
|---|
| 38 | | head.unitsPerEm = int(font.info.unitsPerEm) |
|---|
| 39 | | rightNow = long(time.time() - time.timezone) |
|---|
| 40 | | head.created = rightNow |
|---|
| 41 | | head.modified = rightNow |
|---|
| 42 | | head.xMin = 0 |
|---|
| 43 | | head.yMin = 0 |
|---|
| 44 | | head.xMax = 0 |
|---|
| 45 | | head.yMax = 0 |
|---|
| 46 | | head.macStyle = 0 # XXX this is a guess |
|---|
| 47 | | head.lowestRecPPEM = 3 # XXX FontValidator describes this as "unreasonably small" |
|---|
| 48 | | head.fontDirectionHint = 2 # XXX this is a guess |
|---|
| 49 | | head.indexToLocFormat = 0 # XXX this is a guess |
|---|
| 50 | | head.glyphDataFormat = 0 |
|---|
| 51 | | |
|---|
| 52 | | def _setupTable_name(otf, font): |
|---|
| 53 | | # this table must exist, but it can be empty. |
|---|
| 54 | | otf["name"] = newTable("name") |
|---|
| 55 | | |
|---|
| 56 | | def _setupTable_maxp(otf, font): |
|---|
| 57 | | otf["maxp"] = maxp = newTable("maxp") |
|---|
| 58 | | maxp.tableVersion = 0x00005000 |
|---|
| 59 | | |
|---|
| 60 | | def _setupTable_cmap(otf, font): |
|---|
| 61 | | # XXX is this necessary for the outline source? |
|---|
| 62 | | # XXX need to make sure that these are the proper tables to write |
|---|
| 63 | | from fontTools.ttLib.tables._c_m_a_p import cmap_format_4, cmap_format_6 |
|---|
| 64 | | cmap4_0_3 = cmap_format_4(4) |
|---|
| 65 | | cmap4_0_3.platformID = 0 |
|---|
| 66 | | cmap4_0_3.platEncID = 3 |
|---|
| 67 | | cmap4_0_3.language = 0 |
|---|
| 68 | | cmap4_0_3.cmap = {} |
|---|
| 69 | | |
|---|
| 70 | | cmap6_1_0 = cmap_format_4(6) |
|---|
| 71 | | cmap6_1_0.platformID = 1 |
|---|
| 72 | | cmap6_1_0.platEncID = 0 |
|---|
| 73 | | cmap6_1_0.language = 0 |
|---|
| 74 | | cmap6_1_0.cmap = {} |
|---|
| 75 | | |
|---|
| 76 | | cmap4_3_1 = cmap_format_4(4) |
|---|
| 77 | | cmap4_3_1.platformID = 3 |
|---|
| 78 | | cmap4_3_1.platEncID = 1 |
|---|
| 79 | | cmap4_3_1.language = 0 |
|---|
| 80 | | cmap4_3_1.cmap = {} |
|---|
| 81 | | |
|---|
| 82 | | otf["cmap"] = cmap = newTable("cmap") |
|---|
| 83 | | cmap.tableVersion = 0 |
|---|
| 84 | | cmap.tables = [cmap4_0_3]#, cmap6_1_0, cmap4_3_1] # XXX more tables? one of this is preventing compile |
|---|
| 85 | | |
|---|
| 86 | | def _setupTable_OS2(otf, font): |
|---|
| 87 | | otf["OS/2"] = os2 = newTable("OS/2") |
|---|
| 88 | | os2.version = 0x0003 # XXX has this been bumped up? |
|---|
| 89 | | os2.xAvgCharWidth = int(round(font.info.unitsPerEm * 0.5)) # XXX calculate? |
|---|
| 90 | | os2.usWeightClass = 400 |
|---|
| 91 | | os2.usWidthClass = 5 |
|---|
| 92 | | os2.fsType = 0x0000 |
|---|
| 93 | | superAndSubscriptSize = int(round(font.info.ascender * 0.85)) # XXX what should the default be? |
|---|
| 94 | | os2.ySubscriptXSize = superAndSubscriptSize |
|---|
| 95 | | os2.ySubscriptYSize = superAndSubscriptSize |
|---|
| 96 | | os2.ySubscriptXOffset = 0 # XXX what should the default be? |
|---|
| 97 | | os2.ySubscriptYOffset = int(round(font.info.descender * 0.5)) # XXX what should the default be? |
|---|
| 98 | | os2.ySuperscriptXSize = superAndSubscriptSize |
|---|
| 99 | | os2.ySuperscriptYSize = superAndSubscriptSize |
|---|
| 100 | | os2.ySuperscriptXOffset = 0 # XXX what should the default be? |
|---|
| 101 | | os2.ySuperscriptYOffset = font.info.ascender - superAndSubscriptSize # XXX what should the default be? |
|---|
| 102 | | os2.yStrikeoutSize = int(round(font.info.unitsPerEm * 0.05)) # XXX what should the default be? |
|---|
| 103 | | os2.yStrikeoutPosition = int(round(font.info.unitsPerEm * .23)) # XXX what should the default be? |
|---|
| 104 | | os2.sFamilyClass = 0 |
|---|
| 105 | | panose = Panose() |
|---|
| 106 | | panose.bFamilyType = 2 |
|---|
| 107 | | panose.bSerifStyle = 0 |
|---|
| 108 | | panose.bWeight = 0 |
|---|
| 109 | | panose.bProportion = 0 |
|---|
| 110 | | panose.bContrast = 0 |
|---|
| 111 | | panose.bStrokeVariation = 0 |
|---|
| 112 | | panose.bArmStyle = 0 |
|---|
| 113 | | panose.bLetterForm = 0 |
|---|
| 114 | | panose.bMidline = 0 |
|---|
| 115 | | panose.bXHeight = 0 |
|---|
| 116 | | os2.panose = panose |
|---|
| 117 | | os2.ulUnicodeRange1 = 0 |
|---|
| 118 | | os2.ulUnicodeRange2 = 0 |
|---|
| 119 | | os2.ulUnicodeRange3 = 0 |
|---|
| 120 | | os2.ulUnicodeRange4 = 0 |
|---|
| 121 | | os2.achVendID = "None" # XXX get vendor code from font |
|---|
| 122 | | os2.fsSelection = 64 # XXX this is a guess |
|---|
| 123 | | os2.fsFirstCharIndex = 0 # usFirstCharIndex |
|---|
| 124 | | os2.fsLastCharIndex = 0 # usLastCharIndex |
|---|
| 125 | | os2.sTypoAscender = font.info.ascender |
|---|
| 126 | | os2.sTypoDescender = -font.info.descender |
|---|
| 127 | | os2.sTypoLineGap = 0 |
|---|
| 128 | | os2.usWinAscent = font.info.ascender |
|---|
| 129 | | os2.usWinDescent = font.info.descender |
|---|
| 130 | | os2.ulCodePageRange1 = 0 |
|---|
| 131 | | os2.ulCodePageRange2 = 0 |
|---|
| 132 | | os2.sxHeight = int(round(font.info.ascender * 0.5)) |
|---|
| 133 | | os2.sCapHeight = font.info.ascender |
|---|
| 134 | | os2.usDefaultChar = 0 |
|---|
| 135 | | os2.usBreakChar = 1 |
|---|
| 136 | | os2.usMaxContex = 0 # usMaxContext |
|---|
| 137 | | |
|---|
| 138 | | def _setupTable_hmtx(otf, font): |
|---|
| 139 | | # this is required, but it can be empty |
|---|
| 140 | | otf["hmtx"] = hmtx = newTable("hmtx") |
|---|
| 141 | | hmtx.metrics = {} |
|---|
| 142 | | |
|---|
| 143 | | def _setupTable_hhea(otf, font): |
|---|
| 144 | | otf["hhea"] = hhea = newTable("hhea") |
|---|
| 145 | | hhea.tableVersion = 1.0 |
|---|
| 146 | | hhea.ascent = int(font.info.ascender) |
|---|
| 147 | | hhea.descent = -int(font.info.descender) |
|---|
| 148 | | hhea.lineGap = 0 |
|---|
| 149 | | hhea.advanceWidthMax = 0 |
|---|
| 150 | | hhea.minLeftSideBearing = 0 |
|---|
| 151 | | hhea.minRightSideBearing = 0 |
|---|
| 152 | | hhea.xMaxExtent = 0 |
|---|
| 153 | | hhea.caretSlopeRise = 1 |
|---|
| 154 | | hhea.caretSlopeRun = 0 |
|---|
| 155 | | hhea.caretOffset = 0 # XXX this is a guess |
|---|
| 156 | | hhea.reserved0 = 0 |
|---|
| 157 | | hhea.reserved1 = 0 |
|---|
| 158 | | hhea.reserved2 = 0 |
|---|
| 159 | | hhea.reserved3 = 0 |
|---|
| 160 | | hhea.metricDataFormat = 0 |
|---|
| 161 | | hhea.numberOfHMetrics = 0 |
|---|
| 162 | | |
|---|
| 163 | | def _setupTable_post(otf, font): |
|---|
| 164 | | otf["post"] = post = newTable("post") |
|---|
| 165 | | post.formatType = 3.0 |
|---|
| 166 | | italicAngle = font.info.italicAngle |
|---|
| 167 | | if italicAngle is None: |
|---|
| 168 | | italicAngle = 0 |
|---|
| 169 | | post.italicAngle = italicAngle |
|---|
| 170 | | post.underlinePosition = -int(round(font.info.descender * 0.3)) # XXX this is a guess |
|---|
| 171 | | post.underlineThickness = int(round(font.info.unitsPerEm * .05)) # XXX this is a guess |
|---|
| 172 | | post.isFixedPitch = 0 |
|---|
| 173 | | post.minMemType42 = 0 # XXX this is a guess |
|---|
| 174 | | post.maxMemType42 = 0 # XXX this is a guess |
|---|
| 175 | | post.minMemType1 = 0 # XXX this is a guess |
|---|
| 176 | | post.maxMemType1 = 0 # XXX this is a guess |
|---|
| 177 | | |
|---|
| 178 | | def _setupTable_CFF(otf, font): |
|---|
| 179 | | otf["CFF "] = cff = newTable("CFF ") |
|---|
| 180 | | cff = cff.cff |
|---|
| 181 | | cff.major = 1 |
|---|
| 182 | | cff.minor = 0 |
|---|
| 183 | | cff.hdrSize = 4 |
|---|
| 184 | | cff.offSize = 4 |
|---|
| 185 | | cff.fontNames = [] # XXX need a real font name! |
|---|
| 186 | | strings = IndexedStrings() |
|---|
| 187 | | cff.strings = strings |
|---|
| 188 | | private = PrivateDict(strings=strings) |
|---|
| 189 | | private.rawDict.update(private.defaults) |
|---|
| 190 | | globalSubrs = GlobalSubrsIndex(private=private) |
|---|
| 191 | | topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) |
|---|
| 192 | | topDict.Private = private |
|---|
| 193 | | topDict.CharStrings = CharStrings(file=None, charset=None, |
|---|
| 194 | | globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None) |
|---|
| 195 | | topDict.CharStrings.charStringsAreIndexed = True |
|---|
| 196 | | topDict.charset = [] |
|---|
| 197 | | topDict.CharStrings.charStringsIndex = SubrsIndex(private=private, globalSubrs=globalSubrs) |
|---|
| 198 | | cff.topDictIndex = topDictIndex = TopDictIndex() |
|---|
| 199 | | topDictIndex.append(topDict) |
|---|
| 200 | | topDictIndex.strings = strings |
|---|
| 201 | | cff.GlobalSubrs = globalSubrs |
|---|
| 202 | | # populate data from the font. |
|---|
| 203 | | # this is required for a basic CFF table. |
|---|
| 204 | | info = font.info |
|---|
| 205 | | cff.fontNames.append(info.fontName) |
|---|
| 206 | | topDict = cff.topDictIndex[0] |
|---|
| 207 | | if hasattr(info, "fullName"): |
|---|
| 208 | | topDict.FullName = makePSName(font) # XXX this should probably draw from a real value |
|---|
| 209 | | if hasattr(info, "familyName"): |
|---|
| 210 | | topDict.FamilyName = info.familyName |
|---|
| 211 | | if hasattr(info, "styleName"): |
|---|
| 212 | | topDict.Weight = info.styleName |
|---|
| 213 | | if hasattr(info, "fontName"): |
|---|
| 214 | | topDict.FontName = makePSName(font) # XXX this should probably draw from a real value |
|---|
| 215 | | |
|---|
| 216 | | def _populate_glyphs(otf, font, glyphOrder): |
|---|
| 217 | | mapping, widths, lefts, rights, fontBBox = _populate_CFF(otf, font, glyphOrder) |
|---|
| 218 | | glyphCount = len(widths) |
|---|
| 219 | | # populate the cmap table |
|---|
| 220 | | # XXX do we need to do this for the outline source? |
|---|
| 221 | | cmap = otf["cmap"] |
|---|
| 222 | | for unicodeValue, glyphName in mapping.items(): |
|---|
| 223 | | for table in cmap.tables: |
|---|
| 224 | | pID = table.platformID |
|---|
| 225 | | eID = table.platEncID |
|---|
| 226 | | # XXX are these the only valid tables? |
|---|
| 227 | | if (pID, eID) in [(0, 3), (3, 1), (0, 4), (3, 10)]: |
|---|
| 228 | | table.cmap[unicodeValue] = glyphName |
|---|
| 229 | | # populate the hmtx table |
|---|
| 230 | | hmtx = otf["hmtx"] |
|---|
| 231 | | for glyphName, width in widths.items(): |
|---|
| 232 | | left = lefts[glyphName] |
|---|
| 233 | | right = rights[glyphName] |
|---|
| 234 | | hmtx[glyphName] = (width, left) |
|---|
| 235 | | # update the OS/2 table |
|---|
| 236 | | os2 = otf["OS/2"] |
|---|
| 237 | | # the OS/2 doc states: |
|---|
| 238 | | # """ |
|---|
| 239 | | # The value for xAvgCharWidth is calculated by obtaining the arithmetic |
|---|
| 240 | | # average of the width of all non-zero width glyphs in the font. |
|---|
| 241 | | # """ |
|---|
| 242 | | # "non-zero width glyphs"? does that mean that glyphs with |
|---|
| 243 | | # a width of zero should not be counted? that doesn't seem right. |
|---|
| 244 | | avgWidth = int(round(sum(widths.values()) / glyphCount)) |
|---|
| 245 | | os2.xAvgCharWidth = avgWidth |
|---|
| 246 | | minIndex = 32 # it is safe to asume that this is present since a space glyph is inserted |
|---|
| 247 | | maxIndex = max(mapping.keys()) |
|---|
| 248 | | if maxIndex >= 0xFFFF: |
|---|
| 249 | | # see OS/2 docs |
|---|
| 250 | | # need to find a value lower than 0xFFFF. |
|---|
| 251 | | # shouldn't get to this point though. |
|---|
| 252 | | raise NotImplementedError |
|---|
| 253 | | os2.fsFirstCharIndex = minIndex |
|---|
| 254 | | os2.fsLastCharIndex = maxIndex |
|---|
| 255 | | os2.usBreakChar = 32 |
|---|
| 256 | | os2.usDefaultChar = 32 |
|---|
| 257 | | # update the hhea table |
|---|
| 258 | | hhea = otf["hhea"] |
|---|
| 259 | | hhea.advanceWidthMax = max(widths.values()) |
|---|
| 260 | | hhea.minLeftSideBearing = min(lefts.values()) |
|---|
| 261 | | rightSidebearing = [widths[glyphName] - rights[glyphName] for glyphName in widths.keys()] |
|---|
| 262 | | hhea.minRightSideBearing = min(rightSidebearing) |
|---|
| 263 | | hhea.xMaxExtent = max(rights.values()) # XXX the docs give an equation that is murky: Max(lsb + (xMax - xMin)) |
|---|
| 264 | | hhea.numberOfHMetrics = glyphCount |
|---|
| 265 | | # update the head table |
|---|
| 266 | | head = otf["head"] |
|---|
| 267 | | head.xMin, head.yMin, head.xMax, head.yMax = fontBBox |
|---|
| 268 | | # update the post table |
|---|
| 269 | | post = otf["post"] |
|---|
| 270 | | # XXX make a guess at this? |
|---|
| 271 | | isFixedPitch = True |
|---|
| 272 | | testWidth = None |
|---|
| 273 | | for width in widths.values(): |
|---|
| 274 | | if testWidth is None: |
|---|
| 275 | | testWidth = width |
|---|
| 276 | | continue |
|---|
| 277 | | if width != testWidth: |
|---|
| 278 | | isFixedPitch = False |
|---|
| 279 | | break |
|---|
| 280 | | post.isFixedPitch = isFixedPitch |
|---|
| 281 | | |
|---|
| 282 | | def _populate_CFF(otf, font, glyphOrder): |
|---|
| 283 | | orderedGlyphs = _getOrderedGlyphs(font, glyphOrder) |
|---|
| 284 | | # as the CFF table is built, a bunch of info |
|---|
| 285 | | # for the other tables is stored. |
|---|
| 286 | | mapping = {} |
|---|
| 287 | | widths = {} |
|---|
| 288 | | lefts = {} |
|---|
| 289 | | rights = {} |
|---|
| 290 | | fontBBox = getFontBBox(font) |
|---|
| 291 | | # build the CFF table |
|---|
| 292 | | cff = otf["CFF "].cff |
|---|
| 293 | | topDict = cff.topDictIndex[0] |
|---|
| 294 | | charStrings = topDict.CharStrings |
|---|
| 295 | | charStringsIndex = charStrings.charStringsIndex |
|---|
| 296 | | private = charStringsIndex.private |
|---|
| 297 | | globalSubrs = charStringsIndex.globalSubrs |
|---|
| 298 | | for glyph in orderedGlyphs: |
|---|
| 299 | | glyphName = glyph.name |
|---|
| 300 | | glyphWidth = glyph.width |
|---|
| 301 | | unicodes = glyph.unicodes |
|---|
| 302 | | if hasattr(glyph, "box"): |
|---|
| 303 | | bounds = glyph.box |
|---|
| 304 | | else: |
|---|
| 305 | | bounds = glyph.bounds |
|---|
| 306 | | if bounds is not None: |
|---|
| 307 | | xMin, yMin, xMax, yMax = bounds |
|---|
| 308 | | else: |
|---|
| 309 | | xMin = 0 |
|---|
| 310 | | xMax = 0 |
|---|
| 311 | | # write the char string |
|---|
| 312 | | pen = T2CharStringPen(glyphWidth, font) |
|---|
| | 11 | class OutlineOTFCompiler(object): |
|---|
| | 12 | |
|---|
| | 13 | def __init__(self, font, path, glyphOrder=None): |
|---|
| | 14 | self.ufo = font |
|---|
| | 15 | self.path = path |
|---|
| | 16 | # make any missing glyphs and store them locally |
|---|
| | 17 | missingRequiredGlyphs = self.makeMissingRequiredGlyphs() |
|---|
| | 18 | # make a dict of all glyphs |
|---|
| | 19 | self.allGlyphs = {} |
|---|
| | 20 | for glyph in font: |
|---|
| | 21 | self.allGlyphs[glyph.name] = glyph |
|---|
| | 22 | self.allGlyphs.update(missingRequiredGlyphs) |
|---|
| | 23 | # store the glyph order |
|---|
| | 24 | if glyphOrder is None: |
|---|
| | 25 | glyphOrder = sorted(self.allGlyphs.keys()) |
|---|
| | 26 | self.glyphOrder = self.makeOfficialGlyphOrder(glyphOrder) |
|---|
| | 27 | # make a reusable bounding box |
|---|
| | 28 | self.fontBoundingBox = self.makeFontBoundingBox() |
|---|
| | 29 | # make a reusable character mapping |
|---|
| | 30 | self.unicodeToGlyphNameMapping = self.makeUnicodeToGlyphNameMapping() |
|---|
| | 31 | |
|---|
| | 32 | # ----------- |
|---|
| | 33 | # Main Method |
|---|
| | 34 | # ----------- |
|---|
| | 35 | |
|---|
| | 36 | def compile(self): |
|---|
| | 37 | self.otf = TTFont(sfntVersion="OTTO") |
|---|
| | 38 | # populate basic tables |
|---|
| | 39 | self.setupTable_head() |
|---|
| | 40 | self.setupTable_hhea() |
|---|
| | 41 | self.setupTable_hmtx() |
|---|
| | 42 | self.setupTable_name() |
|---|
| | 43 | self.setupTable_maxp() |
|---|
| | 44 | self.setupTable_cmap() |
|---|
| | 45 | self.setupTable_OS2() |
|---|
| | 46 | self.setupTable_post() |
|---|
| | 47 | self.setupTable_CFF() |
|---|
| | 48 | self.setupOtherTables() |
|---|
| | 49 | # write the file |
|---|
| | 50 | self.otf.save(self.path) |
|---|
| | 51 | # discard the object |
|---|
| | 52 | self.otf.close() |
|---|
| | 53 | del self.otf |
|---|
| | 54 | |
|---|
| | 55 | # ----- |
|---|
| | 56 | # Tools |
|---|
| | 57 | # ----- |
|---|
| | 58 | |
|---|
| | 59 | def makeFontBoundingBox(self): |
|---|
| | 60 | return getFontBBox(self.allGlyphs.values()) |
|---|
| | 61 | |
|---|
| | 62 | def makeUnicodeToGlyphNameMapping(self): |
|---|
| | 63 | mapping = {} |
|---|
| | 64 | for glyphName, glyph in self.allGlyphs.items(): |
|---|
| | 65 | unicodes = glyph.unicodes |
|---|
| | 66 | for uni in unicodes: |
|---|
| | 67 | mapping[uni] = glyphName |
|---|
| | 68 | return mapping |
|---|
| | 69 | |
|---|
| | 70 | def makeMissingRequiredGlyphs(self): |
|---|
| | 71 | glyphs = {} |
|---|
| | 72 | defaultWidth = int(round(self.ufo.info.unitsPerEm * 0.5)) |
|---|
| | 73 | if ".notdef" not in self.ufo: |
|---|
| | 74 | glyphs[".notdef"] = StubGlyph(name=".notdef", width=defaultWidth, unitsPerEm=self.ufo.info.unitsPerEm, ascender=self.ufo.info.ascender, descender=self.ufo.info.descender) |
|---|
| | 75 | if "space" not in self.ufo: |
|---|
| | 76 | glyphs["space"] = StubGlyph(name="space", width=defaultWidth, unitsPerEm=self.ufo.info.unitsPerEm, ascender=self.ufo.info.ascender, descender=self.ufo.info.descender, unicodes=[32]) |
|---|
| | 77 | return glyphs |
|---|
| | 78 | |
|---|
| | 79 | def makeOfficialGlyphOrder(self, glyphOrder): |
|---|
| | 80 | allGlyphs = self.allGlyphs |
|---|
| | 81 | orderedGlyphs = [".notdef", "space"] |
|---|
| | 82 | for glyphName in glyphOrder: |
|---|
| | 83 | if glyphName in [".notdef", "space"]: |
|---|
| | 84 | continue |
|---|
| | 85 | orderedGlyphs.append(glyphName) |
|---|
| | 86 | for glyphName in sorted(self.allGlyphs.keys()): |
|---|
| | 87 | if glyphName not in orderedGlyphs: |
|---|
| | 88 | orderedGlyphs.append(glyphName) |
|---|
| | 89 | return orderedGlyphs |
|---|
| | 90 | |
|---|
| | 91 | def getCharStringForGlyph(self, glyph, private, globalSubrs): |
|---|
| | 92 | pen = T2CharStringPen(glyph.width, self.allGlyphs) |
|---|
| 315 | | exists = charStrings.has_key(glyphName) |
|---|
| 316 | | if exists: |
|---|
| 317 | | # XXX a glyph already has this name. should we choke? |
|---|
| 318 | | glyphID = charStrings.charStrings[glyphName] |
|---|
| 319 | | charStringsIndex.items[glyphID] = charString |
|---|
| 320 | | else: |
|---|
| 321 | | charStringsIndex.append(charString) |
|---|
| 322 | | glyphID = len(topDict.charset) |
|---|
| 323 | | charStrings.charStrings[glyphName] = glyphID |
|---|
| 324 | | topDict.charset.append(glyphName) |
|---|
| 325 | | # store needed values |
|---|
| 326 | | if unicodes: |
|---|
| 327 | | for unicodeValue in unicodes: |
|---|
| 328 | | mapping[unicodeValue] = glyphName |
|---|
| 329 | | widths[glyphName] = glyphWidth |
|---|
| 330 | | lefts[glyphName] = xMin |
|---|
| 331 | | rights[glyphName] = xMax |
|---|
| 332 | | topDict.FontBBox = fontBBox |
|---|
| 333 | | # write the glyph order |
|---|
| 334 | | glyphOrder = [glyph.name for glyph in orderedGlyphs] |
|---|
| 335 | | otf.setGlyphOrder(glyphOrder) |
|---|
| 336 | | # return the saved data |
|---|
| 337 | | return mapping, widths, lefts, rights, fontBBox |
|---|
| 338 | | |
|---|
| 339 | | def _getOrderedGlyphs(font, glyphOrder): |
|---|
| 340 | | orderedGlyphs = [] |
|---|
| 341 | | defaultWidth = int(round(font.info.unitsPerEm * 0.5)) |
|---|
| 342 | | glyphOrder = list(glyphOrder) |
|---|
| 343 | | # .notdef should be the first glyph. create it if it does not exist. |
|---|
| 344 | | if ".notdef" not in font: |
|---|
| 345 | | notdef = StubGlyph(name=".notdef", width=defaultWidth, unitsPerEm=font.info.unitsPerEm, ascender=font.info.ascender, descender=font.info.descender) |
|---|
| 346 | | else: |
|---|
| 347 | | notdef = font[".notdef"] |
|---|
| 348 | | orderedGlyphs.append(notdef) |
|---|
| 349 | | # space should be the second glyph. create it if it does not exist. |
|---|
| 350 | | if "space" not in font: |
|---|
| 351 | | space = StubGlyph(name="space", width=defaultWidth, unitsPerEm=font.info.unitsPerEm, ascender=font.info.ascender, descender=font.info.descender, unicodes=[32]) |
|---|
| 352 | | else: |
|---|
| 353 | | space = font["space"] |
|---|
| 354 | | orderedGlyphs.append(space) |
|---|
| 355 | | # make sure no glyphs are missing from the order |
|---|
| 356 | | for glyphName in sorted(font.keys()): |
|---|
| 357 | | if glyphName not in glyphOrder: |
|---|
| 358 | | glyphOrder.append(glyphName) |
|---|
| 359 | | # now gather the glyphs |
|---|
| 360 | | for glyphName in glyphOrder: |
|---|
| 361 | | if glyphName in [".notdef", "space"]: |
|---|
| 362 | | continue |
|---|
| 363 | | orderedGlyphs.append(font[glyphName]) |
|---|
| 364 | | # done. |
|---|
| 365 | | return orderedGlyphs |
|---|
| | 95 | return charString |
|---|
| | 96 | |
|---|
| | 97 | # -------------- |
|---|
| | 98 | # Table Builders |
|---|
| | 99 | # -------------- |
|---|
| | 100 | |
|---|
| | 101 | def setupTable_head(self): |
|---|
| | 102 | self.otf["head"] = head = newTable("head") |
|---|
| | 103 | head.checkSumAdjustment = 0 # XXX this is a guess |
|---|
| | 104 | head.tableVersion = 1.0 |
|---|
| | 105 | head.fontRevision = 1.0 |
|---|
| | 106 | head.magicNumber = 0x5F0F3CF5 |
|---|
| | 107 | # upm |
|---|
| | 108 | head.unitsPerEm = int(self.ufo.info.unitsPerEm) |
|---|
| | 109 | # times |
|---|
| | 110 | rightNow = parse_date(time.asctime(time.gmtime())) |
|---|
| | 111 | head.created = rightNow |
|---|
| | 112 | head.modified = rightNow |
|---|
| | 113 | # bounding box |
|---|
| | 114 | xMin, yMin, xMax, yMax = self.fontBoundingBox |
|---|
| | 115 | head.xMin = xMin |
|---|
| | 116 | head.yMin = yMin |
|---|
| | 117 | head.xMax = xMax |
|---|
| | 118 | head.yMax = yMax |
|---|
| | 119 | # style mapping |
|---|
| | 120 | head.macStyle = 0 # XXX this is a guess |
|---|
| | 121 | # misc |
|---|
| | 122 | head.flags = 3 # XXX this is a guess |
|---|
| | 123 | head.lowestRecPPEM = 3 # XXX FontValidator describes this as "unreasonably small" |
|---|
| | 124 | head.fontDirectionHint = 2 # XXX this is a guess |
|---|
| | 125 | head.indexToLocFormat = 0 # XXX this is a guess |
|---|
| | 126 | head.glyphDataFormat = 0 |
|---|
| | 127 | |
|---|
| | 128 | def setupTable_name(self): |
|---|
| | 129 | self.otf["name"] = newTable("name") |
|---|
| | 130 | |
|---|
| | 131 | def setupTable_maxp(self): |
|---|
| | 132 | self.otf["maxp"] = maxp = newTable("maxp") |
|---|
| | 133 | maxp.tableVersion = 0x00005000 |
|---|
| | 134 | |
|---|
| | 135 | def setupTable_cmap(self): |
|---|
| | 136 | from fontTools.ttLib.tables._c_m_a_p import cmap_format_4 |
|---|
| | 137 | # mac |
|---|
| | 138 | cmap4_0_3 = cmap_format_4(4) |
|---|
| | 139 | cmap4_0_3.platformID = 0 |
|---|
| | 140 | cmap4_0_3.platEncID = 3 |
|---|
| | 141 | cmap4_0_3.language = 0 |
|---|
| | 142 | cmap4_0_3.cmap = dict(self.unicodeToGlyphNameMapping) |
|---|
| | 143 | # windows |
|---|
| | 144 | cmap4_3_1 = cmap_format_4(4) |
|---|
| | 145 | cmap4_3_1.platformID = 3 |
|---|
| | 146 | cmap4_3_1.platEncID = 1 |
|---|
| | 147 | cmap4_3_1.language = 0 |
|---|
| | 148 | cmap4_3_1.cmap = dict(self.unicodeToGlyphNameMapping) |
|---|
| | 149 | # store |
|---|
| | 150 | self.otf["cmap"] = cmap = newTable("cmap") |
|---|
| | 151 | cmap.tableVersion = 0 |
|---|
| | 152 | cmap.tables = [cmap4_0_3, cmap4_3_1] |
|---|
| | 153 | |
|---|
| | 154 | def setupTable_OS2(self): |
|---|
| | 155 | self.otf["OS/2"] = os2 = newTable("OS/2") |
|---|
| | 156 | os2.version = 0x0003 # XXX has this been bumped up? |
|---|
| | 157 | # average glyph width |
|---|
| | 158 | widths = [glyph.width for glyph in self.allGlyphs.values() if glyph.width > 0] |
|---|
| | 159 | os2.xAvgCharWidth = int(round(sum(widths) / len(widths))) |
|---|
| | 160 | # weight and width classes |
|---|
| | 161 | os2.usWeightClass = 400 |
|---|
| | 162 | os2.usWidthClass = 5 |
|---|
| | 163 | # embedding |
|---|
| | 164 | os2.fsType = 0 |
|---|
| | 165 | # superscript and subscript |
|---|
| | 166 | superAndSubscriptSize = int(round(self.ufo.info.ascender * 0.85)) # XXX what should the default be? |
|---|
| | 167 | os2.ySubscriptXSize = superAndSubscriptSize |
|---|
| | 168 | os2.ySubscriptYSize = superAndSubscriptSize |
|---|
| | 169 | os2.ySubscriptXOffset = 0 # XXX what should the default be? |
|---|
| | 170 | os2.ySubscriptYOffset = int(round(self.ufo.info.descender * 0.5)) # XXX what should the default be? |
|---|
| | 171 | os2.ySuperscriptXSize = superAndSubscriptSize |
|---|
| | 172 | os2.ySuperscriptYSize = superAndSubscriptSize |
|---|
| | 173 | os2.ySuperscriptXOffset = 0 # XXX what should the default be? |
|---|
| | 174 | os2.ySuperscriptYOffset = self.ufo.info.ascender - superAndSubscriptSize # XXX what should the default be? |
|---|
| | 175 | os2.yStrikeoutSize = int(round(self.ufo.info.unitsPerEm * 0.05)) # XXX what should the default be? |
|---|
| | 176 | os2.yStrikeoutPosition = int(round(self.ufo.info.unitsPerEm * .23)) # XXX what should the default be? |
|---|
| | 177 | os2.sFamilyClass = 0 |
|---|
| | 178 | # Panose |
|---|
| | 179 | panose = Panose() |
|---|
| | 180 | panose.bFamilyType = 0 |
|---|
| | 181 | panose.bSerifStyle = 0 |
|---|
| | 182 | panose.bWeight = 0 |
|---|
| | 183 | panose.bProportion = 0 |
|---|
| | 184 | panose.bContrast = 0 |
|---|
| | 185 | panose.bStrokeVariation = 0 |
|---|
| | 186 | panose.bArmStyle = 0 |
|---|
| | 187 | panose.bLetterForm = 0 |
|---|
| | 188 | panose.bMidline = 0 |
|---|
| | 189 | panose.bXHeight = 0 |
|---|
| | 190 | os2.panose = panose |
|---|
| | 191 | # Unicode and code page ranges |
|---|
| | 192 | os2.ulUnicodeRange1 = 0 |
|---|
| | 193 | os2.ulUnicodeRange2 = 0 |
|---|
| | 194 | os2.ulUnicodeRange3 = 0 |
|---|
| | 195 | os2.ulUnicodeRange4 = 0 |
|---|
| | 196 | os2.ulCodePageRange1 = 0 |
|---|
| | 197 | os2.ulCodePageRange2 = 0 |
|---|
| | 198 | # vendor id |
|---|
| | 199 | os2.achVendID = "None" # XXX get vendor code from font |
|---|
| | 200 | # vertical metrics |
|---|
| | 201 | os2.sxHeight = int(round(self.ufo.info.ascender * 0.5)) |
|---|
| | 202 | os2.sCapHeight = self.ufo.info.ascender |
|---|
| | 203 | os2.sTypoAscender = self.ufo.info.unitsPerEm + self.ufo.info.descender |
|---|
| | 204 | os2.sTypoDescender = self.ufo.info.descender |
|---|
| | 205 | os2.sTypoLineGap = 50 |
|---|
| | 206 | os2.usWinAscent = self.fontBoundingBox[3] |
|---|
| | 207 | os2.usWinDescent = self.fontBoundingBox[1] |
|---|
| | 208 | # style mapping |
|---|
| | 209 | os2.fsSelection = 0 # XXX this is a guess |
|---|
| | 210 | # characetr indexes |
|---|
| | 211 | unicodes = [i for i in self.unicodeToGlyphNameMapping.keys() if i is not None] |
|---|
| | 212 | minIndex = min(unicodes) |
|---|
| | 213 | maxIndex = max(unicodes) |
|---|
| | 214 | if maxIndex >= 0xFFFF: |
|---|
| | 215 | # see OS/2 docs |
|---|
| | 216 | # need to find a value lower than 0xFFFF. |
|---|
| | 217 | # shouldn't get to this point though. |
|---|
| | 218 | raise NotImplementedError |
|---|
| | 219 | os2.fsFirstCharIndex = minIndex |
|---|
| | 220 | os2.fsLastCharIndex = maxIndex |
|---|
| | 221 | os2.usBreakChar = 32 |
|---|
| | 222 | os2.usDefaultChar = 0 |
|---|
| | 223 | # maximum contextual lookup length |
|---|
| | 224 | os2.usMaxContex = 0 |
|---|
| | 225 | |
|---|
| | 226 | def setupTable_hmtx(self): |
|---|
| | 227 | self.otf["hmtx"] = hmtx = newTable("hmtx") |
|---|
| | 228 | hmtx.metrics = {} |
|---|
| | 229 | for glyphName, glyph in self.allGlyphs.items(): |
|---|
| | 230 | width = glyph.width |
|---|
| | 231 | left = 0 |
|---|
| | 232 | if len(glyph) or len(glyph.components): |
|---|
| | 233 | left = glyph.leftMargin |
|---|
| | 234 | hmtx[glyphName] = (width, left) |
|---|
| | 235 | |
|---|
| | 236 | def setupTable_hhea(self): |
|---|
| | 237 | self.otf["hhea"] = hhea = newTable("hhea") |
|---|
| | 238 | hhea.tableVersion = 1.0 |
|---|
| | 239 | # vertical metrics |
|---|
| | 240 | hhea.ascent = int(self.ufo.info.unitsPerEm + self.ufo.info.descender) |
|---|
| | 241 | hhea.descent = int(self.ufo.info.descender) |
|---|
| | 242 | hhea.lineGap = 50 |
|---|
| | 243 | # horizontal metrics |
|---|
| | 244 | widths = [] |
|---|
| | 245 | lefts = [] |
|---|
| | 246 | rights = [] |
|---|
| | 247 | extents = [] |
|---|
| | 248 | for glyph in self.allGlyphs.values(): |
|---|
| | 249 | left = glyph.leftMargin |
|---|
| | 250 | right = glyph.rightMargin |
|---|
| | 251 | if left is None: |
|---|
| | 252 | left = 0 |
|---|
| | 253 | if right is None: |
|---|
| | 254 | right = 0 |
|---|
| | 255 | widths.append(glyph.width) |
|---|
| | 256 | lefts.append(left) |
|---|
| | 257 | rights.append(right) |
|---|
| | 258 | bounds = glyph.bounds |
|---|
| | 259 | if bounds is not None: |
|---|
| | 260 | xMin, yMin, xMax, yMax = glyph.bounds |
|---|
| | 261 | else: |
|---|
| | 262 | xMin = 0 |
|---|
| | 263 | xMax = 0 |
|---|
| | 264 | extent = left + (xMax - xMin) # equation from spec for calculating xMaxExtent: Max(lsb + (xMax - xMin)) |
|---|
| | 265 | extents.append(extent) |
|---|
| | 266 | hhea.advanceWidthMax = max(widths) |
|---|
| | 267 | hhea.minLeftSideBearing = min(lefts) |
|---|
| | 268 | hhea.minRightSideBearing = min(rights) |
|---|
| | 269 | hhea.xMaxExtent = max(extents) |
|---|
| | 270 | # misc |
|---|
| | 271 | hhea.caretSlopeRise = 1 |
|---|
| | 272 | hhea.caretSlopeRun = 0 |
|---|
| | 273 | hhea.caretOffset = 0 # XXX this is a guess |
|---|
| | 274 | hhea.reserved0 = 0 |
|---|
| | 275 | hhea.reserved1 = 0 |
|---|
| | 276 | hhea.reserved2 = 0 |
|---|
| | 277 | hhea.reserved3 = 0 |
|---|
| | 278 | hhea.metricDataFormat = 0 |
|---|
| | 279 | # glyph count |
|---|
| | 280 | hhea.numberOfHMetrics = len(self.allGlyphs) |
|---|
| | 281 | |
|---|
| | 282 | def setupTable_post(self): |
|---|
| | 283 | self.otf["post"] = post = newTable("post") |
|---|
| | 284 | post.formatType = 3.0 |
|---|
| | 285 | # italic angle |
|---|
| | 286 | italicAngle = self.ufo.info.italicAngle |
|---|
| | 287 | if italicAngle is None: |
|---|
| | 288 | italicAngle = 0 |
|---|
| | 289 | post.italicAngle = italicAngle |
|---|
| | 290 | # underline |
|---|
| | 291 | post.underlinePosition = int(round(self.ufo.info.descender * 0.3)) # XXX this is a guess |
|---|
| | 292 | post.underlineThickness = int(round(self.ufo.info.unitsPerEm * .05)) # XXX this is a guess |
|---|
| | 293 | # determine if the font has a fixed width |
|---|
| | 294 | widths = set([glyph.width for glyph in self.allGlyphs.values()]) |
|---|
| | 295 | post.isFixedPitch = bool(len(widths) == 1) |
|---|
| | 296 | # misc |
|---|
| | 297 | post.minMemType42 = 0 # XXX this is a guess |
|---|
| | 298 | post.maxMemType42 = 0 # XXX this is a guess |
|---|
| | 299 | post.minMemType1 = 0 # XXX this is a guess |
|---|
| | 300 | post.maxMemType1 = 0 # XXX this is a guess |
|---|
| | 301 | |
|---|
| | 302 | def setupTable_CFF(self): |
|---|
| | 303 | self.otf["CFF "] = cff = newTable("CFF ") |
|---|
| | 304 | cff = cff.cff |
|---|
| | 305 | # set up the basics |
|---|
| | 306 | cff.major = 1 |
|---|
| | 307 | cff.minor = 0 |
|---|
| | 308 | cff.hdrSize = 4 |
|---|
| | 309 | cff.offSize = 4 |
|---|
| | 310 | cff.fontNames = [] |
|---|
| | 311 | strings = IndexedStrings() |
|---|
| | 312 | cff.strings = strings |
|---|
| | 313 | private = PrivateDict(strings=strings) |
|---|
| | 314 | private.rawDict.update(private.defaults) |
|---|
| | 315 | globalSubrs = GlobalSubrsIndex(private=private) |
|---|
| | 316 | topDict = TopDict(GlobalSubrs=globalSubrs, strings=strings) |
|---|
| | 317 | topDict.Private = private |
|---|
| | 318 | charStrings = topDict.CharStrings = CharStrings(file=None, charset=None, |
|---|
| | 319 | globalSubrs=globalSubrs, private=private, fdSelect=None, fdArray=None) |
|---|
| | 320 | charStrings.charStringsAreIndexed = True |
|---|
| | 321 | topDict.charset = [] |
|---|
| | 322 | charStringsIndex = charStrings.charStringsIndex = SubrsIndex(private=private, globalSubrs=globalSubrs) |
|---|
| | 323 | cff.topDictIndex = topDictIndex = TopDictIndex() |
|---|
| | 324 | topDictIndex.append(topDict) |
|---|
| | 325 | topDictIndex.strings = strings |
|---|
| | 326 | cff.GlobalSubrs = globalSubrs |
|---|
| | 327 | # populate naming data |
|---|
| | 328 | info = self.ufo.info |
|---|
| | 329 | psName = makePSName(self.ufo) |
|---|
| | 330 | cff.fontNames.append(psName) |
|---|
| | 331 | topDict = cff.topDictIndex[0] |
|---|
| | 332 | topDict.FullName = "%s %s" % (info.familyName, info.styleName) |
|---|
| | 333 | topDict.FamilyName = info.familyName |
|---|
| | 334 | topDict.Weight = info.styleName |
|---|
| | 335 | topDict.FontName = psName |
|---|
| | 336 | # populate glyphs |
|---|
| | 337 | for glyphName in self.glyphOrder: |
|---|
| | 338 | glyph = self.allGlyphs[glyphName] |
|---|
| | 339 | glyphWidth = glyph.width |
|---|
| | 340 | unicodes = glyph.unicodes |
|---|
| | 341 | bounds = glyph.bounds |
|---|
| | 342 | if bounds is not None: |
|---|
| | 343 | xMin, yMin, xMax, yMax = bounds |
|---|
| | 344 | else: |
|---|
| | 345 | xMin = 0 |
|---|
| | 346 | xMax = 0 |
|---|
| | 347 | charString = self.getCharStringForGlyph(glyph, private, globalSubrs) |
|---|
| | 348 | # add to the font |
|---|
| | 349 | exists = charStrings.has_key(glyphName) |
|---|
| | 350 | if exists: |
|---|
| | 351 | # XXX a glyph already has this name. should we choke? |
|---|
| | 352 | glyphID = charStrings.charStrings[glyphName] |
|---|
| | 353 | charStringsIndex.items[glyphID] = charString |
|---|
| | 354 | else: |
|---|
| | 355 | charStringsIndex.append(charString) |
|---|
| | 356 | glyphID = len(topDict.charset) |
|---|
| | 357 | charStrings.charStrings[glyphName] = glyphID |
|---|
| | 358 | topDict.charset.append(glyphName) |
|---|
| | 359 | topDict.FontBBox = self.fontBoundingBox |
|---|
| | 360 | # write the glyph order |
|---|
| | 361 | self.otf.setGlyphOrder(self.glyphOrder) |
|---|
| | 362 | |
|---|
| | 363 | def setupOtherTables(self): |
|---|
| | 364 | pass |
|---|
| | 365 | |
|---|