summaryrefslogtreecommitdiff
path: root/scripts
diff options
context:
space:
mode:
authorRoozbeh Pournader <roozbeh@google.com>2014-12-01 13:36:03 -0800
committerJames Godfrey-Kittle <jamesgk@google.com>2015-04-16 12:16:23 -0700
commit6ca0d935997f3a1f1bd53169a6b2f10ab4ae75d3 (patch)
tree485ce7f391971627cf08257f77b783fa8bb1afb4 /scripts
parent6f8d5ea14c2d9fe8916bfd85c67fbf254b5b78da (diff)
Add targets and scripts for web fonts.
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/force_yminmax.py2
-rwxr-xr-xscripts/run_web_tests.py143
-rw-r--r--scripts/subset_for_web.py53
-rwxr-xr-xscripts/touchup_for_web.py185
4 files changed, 382 insertions, 1 deletions
diff --git a/scripts/force_yminmax.py b/scripts/force_yminmax.py
index 454414c..36420b1 100755
--- a/scripts/force_yminmax.py
+++ b/scripts/force_yminmax.py
@@ -1,5 +1,5 @@
#!/usr/bin/python
-"""Post-subset changes for Roboto for Android."""
+"""Post-subset changes for Roboto."""
import sys
diff --git a/scripts/run_web_tests.py b/scripts/run_web_tests.py
new file mode 100755
index 0000000..779abdc
--- /dev/null
+++ b/scripts/run_web_tests.py
@@ -0,0 +1,143 @@
+#!/usr/bin/python
+"""Test assumptions that Android relies on."""
+
+import glob
+import unittest
+
+from fontTools import ttLib
+from nototools import coverage
+from nototools import font_data
+
+
+def load_fonts():
+ """Load all web fonts."""
+ all_font_files = glob.glob('out/web/*.ttf')
+ all_fonts = [ttLib.TTFont(font) for font in all_font_files]
+ assert len(all_font_files) == 12
+ return all_font_files, all_fonts
+
+
+class TestVerticalMetrics(unittest.TestCase):
+ """Test the vertical metrics of fonts."""
+
+ def setUp(self):
+ _, self.fonts = load_fonts()
+
+ def test_ymin_ymax(self):
+ """Tests yMin and yMax to be equal to the old values."""
+ for font in self.fonts:
+ head_table = font['head']
+ self.assertEqual(head_table.yMin, -555)
+ self.assertEqual(head_table.yMax, 2163)
+
+ def test_other_metrics(self):
+ """Tests other vertical metrics to be equal to the old values."""
+ for font in self.fonts:
+ hhea_table = font['hhea']
+ self.assertEqual(hhea_table.descent, -500)
+ self.assertEqual(hhea_table.ascent, 1900)
+
+ os2_table = font['OS/2']
+ self.assertEqual(os2_table.sTypoDescender, -512)
+ self.assertEqual(os2_table.sTypoAscender, 1536)
+ self.assertEqual(os2_table.sTypoLineGap, 102)
+ self.assertEqual(os2_table.usWinDescent, 512)
+ self.assertEqual(os2_table.usWinAscent, 1946)
+
+
+class TestCharacterCoverage(unittest.TestCase):
+ """Tests character coverage."""
+
+ def setUp(self):
+ _, self.fonts = load_fonts()
+ self.LEGACY_PUA = frozenset({0xEE01, 0xEE02, 0xF6C3})
+
+ 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)
+
+ 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)
+
+ 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 TestItalicAngle(unittest.TestCase):
+ """Test the italic angle of fonts."""
+
+ def setUp(self):
+ _, self.fonts = load_fonts()
+
+ def test_italic_angle(self):
+ """Tests the italic angle of fonts to be correct."""
+ for font in self.fonts:
+ post_table = font['post']
+ if 'Italic' in font_data.font_name(font):
+ expected_angle = -12.0
+ else:
+ expected_angle = 0.0
+ self.assertEqual(post_table.italicAngle, expected_angle)
+
+
+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']
+
+ 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 TestHints(unittest.TestCase):
+ """Tests hints."""
+
+ def setUp(self):
+ _, self.fonts = load_fonts()
+
+ # FIXME: remove as soon as issue 100 is fixed
+ bad_glyphs = ['uniFB01', 'uniFB02', 'uniFB03',
+ 'uniFB04', 'uniFFFC', 'uni048C']
+ self.known_missing_hints = [
+ (g, 'Roboto Light Italic') for g in bad_glyphs]
+
+ def test_digit_widths(self):
+ """Tests all glyphs and makes sure non-composite ones have hints."""
+ missing_hints = []
+ for font in self.fonts:
+ glyf_table = font['glyf']
+ for glyph_name in font.getGlyphOrder():
+ glyph = glyf_table[glyph_name]
+ if glyph.numberOfContours <= 0: # composite or empty glyph
+ continue
+ if len(glyph.program.bytecode) <= 0:
+ missing_hints.append(
+ (glyph_name, font_data.font_name(font)))
+
+ self.assertTrue(missing_hints <= self.known_missing_hints)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/scripts/subset_for_web.py b/scripts/subset_for_web.py
new file mode 100644
index 0000000..e100dfc
--- /dev/null
+++ b/scripts/subset_for_web.py
@@ -0,0 +1,53 @@
+#!/usr/bin/python
+#
+# Copyright 2014 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.
+
+"""Subset for web fonts."""
+
+__author__ = 'roozbeh@google.com (Roozbeh Pournader)'
+
+import sys
+
+from nototools import subset
+
+
+def read_charlist(filename):
+ """Returns a list of characters read from a charset text file."""
+ with open(filename) as datafile:
+ charlist = []
+ for line in datafile:
+ if '#' in line:
+ line = line[:line.index('#')]
+ line = line.strip()
+ if not line:
+ continue
+ if line.startswith('U+'):
+ line = line[2:]
+ char = int(line, 16)
+ charlist.append(char)
+ return charlist
+
+
+def main(argv):
+ """Subset the first argument to second, dropping unused parts of the font.
+ """
+ charlist = read_charlist('res/web_charset.txt')
+ # Add private use characters for legacy reasons
+ charlist += [0xEE01, 0xEE02, 0xF6C3]
+ subset.subset_font(argv[1], argv[2], include=charlist)
+
+
+if __name__ == '__main__':
+ main(sys.argv)
diff --git a/scripts/touchup_for_web.py b/scripts/touchup_for_web.py
new file mode 100755
index 0000000..0f30728
--- /dev/null
+++ b/scripts/touchup_for_web.py
@@ -0,0 +1,185 @@
+#!/usr/bin/python
+"""Post-build web fonts changes for Roboto."""
+
+import collections
+import os
+from os import path
+import sys
+
+from fontTools import ttLib
+from nototools import font_data
+from nototools import unicode_data
+
+
+def drop_lookup(table, lookup_number):
+ """Drop a lookup from an OpenType table by number.
+
+ Actually remove pointers from features to the lookup, which should be less
+ intrusive.
+ """
+ for feature in table.table.FeatureList.FeatureRecord:
+ if lookup_number in feature.Feature.LookupListIndex:
+ feature.Feature.LookupListIndex.remove(lookup_number)
+ feature.Feature.LookupCount -= 1
+
+
+def get_font_name(font):
+ """Gets the name of the font from the name table."""
+ return font_data.get_name_records(font)[4]
+
+
+DIGITS = ['zero', 'one', 'two', 'three', 'four',
+ 'five', 'six', 'seven', 'eight', 'nine']
+
+def fix_digit_widths(font):
+ """Change all digit widths in the font to be the same."""
+ hmtx_table = font['hmtx']
+ widths = [hmtx_table[digit][0] for digit in DIGITS]
+ if len(set(widths)) > 1:
+ width_counter = collections.Counter(widths)
+ most_common_width = width_counter.most_common(1)[0][0]
+ print 'Digit widths were %s.' % repr(widths)
+ print 'Setting all glyph widths to %d.' % most_common_width
+ for digit in DIGITS:
+ assert abs(hmtx_table[digit][0] - most_common_width) <= 1
+ hmtx_table[digit][0] = most_common_width
+
+
+_MAP_SPACING_TO_COMBINING = {
+ 'acute': 'acutecomb',
+ 'breve': 'brevenosp',
+ 'caron': 'uni030C',
+ 'cedilla': 'cedillanosp',
+ 'circumflex': 'circumflexnosp',
+ 'dieresis': 'dieresisnosp',
+ 'dotaccent': 'dotnosp',
+ 'grave': 'gravecomb',
+ 'hungarumlaut': 'acutedblnosp',
+ 'macron': 'macroncomb',
+ 'ogonek': 'ogoneknosp',
+ 'tilde': 'tildecomb',
+ 'ring': 'ringnosp',
+ 'tonos': 'acutecomb',
+ 'uni02F3': 'ringsubnosp',
+}
+
+def fix_ccmp_lookup(font):
+ """Fixes the broken ccmp lookup."""
+ cmap = font_data.get_cmap(font)
+ reverse_cmap = {name: code for (code, name) in cmap.items()}
+
+ # Where we know the bad 'ccmp' is
+ ccmp_lookup = font['GSUB'].table.LookupList.Lookup[2]
+ assert ccmp_lookup.LookupType == 4
+ assert ccmp_lookup.SubTableCount == 1
+ ligatures = ccmp_lookup.SubTable[0].ligatures
+ for first_char, ligtable in ligatures.iteritems():
+ ligatures_to_delete = []
+ for index, ligature in enumerate(ligtable):
+ assert len(ligature.Component) == 1
+ component = ligature.Component[0]
+ if (component.endswith('comb')
+ or component in ['commaaccent',
+ 'commaaccentrotate',
+ 'ringacute']):
+ continue
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=54
+ if first_char == 'a' and component == 'uni02BE':
+ ligatures_to_delete.append(index)
+ continue
+ char = reverse_cmap[component]
+ general_category = unicode_data.category(char)
+ if general_category != 'Mn': # not a combining mark
+ ligature.Component[0] = _MAP_SPACING_TO_COMBINING[component]
+ ligatures[first_char] = [
+ ligature for (index, ligature) in enumerate(ligtable)
+ if index not in ligatures_to_delete]
+
+
+def apply_temporary_fixes(font):
+ """Apply some temporary fixes.
+ """
+ # Make sure macStyle is correct
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=8
+ font_name = get_font_name(font)
+ bold = ('Bold' in font_name) or ('Black' in font_name)
+ italic = 'Italic' in font_name
+ font['head'].macStyle = (italic << 1) | bold
+
+ # Mark the font free for installation, embedding, etc.
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=29
+ os2 = font['OS/2']
+ os2.fsType = 0
+
+ # Set the font vendor to Google
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=46
+ os2.achVendID = 'GOOG'
+
+ # Drop the lookup forming the ff ligature
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=47
+ drop_lookup(font['GSUB'], 5)
+
+ # Correct the ccmp lookup to use combining marks instead of spacing ones
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=48
+ fix_ccmp_lookup(font)
+
+ # Fix the digit widths
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=49
+ fix_digit_widths(font)
+
+ # Add cmap for U+2117 SOUND RECORDING COPYRIGHT
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=44
+ font_data.add_to_cmap(font, {0x2117: 'published'})
+
+ # Fix version number from buildnumber.txt
+ # https://code.google.com/a/google.com/p/roboto/issues/detail?id=50
+ 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' % (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_web_specific_fixes(font):
+ """Apply fixes needed for web fonts."""
+ # Set ascent, descent, and lineGap values to Android K values
+ hhea = font['hhea']
+ hhea.ascent = 1900
+ hhea.descent = -500
+ hhea.lineGap = 0
+
+ os2 = font['OS/2']
+ os2.sTypoAscender = 1536
+ os2.sTypoDescender = -512
+ os2.sTypoLineGap = 102
+ os2.usWinAscent = 1946
+ os2.usWinDescent = 512
+
+
+def correct_font(source_font_name, target_font_name):
+ """Corrects metrics and other meta information."""
+ font = ttLib.TTFont(source_font_name)
+# apply_temporary_fixes(font)
+ apply_web_specific_fixes(font)
+ font.save(target_font_name)
+
+
+def main(argv):
+ """Correct the font specified in the command line."""
+ correct_font(argv[1], argv[2])
+
+
+if __name__ == "__main__":
+ main(sys.argv)