summaryrefslogtreecommitdiff
path: root/scripts/touchup_for_android.py
blob: bd21fc5a9d7611553614ce78c2a9894962091a5e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#!/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 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_android_specific_fixes(font):
    """Apply fixes needed for Android."""
    # Set ascent, descent, and lineGap values to Android K values
    hhea = font['hhea']
    hhea.ascent = 1900
    hhea.descent = -500
    hhea.lineGap = 0

    # Remove tab, combining keycap, the arrows, and unassigned characters
    # from the cmap table
    # https://code.google.com/a/google.com/p/roboto/issues/detail?id=51
    # https://code.google.com/a/google.com/p/roboto/issues/detail?id=52
    # https://code.google.com/a/google.com/p/roboto/issues/detail?id=53
    font_data.delete_from_cmap(font, [
        0x0009, 0x20E3, 0x2072, 0x2073, 0x208F, 0x2191, 0x2193])

    # Drop tables not useful on Android
    for table in ['LTSH', 'hdmx', 'VDMX', 'gasp']:
        if table in font:
            del font[table]


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_android_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)