| 1 | import os |
|---|
| 2 | import shutil |
|---|
| 3 | import re |
|---|
| 4 | from fontInfoData import getAttrWithFallback, intListToNum |
|---|
| 5 | from outlineOTF import OutlineOTFCompiler |
|---|
| 6 | from featureTableWriter import FeatureTableWriter, winStr, macStr |
|---|
| 7 | from kernFeatureWriter import KernFeatureWriter |
|---|
| 8 | try: |
|---|
| 9 | sorted |
|---|
| 10 | except NameError: |
|---|
| 11 | def sorted(l): |
|---|
| 12 | l = list(l) |
|---|
| 13 | l.sort() |
|---|
| 14 | return l |
|---|
| 15 | |
|---|
| 16 | |
|---|
| 17 | class MakeOTFPartsCompiler(object): |
|---|
| 18 | |
|---|
| 19 | """ |
|---|
| 20 | This object will create the "parts" needed by the FDK. |
|---|
| 21 | The only external method is :meth:`ufo2fdk.tools.makeotfParts.compile`. |
|---|
| 22 | There is one attribute, :attr:`ufo2fdk.tools.makeotfParts.path` |
|---|
| 23 | that may be referenced externally. That is a dictionary of |
|---|
| 24 | paths to the various parts. |
|---|
| 25 | |
|---|
| 26 | When creating this object, you must provide a *font* |
|---|
| 27 | object and a *path* indicating where the parts should |
|---|
| 28 | be saved. Optionally, you can provide a *glyphOrder* |
|---|
| 29 | list of glyph names indicating the order of the glyphs |
|---|
| 30 | in the font. You may also provide an *outlineCompilerClass* |
|---|
| 31 | argument that will serve as the outline source compiler. |
|---|
| 32 | The class passed for this argument must be a subclass of |
|---|
| 33 | :class:`ufo2fdk.tools.outlineOTF.OutlineOTFCompiler`. |
|---|
| 34 | """ |
|---|
| 35 | |
|---|
| 36 | def __init__(self, font, path, glyphOrder=None, glyphDesignNameToFinalNameMap=None, outlineCompilerClass=OutlineOTFCompiler): |
|---|
| 37 | self.font = font |
|---|
| 38 | self.path = path |
|---|
| 39 | self.outlineCompilerClass = outlineCompilerClass |
|---|
| 40 | # store the glyph order |
|---|
| 41 | if glyphOrder is None: |
|---|
| 42 | glyphOrder = sorted(font.keys()) |
|---|
| 43 | self.glyphOrder = glyphOrder |
|---|
| 44 | # set up the production names |
|---|
| 45 | self.glyphDesignNameToFinalNameMap = self.makeGlyphDesignNameToFinalNameMap(glyphDesignNameToFinalNameMap) |
|---|
| 46 | # make the paths for all files |
|---|
| 47 | self.paths = dict( |
|---|
| 48 | outlineSource=os.path.join(path, "font.otf"), |
|---|
| 49 | menuName=os.path.join(path, "menuname"), |
|---|
| 50 | glyphOrder=os.path.join(path, "glyphOrder"), |
|---|
| 51 | fontInfo=os.path.join(path, "fontinfo"), |
|---|
| 52 | features=os.path.join(path, "features") |
|---|
| 53 | ) |
|---|
| 54 | |
|---|
| 55 | def makeGlyphDesignNameToFinalNameMap(self, providedMap): |
|---|
| 56 | """ |
|---|
| 57 | Make a map of {glyphName : makeotfLegalGlyphName} |
|---|
| 58 | for all gyphs in the glyph order.. |
|---|
| 59 | """ |
|---|
| 60 | finalMap = {} |
|---|
| 61 | if providedMap is not None: |
|---|
| 62 | finalMap.update(providedMap) |
|---|
| 63 | # gather glyphs that need a final name |
|---|
| 64 | needFinalName = [glyphName for glyphName in self.glyphOrder if glyphName not in finalMap] |
|---|
| 65 | # store names that don't need to be changed |
|---|
| 66 | for glyphName in needFinalName: |
|---|
| 67 | if isLegalGlyphName(glyphName): |
|---|
| 68 | finalMap[glyphName] = glyphName |
|---|
| 69 | # make names for the rest |
|---|
| 70 | for glyphName in needFinalName: |
|---|
| 71 | if glyphName in finalMap: |
|---|
| 72 | continue |
|---|
| 73 | uniValue = None |
|---|
| 74 | if glyphName in self.font: |
|---|
| 75 | uniValue = self.font[glyphName].unicode |
|---|
| 76 | finalMap[glyphName] = normalizeGlyphName(glyphName, uniValue, finalMap.values()) |
|---|
| 77 | # done |
|---|
| 78 | return finalMap |
|---|
| 79 | |
|---|
| 80 | def compile(self): |
|---|
| 81 | """ |
|---|
| 82 | Compile the parts. |
|---|
| 83 | """ |
|---|
| 84 | # set up the parts directory removing |
|---|
| 85 | # an existing directory if necessary. |
|---|
| 86 | if os.path.exists(self.path): |
|---|
| 87 | shutil.rmtree(self.path) |
|---|
| 88 | os.mkdir(self.path) |
|---|
| 89 | # build the parts |
|---|
| 90 | self.setupFile_outlineSource(self.paths["outlineSource"]) |
|---|
| 91 | self.setupFile_menuName(self.paths["menuName"]) |
|---|
| 92 | self.setupFile_glyphOrder(self.paths["glyphOrder"]) |
|---|
| 93 | self.setupFile_fontInfo(self.paths["fontInfo"]) |
|---|
| 94 | self.setupFile_features(self.paths["features"]) |
|---|
| 95 | |
|---|
| 96 | def setupFile_outlineSource(self, path): |
|---|
| 97 | """ |
|---|
| 98 | Make the outline source file. |
|---|
| 99 | |
|---|
| 100 | **This should not be called externally.** Subclasses |
|---|
| 101 | may override this method to handle the file creation |
|---|
| 102 | in a different way if desired. |
|---|
| 103 | """ |
|---|
| 104 | c = self.outlineCompilerClass(self.font, path, self.glyphOrder) |
|---|
| 105 | c.compile() |
|---|
| 106 | |
|---|
| 107 | def setupFile_menuName(self, path): |
|---|
| 108 | """ |
|---|
| 109 | Make the menu name source file. This gets the values for |
|---|
| 110 | the file using the fallback system as described below: |
|---|
| 111 | |
|---|
| 112 | ==== === |
|---|
| 113 | [PS] postscriptFontName |
|---|
| 114 | f= openTypeNamePreferredFamilyName |
|---|
| 115 | s= openTypeNamePreferredSubfamilyName |
|---|
| 116 | l= styleMapFamilyName |
|---|
| 117 | m=1, openTypeNameCompatibleFullName |
|---|
| 118 | ==== === |
|---|
| 119 | |
|---|
| 120 | **This should not be called externally.** Subclasses |
|---|
| 121 | may override this method to handle the file creation |
|---|
| 122 | in a different way if desired. |
|---|
| 123 | """ |
|---|
| 124 | psName = getAttrWithFallback(self.font.info,"postscriptFontName") |
|---|
| 125 | lines = [ |
|---|
| 126 | "[%s]" % psName |
|---|
| 127 | ] |
|---|
| 128 | # family name |
|---|
| 129 | familyName = getAttrWithFallback(self.font.info,"openTypeNamePreferredFamilyName") |
|---|
| 130 | encodedFamilyName = winStr(familyName) |
|---|
| 131 | lines.append("f=%s" % encodedFamilyName) |
|---|
| 132 | if encodedFamilyName != familyName: |
|---|
| 133 | lines.append("f=1,%s" % macStr(familyName)) |
|---|
| 134 | # style name |
|---|
| 135 | styleName = getAttrWithFallback(self.font.info,"openTypeNamePreferredSubfamilyName") |
|---|
| 136 | encodedStyleName = winStr(styleName) |
|---|
| 137 | lines.append("s=%s" % encodedStyleName) |
|---|
| 138 | if encodedStyleName != styleName: |
|---|
| 139 | lines.append("s=1,%s" % macStr(styleName)) |
|---|
| 140 | # compatible name |
|---|
| 141 | winCompatible = getAttrWithFallback(self.font.info,"styleMapFamilyName") |
|---|
| 142 | ## the second qualification here is in place for Mac Office <= 2004. |
|---|
| 143 | ## in that app the menu name is pulled from name ID 18. the font |
|---|
| 144 | ## may have standard naming data that combines to a length longer |
|---|
| 145 | ## than the app can handle (see Adobe Tech Note #5088). the designer |
|---|
| 146 | ## may have created a specific openTypeNameCompatibleFullName to |
|---|
| 147 | ## get around this problem. sigh, old app bugs live long lives. |
|---|
| 148 | if winCompatible != familyName or self.font.info.openTypeNameCompatibleFullName is not None: |
|---|
| 149 | # windows |
|---|
| 150 | l = "l=%s" % winCompatible |
|---|
| 151 | lines.append(l) |
|---|
| 152 | # mac |
|---|
| 153 | macCompatible = getAttrWithFallback(self.font.info,"openTypeNameCompatibleFullName") |
|---|
| 154 | l = "m=1,%s" % macCompatible |
|---|
| 155 | lines.append(l) |
|---|
| 156 | text = "\n".join(lines) + "\n" |
|---|
| 157 | f = open(path, "wb") |
|---|
| 158 | f.write(text) |
|---|
| 159 | f.close() |
|---|
| 160 | |
|---|
| 161 | def setupFile_glyphOrder(self, path): |
|---|
| 162 | """ |
|---|
| 163 | Make the glyph order source file. |
|---|
| 164 | |
|---|
| 165 | **This should not be called externally.** Subclasses |
|---|
| 166 | may override this method to handle the file creation |
|---|
| 167 | in a different way if desired. |
|---|
| 168 | """ |
|---|
| 169 | lines = [] |
|---|
| 170 | for designName in self.glyphOrder: |
|---|
| 171 | finalName = self.glyphDesignNameToFinalNameMap[designName] |
|---|
| 172 | if designName in self.font and self.font[designName].unicode is not None: |
|---|
| 173 | code = self.font[designName].unicode |
|---|
| 174 | code = hex(code)[2:].upper() |
|---|
| 175 | if len(code) < 4: |
|---|
| 176 | code = code.zfill(4) |
|---|
| 177 | line = "%s %s uni%s" % (finalName, glyphName, code) |
|---|
| 178 | else: |
|---|
| 179 | line = "%s %s" % (finalName, glyphName) |
|---|
| 180 | lines.append(line) |
|---|
| 181 | text = "\n".join(lines) + "\n" |
|---|
| 182 | f = open(path, "wb") |
|---|
| 183 | f.write(text) |
|---|
| 184 | f.close() |
|---|
| 185 | |
|---|
| 186 | def setupFile_fontInfo(self, path): |
|---|
| 187 | """ |
|---|
| 188 | Make the font info source file. This gets the values for |
|---|
| 189 | the file using the fallback system as described below: |
|---|
| 190 | |
|---|
| 191 | ========================== === |
|---|
| 192 | IsItalicStyle styleMapStyleName |
|---|
| 193 | IsBoldStyle styleMapStyleName |
|---|
| 194 | PreferOS/2TypoMetrics openTypeOS2Selection |
|---|
| 195 | IsOS/2WidthWeigthSlopeOnly openTypeOS2Selection |
|---|
| 196 | IsOS/2OBLIQUE openTypeOS2Selection |
|---|
| 197 | ========================== === |
|---|
| 198 | |
|---|
| 199 | **This should not be called externally.** Subclasses |
|---|
| 200 | may override this method to handle the file creation |
|---|
| 201 | in a different way if desired. |
|---|
| 202 | """ |
|---|
| 203 | lines = [] |
|---|
| 204 | # style mapping |
|---|
| 205 | styleMapStyleName = getAttrWithFallback(self.font.info,"styleMapStyleName") |
|---|
| 206 | if styleMapStyleName in ("italic", "bold italic"): |
|---|
| 207 | lines.append("IsItalicStyle true") |
|---|
| 208 | else: |
|---|
| 209 | lines.append("IsItalicStyle false") |
|---|
| 210 | if styleMapStyleName in ("bold", "bold italic"): |
|---|
| 211 | lines.append("IsBoldStyle true") |
|---|
| 212 | else: |
|---|
| 213 | lines.append("IsBoldStyle false") |
|---|
| 214 | # fsSelection bits |
|---|
| 215 | selection = getAttrWithFallback(self.font.info,"openTypeOS2Selection") |
|---|
| 216 | if 7 in selection: |
|---|
| 217 | lines.append("PreferOS/2TypoMetrics true") |
|---|
| 218 | else: |
|---|
| 219 | lines.append("PreferOS/2TypoMetrics false") |
|---|
| 220 | if 8 in selection: |
|---|
| 221 | lines.append("IsOS/2WidthWeigthSlopeOnly true") |
|---|
| 222 | else: |
|---|
| 223 | lines.append("IsOS/2WidthWeigthSlopeOnly false") |
|---|
| 224 | if 9 in selection: |
|---|
| 225 | lines.append("IsOS/2OBLIQUE true") |
|---|
| 226 | else: |
|---|
| 227 | lines.append("IsOS/2OBLIQUE false") |
|---|
| 228 | # write the file |
|---|
| 229 | if lines: |
|---|
| 230 | f = open(path, "wb") |
|---|
| 231 | f.write("\n".join(lines)) |
|---|
| 232 | f.close() |
|---|
| 233 | |
|---|
| 234 | def setupFile_features(self, path): |
|---|
| 235 | """ |
|---|
| 236 | Make the features source file. If any tables |
|---|
| 237 | or the kern feature are defined in the font's |
|---|
| 238 | features, they will not be overwritten. |
|---|
| 239 | |
|---|
| 240 | **This should not be called externally.** Subclasses |
|---|
| 241 | may override this method to handle the file creation |
|---|
| 242 | in a different way if desired. |
|---|
| 243 | """ |
|---|
| 244 | # force absolute includes into the features |
|---|
| 245 | if self.font.path is None: |
|---|
| 246 | existingFeaturePath = None |
|---|
| 247 | existing = self.font.features.text |
|---|
| 248 | if existing is None: |
|---|
| 249 | existing = "" |
|---|
| 250 | else: |
|---|
| 251 | existingFeaturePath = os.path.join(self.font.path, "features.fea") |
|---|
| 252 | existing = forceAbsoluteIncludesInFeatures(self.font.features.text, os.path.dirname(self.font.path)) |
|---|
| 253 | # break the features into parts |
|---|
| 254 | features, tables = extractFeaturesAndTables(existing, scannedFiles=[existingFeaturePath]) |
|---|
| 255 | # build tables that are not in the existing features |
|---|
| 256 | autoTables = {} |
|---|
| 257 | if "head" not in tables: |
|---|
| 258 | autoTables["head"] = self.writeFeatures_head() |
|---|
| 259 | if "hhea" not in tables: |
|---|
| 260 | autoTables["hhea"] = self.writeFeatures_hhea() |
|---|
| 261 | if "OS/2" not in tables: |
|---|
| 262 | autoTables["OS/2"] = self.writeFeatures_OS2() |
|---|
| 263 | if "name" not in tables: |
|---|
| 264 | autoTables["name"] = self.writeFeatures_name() |
|---|
| 265 | # build the kern feature if necessary |
|---|
| 266 | autoFeatures = {} |
|---|
| 267 | if "kern" not in features and len(self.font.kerning): |
|---|
| 268 | autoFeatures["kern"] = self.writeFeatures_kern() |
|---|
| 269 | # write the features |
|---|
| 270 | features = [existing] |
|---|
| 271 | for name, text in sorted(autoFeatures.items()): |
|---|
| 272 | features.append(text) |
|---|
| 273 | for name, text in sorted(autoTables.items()): |
|---|
| 274 | features.append(text) |
|---|
| 275 | features = "\n\n".join(features) |
|---|
| 276 | # write the result |
|---|
| 277 | f = open(path, "wb") |
|---|
| 278 | f.write(features) |
|---|
| 279 | f.close() |
|---|
| 280 | |
|---|
| 281 | def writeFeatures_kern(self): |
|---|
| 282 | """ |
|---|
| 283 | Write the kern feature to a string and return it. |
|---|
| 284 | |
|---|
| 285 | **This should not be called externally.** Subclasses |
|---|
| 286 | may override this method to handle the string creation |
|---|
| 287 | in a different way if desired. |
|---|
| 288 | """ |
|---|
| 289 | writer = KernFeatureWriter(self.font) |
|---|
| 290 | return writer.write() |
|---|
| 291 | |
|---|
| 292 | def writeFeatures_head(self): |
|---|
| 293 | """ |
|---|
| 294 | Write the head to a string and return it. |
|---|
| 295 | |
|---|
| 296 | This gets the values for the file using the fallback |
|---|
| 297 | system as described below: |
|---|
| 298 | |
|---|
| 299 | ===== === |
|---|
| 300 | X.XXX versionMajor.versionMinor |
|---|
| 301 | ===== === |
|---|
| 302 | |
|---|
| 303 | **This should not be called externally.** Subclasses |
|---|
| 304 | may override this method to handle the string creation |
|---|
| 305 | in a different way if desired. |
|---|
| 306 | """ |
|---|
| 307 | versionMajor = getAttrWithFallback(self.font.info, "versionMajor") |
|---|
| 308 | versionMinor = getAttrWithFallback(self.font.info, "versionMinor") |
|---|
| 309 | value = "%d.%s" % (versionMajor, str(versionMinor).zfill(3)) |
|---|
| 310 | writer = FeatureTableWriter("head") |
|---|
| 311 | writer.addLineWithKeyValue("FontRevision", value) |
|---|
| 312 | return writer.write() |
|---|
| 313 | |
|---|
| 314 | def writeFeatures_hhea(self): |
|---|
| 315 | """ |
|---|
| 316 | Write the hhea to a string and return it. |
|---|
| 317 | |
|---|
| 318 | This gets the values for the file using the fallback |
|---|
| 319 | system as described below: |
|---|
| 320 | |
|---|
| 321 | =========== === |
|---|
| 322 | Ascender openTypeHheaAscender |
|---|
| 323 | Descender openTypeHheaDescender |
|---|
| 324 | LineGap openTypeHheaLineGap |
|---|
| 325 | CaretOffset openTypeHheaCaretOffset |
|---|
| 326 | =========== === |
|---|
| 327 | |
|---|
| 328 | **This should not be called externally.** Subclasses |
|---|
| 329 | may override this method to handle the string creation |
|---|
| 330 | in a different way if desired. |
|---|
| 331 | """ |
|---|
| 332 | ascender = getAttrWithFallback(self.font.info, "openTypeHheaAscender") |
|---|
| 333 | descender = getAttrWithFallback(self.font.info, "openTypeHheaDescender") |
|---|
| 334 | lineGap = getAttrWithFallback(self.font.info, "openTypeHheaLineGap") |
|---|
| 335 | caret = getAttrWithFallback(self.font.info, "openTypeHheaCaretOffset") |
|---|
| 336 | writer = FeatureTableWriter("hhea") |
|---|
| 337 | writer.addLineWithKeyValue("Ascender", _roundInt(ascender)) |
|---|
| 338 | writer.addLineWithKeyValue("Descender", _roundInt(descender)) |
|---|
| 339 | writer.addLineWithKeyValue("LineGap", _roundInt(lineGap)) |
|---|
| 340 | writer.addLineWithKeyValue("CaretOffset", _roundInt(caret)) |
|---|
| 341 | return writer.write() |
|---|
| 342 | |
|---|
| 343 | def writeFeatures_name(self): |
|---|
| 344 | """ |
|---|
| 345 | Write the name to a string and return it. |
|---|
| 346 | |
|---|
| 347 | This gets the values for the file using the fallback |
|---|
| 348 | system as described below: |
|---|
| 349 | |
|---|
| 350 | ========= === |
|---|
| 351 | nameid 0 copyright |
|---|
| 352 | nameid 7 trademark |
|---|
| 353 | nameid 8 openTypeNameManufacturer |
|---|
| 354 | nameid 9 openTypeNameDesigner |
|---|
| 355 | nameid 10 openTypeNameDescription |
|---|
| 356 | nameid 11 openTypeNameManufacturerURL |
|---|
| 357 | nameid 12 openTypeNameDesignerURL |
|---|
| 358 | nameid 13 openTypeNameLicense |
|---|
| 359 | nameid 14 openTypeNameLicenseURL |
|---|
| 360 | nameid 19 openTypeNameSampleText |
|---|
| 361 | ========= === |
|---|
| 362 | |
|---|
| 363 | **This should not be called externally.** Subclasses |
|---|
| 364 | may override this method to handle the string creation |
|---|
| 365 | in a different way if desired. |
|---|
| 366 | """ |
|---|
| 367 | idToAttr = [ |
|---|
| 368 | (0 , "copyright"), |
|---|
| 369 | (7 , "trademark"), |
|---|
| 370 | (8 , "openTypeNameManufacturer"), |
|---|
| 371 | (9 , "openTypeNameDesigner"), |
|---|
| 372 | (10 , "openTypeNameDescription"), |
|---|
| 373 | (11 , "openTypeNameManufacturerURL"), |
|---|
| 374 | (12 , "openTypeNameDesignerURL"), |
|---|
| 375 | (13 , "openTypeNameLicense"), |
|---|
| 376 | (14 , "openTypeNameLicenseURL"), |
|---|
| 377 | (19 , "openTypeNameSampleText") |
|---|
| 378 | ] |
|---|
| 379 | multilineNameTableEntries = {} |
|---|
| 380 | lines = [] |
|---|
| 381 | for id, attr in idToAttr: |
|---|
| 382 | value = getAttrWithFallback(self.font.info, attr) |
|---|
| 383 | if value is None: |
|---|
| 384 | continue |
|---|
| 385 | s = 'nameid %d "%s";' % (id, winStr(value)) |
|---|
| 386 | lines.append(s) |
|---|
| 387 | s = 'nameid %d 1 "%s";' % (id, macStr(value)) |
|---|
| 388 | lines.append(s) |
|---|
| 389 | if not lines: |
|---|
| 390 | return "" |
|---|
| 391 | writer = FeatureTableWriter("name") |
|---|
| 392 | for line in lines: |
|---|
| 393 | writer.addLine(line) |
|---|
| 394 | return writer.write() |
|---|
| 395 | |
|---|
| 396 | def writeFeatures_OS2(self): |
|---|
| 397 | """ |
|---|
| 398 | Write the OS/2 to a string and return it. |
|---|
| 399 | |
|---|
| 400 | This gets the values for the file using the fallback |
|---|
| 401 | system as described below: |
|---|
| 402 | |
|---|
| 403 | ============= === |
|---|
| 404 | FSType openTypeOS2Type |
|---|
| 405 | Panose openTypeOS2Panose |
|---|
| 406 | UnicodeRange openTypeOS2UnicodeRanges |
|---|
| 407 | CodePageRange openTypeOS2CodePageRanges |
|---|
| 408 | TypoAscender openTypeOS2TypoAscender |
|---|
| 409 | TypoDescender openTypeOS2TypoDescender |
|---|
| 410 | TypoLineGap openTypeOS2TypoLineGap |
|---|
| 411 | winAscent openTypeOS2WinAscent |
|---|
| 412 | winDescent openTypeOS2WinDescent |
|---|
| 413 | XHeight xHeight |
|---|
| 414 | CapHeight capHeight |
|---|
| 415 | WeightClass openTypeOS2WeightClass |
|---|
| 416 | WidthClass openTypeOS2WidthClass |
|---|
| 417 | Vendor openTypeOS2VendorID |
|---|
| 418 | ============= === |
|---|
| 419 | |
|---|
| 420 | **This should not be called externally.** Subclasses |
|---|
| 421 | may override this method to handle the string creation |
|---|
| 422 | in a different way if desired. |
|---|
| 423 | """ |
|---|
| 424 | codePageBitTranslation = { |
|---|
| 425 | 0 : "1252", |
|---|
| 426 | 1 : "1250", |
|---|
| 427 | 2 : "1251", |
|---|
| 428 | 3 : "1253", |
|---|
| 429 | 4 : "1254", |
|---|
| 430 | 5 : "1255", |
|---|
| 431 | 6 : "1256", |
|---|
| 432 | 7 : "1257", |
|---|
| 433 | 8 : "1258", |
|---|
| 434 | 16 : "874", |
|---|
| 435 | 17 : "932", |
|---|
| 436 | 18 : "936", |
|---|
| 437 | 19 : "949", |
|---|
| 438 | 20 : "950", |
|---|
| 439 | 21 : "1361", |
|---|
| 440 | 48 : "869", |
|---|
| 441 | 49 : "866", |
|---|
| 442 | 50 : "865", |
|---|
| 443 | 51 : "864", |
|---|
| 444 | 52 : "863", |
|---|
| 445 | 53 : "862", |
|---|
| 446 | 54 : "861", |
|---|
| 447 | 55 : "860", |
|---|
| 448 | 56 : "857", |
|---|
| 449 | 57 : "855", |
|---|
| 450 | 58 : "852", |
|---|
| 451 | 59 : "775", |
|---|
| 452 | 60 : "737", |
|---|
| 453 | 61 : "708", |
|---|
| 454 | 62 : "850", |
|---|
| 455 | 63 : "437" |
|---|
| 456 | } |
|---|
| 457 | # writer |
|---|
| 458 | writer = FeatureTableWriter("OS/2") |
|---|
| 459 | # type |
|---|
| 460 | writer.addLineWithKeyValue("FSType", intListToNum(getAttrWithFallback(self.font.info, "openTypeOS2Type"), 0, 16)) |
|---|
| 461 | # panose |
|---|
| 462 | panose = [str(i) for i in getAttrWithFallback(self.font.info, "openTypeOS2Panose")] |
|---|
| 463 | writer.addLineWithKeyValue("Panose", " ".join(panose)) |
|---|
| 464 | # unicode ranges |
|---|
| 465 | unicodeRange = [str(i) for i in getAttrWithFallback(self.font.info, "openTypeOS2UnicodeRanges")] |
|---|
| 466 | if unicodeRange: |
|---|
| 467 | writer.addLineWithKeyValue("UnicodeRange", " ".join(unicodeRange)) |
|---|
| 468 | # code page ranges |
|---|
| 469 | codePageRange = [codePageBitTranslation[i] for i in getAttrWithFallback(self.font.info, "openTypeOS2CodePageRanges") if i in codePageBitTranslation] |
|---|
| 470 | if codePageRange: |
|---|
| 471 | writer.addLineWithKeyValue("CodePageRange", " ".join(codePageRange)) |
|---|
| 472 | # vertical metrics |
|---|
| 473 | writer.addLineWithKeyValue("TypoAscender", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2TypoAscender"))) |
|---|
| 474 | writer.addLineWithKeyValue("TypoDescender", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2TypoDescender"))) |
|---|
| 475 | writer.addLineWithKeyValue("TypoLineGap", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2TypoLineGap"))) |
|---|
| 476 | writer.addLineWithKeyValue("winAscent", _roundInt(getAttrWithFallback(self.font.info, "openTypeOS2WinAscent"))) |
|---|
| 477 | writer.addLineWithKeyValue("winDescent", abs(_roundInt(getAttrWithFallback(self.font.info, "openTypeOS2WinDescent")))) |
|---|
| 478 | writer.addLineWithKeyValue("XHeight", _roundInt(getAttrWithFallback(self.font.info, "xHeight"))) |
|---|
| 479 | writer.addLineWithKeyValue("CapHeight", _roundInt(getAttrWithFallback(self.font.info, "capHeight"))) |
|---|
| 480 | writer.addLineWithKeyValue("WeightClass", getAttrWithFallback(self.font.info, "openTypeOS2WeightClass")) |
|---|
| 481 | writer.addLineWithKeyValue("WidthClass", getAttrWithFallback(self.font.info, "openTypeOS2WidthClass")) |
|---|
| 482 | writer.addLineWithKeyValue("Vendor", '"%s"' % getAttrWithFallback(self.font.info, "openTypeOS2VendorID")) |
|---|
| 483 | return writer.write() |
|---|
| 484 | |
|---|
| 485 | |
|---|
| 486 | # ----------- |
|---|
| 487 | # Glyph Names |
|---|
| 488 | # ----------- |
|---|
| 489 | |
|---|
| 490 | import unicodedata |
|---|
| 491 | |
|---|
| 492 | _digits = set("0123456789") |
|---|
| 493 | _validCharacters = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.") |
|---|
| 494 | |
|---|
| 495 | def isLegalGlyphName(glyphName): |
|---|
| 496 | """ |
|---|
| 497 | >>> isLegalGlyphName("a") |
|---|
| 498 | True |
|---|
| 499 | >>> isLegalGlyphName(".foo") |
|---|
| 500 | False |
|---|
| 501 | >>> isLegalGlyphName(".notdef") |
|---|
| 502 | True |
|---|
| 503 | >>> isLegalGlyphName("foo.bar") |
|---|
| 504 | True |
|---|
| 505 | >>> isLegalGlyphName("1foo") |
|---|
| 506 | False |
|---|
| 507 | >>> isLegalGlyphName("foo1") |
|---|
| 508 | True |
|---|
| 509 | >>> isLegalGlyphName("f*o") |
|---|
| 510 | False |
|---|
| 511 | >>> isLegalGlyphName("abcdefghijklmnopqrstuvwxyz01234") |
|---|
| 512 | True |
|---|
| 513 | >>> isLegalGlyphName("abcdefghijklmnopqrstuvwxyz012345") |
|---|
| 514 | False |
|---|
| 515 | """ |
|---|
| 516 | # must not start with a digit or period |
|---|
| 517 | if glyphName[0] in _digits: |
|---|
| 518 | return False |
|---|
| 519 | if glyphName[0] == "." and glyphName != ".notdef": |
|---|
| 520 | return False |
|---|
| 521 | # up to 31 characters in length |
|---|
| 522 | if len(glyphName) > 31: |
|---|
| 523 | return False |
|---|
| 524 | # must be entirely comprised of characters from A-Z a-z 0-9 . _ |
|---|
| 525 | for character in glyphName: |
|---|
| 526 | if character not in _validCharacters: |
|---|
| 527 | return False |
|---|
| 528 | # passed |
|---|
| 529 | return True |
|---|
| 530 | |
|---|
| 531 | def normalizeGlyphName(glyphName, uniValue, existing): |
|---|
| 532 | """ |
|---|
| 533 | >>> normalizeGlyphName("a-b-c", None, []) |
|---|
| 534 | 'abc' |
|---|
| 535 | >>> normalizeGlyphName("a-b-c", None, ["abc"]) |
|---|
| 536 | 'abc.1' |
|---|
| 537 | >>> normalizeGlyphName("!", int("0021", 16), []) |
|---|
| 538 | 'uni0021' |
|---|
| 539 | >>> normalizeGlyphName("!", int("0021", 16), ['uni0021']) |
|---|
| 540 | 'uni0021.1' |
|---|
| 541 | >>> normalizeGlyphName("abcdefghijklmnopqrstuvwxyz012345", None, []) |
|---|
| 542 | 'glyph1' |
|---|
| 543 | >>> normalizeGlyphName("a-b-c-d-e-f-g-h-i-j-k-l-m-n-o-p-q-r-s-t-u-v-w-x-y-z-0-1-2-3-4-5", None, []) |
|---|
| 544 | 'glyph1' |
|---|
| 545 | """ |
|---|
| 546 | # convert to unicode |
|---|
| 547 | glyphName = unicode(glyphName) |
|---|
| 548 | # remove illegal characters |
|---|
| 549 | glyphName = unicodedata.normalize("NFKD", glyphName) |
|---|
| 550 | glyphName = glyphName.encode("ascii", "ignore") |
|---|
| 551 | glyphName = "".join([c for c in glyphName if c in _validCharacters]) |
|---|
| 552 | # no new name |
|---|
| 553 | if not glyphName: |
|---|
| 554 | # quickly try to apply the adobe standard |
|---|
| 555 | if uniValue: |
|---|
| 556 | if uniValue > 0x0000 and uniValue < 0xFFFF: |
|---|
| 557 | prefix = "uni" |
|---|
| 558 | else: |
|---|
| 559 | prefix = "u" |
|---|
| 560 | glyphName = prefix + hex(uniValue)[2:].zfill(4).upper() |
|---|
| 561 | # hit test |
|---|
| 562 | if glyphName in existing: |
|---|
| 563 | glyphName = _makeUniqueGlyphName(glyphName, existing) |
|---|
| 564 | # test for validity |
|---|
| 565 | if not isLegalGlyphName(glyphName): |
|---|
| 566 | glyphName = None |
|---|
| 567 | # fallback |
|---|
| 568 | if not glyphName: |
|---|
| 569 | glyphName = _makeUniqueFallbackGlyphName(existing) |
|---|
| 570 | return glyphName |
|---|
| 571 | |
|---|
| 572 | def _makeUniqueGlyphName(glyphName, existing, number=1): |
|---|
| 573 | """ |
|---|
| 574 | _makeUniqueGlyphName("abc", ["abc"]) |
|---|
| 575 | 'abc.1' |
|---|
| 576 | """ |
|---|
| 577 | newName = "%s.%d" % (glyphName, number) |
|---|
| 578 | if newName in existing: |
|---|
| 579 | return _makeUniqueGlyphName(glyphName, existing, number+1) |
|---|
| 580 | return newName |
|---|
| 581 | |
|---|
| 582 | def _makeUniqueFallbackGlyphName(existing, number=1): |
|---|
| 583 | """ |
|---|
| 584 | >>> _makeUniqueFallbackGlyphName([]) |
|---|
| 585 | 'glyph1' |
|---|
| 586 | >>> _makeUniqueFallbackGlyphName(["glyph1"]) |
|---|
| 587 | 'glyph2' |
|---|
| 588 | """ |
|---|
| 589 | assert number < 100000 # arbitrary, but come on. 100,000 illegal glyph names? |
|---|
| 590 | name = "glyph%d" % number |
|---|
| 591 | if name in existing: |
|---|
| 592 | return _makeUniqueFallbackGlyphName(existing, number+1) |
|---|
| 593 | return name |
|---|
| 594 | |
|---|
| 595 | # -------- |
|---|
| 596 | # Features |
|---|
| 597 | # -------- |
|---|
| 598 | |
|---|
| 599 | includeRE = re.compile( |
|---|
| 600 | "include" |
|---|
| 601 | "\s*" |
|---|
| 602 | "\(" |
|---|
| 603 | "([^\)]+)" |
|---|
| 604 | "\)" |
|---|
| 605 | ) |
|---|
| 606 | |
|---|
| 607 | def forceAbsoluteIncludesInFeatures(text, directory): |
|---|
| 608 | """ |
|---|
| 609 | Convert relative includes in the *text* |
|---|
| 610 | to absolute includes. |
|---|
| 611 | """ |
|---|
| 612 | for includePath in includeRE.findall(text): |
|---|
| 613 | currentDirectory = directory |
|---|
| 614 | parts = includePath.split("/") |
|---|
| 615 | for index, part in enumerate(parts): |
|---|
| 616 | part = part.strip() |
|---|
| 617 | if not part: |
|---|
| 618 | continue |
|---|
| 619 | if part == "..": |
|---|
| 620 | currentDirectory = os.path.dirname(currentDirectory) |
|---|
| 621 | elif part == ".": |
|---|
| 622 | continue |
|---|
| 623 | else: |
|---|
| 624 | break |
|---|
| 625 | subPath = "/".join(parts[index:]) |
|---|
| 626 | srcPath = os.path.join(currentDirectory, subPath) |
|---|
| 627 | text = text.replace(includePath, srcPath) |
|---|
| 628 | return text |
|---|
| 629 | |
|---|
| 630 | def _roundInt(value): |
|---|
| 631 | return int(round(value)) |
|---|
| 632 | |
|---|
| 633 | # ---------------------- |
|---|
| 634 | # Basic Feature Splitter |
|---|
| 635 | # ---------------------- |
|---|
| 636 | |
|---|
| 637 | stringRE = re.compile( |
|---|
| 638 | "(\"[^$\"]*\")" |
|---|
| 639 | ) |
|---|
| 640 | featureTableStartRE = re.compile( |
|---|
| 641 | "(" |
|---|
| 642 | "feature" |
|---|
| 643 | "\s+" |
|---|
| 644 | "\S{4}" |
|---|
| 645 | "\s*" |
|---|
| 646 | "\{" |
|---|
| 647 | "|" |
|---|
| 648 | "table" |
|---|
| 649 | "\s+" |
|---|
| 650 | "\S{4}" |
|---|
| 651 | "\s*" |
|---|
| 652 | "\{" |
|---|
| 653 | ")", |
|---|
| 654 | re.MULTILINE |
|---|
| 655 | ) |
|---|
| 656 | featureNameRE = re.compile( |
|---|
| 657 | "feature" |
|---|
| 658 | "\s+" |
|---|
| 659 | "(\S{4})" |
|---|
| 660 | "\s*" |
|---|
| 661 | "\{" |
|---|
| 662 | ) |
|---|
| 663 | tableNameRE = re.compile( |
|---|
| 664 | "table" |
|---|
| 665 | "\s+" |
|---|
| 666 | "(\S{4})" |
|---|
| 667 | "\s*" |
|---|
| 668 | "\{" |
|---|
| 669 | ) |
|---|
| 670 | |
|---|
| 671 | def extractFeaturesAndTables(text, scannedFiles=[]): |
|---|
| 672 | # strip all comments |
|---|
| 673 | decommentedLines = [line.split("#")[0] for line in text.splitlines()] |
|---|
| 674 | text = "\n".join(decommentedLines) |
|---|
| 675 | # replace all strings with temporary placeholders. |
|---|
| 676 | destringedLines = [] |
|---|
| 677 | stringReplacements = {} |
|---|
| 678 | for line in text.splitlines(): |
|---|
| 679 | if "\"" in line: |
|---|
| 680 | line = line.replace("\\\"", "__ufo2fdk_temp_escaped_quote__") |
|---|
| 681 | for found in stringRE.findall(line): |
|---|
| 682 | temp = "__ufo2fdk_temp_string_%d__" % len(stringReplacements) |
|---|
| 683 | line = line.replace(found, temp, 1) |
|---|
| 684 | stringReplacements[temp] = found.replace("__ufo2fdk_temp_escaped_quote__", "\\\"") |
|---|
| 685 | line = line.replace("__ufo2fdk_temp_escaped_quote__", "\\\"") |
|---|
| 686 | destringedLines.append(line) |
|---|
| 687 | text = "\n".join(destringedLines) |
|---|
| 688 | # extract all includes |
|---|
| 689 | includes = includeRE.findall(text) |
|---|
| 690 | # slice off the text that comes before |
|---|
| 691 | # the first feature/table definition |
|---|
| 692 | precedingText = "" |
|---|
| 693 | startMatch = featureTableStartRE.search(text) |
|---|
| 694 | if startMatch is not None: |
|---|
| 695 | start, end = startMatch.span() |
|---|
| 696 | precedingText = text[:start].strip() |
|---|
| 697 | text = text[start:] |
|---|
| 698 | else: |
|---|
| 699 | precedingText = text |
|---|
| 700 | text = "" |
|---|
| 701 | # break the features |
|---|
| 702 | broken = _textBreakRecurse(text) |
|---|
| 703 | # organize into tables and features |
|---|
| 704 | features = {} |
|---|
| 705 | tables = {} |
|---|
| 706 | for text in broken: |
|---|
| 707 | text = text.strip() |
|---|
| 708 | if not text: |
|---|
| 709 | continue |
|---|
| 710 | # replace the strings |
|---|
| 711 | finalText = text |
|---|
| 712 | for temp, original in stringReplacements.items(): |
|---|
| 713 | if temp in finalText: |
|---|
| 714 | del stringReplacements[temp] |
|---|
| 715 | finalText = finalText.replace(temp, original, 1) |
|---|
| 716 | finalText = finalText.strip() |
|---|
| 717 | # grab feature or table names and store |
|---|
| 718 | featureMatch = featureNameRE.search(text) |
|---|
| 719 | if featureMatch is not None: |
|---|
| 720 | features[featureMatch.group(1)] = finalText |
|---|
| 721 | else: |
|---|
| 722 | tableMatch = tableNameRE.search(text) |
|---|
| 723 | tables[tableMatch.group(1)] = finalText |
|---|
| 724 | # scan all includes |
|---|
| 725 | for path in includes: |
|---|
| 726 | if path in scannedFiles: |
|---|
| 727 | continue |
|---|
| 728 | scannedFiles.append(path) |
|---|
| 729 | if os.path.exists(path): |
|---|
| 730 | f = open(path, "r") |
|---|
| 731 | text = f.read() |
|---|
| 732 | f.close() |
|---|
| 733 | f, t = extractFeaturesAndTables(text, scannedFiles) |
|---|
| 734 | features.update(f) |
|---|
| 735 | tables.update(t) |
|---|
| 736 | return features, tables |
|---|
| 737 | |
|---|
| 738 | def _textBreakRecurse(text): |
|---|
| 739 | matched = [] |
|---|
| 740 | match = featureTableStartRE.search(text) |
|---|
| 741 | if match is None: |
|---|
| 742 | matched.append(text) |
|---|
| 743 | else: |
|---|
| 744 | start, end = match.span() |
|---|
| 745 | # add any preceding text to the previous item |
|---|
| 746 | if start != 0: |
|---|
| 747 | precedingText = matched.pop(0) |
|---|
| 748 | precedingText += text[:start] |
|---|
| 749 | matched.insert(0, precedingText) |
|---|
| 750 | # look ahead to see if there is another feature |
|---|
| 751 | next = text[end:] |
|---|
| 752 | nextMatch = featureTableStartRE.search(next) |
|---|
| 753 | if nextMatch is None: |
|---|
| 754 | # if nothing has been found, add |
|---|
| 755 | # the remaining text to the feature |
|---|
| 756 | matchedText = text[start:] |
|---|
| 757 | matched.append(matchedText) |
|---|
| 758 | else: |
|---|
| 759 | # if one has been found, grab all text |
|---|
| 760 | # from before the feature start and add |
|---|
| 761 | # it to the current feature. |
|---|
| 762 | nextStart, nextEnd = nextMatch.span() |
|---|
| 763 | matchedText = text[:end + nextStart] |
|---|
| 764 | matched.append(matchedText) |
|---|
| 765 | # recurse through the remaining text |
|---|
| 766 | matched += _textBreakRecurse(next[nextStart:]) |
|---|
| 767 | return matched |
|---|
| 768 | |
|---|
| 769 | |
|---|
| 770 | extractFeaturesAndTablesTestText = """ |
|---|
| 771 | @foo = [bar]; |
|---|
| 772 | |
|---|
| 773 | # test commented item |
|---|
| 774 | #feature fts1 { |
|---|
| 775 | # sub foo by bar; |
|---|
| 776 | #} fts1; |
|---|
| 777 | |
|---|
| 778 | feature fts2 { |
|---|
| 779 | sub foo by bar; |
|---|
| 780 | } fts2; |
|---|
| 781 | |
|---|
| 782 | table tts1 { |
|---|
| 783 | nameid 1 "feature this { is not really a \\\"feature that { other thing is"; |
|---|
| 784 | } tts1;feature fts3 { sub a by b;} fts3; |
|---|
| 785 | """ |
|---|
| 786 | |
|---|
| 787 | extractFeaturesAndTablesTestResult = ( |
|---|
| 788 | { |
|---|
| 789 | 'fts2': 'feature fts2 {\n sub foo by bar;\n} fts2;', |
|---|
| 790 | 'fts3': 'feature fts3 { sub a by b;} fts3;' |
|---|
| 791 | }, |
|---|
| 792 | { |
|---|
| 793 | 'tts1': 'table tts1 {\n nameid 1 "feature this { is not really a \\"feature that { other thing is";\n} tts1;' |
|---|
| 794 | } |
|---|
| 795 | ) |
|---|
| 796 | |
|---|
| 797 | def testBreakFeaturesAndTables(): |
|---|
| 798 | """ |
|---|
| 799 | >>> r = extractFeaturesAndTables(extractFeaturesAndTablesTestText) |
|---|
| 800 | >>> r == extractFeaturesAndTablesTestResult |
|---|
| 801 | True |
|---|
| 802 | """ |
|---|
| 803 | |
|---|
| 804 | if __name__ == "__main__": |
|---|
| 805 | import doctest |
|---|
| 806 | doctest.testmod() |
|---|