Initial public commit
This commit is contained in:
commit
3b1fffade1
6648 changed files with 363948 additions and 0 deletions
189
misc/pylib/fontbuild/features.py
Executable file
189
misc/pylib/fontbuild/features.py
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
# Copyright 2015 Google Inc. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import re
|
||||
|
||||
from feaTools import parser
|
||||
from feaTools.writers.fdkSyntaxWriter import FDKSyntaxFeatureWriter
|
||||
|
||||
|
||||
class FilterFeatureWriter(FDKSyntaxFeatureWriter):
|
||||
"""Feature writer to detect invalid references and duplicate definitions."""
|
||||
|
||||
def __init__(self, refs=set(), name=None, isFeature=False):
|
||||
"""Initializes the set of known references, empty by default."""
|
||||
self.refs = refs
|
||||
self.featureNames = set()
|
||||
self.lookupNames = set()
|
||||
self.tableNames = set()
|
||||
self.languageSystems = set()
|
||||
super(FilterFeatureWriter, self).__init__(
|
||||
name=name, isFeature=isFeature)
|
||||
|
||||
# error to print when undefined reference is found in glyph class
|
||||
self.classErr = ('Undefined reference "%s" removed from glyph class '
|
||||
'definition %s.')
|
||||
|
||||
# error to print when undefined reference is found in sub or pos rule
|
||||
subErr = ['Substitution rule with undefined reference "%s" removed']
|
||||
if self._name:
|
||||
subErr.append(" from ")
|
||||
subErr.append("feature" if self._isFeature else "lookup")
|
||||
subErr.append(' "%s"' % self._name)
|
||||
subErr.append(".")
|
||||
self.subErr = "".join(subErr)
|
||||
self.posErr = self.subErr.replace("Substitution", "Positioning")
|
||||
|
||||
def _subwriter(self, name, isFeature):
|
||||
"""Use this class for nested expressions e.g. in feature definitions."""
|
||||
return FilterFeatureWriter(self.refs, name, isFeature)
|
||||
|
||||
def _flattenRefs(self, refs, flatRefs):
|
||||
"""Flatten a list of references."""
|
||||
for ref in refs:
|
||||
if type(ref) == list:
|
||||
self._flattenRefs(ref, flatRefs)
|
||||
elif ref != "'": # ignore contextual class markings
|
||||
flatRefs.append(ref)
|
||||
|
||||
def _checkRefs(self, refs, errorMsg):
|
||||
"""Check a list of references found in a sub or pos rule."""
|
||||
flatRefs = []
|
||||
self._flattenRefs(refs, flatRefs)
|
||||
for ref in flatRefs:
|
||||
# trailing apostrophes should be ignored
|
||||
if ref[-1] == "'":
|
||||
ref = ref[:-1]
|
||||
if ref not in self.refs:
|
||||
print errorMsg % ref
|
||||
# insert an empty instruction so that we can't end up with an
|
||||
# empty block, which is illegal syntax
|
||||
super(FilterFeatureWriter, self).rawText(";")
|
||||
return False
|
||||
return True
|
||||
|
||||
def classDefinition(self, name, contents):
|
||||
"""Check that contents are valid, then add name to known references."""
|
||||
if name in self.refs:
|
||||
return
|
||||
newContents = []
|
||||
for ref in contents:
|
||||
if ref not in self.refs and ref != "-":
|
||||
print self.classErr % (ref, name)
|
||||
else:
|
||||
newContents.append(ref)
|
||||
self.refs.add(name)
|
||||
super(FilterFeatureWriter, self).classDefinition(name, newContents)
|
||||
|
||||
def gsubType1(self, target, replacement):
|
||||
"""Check a sub rule with one-to-one replacement."""
|
||||
if self._checkRefs([target, replacement], self.subErr):
|
||||
super(FilterFeatureWriter, self).gsubType1(target, replacement)
|
||||
|
||||
def gsubType4(self, target, replacement):
|
||||
"""Check a sub rule with many-to-one replacement."""
|
||||
if self._checkRefs([target, replacement], self.subErr):
|
||||
super(FilterFeatureWriter, self).gsubType4(target, replacement)
|
||||
|
||||
def gsubType6(self, precedingContext, target, trailingContext, replacement):
|
||||
"""Check a sub rule with contextual replacement."""
|
||||
refs = [precedingContext, target, trailingContext, replacement]
|
||||
if self._checkRefs(refs, self.subErr):
|
||||
super(FilterFeatureWriter, self).gsubType6(
|
||||
precedingContext, target, trailingContext, replacement)
|
||||
|
||||
def gposType1(self, target, value):
|
||||
"""Check a single positioning rule."""
|
||||
if self._checkRefs([target], self.posErr):
|
||||
super(FilterFeatureWriter, self).gposType1(target, value)
|
||||
|
||||
def gposType2(self, target, value, needEnum=False):
|
||||
"""Check a pair positioning rule."""
|
||||
if self._checkRefs(target, self.posErr):
|
||||
super(FilterFeatureWriter, self).gposType2(target, value, needEnum)
|
||||
|
||||
# these rules may contain references, but they aren't present in Roboto
|
||||
def gsubType3(self, target, replacement):
|
||||
raise NotImplementedError
|
||||
|
||||
def feature(self, name):
|
||||
"""Adds a feature definition only once."""
|
||||
if name not in self.featureNames:
|
||||
self.featureNames.add(name)
|
||||
return super(FilterFeatureWriter, self).feature(name)
|
||||
# we must return a new writer even if we don't add it to this one
|
||||
return FDKSyntaxFeatureWriter(name, True)
|
||||
|
||||
def lookup(self, name):
|
||||
"""Adds a lookup block only once."""
|
||||
if name not in self.lookupNames:
|
||||
self.lookupNames.add(name)
|
||||
return super(FilterFeatureWriter, self).lookup(name)
|
||||
# we must return a new writer even if we don't add it to this one
|
||||
return FDKSyntaxFeatureWriter(name, False)
|
||||
|
||||
def languageSystem(self, langTag, scriptTag):
|
||||
"""Adds a language system instruction only once."""
|
||||
system = (langTag, scriptTag)
|
||||
if system not in self.languageSystems:
|
||||
self.languageSystems.add(system)
|
||||
super(FilterFeatureWriter, self).languageSystem(langTag, scriptTag)
|
||||
|
||||
def table(self, name, data):
|
||||
"""Adds a table only once."""
|
||||
if name in self.tableNames:
|
||||
return
|
||||
self.tableNames.add(name)
|
||||
self._instructions.append("table %s {" % name)
|
||||
self._instructions.extend([" %s %s;" % line for line in data])
|
||||
self._instructions.append("} %s;" % name)
|
||||
|
||||
|
||||
def compileFeatureRE(name):
|
||||
"""Compiles a feature-matching regex."""
|
||||
|
||||
# this is the pattern used internally by feaTools:
|
||||
# https://github.com/typesupply/feaTools/blob/master/Lib/feaTools/parser.py
|
||||
featureRE = list(parser.featureContentRE)
|
||||
featureRE.insert(2, name)
|
||||
featureRE.insert(6, name)
|
||||
return re.compile("".join(featureRE))
|
||||
|
||||
|
||||
def updateFeature(font, name, value):
|
||||
"""Add a feature definition, or replace existing one."""
|
||||
featureRE = compileFeatureRE(name)
|
||||
if featureRE.search(font.features.text):
|
||||
font.features.text = featureRE.sub(value, font.features.text)
|
||||
else:
|
||||
font.features.text += "\n" + value
|
||||
|
||||
|
||||
def readFeatureFile(font, text, prepend=True):
|
||||
"""Incorporate valid definitions from feature text into font."""
|
||||
writer = FilterFeatureWriter(set(font.keys()))
|
||||
if prepend:
|
||||
text += font.features.text
|
||||
else:
|
||||
text = font.features.text + text
|
||||
parser.parseFeatures(writer, text)
|
||||
font.features.text = writer.write()
|
||||
|
||||
|
||||
def writeFeatureFile(font, path):
|
||||
"""Write the font's features to an external file."""
|
||||
fout = open(path, "w")
|
||||
fout.write(font.features.text)
|
||||
fout.close()
|
||||
Reference in a new issue