diff options
Diffstat (limited to 'scripts')
31 files changed, 1557 insertions, 731 deletions
diff --git a/scripts/build-v2.py b/scripts/build-v2.py index 7002c8b..58842d6 100644 --- a/scripts/build-v2.py +++ b/scripts/build-v2.py @@ -1,95 +1,146 @@ -import sys
-
-sys.path.insert(0,"%s/scripts/lib"%BASEDIR)
-
-from robofab.world import RFont
-from fontTools.misc.transform import Transform
-from fontbuild.Build import FontProject,swapGlyphs,transformGlyphMembers
-from fontbuild.mix import Mix,Master
-from fontbuild.italics import condenseGlyph, transformFLGlyphMembers
-
-# Masters
-
-rg = Master("%s/src/v2/Roboto_Regular.vfb"%BASEDIR)
-bd = Master("%s/src/v2/Roboto_Bold.vfb"%BASEDIR)
-th = Master("%s/src/v2/Roboto_Thin.vfb"%BASEDIR)
-
-# build condensed masters
-
-condensed = Font(th.font)
-
-lessCondensed = "plusminus \
-bracketleft bracketright dieresis \
-macron percent \
-multiply degree at i j zero one two \
-three four five six seven eight nine braceright braceleft".split()
-uncondensed = "tonos breve acute grave quotesingle quotedbl asterisk \
-period currency registered copyright bullet ring degree dieresis comma bar brokenbar dotaccent \
-dotbelow colon semicolon uniFFFC uniFFFD uni0488 uni0489 ringbelow estimated".split()
-moreCondensed = "z Z M W A V".split()
-
-
-def condenseFont(font, scale=.8, stemWidth=185):
- f = RFont(font)
-
- xscale = scale
- CAPS = "A B C.cn D.cn E F G.cn H I J K L M N O.cn P Q.cn R S T U.cn V W X Y Z one two three four five six seven eight nine zero".split()
- LC = "a.cn b.cn c.cn d.cn e.cn f g.cn h i j k l m n o.cn p.cn q.cn r s t u v w x y z".split()
- # for g in [f[name] for name in LC]:
- for g in f:
- if (len(g) > 0):
- # print g.name
- if g.name in lessCondensed:
- scale = xscale * 1.1
- if g.name in uncondensed:
- continue
- if g.name in moreCondensed:
- scale = xscale * .90
- # g2 = condenseGlyph(g, xscale)
- # g.clear()
- # g2.drawPoints(g.getPointPen())
- m = Transform(xscale, 0, 0, 1, 20, 0)
- g.transform(m)
- transformFLGlyphMembers(g,m,transformAnchors=False)
- g.width += 40
- return f
-
-
-proj = FontProject(rg.font, BASEDIR, "res/roboto.cfg", th.ffont)
-proj.incrementBuildNumber()
-
-# FAMILYNAME = "Roboto 2 DRAFT"
-# FAMILYNAME = "Roboto2"
-FAMILYNAME = "Roboto"
-
-proj.buldVFBandFEA = True
-proj.generateFont(th.font,"%s/Thin/Regular/Th"%FAMILYNAME)
-proj.generateFont(Mix([th,rg], 0.45),"%s/Light/Regular/Lt"%FAMILYNAME)
-proj.generateFont(Mix([th,rg], Point(0.90, 0.92)),"%s/Regular/Regular/Rg"%FAMILYNAME)
-proj.generateFont(Mix([rg,bd], 0.35),"%s/Medium/Regular/Lt"%FAMILYNAME)
-proj.generateFont(Mix([rg,bd], Point(0.73, 0.73)),"%s/Bold/Bold/Rg"%FAMILYNAME)
-proj.generateFont(Mix([rg,bd], Point(1.125, 1.0)),"%s/Black/Bold/Bk"%FAMILYNAME)
-
-proj.generateFont(th.font,"%s/Thin Italic/Italic/Th"%FAMILYNAME, italic=True, stemWidth=80)
-proj.generateFont(Mix([th,rg], 0.45),"%s/Light Italic/Italic/Lt"%FAMILYNAME, italic=True, stemWidth=120)
-proj.generateFont(Mix([th,rg], Point(0.90, 0.92)),"%s/Italic/Italic/Rg"%FAMILYNAME, italic=True, stemWidth=185)
-proj.generateFont(Mix([rg,bd], 0.35),"%s/Medium Italic/Bold Italic/Lt"%FAMILYNAME, italic=True, stemWidth=230)
-proj.generateFont(Mix([rg,bd], Point(0.73, 0.73)),"%s/Bold Italic/Bold Italic/Rg"%FAMILYNAME, italic=True, stemWidth=290)
-proj.generateFont(Mix([rg,bd], Point(1.125, 1.0)),"%s/Black Italic/Bold Italic/Bk"%FAMILYNAME, italic=True, stemWidth=290)
-
-thcn1 = Master(condenseFont(Font(th.font), .84, 40).naked())
-cn1 = Master( rg.ffont.addDiff(thcn1.ffont, th.ffont))
-bdcn1 = Master( bd.ffont.addDiff(thcn1.ffont, th.ffont))
-
-proj.generateFont(Mix([thcn1,cn1], Point(0.45, 0.47)), "%s Condensed/Light/Regular/Lt"%FAMILYNAME, swapSuffixes=[".cn"])
-proj.generateFont(Mix([thcn1,cn1], Point(0.9, 0.92)), "%s Condensed/Regular/Regular/Rg"%FAMILYNAME, swapSuffixes=[".cn"])
-proj.generateFont(Mix([cn1,bdcn1], Point(0.75, 0.75)), "%s Condensed/Bold/Bold/Rg"%FAMILYNAME, swapSuffixes=[".cn"])
-
-proj.generateFont(Mix([thcn1,cn1], Point(0.45, 0.47)), "%s Condensed/Light Italic/Italic/Lt"%FAMILYNAME, italic=True, swapSuffixes=[".cn"], stemWidth=120)
-proj.generateFont(Mix([thcn1,cn1], Point(0.9, 0.92)), "%s Condensed/Italic/Italic/Rg"%FAMILYNAME, italic=True, swapSuffixes=[".cn"], stemWidth=185)
-proj.generateFont(Mix([cn1,bdcn1], Point(0.75, 0.75)), "%s Condensed/Bold Italic/Bold Italic/Rg"%FAMILYNAME, italic=True, swapSuffixes=[".cn"], stemWidth=240)
-
-for i in range(len(fl)):
- fl.Close(0)
-
-sys.exit(0)
+# 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 os +import sys + +from fontTools.misc.transform import Transform +from robofab.objects.objectsRF import RPoint + +from fontbuild.Build import FontProject +from fontbuild.italics import condenseGlyph +from fontbuild.italics import transformFLGlyphMembers +from fontbuild.mix import Master +from fontbuild.mix import Mix + +# The root of the Roboto tree +BASEDIR = os.path.abspath( + os.path.join(os.path.dirname(__file__), os.pardir)) + +# Masters + +rg = Master("%s/src/v2/Roboto_Regular.ufo" % BASEDIR, + anchorPath="%s/res/anchors_regular.json" % BASEDIR) +bd = Master("%s/src/v2/Roboto_Bold.ufo" % BASEDIR, + anchorPath="%s/res/anchors_bold.json" % BASEDIR) +th = Master("%s/src/v2/Roboto_Thin.ufo" % BASEDIR, + anchorPath="%s/res/anchors_thin.json" % BASEDIR) + +# build condensed masters + +lessCondensed = ( + "plusminus bracketleft bracketright dieresis macron " + "percent multiply degree at i j " + "zero one two three four five six seven eight nine " + "braceright braceleft").split() +uncondensed = ( + "tonos breve acute grave quotesingle quotedbl asterisk " + "period currency registered copyright bullet ring degree " + "dieresis comma bar brokenbar dotaccent dotbelow " + "colon semicolon uniFFFC uniFFFD uni0488 uni0489 ringbelow " + "estimated").split() +moreCondensed = "z Z M W A V".split() + + +def condenseFont(font, scale=.8, stemWidth=185): + f = font.copy() + + xscale = scale + CAPS = ("A B C.cn D.cn E F G.cn H I J K L M N O.cn P Q.cn R S T U.cn V W X " + "Y Z one two three four five six seven eight nine zero").split() + LC = ("a.cn b.cn c.cn d.cn e.cn f g.cn h i j k l m n o.cn p.cn q.cn r s t " + "u v w x y z").split() + # for g in [f[name] for name in LC]: + for g in f: + if len(g) > 0: + # print g.name + if g.name in lessCondensed: + scale = xscale * 1.1 + if g.name in uncondensed: + continue + if g.name in moreCondensed: + scale = xscale * .90 + # g2 = condenseGlyph(g, xscale) + # g.clear() + # g2.drawPoints(g.getPointPen()) + m = Transform(xscale, 0, 0, 1, 20, 0) + g.transform(m) + transformFLGlyphMembers(g, m, transformAnchors=False) + if g.width != 0: + g.width += 40 + return f + + +proj = FontProject(rg.font, BASEDIR, "res/roboto.cfg", th.ffont) +#proj.incrementBuildNumber() + +# FAMILYNAME = "Roboto 2 DRAFT" +# FAMILYNAME = "Roboto2" +FAMILYNAME = "Roboto" + +proj.buildOTF = True +#proj.autohintOTF = True +proj.buildTTF = True + +proj.generateFont(th.font, "%s/Thin/Regular/Th"%FAMILYNAME) +proj.generateFont(Mix([th, rg], 0.45), "%s/Light/Regular/Lt"%FAMILYNAME) +proj.generateFont(Mix([th, rg], RPoint(0.90, 0.92)), + "%s/Regular/Regular/Rg"%FAMILYNAME) +proj.generateFont(Mix([rg, bd], 0.35), "%s/Medium/Regular/Lt"%FAMILYNAME) +proj.generateFont(Mix([rg, bd], RPoint(0.73, 0.73)), + "%s/Bold/Bold/Rg"%FAMILYNAME) +proj.generateFont(Mix([rg, bd], RPoint(1.125, 1.0)), + "%s/Black/Bold/Bk"%FAMILYNAME) + +proj.generateFont(th.font, "%s/Thin Italic/Italic/Th"%FAMILYNAME, + italic=True, stemWidth=80) +proj.generateFont(Mix([th, rg], 0.45), "%s/Light Italic/Italic/Lt"%FAMILYNAME, + italic=True, stemWidth=120) +proj.generateFont(Mix([th, rg], RPoint(0.90, 0.92)), + "%s/Italic/Italic/Rg"%FAMILYNAME, italic=True, stemWidth=185) +proj.generateFont(Mix([rg, bd], 0.35), "%s/Medium Italic/Italic/Lt"%FAMILYNAME, + italic=True, stemWidth=230) +proj.generateFont(Mix([rg, bd], RPoint(0.73, 0.73)), + "%s/Bold Italic/Bold Italic/Rg"%FAMILYNAME, + italic=True, stemWidth=290) +proj.generateFont(Mix([rg, bd], RPoint(1.125, 1.0)), + "%s/Black Italic/Bold Italic/Bk"%FAMILYNAME, + italic=True, stemWidth=290) + +thcn1 = Master(condenseFont(th.font, .84, 40)) +cn1 = Master(rg.ffont.addDiff(thcn1.ffont, th.ffont)) +bdcn1 = Master(bd.ffont.addDiff(thcn1.ffont, th.ffont)) + +proj.generateFont(Mix([thcn1, cn1], RPoint(0.45, 0.47)), + "%s Condensed/Light/Regular/Lt"%FAMILYNAME, + swapSuffixes=[".cn"]) +proj.generateFont(Mix([thcn1, cn1], RPoint(0.9, 0.92)), + "%s Condensed/Regular/Regular/Rg"%FAMILYNAME, + swapSuffixes=[".cn"]) +proj.generateFont(Mix([cn1, bdcn1], RPoint(0.75, 0.75)), + "%s Condensed/Bold/Bold/Rg"%FAMILYNAME, + swapSuffixes=[".cn"]) + +proj.generateFont(Mix([thcn1, cn1], RPoint(0.45, 0.47)), + "%s Condensed/Light Italic/Italic/Lt"%FAMILYNAME, + italic=True, swapSuffixes=[".cn"], stemWidth=120) +proj.generateFont(Mix([thcn1, cn1], RPoint(0.9, 0.92)), + "%s Condensed/Italic/Italic/Rg"%FAMILYNAME, + italic=True, swapSuffixes=[".cn"], stemWidth=185) +proj.generateFont(Mix([cn1, bdcn1], RPoint(0.75, 0.75)), + "%s Condensed/Bold Italic/Bold Italic/Rg"%FAMILYNAME, + italic=True, swapSuffixes=[".cn"], stemWidth=240) + +sys.exit(0) diff --git a/scripts/common_tests.py b/scripts/common_tests.py index 9e6f4c7..f5a7793 100644 --- a/scripts/common_tests.py +++ b/scripts/common_tests.py @@ -1,14 +1,28 @@ +# coding=UTF-8 +# +# 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. + """Common tests for different targets.""" import glob -import sys import unittest from fontTools import ttLib from nototools import coverage from nototools import font_data -sys.path.append('./third_party/freetype-py') import freetype import layout @@ -155,7 +169,7 @@ class TestDigitWidths(FontTest): """Tests the width of digits.""" def setUp(self): - _, self.fonts = self.loaded_fonts + self.font_files, self.fonts = self.loaded_fonts self.digits = [ 'zero', 'one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine'] @@ -167,6 +181,17 @@ class TestDigitWidths(FontTest): widths = [hmtx_table[digit][0] for digit in self.digits] self.assertEqual(len(set(widths)), 1) + def test_superscript_digits(self): + """Tests that 'numr' features maps digits to Unicode superscripts.""" + ascii_digits = '0123456789' + superscript_digits = u'⁰¹²³⁴⁵⁶⁷⁸⁹' + for font_file in self.font_files: + numr_glyphs = layout.get_advances( + ascii_digits, font_file, '--features=numr') + superscript_glyphs = layout.get_advances( + superscript_digits, font_file) + self.assertEqual(superscript_glyphs, numr_glyphs) + class TestCharacterCoverage(FontTest): """Tests character coverage.""" @@ -220,12 +245,51 @@ class TestLigatures(FontTest): advances = layout.get_advances('ff', fontfile) self.assertEqual(len(advances), 2) + def test_st_ligatures(self): + """Tests that st ligatures are formed by dlig.""" + for fontfile in self.fontfiles: + for combination in [u'st', u'ſt']: + normal = layout.get_glyphs(combination, fontfile) + ligated = layout.get_glyphs( + combination, fontfile, '--features=dlig') + self.assertTrue(len(normal) == 2 and len(ligated) == 1) + + +class TestFeatures(FontTest): + """Tests typographic features.""" + + def setUp(self): + self.fontfiles, _ = self.loaded_fonts + + def test_smcp_coverage(self): + """Tests that smcp is supported for our required set.""" + with open('res/smcp_requirements.txt') as smcp_reqs_file: + smcp_reqs_list = [] + for line in smcp_reqs_file.readlines(): + line = line[:line.index(' #')] + smcp_reqs_list.append(unichr(int(line, 16))) + + for fontfile in self.fontfiles: + chars_with_no_smcp = [] + for char in smcp_reqs_list: + normal = layout.get_glyphs(char, fontfile) + smcp = layout.get_glyphs(char, fontfile, '--features=smcp') + if normal == smcp: + chars_with_no_smcp.append(char) + self.assertEqual( + chars_with_no_smcp, [], + ("smcp feature is not applied to '%s'" % + u''.join(chars_with_no_smcp).encode('UTF-8'))) + + +EXPECTED_YMIN = -555 +EXPECTED_YMAX = 2163 class TestVerticalMetrics(FontTest): """Test the vertical metrics of fonts.""" def setUp(self): - _, self.fonts = self.loaded_fonts + self.font_files, self.fonts = self.loaded_fonts def test_ymin_ymax(self): """Tests yMin and yMax to be equal to Roboto v1 values. @@ -234,8 +298,25 @@ class TestVerticalMetrics(FontTest): """ for font in self.fonts: head_table = font['head'] - self.assertEqual(head_table.yMin, -555) - self.assertEqual(head_table.yMax, 2163) + self.assertEqual(head_table.yMin, EXPECTED_YMIN) + self.assertEqual(head_table.yMax, EXPECTED_YMAX) + + def test_glyphs_ymin_ymax(self): + """Tests yMin and yMax of all glyphs to not go outside the range.""" + for font_file, font in zip(self.font_files, self.fonts): + glyf_table = font['glyf'] + for glyph_name in glyf_table.glyphOrder: + try: + y_min = glyf_table[glyph_name].yMin + y_max = glyf_table[glyph_name].yMax + except AttributeError: + continue + + self.assertTrue( + EXPECTED_YMIN <= y_min and y_max <= EXPECTED_YMAX, + ('The vertical metrics for glyph %s in %s exceed the ' + 'acceptable range: yMin=%d, yMax=%d' % ( + glyph_name, font_file, y_min, y_max))) def test_hhea_table_metrics(self): """Tests ascent, descent, and lineGap to be equal to Roboto v1 values. diff --git a/scripts/coverage_test.py b/scripts/coverage_test.py index 92974c7..3a1a3e0 100755 --- a/scripts/coverage_test.py +++ b/scripts/coverage_test.py @@ -1,4 +1,19 @@ #!/usr/bin/env python +# +# 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. + """Routines for checking character coverage of Roboto fonts. This scripts takes the name of the directory where the fonts are and checks @@ -117,6 +132,10 @@ def main(): full_coverage_required, exceptions)) + # Skip Unicode 8.0 characters + required_set = {ch for ch in required_set + if float(unicode_data.age(ch)) <= 7.0} + # Skip ASCII and C1 controls required_set -= set(range(0, 0x20) + range(0x7F, 0xA0)) diff --git a/scripts/force_yminmax.py b/scripts/force_yminmax.py index 36420b1..92284be 100755 --- a/scripts/force_yminmax.py +++ b/scripts/force_yminmax.py @@ -1,5 +1,20 @@ #!/usr/bin/python -"""Post-subset changes for Roboto.""" +# +# 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. + +"""Post-subset changes for Roboto for Android.""" import sys diff --git a/scripts/layout.py b/scripts/layout.py index fbcaa82..343f359 100644 --- a/scripts/layout.py +++ b/scripts/layout.py @@ -1,20 +1,58 @@ +# 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. + """Test general health of the fonts.""" import json from nototools import render +def _run_harfbuzz(text, font, language, extra_parameters=None): + """Run harfbuzz on some text and return the shaped list.""" + try: + # if extra_parameters is a string, split it into a list + extra_parameters = extra_parameters.split(' ') + except AttributeError: + pass + hb_output = render.run_harfbuzz_on_text( + text, font, language, extra_parameters) + return json.loads(hb_output) + + _advance_cache = {} -def get_advances(text, font): +def get_advances(text, font, extra_parameters=None): """Get a list of horizontal advances for text rendered in a font.""" try: - return _advance_cache[(text, font)] + return _advance_cache[(text, font, extra_parameters)] except KeyError: pass - hb_output = render.run_harfbuzz_on_text(text, font, None) - hb_output = json.loads(hb_output) + hb_output = _run_harfbuzz(text, font, None, extra_parameters) advances = [glyph['ax'] for glyph in hb_output] - _advance_cache[(text, font)] = advances + _advance_cache[(text, font, extra_parameters)] = advances return advances + +_shape_cache = {} +def get_glyphs(text, font, extra_parameters=None): + """Get a list of shaped glyphs for text rendered in a font.""" + try: + return _shape_cache[(text, font, extra_parameters)] + except KeyError: + pass + + hb_output = _run_harfbuzz(text, font, None, extra_parameters) + shapes = [glyph['g'] for glyph in hb_output] + _shape_cache[(text, font, extra_parameters)] = shapes + return shapes diff --git a/scripts/lib/fontbuild/Build.py b/scripts/lib/fontbuild/Build.py index 2a7e670..490fe45 100644 --- a/scripts/lib/fontbuild/Build.py +++ b/scripts/lib/fontbuild/Build.py @@ -1,17 +1,35 @@ -from FL import * +# 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. + + +from booleanOperations import BooleanOperationManager +from robofab.world import OpenFont from fontbuild.mix import Mix,Master,narrowFLGlyph -from fontbuild.instanceNames import setNames +from fontbuild.instanceNames import setNamesRF from fontbuild.italics import italicizeGlyph from fontbuild.convertCurves import glyphCurvesToQuadratic from fontbuild.mitreGlyph import mitreGlyph from fontbuild.generateGlyph import generateGlyph from fontTools.misc.transform import Transform -from fontbuild.kerning import generateFLKernClassesFromOTString -from fontbuild.features import CreateFeaFile +from fontbuild.kerning import makeKernFeature +from fontbuild.features import readFeatureFile, writeFeatureFile from fontbuild.markFeature import GenerateFeature_mark from fontbuild.mkmkFeature import GenerateFeature_mkmk +from fontbuild.decomposeGlyph import decomposeGlyph import ConfigParser import os +import sys class FontProject: @@ -29,7 +47,16 @@ class FontProject: self.ot_classes = open(self.basedir + "/" + self.config.get("res","ot_classesfile")).read() self.ot_kerningclasses = open(self.basedir + "/" + self.config.get("res","ot_kerningclassesfile")).read() #self.ot_features = open(self.basedir + "/" + self.config.get("res","ot_featuresfile")).read() + adobeGlyphList = open(self.basedir + "/" + self.config.get("res", "agl_glyphlistfile")).readlines() + self.adobeGlyphList = dict([line.split(";") for line in adobeGlyphList if not line.startswith("#")]) + # map exceptional glyph names in Roboto to names in the AGL + roboNames = ( + ('Obar', 'Ocenteredtilde'), ('obar', 'obarred'), + ('eturn', 'eturned'), ('Iota1', 'Iotaafrican')) + for roboName, aglName in roboNames: + self.adobeGlyphList[roboName] = self.adobeGlyphList[aglName] + self.builddir = "out" self.decompose = self.config.get("glyphs","decompose").split() self.predecompose = self.config.get("glyphs","predecompose").split() @@ -38,7 +65,9 @@ class FontProject: self.noItalic = self.config.get("glyphs","noitalic").split() self.buildnumber = self.loadBuildNumber() - self.buldVFBandFEA = False + self.buildOTF = False + self.autohintOTF = False + self.buildTTF = False def loadBuildNumber(self): @@ -58,7 +87,14 @@ class FontProject: versionFile.close() else: raise Exception("Empty build number") - + + def generateOutputPath(self, font, ext): + family = font.info.familyName.replace(" ", "") + style = font.info.styleName.replace(" ", "") + path = os.path.join(self.basedir, self.builddir, family + ext.upper()) + if not os.path.exists(path): + os.makedirs(path) + return os.path.join(path, "%s-%s.%s" % (family, style, ext)) def generateFont(self, mix, names, italic=False, swapSuffixes=None, stemWidth=185, kern=True): @@ -68,13 +104,9 @@ class FontProject: if isinstance( mix, Mix): f = mix.generateFont(self.basefont) else: - f = Font(mix) - fl.Add(f) - index = fl.ifont - fl.CallCommand(33239) # Sort glyphs by unicode + f = mix.copy() if italic == True: log(">> Italicizing") - fl.UpdateFont(fl.ifont) tweakAmmount = .085 narrowAmmount = .93 if names.find("Thin") != -1: @@ -82,8 +114,7 @@ class FontProject: if names.find("Condensed") != -1: narrowAmmount = .96 i = 0 - for g in f.glyphs: - + for g in f: i += 1 if i % 10 == 0: print g.name @@ -101,95 +132,65 @@ class FontProject: # print g.name # if self.thinfont != None: # narrowFLGlyph(g,self.thinfont.getGlyph(g.name),factor=narrowAmmount) - - if g.name != "eight" or g.name != "Q": - g.RemoveOverlap() - - # not sure why FontLab sometimes refuses, seems to work if called twice - - if (g.name in self.lessItalic): - italicizeGlyph(g, 9, stemWidth=stemWidth) + if g.name in self.lessItalic: + italicizeGlyph(f, g, 9, stemWidth=stemWidth) elif False == (g.name in self.noItalic): - italicizeGlyph(g, 10, stemWidth=stemWidth) + italicizeGlyph(f, g, 10, stemWidth=stemWidth) #elif g.name != ".notdef": # italicizeGlyph(g, 10, stemWidth=stemWidth) - - g.RemoveOverlap() - if g.width != 0: g.width += 10 - - fl.UpdateGlyph(i-1) - + if swapSuffixes != None: for swap in swapSuffixes: - swapList = [g.name for g in f.glyphs if g.name.endswith(swap)] + swapList = [g.name for g in f if g.name.endswith(swap)] for gname in swapList: print gname - swapGlyphs(f, gname.replace(swap,""), gname) + swapContours(f, gname.replace(swap,""), gname) for gname in self.predecompose: - g = f[f.FindGlyph(gname)] - if g != None: - g.Decompose() + if f.has_key(gname): + decomposeGlyph(f[gname]) log(">> Generating glyphs") - generateGlyphs(f, self.diacriticList) + generateGlyphs(f, self.diacriticList, self.adobeGlyphList) log(">> Copying features") - f.ot_classes = self.ot_classes - copyFeatures(self.basefont,f) - fl.UpdateFont(index) + readFeatureFile(f, self.ot_classes + self.basefont.features.text) log(">> Decomposing") for gname in self.decompose: - g = f[f.FindGlyph(gname)] - if g != None: - g.Decompose() - g.Decompose() - - setNames(f, n, foundry=self.config.get('main','foundry'), - version=self.config.get('main','version'), - build=self.buildnumber) + if f.has_key(gname): + decomposeGlyph(f[gname]) + + setNamesRF(f, n, foundry=self.config.get('main', 'foundry'), + version=self.config.get('main', 'version')) cleanCurves(f) - deleteGlyphs(f,self.deleteList) - + deleteGlyphs(f, self.deleteList) + if kern: log(">> Generating kern classes") - generateFLKernClassesFromOTString(f,self.ot_kerningclasses) - kern = f.MakeKernFeature() - kern_exist = False - for fea_id in range (len(f.features)): - if "kern" == f.features[fea_id].tag: - f.features[fea_id] = kern - kern_exist = True - if (False == kern_exist): - f.features.append(kern) - - directoryName = n[0].replace(" ","") - - if self.buldVFBandFEA: - log(">> Generating VFB files") - directoryPath = "%s/%s/%sVFB"%(self.basedir,self.builddir,directoryName) - if not os.path.exists(directoryPath): - os.makedirs(directoryPath) - flName = "%s/%s.vfb"%(directoryPath,f.font_name) - fl.GenerateFont(fl.ifont,ftFONTLAB,flName) - + readFeatureFile(f, self.ot_kerningclasses) + makeKernFeature(f, self.ot_kerningclasses) + log(">> Generating font files") - directoryPath = "%s/%s/%sTTF"%(self.basedir,self.builddir,directoryName) - if not os.path.exists(directoryPath): - os.makedirs(directoryPath) - ttfName = "%s/%s.ttf"%(directoryPath,f.font_name) - fl.GenerateFont(fl.ifont,ftTRUETYPE,ttfName) - - if self.buldVFBandFEA: - log(">> Generating FEA files") - GenerateFeature_mark(f) - GenerateFeature_mkmk(f) - feaName = "%s/%s.fea"%(directoryPath,f.font_name) - CreateFeaFile(f, feaName) - - f.modified = 0 - #fl.Close(index) + GenerateFeature_mark(f) + GenerateFeature_mkmk(f) + ufoName = self.generateOutputPath(f, "ufo") + f.save(ufoName) + + if self.buildOTF: + log(">> Generating OTF file") + newFont = OpenFont(ufoName) + otfName = self.generateOutputPath(f, "otf") + builtSuccessfully = saveOTF(newFont, otfName, autohint=self.autohintOTF) + if not builtSuccessfully: + sys.exit(1) + + if self.buildTTF: + log(">> Generating TTF file") + import fontforge + otFont = fontforge.open(otfName) + otFont.generate(self.generateOutputPath(f, "ttf")) + def transformGlyphMembers(g, m): g.width = int(g.width * m.a) @@ -211,62 +212,92 @@ def transformGlyphMembers(g, m): s.Transform(m) #c.scale = s -def swapGlyphs(f,gName1,gName2): +def swapContours(f,gName1,gName2): try: - g1 = f.glyphs[f.FindGlyph(gName1)] - g2 = f.glyphs[f.FindGlyph(gName2)] - except IndexError: - log("swapGlyphs failed for %s %s"%(gName1, gName2)) + g1 = f[gName1] + g2 = f[gName2] + except KeyError: + log("swapGlyphs failed for %s %s" % (gName1, gName2)) return - g3 = Glyph(g1) - - g1.Clear() - g1.Insert(g2) - g1.SetMetrics(g2.GetMetrics()) - - g2.Clear() - g2.Insert(g3) - g2.SetMetrics(g3.GetMetrics()) - + g3 = g1.copy() + + while g1.contours: + g1.removeContour(0) + for contour in g2.contours: + g1.appendContour(contour) + g1.width = g2.width + + while g2.contours: + g2.removeContour(0) + for contour in g3.contours: + g2.appendContour(contour) + g2.width = g3.width + + def log(msg): print msg -# def addOTFeatures(f): -# f.ot_classes = ot_classes - -def copyFeatures(f1, f2): - for ft in f1.features: - t = Feature(ft.tag, ft.value) - f2.features.append(t) - #f2.ot_classes = f1.ot_classes - f2.classes = [] - f2.classes = f1.classes -def generateGlyphs(f, glyphNames): +def generateGlyphs(f, glyphNames, glyphList={}): log(">> Generating diacritics") glyphnames = [gname for gname in glyphNames if not gname.startswith("#") and gname != ""] for glyphName in glyphNames: - generateGlyph(f, glyphName) + generateGlyph(f, glyphName, glyphList) def cleanCurves(f): log(">> Removing overlaps") - for g in f.glyphs: - g.UnselectAll() - g.RemoveOverlap() + for g in f: + removeGlyphOverlap(g) - log(">> Mitring sharp corners") - # for g in f.glyphs: + # log(">> Mitring sharp corners") + # for g in f: # mitreGlyph(g, 3., .7) - log(">> Converting curves to quadratic") - # for g in f.glyphs: + # log(">> Converting curves to quadratic") + # for g in f: # glyphCurvesToQuadratic(g) - -def deleteGlyphs(f,deleteList): - fl.Unselect() + + +def deleteGlyphs(f, deleteList): for name in deleteList: - glyphIndex = f.FindGlyph(name) - if glyphIndex != -1: - del f.glyphs[glyphIndex] - fl.UpdateFont() + if f.has_key(name): + f.removeGlyph(name) + + +def removeGlyphOverlap(glyph): + """Remove overlaps in contours from a glyph.""" + #TODO(jamesgk) verify overlaps exist first, as per library's recommendation + manager = BooleanOperationManager() + contours = glyph.contours + glyph.clearContours() + manager.union(contours, glyph.getPointPen()) + + +def saveOTF(font, destFile, autohint=False): + """Save a RoboFab font as an OTF binary using ufo2fdk. + + Returns True on success, False otherwise. + """ + + from ufo2fdk import OTFCompiler + + # glyphs with multiple unicode values must be split up, due to FontTool's + # use of a name -> UV dictionary during cmap compilation + for glyph in font: + if len(glyph.unicodes) > 1: + newUV = glyph.unicodes.pop() + newGlyph = font.newGlyph("uni%04X" % newUV) + newGlyph.appendComponent(glyph.name) + newGlyph.unicode = newUV + newGlyph.width = glyph.width + + compiler = OTFCompiler() + reports = compiler.compile(font, destFile, autohint=autohint) + if autohint: + print reports["autohint"] + print reports["makeotf"] + + successMsg = ("makeotfexe [NOTE] Wrote new font file '%s'." % + os.path.basename(destFile)) + return successMsg in reports["makeotf"] diff --git a/scripts/lib/fontbuild/alignpoints.py b/scripts/lib/fontbuild/alignpoints.py index ed502ed..1133716 100644 --- a/scripts/lib/fontbuild/alignpoints.py +++ b/scripts/lib/fontbuild/alignpoints.py @@ -1,3 +1,16 @@ +# 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 numpy as np @@ -145,4 +158,4 @@ def nearestPoint(a,b,c): # pts = np.array([[1,1],[2,2],[3,3],[4,4]]) # pts2 = np.array([[1,0],[2,0],[3,0],[4,0]]) # print alignPoints(pts2, start = pts[0], end = pts[0]+pts[0]) -# # print findCorner(pts,pts2)
\ No newline at end of file +# # print findCorner(pts,pts2) diff --git a/scripts/lib/fontbuild/anchors.py b/scripts/lib/fontbuild/anchors.py index 08d2d41..51fb57a 100644 --- a/scripts/lib/fontbuild/anchors.py +++ b/scripts/lib/fontbuild/anchors.py @@ -1,18 +1,25 @@ -#import numpy as np -from FL import * +# 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. -def getGlyph(gname,font): - index = font.FindGlyph(gname) - if index != -1: - return font.glyphs[index] - else: - return None +def getGlyph(gname, font): + return font[gname] if font.has_key(gname) else None -def getComponentByName(f,g,componentName): - componentIndex = f.FindGlyph(componentName) + +def getComponentByName(f, g, componentName): for c in g.components: - if c.index == componentIndex: + if c.baseGlyph == componentName: return c def getAnchorByName(g,anchorName): @@ -29,30 +36,21 @@ def moveMarkAnchors(f, g, anchorName, accentName, dx, dy): if g.anchors[i].name == "top": del g.anchors[i] break - anchor2 = Anchor() - #print anchor.x, dx, anchor.y, dy - anchor2.name = "top" - anchor2.x = anchor.x + int(dx) - anchor2.y = anchor.y + int(dy) - g.anchors.append(anchor2) + g.appendAnchor("top", (anchor.x + int(dx), anchor.y + int(dy))) elif anchorName in ["bottom", "bottomu"]: anchors = f[accentName].anchors for anchor in anchors: if "mkmkbottom_acc" == anchor.name: - for n in range(len(g.anchors)): - if g.anchors[n].name == "bottom": - del g.anchors[n] + for anc in g.anchors: + if anc.name == "bottom": + g.removeAnchor(anc) break - anchor2 = Anchor() - #print anchor.x, dx, anchor.y, dy - anchor2.name = "bottom" - anchor2.x = anchor.x + int(dx) - anchor2.y = anchor.y + int(dy) + x = anchor.x + int(dx) for anc in anchors: if "top" == anc.name: - anchor2.x = anc.x + int(dx) - g.anchors.append(anchor2) + x = anc.x + int(dx) + g.appendAnchor("bottom", (x, anchor.y + int(dy))) def alignComponentToAnchor(f,glyphName,baseName,accentName,anchorName): @@ -65,12 +63,11 @@ def alignComponentToAnchor(f,glyphName,baseName,accentName,anchorName): a2 = getAnchorByName(accent,"_" + anchorName) if a1 == None or a2 == None: return - offset = a1.p - a2.p - c = getComponentByName(f,g,accentName) - c.deltas[0].x = offset.x - c.deltas[0].y = offset.y - moveMarkAnchors(f, g, anchorName, accentName, offset.x, offset.y) - + offset = (a1.x - a2.x, a1.y - a2.y) + c = getComponentByName(f, g, accentName) + c.offset = offset + moveMarkAnchors(f, g, anchorName, accentName, offset[0], offset[1]) + def alignComponentsToAnchors(f,glyphName,baseName,accentNames): for a in accentNames: diff --git a/scripts/lib/fontbuild/convertCurves.py b/scripts/lib/fontbuild/convertCurves.py index e900a73..b6efd5c 100644 --- a/scripts/lib/fontbuild/convertCurves.py +++ b/scripts/lib/fontbuild/convertCurves.py @@ -1,4 +1,18 @@ #! /usr/bin/env python +# +# 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. """ Converts a cubic bezier curve to a quadratic spline with @@ -9,7 +23,13 @@ exactly two off curve points. import numpy from numpy import array,cross,dot from fontTools.misc import bezierTools -from FL import * +from robofab.objects.objectsRF import RSegment + +def replaceSegments(contour, segments): + while len(contour): + contour.removeSegment(0) + for s in segments: + contour.appendSegment(s.type, [(p.x, p.y) for p in s.points], s.smooth) def calcIntersect(a,b,c,d): numpy.seterr(all='raise') @@ -51,40 +71,32 @@ def convertToQuadratic(p0,p1,p2,p3): off2 = (on2 - off2) * OFFCURVE_VECTOR_CORRECTION + off2 return (on1,off1,off2,on2) -def cubicNodeToQuadratic(g,nid): +def cubicSegmentToQuadratic(c,sid): - node = g.nodes[nid] - if (node.type != nCURVE): - print "Node type not curve" + segment = c[sid] + if (segment.type != "curve"): + print "Segment type not curve" return - #pNode,junk = getPrevAnchor(g,nid) - pNode = g.nodes[nid-1] #assumes that a nCURVE type will always be proceeded by another point on the same contour - points = convertToQuadratic(pNode[0],node[1],node[2],node[0]) - points = [Point(p[0],p[1]) for p in points] - curve = [ - Node(nOFF, points[1]), - Node(nOFF, points[2]), - Node(nLINE,points[3]) ] - return curve + #pSegment,junk = getPrevAnchor(c,sid) + pSegment = c[sid-1] #assumes that a curve type will always be proceeded by another point on the same contour + points = convertToQuadratic(pSegment.points[-1],segment.points[0], + segment.points[1],segment.points[2]) + return RSegment( + 'qcurve', [[int(i) for i in p] for p in points[1:]], segment.smooth) def glyphCurvesToQuadratic(g): - nodes = [] - for i in range(len(g.nodes)): - n = g.nodes[i] - if n.type == nCURVE: - try: - newNodes = cubicNodeToQuadratic(g, i) - nodes = nodes + newNodes - except Exception: - print g.name, i - raise - else: - nodes.append(Node(g.nodes[i])) - g.Clear() - g.Insert(nodes) - - - - + for c in g: + segments = [] + for i in range(len(c)): + s = c[i] + if s.type == "curve": + try: + segments.append(cubicSegmentToQuadratic(c, i)) + except Exception: + print g.name, i + raise + else: + segments.append(s) + replaceSegments(c, segments) diff --git a/scripts/lib/fontbuild/curveFitPen.py b/scripts/lib/fontbuild/curveFitPen.py index d050479..7c232c0 100644 --- a/scripts/lib/fontbuild/curveFitPen.py +++ b/scripts/lib/fontbuild/curveFitPen.py @@ -1,4 +1,19 @@ #! /opt/local/bin/pythonw2.7 +# +# 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. + __all__ = ["SubsegmentPen","SubsegmentsToCurvesPen", "segmentGlyph", "fitGlyph"] @@ -399,4 +414,4 @@ if __name__ == '__main__': [1,1] ]) print np.array(p.renderCurve(pts,10)) * 10 -
\ No newline at end of file + diff --git a/scripts/lib/fontbuild/decomposeGlyph.py b/scripts/lib/fontbuild/decomposeGlyph.py new file mode 100644 index 0000000..2d7537c --- /dev/null +++ b/scripts/lib/fontbuild/decomposeGlyph.py @@ -0,0 +1,12 @@ +def decomposeGlyph(glyph): + """Moves the components of a glyph to its outline.""" + + font = glyph.getParent() + for component in glyph.components: + componentGlyph = font[component.baseGlyph] + for contour in componentGlyph: + contour = contour.copy() + contour.scale(component.scale) + contour.move(component.offset) + glyph.appendContour(contour) + glyph.clear(contours=False, anchors=False, guides=False) diff --git a/scripts/lib/fontbuild/features.py b/scripts/lib/fontbuild/features.py index 0e89a57..19fec0d 100755 --- a/scripts/lib/fontbuild/features.py +++ b/scripts/lib/fontbuild/features.py @@ -1,16 +1,189 @@ -import string
-from FL import *
-
-def CreateFeaFile(font, path):
- fea_text = font.ot_classes
- for cls in font.classes:
- text = "@" + cls + "];\n"
- text = string.replace(text, ":", "= [")
- text = string.replace(text, "\'", "")
- fea_text += text
- for fea in font.features:
- fea_text += fea.value
- fea_text = string.replace(fea_text, "\r\n", "\n")
- fout = open(path, "w")
- fout.write(fea_text)
- fout.close()
\ No newline at end of file +# 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()
diff --git a/scripts/lib/fontbuild/generateGlyph.py b/scripts/lib/fontbuild/generateGlyph.py index 7bb8ca2..4079046 100644 --- a/scripts/lib/fontbuild/generateGlyph.py +++ b/scripts/lib/fontbuild/generateGlyph.py @@ -1,5 +1,20 @@ +# 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 anchors import alignComponentsToAnchors -from FL import * from string import find def parseComposite(composite): @@ -16,27 +31,17 @@ def parseComposite(composite): accentNames = [i.split(":") for i in accents ] return (glyphName, baseName, accentNames, offset) -def shiftGlyphMembers(g, x): - g.Shift(Point(x,0)) - for c in g.components: - c.deltas[0].x = c.deltas[0].x + x def copyMarkAnchors(f, g, srcname, width): unicode_range = range(0x0030, 0x02B0) + range(0x1E00, 0x1EFF) anchors = f[srcname].anchors for anchor in anchors: if "top_dd" == anchor.name: - anchor1 = Anchor(anchor) - anchor1.x += width - g.anchors.append(anchor1) + g.appendAnchor(anchor.name, (anchor.x + width, anchor.y)) if "bottom_dd" == anchor.name: - anchor1 = Anchor(anchor) - anchor1.x += width - g.anchors.append(anchor1) + g.appendAnchor(anchor.name, (anchor.x + width, anchor.y)) if "top0315" == anchor.name: - anchor1 = Anchor(anchor) - anchor1.x += width - g.anchors.append(anchor1) + g.appendAnchor(anchor.name, (anchor.x + width, anchor.y)) if "top" == anchor.name: if g.unicode == None: if -1 == find(g.name, ".ccmp"): @@ -51,10 +56,7 @@ def copyMarkAnchors(f, g, srcname, width): if anc.name == "parent_top": parenttop_present = 1 if 0 == parenttop_present: - anchor2 = Anchor(anchor) - anchor2.name = "parent_top" -# anchor1.x += width - g.anchors.append(anchor2) + g.appendAnchor("parent_top", anchor.position) if "bottom" == anchor.name: if g.unicode == None: @@ -80,9 +82,7 @@ def copyMarkAnchors(f, g, srcname, width): # g.anchors.append(anchor1) # if "rhotichook" == anchor.name: - # anchor1 = Anchor(anchor) - # anchor1.x += width - # g.anchors.append(anchor1) + # g.appendAnchor(anchor.name, (anchor.x + width, anchor.y)) #print g.anchors for anchor in g.anchors: @@ -101,31 +101,45 @@ def copyMarkAnchors(f, g, srcname, width): anchor_top = Anchor(anchor_parent_top) anchor_top.name = "top" g.anchors.append(anchor_top) - -def generateGlyph(f,gname): - if gname.find("_") != -1: - generateString = gname - g = f.GenerateGlyph(generateString) - if f.FindGlyph(g.name) == -1: - f.glyphs.append(g) - return g + + +def generateGlyph(f,gname,glyphList={}): + glyphName, baseName, accentNames, offset = parseComposite(gname) + + if baseName.find("_") != -1: + g = f.newGlyph(glyphName) + for componentName in baseName.split("_"): + g.appendComponent(componentName, (g.width, 0)) + g.width += f[componentName].width + setUnicodeValue(g, glyphList) + else: - glyphName, baseName, accentNames, offset = parseComposite(gname) - components = [baseName] + [i[0] for i in accentNames] - if len(components) == 1: - components.append("NONE") - generateString = "%s=%s" %("+".join(components), glyphName) - g = f.GenerateGlyph(generateString) - copyMarkAnchors(f, g, baseName, offset[1] + offset[0]) - if f.FindGlyph(g.name) == -1: - f.glyphs.append(g) - g1 = f.glyphs[f.FindGlyph(g.name)] - if (offset[0] != 0 or offset[1] != 0): - g1.width += offset[1] + offset[0] - shiftGlyphMembers(g1,offset[0]) + if not f.has_key(glyphName): + try: + f.compileGlyph(glyphName, baseName, accentNames) + except KeyError as e: + print ("KeyError raised for composition rule '%s', likely %s " + "anchor not found in glyph '%s'" % (gname, e, baseName)) + return + g = f[glyphName] + setUnicodeValue(g, glyphList) + copyMarkAnchors(f, g, baseName, offset[1] + offset[0]) + if offset[0] != 0 or offset[1] != 0: + g.width += offset[1] + offset[0] + g.move((offset[0], 0), anchors=False) if len(accentNames) > 0: - alignComponentsToAnchors(f,glyphName,baseName,accentNames) - return g + alignComponentsToAnchors(f, glyphName, baseName, accentNames) + else: + print ("Existing glyph '%s' found in font, ignoring composition " + "rule '%s'" % (glyphName, gname)) -# generateGlyph(fl.font,"A+ogonek=Aogonek") -# fl.UpdateFont()
\ No newline at end of file + +def setUnicodeValue(glyph, glyphList): + """Try to ensure glyph has a unicode value -- used by FDK to make OTFs.""" + + if glyph.name in glyphList: + glyph.unicode = int(glyphList[glyph.name], 16) + else: + uvNameMatch = re.match("uni([\dA-F]{4})$", glyph.name) + if uvNameMatch: + glyph.unicode = int(uvNameMatch.group(1), 16) diff --git a/scripts/lib/fontbuild/instanceNames.py b/scripts/lib/fontbuild/instanceNames.py index 7aa4daa..eab3024 100644 --- a/scripts/lib/fontbuild/instanceNames.py +++ b/scripts/lib/fontbuild/instanceNames.py @@ -1,3 +1,18 @@ +# 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. + + from datetime import date import re from random import randint @@ -17,7 +32,7 @@ class InstanceNames: def __init__(self,names): if type(names) == type(" "): names = names.split("/") - #print names + #print names self.longfamily = names[0] self.longstyle = names[1] self.shortstyle = names[2] @@ -62,6 +77,8 @@ class InstanceNames: f.info.openTypeNamePreferredSubfamilyName = self.longstyle f.info.macintoshFONDName = re.sub(' ','',self.longfamily) + " " + re.sub(' ','',self.longstyle) + if self.italic: + f.info.italicAngle = -12.0 def setFLNames(self,flFont): @@ -186,8 +203,9 @@ def setNames(f,names,foundry="",version="1.0",build="0000"): i = InstanceNames(names) i.setFLNames(f) -def setNamesRF(f,names,foundry=""): + +def setNamesRF(f, names, foundry="", version="1.0"): InstanceNames.foundry = foundry i = InstanceNames(names) - i.setRFNames(f) -
\ No newline at end of file + version, versionMinor = [int(num) for num in version.split(".")] + i.setRFNames(f, version=version, versionMinor=versionMinor) diff --git a/scripts/lib/fontbuild/italics.py b/scripts/lib/fontbuild/italics.py index 1a42808..c889bd5 100644 --- a/scripts/lib/fontbuild/italics.py +++ b/scripts/lib/fontbuild/italics.py @@ -1,19 +1,31 @@ +# 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. + + from fontTools.misc.transform import Transform -from robofab.world import CurrentFont from robofab.world import RFont from time import clock import numpy as np import math from alignpoints import alignCorners -def italicizeGlyph(g, angle=10, stemWidth=185): +def italicizeGlyph(f, g, angle=10, stemWidth=185): unic = g.unicode #save unicode - - f = CurrentFont() + glyph = f[g.name] - - slope = np.tanh([math.pi * angle / 180]) - + slope = np.tanh(math.pi * angle / 180) + # determine how far on the x axis the glyph should slide # to compensate for the slant. -600 is a magic number # that assumes a 2048 unit em square @@ -24,13 +36,14 @@ def italicizeGlyph(g, angle=10, stemWidth=185): if len(glyph) > 0: g2 = italicize(f[g.name], angle, xoffset=xoffset, stemWidth=stemWidth) - f.insertGlyph(g2, g.name) - + f.insertGlyph(g2, g.name) + transformFLGlyphMembers(f[g.name], m) - + if unic > 0xFFFF: #restore unicode g.unicode = unic + def italicize(glyph, angle=12, stemWidth=180, xoffset=-50): CURVE_CORRECTION_WEIGHT = .03 CORNER_WEIGHT = 10 @@ -272,4 +285,4 @@ def condenseGlyph(glyph, scale=.8, stemWidth=185): gOut = fitGlyph(glyph, gOut, subsegments) for i,seg in enumerate(gOut): gOut[i].points[0].y = glyph[i].points[0].y - return gOut
\ No newline at end of file + return gOut diff --git a/scripts/lib/fontbuild/kerning.py b/scripts/lib/fontbuild/kerning.py index cdece40..302c330 100644 --- a/scripts/lib/fontbuild/kerning.py +++ b/scripts/lib/fontbuild/kerning.py @@ -1,26 +1,112 @@ -import re -from FL import * - -def markKernClassesLR(f): - for i in range(len(f.classes)): - classname = f.classes[i].split(':', 1).pop(0).strip() - if classname.startswith('_'): - l = 0 - r = 0 - if classname.endswith('_L'): - l = 1 - elif classname.endswith('_R'): - r = 1 - elif classname.endswith('_LR'): - l = 1 - r = 1 - f.SetClassFlags(i, l, r) - fl.UpdateFont() - -def generateFLKernClassesFromOTString(f,classString): - classString.replace("\r","\n") - rx = re.compile(r"@(_[\w]+)\s*=\s*\[\s*(\w+?)\s+(.*?)\]\s*;") - classes = ["%s : %s' %s" %(m[0],m[1],m[2]) for m in rx.findall(classString)] - f.classes = classes - markKernClassesLR(f) +# 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. + +from feaTools import parser +from feaTools.writers.baseWriter import AbstractFeatureWriter + + +class KernFeatureWriter(AbstractFeatureWriter): + """Generates a kerning feature based on glyph class definitions. + + Uses the kerning rules contained in an RFont's kerning attribute, as well as + glyph classes from parsed OTF text. Class-based rules are set based on the + existing rules for their key glyphs. + """ + + def __init__(self, font): + self.kerning = font.kerning + self.leftClasses = [] + self.rightClasses = [] + self.classSizes = {} + + def write(self, linesep="\n"): + """Write kern feature.""" + + # maintain collections of different rule types + leftClassKerning, rightClassKerning, classPairKerning = {}, {}, {} + for leftName, leftContents in self.leftClasses: + leftKey = leftContents[0] + + # collect rules with two classes + for rightName, rightContents in self.rightClasses: + rightKey = rightContents[0] + pair = leftKey, rightKey + kerningVal = self.kerning[pair] + if kerningVal is None: + continue + classPairKerning[leftName, rightName] = kerningVal + self.kerning.remove(pair) + + # collect rules with left class and right glyph + for pair, kerningVal in self.kerning.getLeft(leftKey): + leftClassKerning[leftName, pair[1]] = kerningVal + self.kerning.remove(pair) + + # collect rules with left glyph and right class + for rightName, rightContents in self.rightClasses: + rightKey = rightContents[0] + for pair, kerningVal in self.kerning.getRight(rightKey): + rightClassKerning[pair[0], rightName] = kerningVal + self.kerning.remove(pair) + + # write the feature + self.ruleCount = 0 + lines = ["feature kern {"] + lines.append(self._writeKerning(self.kerning, linesep)) + lines.append(self._writeKerning(leftClassKerning, linesep, True)) + lines.append(self._writeKerning(rightClassKerning, linesep, True)) + lines.append(self._writeKerning(classPairKerning, linesep)) + lines.append("} kern;") + return linesep.join(lines) + + def _writeKerning(self, kerning, linesep, enum=False): + """Write kerning rules for a mapping of pairs to values.""" + + lines = [] + enum = "enum " if enum else "" + pairs = kerning.items() + pairs.sort() + for (left, right), val in pairs: + if enum: + rulesAdded = (self.classSizes.get(left, 1) * + self.classSizes.get(right, 1)) + else: + rulesAdded = 1 + self.ruleCount += rulesAdded + if self.ruleCount > 2048: + lines.append(" subtable;") + self.ruleCount = rulesAdded + lines.append(" %spos %s %s %d;" % (enum, left, right, val)) + return linesep.join(lines) + + def classDefinition(self, name, contents): + """Store a class definition as either a left- or right-hand class.""" + + if not name.startswith("@_"): + return + info = (name, contents) + if name.endswith("_L"): + self.leftClasses.append(info) + elif name.endswith("_R"): + self.rightClasses.append(info) + self.classSizes[name] = len(contents) + + +def makeKernFeature(font, text): + """Add a kern feature to the font, using a KernFeatureWriter.""" + + writer = KernFeatureWriter(font) + parser.parseFeatures(writer, text) + font.features.text += writer.write() diff --git a/scripts/lib/fontbuild/markFeature.py b/scripts/lib/fontbuild/markFeature.py index b4a8061..b617ef6 100755 --- a/scripts/lib/fontbuild/markFeature.py +++ b/scripts/lib/fontbuild/markFeature.py @@ -1,4 +1,20 @@ -from FL import *
+# 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.
+
+
+from fontbuild.features import updateFeature
+
aliases = [["uni0430", "a"], ["uni0435", "e"], ["uni0440", "p"], ["uni0441", "c"], ["uni0445", "x"], ["uni0455", "s"], ["uni0456", "i"], ["uni0471", "psi"]]
@@ -11,9 +27,9 @@ def GetAliaseName(gname): def CreateAccNameList(font, acc_anchor_name, bCombAccentOnly = True):
#combrange = range(0x0300,0x0370) + range(0x1AB0,0x1ABF) + range(0x1DC0,0x1DE0)
lst = []
- for g in font.glyphs:
+ for g in font:
if bCombAccentOnly and g.width != 0: #((g.unicode < 0x0300) or (g.unicode > 0x362)):
- continue
+ continue
for anchor in g.anchors:
if acc_anchor_name == anchor.name:
lst.append(g.name)
@@ -21,7 +37,7 @@ def CreateAccNameList(font, acc_anchor_name, bCombAccentOnly = True): def CreateAccGlyphList(font, acc_list, acc_anchor_name):
g_list = []
- for g in font.glyphs:
+ for g in font:
if g.name in acc_list:
for anchor in g.anchors:
if acc_anchor_name == anchor.name:
@@ -32,29 +48,28 @@ def CreateAccGlyphList(font, acc_list, acc_anchor_name): def CreateGlyphList(font, acc_list, anchor_name):
g_list = []
- for g in font.glyphs:
+ for g in font:
if g.name in acc_list:
continue
for anchor in g.anchors:
if anchor_name == anchor.name:
g_list.append([g.name, anchor.x, anchor.y])
- break
+ break
return g_list
def Create_mark_lookup(accent_g_list, base_g_list, lookupname, acc_class, lookAliases = True):
txt = "lookup " + lookupname + " {\n"
for acc in accent_g_list:
- txt += " markClass " + acc[0] + " <anchor " + `acc[1]` + " " + `acc[2]` + "> " + acc_class +";\n"
-
+ txt += " markClass " + acc[0] + " <anchor " + `int(acc[1])` + " " + `int(acc[2])` + "> " + acc_class +";\n"
for base in base_g_list:
- txt += " pos base " + base[0] + " <anchor " + `base[1]` + " " + `base[2]` + "> mark " + acc_class + ";\n"
+ txt += " pos base " + base[0] + " <anchor " + `int(base[1])` + " " + `int(base[2])` + "> mark " + acc_class + ";\n"
if (lookAliases):
base2 = GetAliaseName(base[0])
if (None == base2):
continue
- txt += " pos base " + base2 + " <anchor " + `base[1]` + " " + `base[2]` + "> mark " + acc_class + ";\n"
+ txt += " pos base " + base2 + " <anchor " + `int(base[1])` + " " + `int(base[2])` + "> mark " + acc_class + ";\n"
txt += "} " + lookupname + ";\n"
@@ -77,7 +92,7 @@ def GenerateFeature_mark(font): ]
text = "feature mark {\n"
-
+
for n in range(len(combination_anchor_list)):
accent_name_list = []
@@ -98,15 +113,5 @@ def GenerateFeature_mark(font): text += Create_mark_lookup(accent_mark_list, base_mark_list, lookupname, classname, expand_to_composits)
text += "} mark;\n"
- mark = Feature("mark", text)
-
- not_exist = True
- for n in range(len(font.features)):
- if ('mark' == font.features[n].tag):
- font.features[n] = mark
- not_exist = False
-
- if (not_exist):
- font.features.append(mark)
-
+ updateFeature(font, "mark", text)
diff --git a/scripts/lib/fontbuild/mitreGlyph.py b/scripts/lib/fontbuild/mitreGlyph.py index ab68e4e..d0834ed 100644 --- a/scripts/lib/fontbuild/mitreGlyph.py +++ b/scripts/lib/fontbuild/mitreGlyph.py @@ -1,46 +1,42 @@ +# 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. + + """Mitre Glyph: mitreSize : Length of the segment created by the mitre. The default is 4. -maxAngle : Maximum angle in radians at which nodes will be mitred. The default is .9 (about 50 degrees). +maxAngle : Maximum angle in radians at which segments will be mitred. The default is .9 (about 50 degrees). Works for both inside and outside angles """ -from FL import * import math - -def getContours(g): - nLength = len(g.nodes) - contours = [] - cid = -1 - for i in range(nLength): - n = g.nodes[i] - if n.type == nMOVE: - cid += 1 - contours.append([]) - contours[cid].append(n) - return contours +from robofab.objects.objectsRF import RPoint, RSegment +from fontbuild.convertCurves import replaceSegments def getTangents(contours): tmap = [] for c in contours: clen = len(c) for i in range(clen): - n = c[i] - p = Point(n.x, n.y) - nn = c[(i + 1) % clen] - pn = c[(clen + i - 1) % clen] - if nn.type == nCURVE: - np = Point(nn[1].x,nn[1].y) - else: - np = Point(nn.x,nn.y) - if n.type == nCURVE: - pp = Point(n[2].x,n[2].y) - else: - pp = Point(pn.x,pn.y) - nVect = Point(-p.x + np.x, -p.y + np.y) - pVect = Point(-p.x + pp.x, -p.y + pp.y) - tmap.append((pVect,nVect)) + s = c[i] + p = s.points[-1] + ns = c[(i + 1) % clen] + ps = c[(clen + i - 1) % clen] + np = ns.points[1] if ns.type == 'curve' else ns.points[-1] + pp = s.points[2] if s.type == 'curve' else ps.points[-1] + tmap.append((pp - p, np - p)) return tmap def normalizeVector(p): @@ -48,13 +44,13 @@ def normalizeVector(p): if m != 0: return p*(1/m) else: - return Point(0,0) + return RPoint(0,0) def getMagnitude(p): return math.sqrt(p.x*p.x + p.y*p.y) def getDistance(v1,v2): - return getMagnitude(Point(v1.x - v2.x, v1.y - v2.y)) + return getMagnitude(RPoint(v1.x - v2.x, v1.y - v2.y)) def getAngle(v1,v2): angle = math.atan2(v1.y,v1.x) - math.atan2(v2.y,v2.x) @@ -83,39 +79,33 @@ def getMitreOffset(n,v1,v2,mitreSize=4,maxAngle=.9): return radius = mitreSize / abs(getDistance(v1,v2)) - offset1 = Point(round(v1.x * radius), round(v1.y * radius)) - offset2 = Point(round(v2.x * radius), round(v2.y * radius)) + offset1 = RPoint(round(v1.x * radius), round(v1.y * radius)) + offset2 = RPoint(round(v2.x * radius), round(v2.y * radius)) return offset1, offset2 def mitreGlyph(g,mitreSize,maxAngle): if g == None: return - contours = getContours(g) - tangents = getTangents(contours) - nodes = [] - needsMitring = False - nid = -1 - for c in contours: - for n in c: - nid += 1 - v1, v2 = tangents[nid] - off = getMitreOffset(n,v1,v2,mitreSize,maxAngle) - n1 = Node(n) + tangents = getTangents(g.contours) + sid = -1 + for c in g.contours: + segments = [] + needsMitring = False + for s in c: + sid += 1 + v1, v2 = tangents[sid] + off = getMitreOffset(s,v1,v2,mitreSize,maxAngle) + s1 = s.copy() if off != None: offset1, offset2 = off - n2 = Node(nLINE, Point(n.x + offset2.x, n.y + offset2.y)) - n1[0].x += offset1.x - n1[0].y += offset1.y - nodes.append(n1) - nodes.append(n2) + p2 = s.points[-1] + offset2 + s2 = RSegment('line', [(p2.x, p2.y)]) + s1.points[0] += offset1 + segments.append(s1) + segments.append(s2) needsMitring = True else: - nodes.append(n1) - if needsMitring: - g.Clear() - g.Insert(nodes) - -fl.SetUndo() -mitreGlyph(fl.glyph,8.,.9) -fl.UpdateGlyph()
\ No newline at end of file + segments.append(s1) + if needsMitring: + replaceSegments(c, segments) diff --git a/scripts/lib/fontbuild/mix.py b/scripts/lib/fontbuild/mix.py index c55512a..2c741fb 100644 --- a/scripts/lib/fontbuild/mix.py +++ b/scripts/lib/fontbuild/mix.py @@ -1,6 +1,25 @@ -from FL import * +# 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. + + from numpy import array, append import copy +import json +from robofab.objects.objectsRF import RPoint +from robofab.world import OpenFont +from decomposeGlyph import decomposeGlyph + class FFont: "Font wrapper for floating point operations" @@ -9,32 +28,37 @@ class FFont: self.glyphs = {} self.hstems = [] self.vstems = [] + self.kerning = {} if isinstance(f,FFont): #self.glyphs = [g.copy() for g in f.glyphs] for key,g in f.glyphs.iteritems(): self.glyphs[key] = g.copy() self.hstems = list(f.hstems) self.vstems = list(f.vstems) + self.kerning = dict(f.kerning) elif f != None: self.copyFromFont(f) - - def copyFromFont(self,f): - for g in f.glyphs: + + def copyFromFont(self, f): + for g in f: self.glyphs[g.name] = FGlyph(g) - self.hstems = [s for s in f.stem_snap_h[0]] - self.vstems = [s for s in f.stem_snap_v[0]] - - - def copyToFont(self,f): - for g in f.glyphs: + self.hstems = [s for s in f.info.postscriptStemSnapH] + self.vstems = [s for s in f.info.postscriptStemSnapV] + self.kerning = f.kerning.asDict() + + + def copyToFont(self, f): + for g in f: try: gF = self.glyphs[g.name] gF.copyToGlyph(g) except: print "Copy to glyph failed for" + g.name - f.stem_snap_h[0] = self.hstems - f.stem_snap_v[0] = self.vstems - + f.info.postscriptStemSnapH = self.hstems + f.info.postscriptStemSnapV = self.vstems + for pair in self.kerning: + f.kerning[pair] = self.kerning[pair] + def getGlyph(self, gname): try: return self.glyphs[gname] @@ -46,7 +70,6 @@ class FFont: def addDiff(self,b,c): newFont = FFont(self) - for key,g in newFont.glyphs.iteritems(): gB = b.getGlyph(key) gC = c.getGlyph(key) @@ -54,17 +77,15 @@ class FFont: newFont.glyphs[key] = g.addDiff(gB,gC) except: print "Add diff failed for '%s'" %key - return newFont class FGlyph: "provides a temporary floating point compatible glyph data structure" def __init__(self, g=None): - self.nodes = [] + self.contours = [] self.width = 0. self.components = [] - self.kerning = [] self.anchors = [] if g != None: self.copyFromGlyph(g) @@ -76,27 +97,24 @@ class FGlyph: self.width = len(valuesX) valuesX.append(g.width) for c in g.components: - self.components.append((len(valuesX),len(valuesY))) - valuesX.append(c.scale.x) - valuesY.append(c.scale.y) - valuesX.append(c.delta.x) - valuesY.append(c.delta.y) - + self.components.append((len(valuesX), len(valuesY))) + valuesX.append(c.scale[0]) + valuesY.append(c.scale[1]) + valuesX.append(c.offset[0]) + valuesY.append(c.offset[1]) + for a in g.anchors: self.anchors.append((len(valuesX), len(valuesY))) valuesX.append(a.x) valuesY.append(a.y) - - for i in range(len(g.nodes)): - self.nodes.append([]) - for j in range (len(g.nodes[i])): - self.nodes[i].append( (len(valuesX), len(valuesY)) ) - valuesX.append(g.nodes[i][j].x) - valuesY.append(g.nodes[i][j].y) - - for k in g.kerning: - self.kerning.append(KerningPair(k)) - + + for i in range(len(g)): + self.contours.append([]) + for j in range (len(g[i].points)): + self.contours[i].append((len(valuesX), len(valuesY))) + valuesX.append(g[i].points[j].x) + valuesY.append(g[i].points[j].y) + self.dataX = array(valuesX) self.dataY = array(valuesY) @@ -104,24 +122,23 @@ class FGlyph: g.width = self._derefX(self.width) if len(g.components) == len(self.components): for i in range(len(self.components)): - g.components[i].scale.x = self._derefX( self.components[i][0] + 0) - g.components[i].scale.y = self._derefY( self.components[i][1] + 0) - g.components[i].deltas[0].x = self._derefX( self.components[i][0] + 1) - g.components[i].deltas[0].y = self._derefY( self.components[i][1] + 1) - g.kerning = [] + g.components[i].scale = (self._derefX(self.components[i][0] + 0), + self._derefY(self.components[i][1] + 0)) + g.components[i].offset = (self._derefX(self.components[i][0] + 1), + self._derefY(self.components[i][1] + 1)) if len(g.anchors) == len(self.anchors): for i in range(len(self.anchors)): g.anchors[i].x = self._derefX( self.anchors[i][0]) g.anchors[i].y = self._derefY( self.anchors[i][1]) - for k in self.kerning: - g.kerning.append(KerningPair(k)) - for i in range( len(g.nodes)) : - for j in range (len(g.nodes[i])): - g.nodes[i][j].x = self._derefX( self.nodes[i][j][0] ) - g.nodes[i][j].y = self._derefY( self.nodes[i][j][1] ) - - def isCompatible(self,g): - return len(self.dataX) == len(g.dataX) and len(self.dataY) == len(g.dataY) and len(g.nodes) == len(self.nodes) + for i in range(len(g)) : + for j in range (len(g[i].points)): + g[i].points[j].x = self._derefX(self.contours[i][j][0]) + g[i].points[j].y = self._derefY(self.contours[i][j][1]) + + def isCompatible(self, g): + return (len(self.dataX) == len(g.dataX) and + len(self.dataY) == len(g.dataY) and + len(g.contours) == len(self.contours)) def __add__(self,g): if self.isCompatible(g): @@ -172,15 +189,13 @@ class FGlyph: gF.dataX += (g.dataX - gF.dataX) * v.x gF.dataY += (g.dataY - gF.dataY) * v.y - gF.kerning = interpolateKerns(self,g,v) return gF def copy(self): ng = FGlyph() - ng.nodes = list(self.nodes) + ng.contours = list(self.contours) ng.width = self.width ng.components = list(self.components) - ng.kerning = list(self.kerning) ng.anchors = list(self.anchors) ng.dataX = self.dataX.copy() ng.dataY = self.dataY.copy() @@ -188,10 +203,10 @@ class FGlyph: return ng def _derefX(self,id): - return int(round(self.dataX[id])) + return self.dataX[id] def _derefY(self,id): - return int(round(self.dataY[id])) + return self.dataY[id] def addDiff(self,gB,gC): newGlyph = self + (gB - gC) @@ -200,22 +215,21 @@ class FGlyph: class Master: - - - def __init__(self,font=None,v=0,ifont=None, kernlist=None, overlay=None): - if isinstance(font,FFont): + + def __init__(self, font=None, v=0, kernlist=None, overlay=None, + anchorPath=None): + if isinstance(font, FFont): self.font = None self.ffont = font elif isinstance(font,str): - self.openFont(font,overlay) + self.openFont(font,overlay, anchorPath) elif isinstance(font,Mix): self.font = font else: self.font = font - self.ifont = ifont self.ffont = FFont(font) if isinstance(v,float) or isinstance(v,int): - self.v = Point(v,v) + self.v = RPoint(v, v) else: self.v = v if kernlist != None: @@ -227,37 +241,30 @@ class Master: and not k[0] == ""] #TODO implement class based kerning / external kerning file - def openFont(self, path, overlayPath=None): - fl.Open(path,True) - self.ifont = fl.ifont - for g in fl.font.glyphs: + def openFont(self, path, overlayPath=None, anchorPath=None): + self.font = OpenFont(path) + for g in self.font: size = len(g) csize = len(g.components) if (size > 0 and csize > 0): - g.Decompose() + decomposeGlyph(g) - self.ifont = fl.ifont - self.font = fl.font if overlayPath != None: - fl.Open(overlayPath,True) - ifont = self.ifont + overlayFont = OpenFont(overlayPath) font = self.font - overlayIfont = fl.ifont - overlayFont = fl.font + for overlayGlyph in overlayFont: + font.insertGlyph(overlayGlyph) + + # work around a bug with vfb2ufo in which anchors are dropped from + # glyphs containing components and no contours. "anchorPath" should + # point to the output of src/v2/get_dropped_anchors.py + if anchorPath: + anchorData = json.load(open(anchorPath)) + for glyphName, anchors in anchorData.items(): + glyph = self.font[glyphName] + for name, (x, y) in anchors.items(): + glyph.appendAnchor(str(name), (x, y)) - for overlayGlyph in overlayFont.glyphs: - glyphIndex = font.FindGlyph(overlayGlyph.name) - if glyphIndex != -1: - oldGlyph = Glyph(font.glyphs[glyphIndex]) - kernlist = [KerningPair(k) for k in oldGlyph.kerning] - font.glyphs[glyphIndex] = Glyph(overlayGlyph) - font.glyphs[glyphIndex].kerning = kernlist - if 0 == overlayGlyph: - font.glyphs[glyphIndex].width = 0 - else: - font.glyphs.append(overlayGlyph) - fl.UpdateFont(ifont) - fl.Close(overlayIfont) self.ffont = FFont(self.font) @@ -265,7 +272,7 @@ class Mix: def __init__(self,masters,v): self.masters = masters if isinstance(v,float) or isinstance(v,int): - self.v = Point(v,v) + self.v = RPoint(v,v) else: self.v = v @@ -283,21 +290,20 @@ class Mix: ffont = FFont(self.masters[0].ffont) for key,g in ffont.glyphs.iteritems(): ffont.glyphs[key] = self.mixGlyphs(key) + ffont.kerning = self.mixKerns() return ffont def generateFont(self, baseFont): - newFont = Font(baseFont) + newFont = baseFont.copy() #self.mixStems(newFont) todo _ fix stems code - for g in newFont.glyphs: + for g in newFont: gF = self.mixGlyphs(g.name) if gF == None: g.mark = True else: - # FIX THIS #print gF.name, g.name, len(gF.nodes),len(g.nodes),len(gF.components),len(g.components) - try: - gF.copyToGlyph(g) - except: - "Nodes incompatible" + gF.copyToGlyph(g) + newFont.kerning.clear() + newFont.kerning.update(self.mixKerns() or {}) return newFont def mixGlyphs(self,gname): @@ -309,6 +315,17 @@ class Mix: if gA != None: return gA.copy() + def getKerning(self, master): + if isinstance(master.font, Mix): + return master.font.mixKerns() + return master.ffont.kerning + + def mixKerns(self): + masters = self.masters + kA, kB = self.getKerning(masters[0]), self.getKerning(masters[-1]) + return interpolateKerns(kA, kB, self.v) + + def narrowFLGlyph(g, gThin, factor=.75): gF = FGlyph(g) if not isinstance(gThin,FGlyph): @@ -327,19 +344,13 @@ def interpolate(a,b,v,e=0): le = a+(b-a)*v # linear easing return le + (qe-le) * e -def interpolateKerns(gA,gB,v): - kerns = [] - for kA in gA.kerning: - key = kA.key - matchedKern = None - for kB in gA.kerning: - if key == kB.key: - matchedKern = kB - break +def interpolateKerns(kA, kB, v): + kerns = {} + for pair in kA.keys(): + matchedKern = kB.get(pair) # if matchedkern == None: # matchedkern = Kern(kA) # matchedkern.value = 0 if matchedKern != None: - kernValue = interpolate(kA.value, matchedKern.value, v.x) - kerns.append(KerningPair(kA.key,kernValue)) - return kerns
\ No newline at end of file + kerns[pair] = interpolate(kA[pair], matchedKern, v.x) + return kerns diff --git a/scripts/lib/fontbuild/mkmkFeature.py b/scripts/lib/fontbuild/mkmkFeature.py index fd2a968..16f9313 100755 --- a/scripts/lib/fontbuild/mkmkFeature.py +++ b/scripts/lib/fontbuild/mkmkFeature.py @@ -1,8 +1,24 @@ -from FL import *
+# 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.
+
+
+from fontbuild.features import updateFeature
+
def CreateAccNameList(font, acc_anchor_name):
lst = []
- for g in font.glyphs:
+ for g in font:
for anchor in g.anchors:
if acc_anchor_name == anchor.name:
lst.append(g.name)
@@ -10,7 +26,7 @@ def CreateAccNameList(font, acc_anchor_name): def CreateAccGlyphList(font, acc_list, acc_anchor_name):
g_list = []
- for g in font.glyphs:
+ for g in font:
if g.name in acc_list:
for anchor in g.anchors:
if acc_anchor_name == anchor.name:
@@ -21,7 +37,7 @@ def CreateAccGlyphList(font, acc_list, acc_anchor_name): def CreateGlyphList(font, acc_list, anchor_name):
g_list = []
- for g in font.glyphs:
+ for g in font:
for anchor in g.anchors:
if anchor_name == anchor.name:
g_list.append([g.name, anchor.x, anchor.y])
@@ -32,10 +48,10 @@ def Create_mkmk1(accent_g_list, base_g_list, lookupname, acc_class): txt = "lookup " + lookupname + " {\n"
#acc_class = "@MC_mkmk"
for acc in accent_g_list:
- txt += " markClass " + acc[0] + " <anchor " + `acc[1]` + " " + `acc[2]` + "> " + acc_class +";\n"
+ txt += " markClass " + acc[0] + " <anchor " + `int(acc[1])` + " " + `int(acc[2])` + "> " + acc_class +";\n"
for base in base_g_list:
- txt += " pos mark " + base[0] + " <anchor " + `base[1]` + " " + `base[2]` + "> mark " + acc_class + ";\n"
+ txt += " pos mark " + base[0] + " <anchor " + `int(base[1])` + " " + `int(base[2])` + "> mark " + acc_class + ";\n"
txt += "} " + lookupname + ";\n"
@@ -67,13 +83,5 @@ def GenerateFeature_mkmk(font): text += Create_mkmk1(accent_mark_list, base_mark_list, "mkmk2", "@MC_mkmk_bottom")
text += "} mkmk;\n"
- mkmk = Feature("mkmk", text)
-
- not_exist = True
- for n in range(len(font.features)):
- if ('mkmk' == font.features[n].tag):
- font.features[n] = mkmk
- not_exist = False
- if (not_exist):
- font.features.append(mkmk)
+ updateFeature(font, "mkmk", text)
diff --git a/scripts/render.sh b/scripts/render.sh index 703c21f..2266b2a 100755 --- a/scripts/render.sh +++ b/scripts/render.sh @@ -1,13 +1,50 @@ #!/bin/bash +# +# 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. HARFBUZZ=$HOME/harfbuzz +FONTDIR=$(dirname $0)/../hinted + +input_file=$1 + function render { - echo weT͟Hər | $HARFBUZZ/util/hb-view --font-file=$1 --output-format=png --output-file=$2.png + cat $input_file | $HARFBUZZ/util/hb-view --font-file=$1 --output-format=png --output-file=$2.png --font-size=200 } -render ../out/RobotoTTF/Roboto-Regular.ttf original -render Roboto-Regular.ttf modified +render $FONTDIR/Roboto-Thin.ttf 100 +render $FONTDIR/Roboto-Light.ttf 300 +render $FONTDIR/Roboto-Regular.ttf 400 +render $FONTDIR/Roboto-Medium.ttf 500 +render $FONTDIR/Roboto-Bold.ttf 700 +render $FONTDIR/Roboto-Black.ttf 900 + +render $FONTDIR/Roboto-ThinItalic.ttf i100 +render $FONTDIR/Roboto-LightItalic.ttf i300 +render $FONTDIR/Roboto-Italic.ttf i400 +render $FONTDIR/Roboto-MediumItalic.ttf i500 +render $FONTDIR/Roboto-BoldItalic.ttf i700 +render $FONTDIR/Roboto-BlackItalic.ttf i900 + +render $FONTDIR/RobotoCondensed-Light.ttf c300 +render $FONTDIR/RobotoCondensed-Regular.ttf c400 +render $FONTDIR/RobotoCondensed-Bold.ttf c700 + +render $FONTDIR/RobotoCondensed-LightItalic.ttf ci300 +render $FONTDIR/RobotoCondensed-Italic.ttf ci400 +render $FONTDIR/RobotoCondensed-BoldItalic.ttf ci700 eog *.png diff --git a/scripts/roboto_data.py b/scripts/roboto_data.py index 922c464..2e4f2e9 100644 --- a/scripts/roboto_data.py +++ b/scripts/roboto_data.py @@ -1,4 +1,18 @@ -"""Post-build changes for Roboto for Android.""" +# 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. + +"""General module for Roboto-specific data and methods.""" import os from os import path diff --git a/scripts/run_android_tests.py b/scripts/run_android_tests.py index e277268..e978343 100755 --- a/scripts/run_android_tests.py +++ b/scripts/run_android_tests.py @@ -1,53 +1,167 @@ #!/usr/bin/python +# +# 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. + """Test assumptions that Android relies on.""" +import glob +import json import unittest +from fontTools import ttLib from nototools import coverage +from nototools import font_data +from nototools import render +from nototools import unicode_data + -import common_tests +def load_fonts(): + """Load all fonts built for Android.""" + all_font_files = glob.glob('out/android/*.ttf') + all_fonts = [ttLib.TTFont(font) for font in all_font_files] + assert len(all_font_files) == 18 + return all_font_files, all_fonts -FONTS = common_tests.load_fonts( - ['out/android/*.ttf'], - expected_count=18) -class TestItalicAngle(common_tests.TestItalicAngle): - loaded_fonts = FONTS +class TestVerticalMetrics(unittest.TestCase): + """Test the vertical metrics of fonts.""" + def setUp(self): + _, self.fonts = load_fonts() -class TestMetaInfo(common_tests.TestMetaInfo): - loaded_fonts = FONTS + def test_ymin_ymax(self): + """Tests yMin and yMax to be equal to what Android expects.""" + for font in self.fonts: + head_table = font['head'] + self.assertEqual(head_table.yMin, -555) + self.assertEqual(head_table.yMax, 2163) -class TestNames(common_tests.TestNames): - loaded_fonts = FONTS - family_name = 'Roboto' +class TestDigitWidths(unittest.TestCase): + """Tests the width of digits.""" + def setUp(self): + _, self.fonts = load_fonts() + self.digits = [ + 'zero', 'one', 'two', 'three', 'four', + 'five', 'six', 'seven', 'eight', 'nine'] -class TestDigitWidths(common_tests.TestDigitWidths): - loaded_fonts = FONTS + def test_digit_widths(self): + """Tests all decimal digits to make sure they have the same width.""" + for font in self.fonts: + hmtx_table = font['hmtx'] + widths = [hmtx_table[digit][0] for digit in self.digits] + self.assertEqual(len(set(widths)), 1) -class TestCharacterCoverage(common_tests.TestCharacterCoverage): - loaded_fonts = FONTS +class TestCharacterCoverage(unittest.TestCase): + """Tests character coverage.""" + + def setUp(self): + _, self.fonts = load_fonts() + self.LEGACY_PUA = frozenset({0xEE01, 0xEE02, 0xF6C3}) def test_lack_of_arrows_and_combining_keycap(self): - """Tests that arrows and combining keycap are not in Android fonts.""" + """Tests that arrows and combining keycap are not in the fonts.""" for font in self.fonts: charset = coverage.character_set(font) self.assertNotIn(0x20E3, charset) # COMBINING ENCLOSING KEYCAP self.assertNotIn(0x2191, charset) # UPWARDS ARROW self.assertNotIn(0x2193, charset) # DOWNWARDS ARROW + def test_lack_of_unassigned_chars(self): + """Tests that unassigned characters are not in the fonts.""" + for font in self.fonts: + charset = coverage.character_set(font) + self.assertNotIn(0x2072, charset) + self.assertNotIn(0x2073, charset) + self.assertNotIn(0x208F, charset) -class TestLigatures(common_tests.TestLigatures): - loaded_fonts = FONTS + def test_inclusion_of_sound_recording_copyright(self): + """Tests that sound recording copyright symbol is in the fonts.""" + for font in self.fonts: + charset = coverage.character_set(font) + self.assertIn( + 0x2117, charset, # SOUND RECORDING COPYRIGHT + 'U+2117 not found in %s.' % font_data.font_name(font)) + def test_inclusion_of_legacy_pua(self): + """Tests that legacy PUA characters remain in the fonts.""" + for font in self.fonts: + charset = coverage.character_set(font) + for char in self.LEGACY_PUA: + self.assertIn(char, charset) -class TestVerticalMetrics(common_tests.TestVerticalMetrics): - loaded_fonts = FONTS + def test_non_inclusion_of_other_pua(self): + """Tests that there are not other PUA characters except legacy ones.""" + for font in self.fonts: + charset = coverage.character_set(font) + pua_chars = { + char for char in charset + if 0xE000 <= char <= 0xF8FF or 0xF0000 <= char <= 0x10FFFF} + self.assertTrue(pua_chars <= self.LEGACY_PUA) + + +class TestSpacingMarks(unittest.TestCase): + """Tests that spacing marks are indeed spacing.""" + + def setUp(self): + self.font_files, _ = load_fonts() + charset = coverage.character_set(self.font_files[0]) + self.marks_to_test = [char for char in charset + if unicode_data.category(char) in ['Lm', 'Sk']] + self.advance_cache = {} + + def get_advances(self, text, font): + """Get a list of horizontal advances for text rendered in a font.""" + try: + return self.advance_cache[(text, font)] + except KeyError: + hb_output = render.run_harfbuzz_on_text(text, font, '') + hb_output = json.loads(hb_output) + advances = [glyph['ax'] for glyph in hb_output] + self.advance_cache[(text, font)] = advances + return advances + + def test_individual_spacing_marks(self): + """Tests that spacing marks are spacing by themselves.""" + for font in self.font_files: + print 'Testing %s for stand-alone spacing marks...' % font + for mark in self.marks_to_test: + mark = unichr(mark) + advances = self.get_advances(mark, font) + assert len(advances) == 1 + self.assertNotEqual(advances[0], 0) + + def test_spacing_marks_in_combination(self): + """Tests that spacing marks do not combine with base letters.""" + for font in self.font_files: + print 'Testing %s for spacing marks in combination...' % font + for base_letter in (u'A\u00C6BCDEFGHIJKLMNO\u00D8\u01A0PRST' + u'U\u01AFVWXYZ' + u'a\u00E6bcdefghi\u0131j\u0237klmn' + u'o\u00F8\u01A1prs\u017Ftu\u01B0vwxyz' + u'\u03D2'): + print 'Testing %s combinations' % base_letter + for mark in self.marks_to_test: + mark = unichr(mark) + advances = self.get_advances(base_letter + mark, font) + self.assertEqual(len(advances), 2, + 'The sequence <%04X, %04X> combines, ' + 'but it should not' % (ord(base_letter), ord(mark))) if __name__ == '__main__': unittest.main() - diff --git a/scripts/run_exhaustive_tests.py b/scripts/run_exhaustive_tests.py index 9bf7011..e7425ad 100755 --- a/scripts/run_exhaustive_tests.py +++ b/scripts/run_exhaustive_tests.py @@ -1,4 +1,19 @@ #!/usr/bin/python +# +# 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. + """Time-consuming tests for general health of the fonts.""" import glob @@ -61,5 +76,38 @@ class TestSpacingMarks(unittest.TestCase): 'but it should not' % (ord(base_letter), ord(mark))) +class TestSoftDottedChars(unittest.TestCase): + """Tests that soft-dotted characters lose their dots.""" + + def setUp(self): + self.font_files, _ = load_fonts() + charset = coverage.character_set(self.font_files[0]) + self.marks_to_test = [char for char in charset + if unicode_data.combining(char) == 230] + self.advance_cache = {} + + def test_combinations(self): + """Tests that soft-dotted characters lose their dots when combined.""" + + return # FIXME: Test is currently disabled, since the fonts fail it + + for font in self.font_files: + print 'Testing %s for soft-dotted combinations...' % font + + # TODO: replace the following list with actual derivation based on + # Unicode's soft-dotted property + for base_letter in (u'ij\u012F\u0249\u0268\u029D\u02B2\u03F3\u0456' + u'\u0458\u1D62\u1D96\u1DA4\u1DA8\u1E2D\u1ECB' + u'\u2071\u2C7C'): + print 'Testing %s combinations' % base_letter.encode('UTF-8') + for mark in self.marks_to_test: + mark = unichr(mark) + letter_only = layout.get_glyphs(base_letter, font) + combination = layout.get_glyphs(base_letter + mark, font) + self.assertNotEqual(combination[0], letter_only[0], + "The sequence <%04X, %04X> doesn't lose its dot, " + "but it should" % (ord(base_letter), ord(mark))) + + if __name__ == '__main__': unittest.main() diff --git a/scripts/run_general_tests.py b/scripts/run_general_tests.py index 8b56305..68ca269 100755 --- a/scripts/run_general_tests.py +++ b/scripts/run_general_tests.py @@ -1,4 +1,19 @@ #!/usr/bin/python +# +# 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. + """Test general health of the fonts.""" import unittest @@ -6,7 +21,7 @@ import unittest import common_tests FONTS = common_tests.load_fonts( - ['out/RobotoTTF/*.ttf', 'out/RobotoCondensedTTF/*.ttf'], + ['hinted/*.ttf'], expected_count=18) class TestItalicAngle(common_tests.TestItalicAngle): @@ -31,6 +46,15 @@ class TestLigatures(common_tests.TestLigatures): loaded_fonts = FONTS +class TestFeatures(common_tests.TestFeatures): + loaded_fonts = FONTS + + +class TestVerticalMetrics(common_tests.TestVerticalMetrics): + loaded_fonts = FONTS + test_ymin_ymax = None + test_hhea_table_metrics = None + if __name__ == '__main__': unittest.main() diff --git a/scripts/run_web_tests.py b/scripts/run_web_tests.py index 3bcfd97..3ff483a 100755 --- a/scripts/run_web_tests.py +++ b/scripts/run_web_tests.py @@ -1,4 +1,19 @@ #!/usr/bin/python +# +# 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. + """Test assumptions that web fonts rely on.""" import unittest diff --git a/scripts/subset_for_web.py b/scripts/subset_for_web.py index 574df58..1dce277 100755 --- a/scripts/subset_for_web.py +++ b/scripts/subset_for_web.py @@ -40,42 +40,6 @@ def read_charlist(filename): charlist.append(char) return charlist -LATIN = ( - range(0x0020, 0x007F) + range(0x00A0, 0x0100) + - [0x0131, 0x0152, 0x0153, 0x02C6, 0x02DA, 0x02DC, 0x2013, 0x2014, 0x2018, - 0x2019, 0x201A, 0x201C, 0x201D, 0x201E, 0x2022, 0x2039, 0x203A, 0x2044, - 0x2074, 0x20AC, 0x2212, 0x2215]) - -CYRILLIC = ( - range(0x0400, 0x0460) + - [0x0490, 0x0491, 0x04B0, 0x04B1, 0x20BD, 0x2116]) - -SUBSETS = { - 'cyrillic': LATIN + CYRILLIC, - 'cyrillic-ext': ( - LATIN + CYRILLIC + range(0x0460, 0x0530) + [0x20B4] + - range(0x2DE0, 0x2E00) + range(0xA640, 0xA6A0)), - 'greek': LATIN + range(0x0370, 0x0400), - 'greek-ext': LATIN + range(0x0384, 0x0400) + range(0x1F00, 0x2000), - 'latin': LATIN, - 'latin-ext': ( - LATIN + range(0x0100, 0x0370) + - [0x02BC, 0x0300, 0x0301, 0x0303, 0x030F] + - range(0x1D00, 0x1F00) + - [0x2026] + - range(0x2070, 0x20D0) + - range(0x2C60, 0x2C80) + - range(0xA700, 0xA800)), - 'menu': [ord(c) for c in u' ()DEKNQRabcfgoprtuvĸ΄ΕάαεηικλνКаилрцốữ'], - 'vietnamese': ( - LATIN + - [0x0102, 0x0103, 0x0110, 0x0111, 0x0128, 0x0129, 0x0168, 0x0169, - 0x01A0, 0x01A1, 0x01AF, 0x01B0, 0x02D8, 0x0309, 0x0323] + - range(0x1EA0, 0x1EFA) + [0x20AB]), -} - -SUBSETS = {k: frozenset(v) for k, v in SUBSETS.iteritems()} - def main(argv): """Subset the first argument to second, dropping unused parts of the font. @@ -96,12 +60,6 @@ def main(argv): include=charlist, options={'layout_features': features_to_keep}) - for suffix in SUBSETS.keys(): - subset_filename = target_filename.replace('ttf', suffix) - subset.subset_font( - target_filename, subset_filename, - include=SUBSETS[suffix]) - if __name__ == '__main__': main(sys.argv) diff --git a/scripts/temporary_touchups.py b/scripts/temporary_touchups.py index e284f37..9997adc 100644 --- a/scripts/temporary_touchups.py +++ b/scripts/temporary_touchups.py @@ -1,3 +1,17 @@ +# 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. + """Temporary post-build changes for Roboto.""" from datetime import date diff --git a/scripts/touchup_for_android.py b/scripts/touchup_for_android.py index c67d048..187a432 100755 --- a/scripts/touchup_for_android.py +++ b/scripts/touchup_for_android.py @@ -1,29 +1,67 @@ #!/usr/bin/python +# +# 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. + """Post-build changes for Roboto for Android.""" +import os +from os import path import sys from fontTools import ttLib from nototools import font_data -import temporary_touchups + +def apply_temporary_fixes(font): + """Apply some temporary fixes. + """ + # Fix version number from buildnumber.txt + from datetime import date + + build_number_txt = path.join( + path.dirname(__file__), os.pardir, 'res', 'buildnumber.txt') + build_number = open(build_number_txt).read().strip() + + version_record = 'Version 2.%s; %d' % (build_number, date.today().year) + + for record in font['name'].names: + if record.nameID == 5: + if record.platformID == 1 and record.platEncID == 0: # MacRoman + record.string = version_record + elif record.platformID == 3 and record.platEncID == 1: + # Windows UCS-2 + record.string = version_record.encode('UTF-16BE') + else: + assert False def apply_android_specific_fixes(font): """Apply fixes needed for Android.""" - # Remove tab, combining keycap, and the arrows from the cmap table. - # - # Arrows are removed to maximize consistency of arrows, since the rest - # of the arrows come from Noto Symbols. - # - # And here are the bugs for the other two issues: - # https://code.google.com/a/google.com/p/roboto/issues/detail?id=51 + # Set ascent, descent, and lineGap values to Android K values + hhea = font['hhea'] + hhea.ascent = 1900 + hhea.descent = -500 + hhea.lineGap = 0 + + # Remove combining keycap and the arrows from the cmap table: # https://code.google.com/a/google.com/p/roboto/issues/detail?id=52 font_data.delete_from_cmap(font, [ - 0x0009, # tab - 0x20E3, # combining keycap - 0x2191, 0x2193, # vertical arrows - ]) + 0x20E3, # COMBINING ENCLOSING KEYCAP + 0x2191, # UPWARDS ARROW + 0x2193, # DOWNWARDS ARROW + ]) # Drop tables not useful on Android for table in ['LTSH', 'hdmx', 'VDMX', 'gasp']: @@ -34,8 +72,7 @@ def apply_android_specific_fixes(font): def correct_font(source_font_name, target_font_name): """Corrects metrics and other meta information.""" font = ttLib.TTFont(source_font_name) - temporary_touchups.apply_temporary_fixes(font) - temporary_touchups.update_version_and_revision(font) + apply_temporary_fixes(font) apply_android_specific_fixes(font) font.save(target_font_name) diff --git a/scripts/touchup_for_glass.py b/scripts/touchup_for_glass.py deleted file mode 100755 index 2bc5128..0000000 --- a/scripts/touchup_for_glass.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/python -"""Post-build changes for Glass.""" - -import os -from os import path -import sys - -from fontTools import ttLib -from nototools import font_data - - -def apply_glass_hacks(font): - """Apply glass-specific hacks to a fonttools font instance.""" - # Really ugly hack, expecting the proportional digit one to be at - # glyph01965. - font_data.add_to_cmap(font, {0xEE00: 'glyph01965'}) - - # Fix version number from buildnumber.txt - from datetime import date - - build_number_txt = path.join( - path.dirname(__file__), os.pardir, 'res', 'buildnumber.txt') - build_number = open(build_number_txt).read().strip() - - version_record = 'Version 2.0%s; %d; Glass' % ( - build_number, date.today().year) - - for record in font['name'].names: - if record.nameID == 5: - if record.platformID == 1 and record.platEncID == 0: # MacRoman - record.string = version_record - elif record.platformID == 3 and record.platEncID == 1: - # Windows UCS-2 - record.string = version_record.encode('UTF-16BE') - else: - assert False - - -def hack_font(source_font_name, target_font_name): - """Hacks the source font and saves it as the target.""" - font = ttLib.TTFont(source_font_name) - apply_glass_hacks(font) - font.save(target_font_name) - - -def main(argv): - """Hack the font specified in the command line.""" - hack_font(argv[1], argv[2]) - - -if __name__ == "__main__": - main(sys.argv) diff --git a/scripts/touchup_for_web.py b/scripts/touchup_for_web.py index 628374a..c4f75c1 100755 --- a/scripts/touchup_for_web.py +++ b/scripts/touchup_for_web.py @@ -1,4 +1,19 @@ #!/usr/bin/python +# +# 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. + """Post-build web fonts changes for Roboto.""" import sys |