summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfshownames.py
blob: 191e0f8a107d3c919f5052206007c1587fecf5fd (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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
#!/usr/bin/python3
__doc__ = 'Display name fields and other bits for linking fonts into families'
__url__ = 'http://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2021 SIL International (http://www.sil.org)'
__license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)'
__author__ = 'Bobby de Vos'

from silfont.core import execute, splitfn
from fontTools.ttLib import TTFont
import glob
from operator import attrgetter, methodcaller
import tabulate

WINDOWS_ENGLISH_IDS = 3, 1, 0x409

FAMILY_RELATED_IDS = {
    1: 'Family',
    2: 'Subfamily',
    4: 'Full name',
    6: 'PostScript name',
    16: 'Typographic/Preferred family',
    17: 'Typographic/Preferred subfamily',
    21: 'WWS family',
    22: 'WWS subfamily',
    25: 'Variations PostScript Name Prefix',
}


class FontInfo:
    def __init__(self):
        self.filename = ''
        self.name_table = dict()
        self.weight_class = 0
        self.regular = ''
        self.bold = ''
        self.italic = ''
        self.width = ''
        self.width_name = ''
        self.width_class = 0
        self.wws = ''

    def sort_fullname(self):
        return self.name_table[4]


argspec = [
    ('font', {'help': 'ttf font(s) to run report against; wildcards allowed', 'nargs': "+"}, {'type': 'filename'}),
    ('-b', '--bits', {'help': 'Show bits', 'action': 'store_true'}, {}),
    ('-m', '--multiline', {'help': 'Output multi-line key:values instead of a table', 'action': 'store_true'}, {}),
]


def doit(args):
    logger = args.logger

    font_infos = []
    for pattern in args.font:
        for fullpath in glob.glob(pattern):
            logger.log(f'Processing {fullpath}', 'P')
            try:
                font = TTFont(fullpath)
            except Exception as e:
                logger.log(f'Error opening {fullpath}: {e}', 'E')
                break

            font_info = FontInfo()
            font_info.filename = fullpath
            get_names(font, font_info)
            get_bits(font, font_info)
            font_infos.append(font_info)

    if not font_infos:
        logger.log("No files match the filespec provided for fonts: " + str(args.fonts), "S")

    font_infos.sort(key=methodcaller('sort_fullname'))
    font_infos.sort(key=attrgetter('width_class'), reverse=True)
    font_infos.sort(key=attrgetter('weight_class'))

    rows = list()
    if args.multiline:
        # Multi-line mode
        for font_info in font_infos:
            for line in multiline_names(font_info):
                rows.append(line)
            if args.bits:
                for line in multiline_bits(font_info):
                    rows.append(line)
        align = ['left', 'right']
        if len(font_infos) == 1:
            del align[0]
            for row in rows:
                del row[0]
        output = tabulate.tabulate(rows, tablefmt='plain', colalign=align)
        output = output.replace(': ', ':')
        output = output.replace('#', '')
    else:
        # Table mode

        # Record information for headers
        headers = table_headers(args.bits)

        # Record information for each instance.
        for font_info in font_infos:
            record = table_records(font_info, args.bits)
            rows.append(record)

        # Not all fonts in a family with have the same name ids present,
        # for instance 16: Typographic/Preferred family is only needed in
        # non-RIBBI familes, and even then only for the non-RIBBI instances.
        # Also, not all the bit fields are present in each instance.
        # Therefore, columns with no data in any instance are removed.
        indices = list(range(len(headers)))
        indices.reverse()
        for index in indices:
            empty = True
            for row in rows:
                data = row[index]
                if data:
                    empty = False
            if empty:
                for row in rows + [headers]:
                    del row[index]

        # Format 'pipe' is nicer for GitHub, but is wider on a command line
        output = tabulate.tabulate(rows, headers, tablefmt='simple')

    # Print output from either mode
    if args.quiet:
        print(output)
    else:
        logger.log('The following family-related values were found in the name, head, and OS/2 tables\n' + output, 'P')


def get_names(font, font_info):
    table = font['name']
    (platform_id, encoding_id, language_id) = WINDOWS_ENGLISH_IDS

    for name_id in FAMILY_RELATED_IDS:
        record = table.getName(
            nameID=name_id,
            platformID=platform_id,
            platEncID=encoding_id,
            langID=language_id
        )
        if record:
            font_info.name_table[name_id] = str(record)


def get_bits(font, font_info):
    os2 = font['OS/2']
    head = font['head']
    font_info.weight_class = os2.usWeightClass
    font_info.regular = bit2code(os2.fsSelection, 6, 'W-')
    font_info.bold = bit2code(os2.fsSelection, 5, 'W')
    font_info.bold += bit2code(head.macStyle, 0, 'M')
    font_info.italic = bit2code(os2.fsSelection, 0, 'W')
    font_info.italic += bit2code(head.macStyle, 1, 'M')
    font_info.width_class = os2.usWidthClass
    font_info.width = str(font_info.width_class)
    if font_info.width_class == 5:
        font_info.width_name = 'Width-Normal'
    if font_info.width_class < 5:
        font_info.width_name = 'Width-Condensed'
        font_info.width += bit2code(head.macStyle, 5, 'M')
    if font_info.width_class > 5:
        font_info.width_name = 'Width-Extended'
        font_info.width += bit2code(head.macStyle, 6, 'M')
    font_info.wws = bit2code(os2.fsSelection, 8, '8')


def bit2code(bit_field, bit, code_letter):
    code = ''
    if bit_field & 1 << bit:
        code = code_letter
    return code


def multiline_names(font_info):
    for name_id in sorted(font_info.name_table):
        line = [font_info.filename + ':',
                str(name_id) + ':',
                FAMILY_RELATED_IDS[name_id] + ':',
                font_info.name_table[name_id]
                ]
        yield line


def multiline_bits(font_info):
    labels = ('usWeightClass', 'Regular', 'Bold', 'Italic', font_info.width_name, 'WWS')
    values = (font_info.weight_class, font_info.regular, font_info.bold, font_info.italic, font_info.width, font_info.wws)
    for label, value in zip(labels, values):
        if not value:
            continue
        line = [font_info.filename + ':',
                '#',
                str(label) + ':',
                value
                ]
        yield line


def table_headers(bits):
    headers = ['filename']
    for name_id in sorted(FAMILY_RELATED_IDS):
        name_id_key = FAMILY_RELATED_IDS[name_id]
        header = f'{name_id}: {name_id_key}'
        if len(header) > 20:
            header = header.replace(' ', '\n')
            header = header.replace('/', '\n')
        headers.append(header)
    if bits:
        headers.extend(['wght', 'R', 'B', 'I', 'wdth', 'WWS'])
    return headers


def table_records(font_info, bits):
    record = [font_info.filename]
    for name_id in sorted(FAMILY_RELATED_IDS):
        name_id_value = font_info.name_table.get(name_id, '')
        record.append(name_id_value)
    if bits:
        record.append(font_info.weight_class)
        record.append(font_info.regular)
        record.append(font_info.bold)
        record.append(font_info.italic)
        record.append(font_info.width)
        record.append(font_info.wws)
    return record


def cmd(): execute('FT', doit, argspec)


if __name__ == '__main__':
    cmd()