Changeset 422


Ignore:
Timestamp:
02/03/09 19:09:30 (4 years ago)
Author:
tal
Message:
Significantly revised the way features are handled.
File:
1 edited

Legend:

Unmodified
Added
Removed
  • packages/ufo2fdk/trunk/Lib/ufo2fdk/makeotfParts.py

    r390 r422  
    55from outlineOTF import OutlineOTFCompiler 
    66from featureTableWriter import FeatureTableWriter, winStr, macStr 
     7from kernFeatureWriter import KernFeatureWriter 
    78 
    89 
     
    192193    def setupFile_features(self, path): 
    193194        """ 
    194         Make the features source file. 
     195        Make the features source file. If any tables 
     196        or the kern feature are defined in the font's 
     197        features, they will not be overwritten. 
    195198 
    196199        **This should not be called externally.** Subclasses 
     
    199202        """ 
    200203        # force absolute includes into the features 
     204        existingFeaturePath = os.path.join(self.font.path, "features.fea") 
    201205        existing = forceAbsoluteIncludesInFeatures(self.font.features.text, self.font.path) 
    202206        # break the features into parts 
    203         initialText, features, tables = breakFeaturesAndTables(existing) 
    204         # extract the relevant parts from the features and tables 
    205         gsubGpos = [initialText] 
    206         kernFeature = None 
    207         for name, text in features: 
    208             if name == "kern": 
    209                 kernFeature = text 
    210             else: 
    211                 gsubGpos.append(text) 
    212         gsubGpos = "\n".join(gsubGpos).strip() 
    213         if not gsubGpos: 
    214             gsubGpos = None 
    215         unknownTables = [] 
    216         headTable = None 
    217         hheaTable = None 
    218         os2Table = None 
    219         nameTable = None 
    220         for name, text in tables: 
    221             if name == "head": 
    222                 headTable = text 
    223             elif name == "hhea": 
    224                 hheaTable = text 
    225             elif name == "OS/2": 
    226                 os2Table = text 
    227             elif name == "name": 
    228                 nameTable = text 
    229             else: 
    230                 unknownTables.append(text) 
    231         unknownTables = "\n".join(unknownTables) 
    232         # compile the new features 
    233         features = [ 
    234             self.writeFeatures_basic(gsubGpos), 
    235             "", 
    236             self.writeFeatures_kern(kernFeature), 
    237             "", 
    238             self.writeFeatures_head(headTable), 
    239             "", 
    240             self.writeFeatures_hhea(hheaTable), 
    241             "", 
    242             self.writeFeatures_OS2(os2Table), 
    243             "", 
    244             self.writeFeatures_name(nameTable), 
    245             "", 
    246             unknownTables 
    247         ] 
    248         features = "\n".join(features) 
     207        features, tables = extractFeaturesAndTables(existing, scannedFiles=[existingFeaturePath]) 
     208        # build tables that are not in the existing features 
     209        autoTables = {} 
     210        if "head" not in tables: 
     211            autoTables["head"] = self.writeFeatures_head() 
     212        if "hhea" not in tables: 
     213            autoTables["hhea"] = self.writeFeatures_hhea() 
     214        if "OS/2" not in tables: 
     215            autoTables["OS/2"] = self.writeFeatures_OS2() 
     216        if "name" not in tables: 
     217            autoTables["name"] = self.writeFeatures_name() 
     218        # build the kern feature if necessary 
     219        autoFeatures = {} 
     220        if "kern" not in features and len(self.font.kerning): 
     221            autoFeatures["kern"] = self.writeFeatures_kern() 
     222        # write the features 
     223        features = [existing] 
     224        for name, text in sorted(autoFeatures.items()): 
     225            features.append(text) 
     226        for name, text in sorted(autoTables.items()): 
     227            features.append(text) 
     228        features = "\n\n".join(features) 
    249229        # write the result 
    250230        f = open(path, "wb") 
     
    252232        f.close() 
    253233 
    254     def writeFeatures_basic(self, existing): 
    255         """ 
    256         Write the GSUB and GPOS features, excluding kern, 
    257         to a string and return it. *existing* will be 
    258         the existing GSUB and GPOS features in the font 
    259         excluding kern. 
     234    def writeFeatures_kern(self): 
     235        """ 
     236        Write the kern feature to a string and return it. 
    260237 
    261238        **This should not be called externally.** Subclasses 
     
    263240        in a different way if desired. 
    264241        """ 
    265         if existing is None: 
    266             return "" 
    267         return existing 
    268  
    269     def writeFeatures_kern(self, existing): 
    270         """ 
    271         Write the kern feature to a string and return it. 
    272         *existing* will be the existing kern feature in the font. 
    273  
    274         **This should not be called externally.** Subclasses 
    275         may override this method to handle the string creation 
    276         in a different way if desired. 
    277         """ 
    278         if existing is not None: 
    279             return existing 
    280         font = self.font 
    281         if not font.kerning.items(): 
    282             return "" 
    283         neededLeftGroups = set() 
    284         neededRightGroups = set() 
    285         noGroup = {} 
    286         leftGroup = {} 
    287         rightGroup = {} 
    288         bothGroup = {} 
    289         for (left, right), value in font.kerning.items(): 
    290             if left.startswith("@") and right.startswith("@"): 
    291                 bothGroup[left, right] = _roundInt(value) 
    292                 neededLeftGroups.add(left) 
    293                 neededRightGroups.add(right) 
    294             elif left.startswith("@"): 
    295                 leftGroup[left, right] = _roundInt(value) 
    296                 neededLeftGroups.add(left) 
    297             elif right.startswith("@"): 
    298                 rightGroup[left, right] = _roundInt(value) 
    299                 neededRightGroups.add(right) 
    300             else: 
    301                 noGroup[left, right] = _roundInt(value) 
    302         lines = ["feature kern {"] 
    303         emptyGroups = set() 
    304         for groupName in sorted(neededLeftGroups): 
    305             contents = font.groups.get(groupName, []) 
    306             if not contents: 
    307                 emptyGroups.add(groupName) 
    308                 continue 
    309             lines.append("    %s = [%s];" % (groupName, " ".join(contents))) 
    310         for groupName in sorted(neededRightGroups): 
    311             contents = font.groups.get(groupName, []) 
    312             if not contents: 
    313                 emptyGroups.add(groupName) 
    314                 continue 
    315             lines.append("    %s = [%s];" % (groupName, " ".join(contents))) 
    316         for (left, right), value in sorted(noGroup.items()): 
    317             lines.append("    pos %s %s %d;" % (left, right, value)) 
    318         for (left, right), value in sorted(leftGroup.items()): 
    319             if left in emptyGroups: 
    320                 continue 
    321             lines.append("    pos %s %s %d;" % (left, right, value)) 
    322         for (left, right), value in sorted(rightGroup.items()): 
    323             if right in emptyGroups: 
    324                 continue 
    325             lines.append("    pos %s %s %d;" % (left, right, value)) 
    326         for (left, right), value in sorted(bothGroup.items()): 
    327             if left in emptyGroups or right in emptyGroups: 
    328                 continue 
    329             lines.append("    pos %s %s %d;" % (left, right, value)) 
    330         lines.append("} kern;") 
    331         return "\n".join(lines) 
    332  
    333     def writeFeatures_head(self, existing): 
     242        writer = KernFeatureWriter(self.font) 
     243        return writer.write() 
     244 
     245    def writeFeatures_head(self): 
    334246        """ 
    335247        Write the head to a string and return it. 
    336         *existing* will be the existing head table in the font. 
    337248 
    338249        This gets the values for the file using the fallback 
     
    347258        in a different way if desired. 
    348259        """ 
    349         if existing is not None: 
    350             return existing 
    351260        versionMajor = getattr(self.font.info, "versionMajor") 
    352261        versionMinor = getattr(self.font.info, "versionMinor") 
     
    356265        return writer.write() 
    357266 
    358     def writeFeatures_hhea(self, existing): 
     267    def writeFeatures_hhea(self): 
    359268        """ 
    360269        Write the hhea to a string and return it. 
    361         *existing* will be the existing hhea table in the font. 
    362270 
    363271        This gets the values for the file using the fallback 
     
    375283        in a different way if desired. 
    376284        """ 
    377         if existing is not None: 
    378             return existing 
    379285        ascender = getAttrWithFallback(self.font.info, "openTypeHheaAscender") 
    380286        descender = getAttrWithFallback(self.font.info, "openTypeHheaDescender") 
     
    388294        return writer.write() 
    389295 
    390     def writeFeatures_name(self, existing): 
     296    def writeFeatures_name(self): 
    391297        """ 
    392298        Write the name to a string and return it. 
    393         *existing* will be the existing name table in the font. 
    394299 
    395300        This gets the values for the file using the fallback 
     
    413318        in a different way if desired. 
    414319        """ 
    415         if existing is not None: 
    416             return existing 
    417320        idToAttr = [ 
    418321            (0  , "copyright"), 
     
    444347        return writer.write() 
    445348 
    446     def writeFeatures_OS2(self, existing): 
     349    def writeFeatures_OS2(self): 
    447350        """ 
    448351        Write the OS/2 to a string and return it. 
    449         *existing* will be the existing OS/2 table in the font. 
    450352 
    451353        This gets the values for the file using the fallback 
     
    473375        in a different way if desired. 
    474376        """ 
    475         if existing is not None: 
    476             return existing 
    477377        codePageBitTranslation = { 
    478378            0  : "1252", 
     
    537437 
    538438 
     439includeRE = re.compile( 
     440    "include" 
     441    "\(" 
     442    "([^\)]+)" 
     443    "\)" 
     444    ) 
     445 
    539446def forceAbsoluteIncludesInFeatures(text, directory): 
    540447    """ 
     
    542449    to absolute includes. 
    543450    """ 
    544     includeRE = re.compile( 
    545         "include" 
    546         "\(" 
    547         "([^\)]+)" 
    548         "\)" 
    549         ) 
    550451    for includePath in includeRE.findall(text): 
    551452        currentDirectory = directory 
     
    605506) 
    606507 
    607 def breakFeaturesAndTables(text): 
     508def extractFeaturesAndTables(text, scannedFiles=[]): 
    608509    # strip all comments 
    609510    decommentedLines = [line.split("#")[0] for line in text.splitlines()] 
     
    622523        destringedLines.append(line) 
    623524    text = "\n".join(destringedLines) 
     525    # extract all includes 
     526    includes = includeRE.findall(text) 
    624527    # slice off the text that comes before 
    625528    # the first feature/table definition 
     
    636539    broken = _textBreakRecurse(text) 
    637540    # organize into tables and features 
    638     features = [] 
    639     tables = [] 
     541    features = {} 
     542    tables = {} 
    640543    for text in broken: 
    641544        text = text.strip() 
     
    652555        featureMatch = featureNameRE.search(text) 
    653556        if featureMatch is not None: 
    654             features.append((featureMatch.group(1), finalText)) 
     557            features[featureMatch.group(1)] = finalText 
    655558        else: 
    656559            tableMatch = tableNameRE.search(text) 
    657             tables.append((tableMatch.group(1), finalText)) 
    658     return precedingText, features, tables 
     560            tables[tableMatch.group(1)] = finalText 
     561    # scan all includes 
     562    for path in includes: 
     563        if path in scannedFiles: 
     564            continue 
     565        scannedFiles.append(path) 
     566        if os.path.exists(path): 
     567            f = open(path, "r") 
     568            text = f.read() 
     569            f.close() 
     570            f, t = extractFeaturesAndTables(text, scannedFiles) 
     571            features.update(f) 
     572            tables.update(t) 
     573    return features, tables 
    659574 
    660575def _textBreakRecurse(text): 
     
    690605 
    691606 
    692 breakFeaturesAndTablesTestText = """ 
     607extractFeaturesAndTablesTestText = """ 
    693608@foo = [bar]; 
    694609 
     
    707622""" 
    708623 
    709 breakFeaturesAndTablesTestResult = ('@foo = [bar];', 
    710  [('fts2', 'feature fts2 {\n    sub foo by bar;\n} fts2;'), 
    711   ('fts3', 'feature fts3 { sub a by b;} fts3;')], 
    712  [('tts1', 
    713    'table tts1 {\n    nameid 1 "feature this { is not really a \\"feature that { other thing is";\n} tts1;')]) 
     624#extractFeaturesAndTablesTestResult = ('@foo = [bar];', 
     625# [('fts2', 'feature fts2 {\n    sub foo by bar;\n} fts2;'), 
     626#  ('fts3', 'feature fts3 { sub a by b;} fts3;')], 
     627# [('tts1', 
     628#   'table tts1 {\n    nameid 1 "feature this { is not really a \\"feature that { other thing is";\n} tts1;')]) 
     629 
     630extractFeaturesAndTablesTestResult = ( 
     631    { 
     632        'fts2': 'feature fts2 {\n    sub foo by bar;\n} fts2;', 
     633        'fts3': 'feature fts3 { sub a by b;} fts3;' 
     634    }, 
     635    { 
     636        'tts1': 'table tts1 {\n    nameid 1 "feature this { is not really a \\"feature that { other thing is";\n} tts1;' 
     637    } 
     638) 
    714639 
    715640def testBreakFeaturesAndTables(): 
    716641    """ 
    717     >>> r = breakFeaturesAndTables(breakFeaturesAndTablesTestText) 
    718     >>> r == breakFeaturesAndTablesTestResult 
     642    >>> r = extractFeaturesAndTables(extractFeaturesAndTablesTestText) 
     643    >>> r == extractFeaturesAndTablesTestResult 
    719644    True 
    720645    """ 
Note: See TracChangeset for help on using the changeset viewer.