summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
Diffstat (limited to 'scripts')
-rw-r--r--scripts/build-v2.py241
-rw-r--r--scripts/common_tests.py93
-rwxr-xr-xscripts/coverage_test.py19
-rwxr-xr-xscripts/force_yminmax.py17
-rw-r--r--scripts/layout.py48
-rw-r--r--scripts/lib/fontbuild/Build.py277
-rw-r--r--scripts/lib/fontbuild/alignpoints.py15
-rw-r--r--scripts/lib/fontbuild/anchors.py63
-rw-r--r--scripts/lib/fontbuild/convertCurves.py76
-rw-r--r--scripts/lib/fontbuild/curveFitPen.py17
-rw-r--r--scripts/lib/fontbuild/decomposeGlyph.py12
-rwxr-xr-xscripts/lib/fontbuild/features.py205
-rw-r--r--scripts/lib/fontbuild/generateGlyph.py106
-rw-r--r--scripts/lib/fontbuild/instanceNames.py26
-rw-r--r--scripts/lib/fontbuild/italics.py35
-rw-r--r--scripts/lib/fontbuild/kerning.py136
-rwxr-xr-xscripts/lib/fontbuild/markFeature.py49
-rw-r--r--scripts/lib/fontbuild/mitreGlyph.py104
-rw-r--r--scripts/lib/fontbuild/mix.py219
-rwxr-xr-xscripts/lib/fontbuild/mkmkFeature.py38
-rwxr-xr-xscripts/render.sh43
-rw-r--r--scripts/roboto_data.py16
-rwxr-xr-xscripts/run_android_tests.py156
-rwxr-xr-xscripts/run_exhaustive_tests.py48
-rwxr-xr-xscripts/run_general_tests.py26
-rwxr-xr-xscripts/run_web_tests.py15
-rwxr-xr-xscripts/subset_for_web.py42
-rw-r--r--scripts/temporary_touchups.py14
-rwxr-xr-xscripts/touchup_for_android.py65
-rwxr-xr-xscripts/touchup_for_glass.py52
-rwxr-xr-xscripts/touchup_for_web.py15
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