File: fontbuilder.py

package info (click to toggle)
fonts-fantasque-sans 1.8.0%2Bgit20220316%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 18,104 kB
  • sloc: python: 269; sh: 214; makefile: 23
file content (227 lines) | stat: -rw-r--r-- 8,003 bytes parent folder | download | duplicates (2)
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
# Adapted from https://github.com/larsenwork/monoid
# Copyright 2015 Chase Colman (chase@colman.io)
# LICENSE: MIT
# vim: sts=4 sw=4 ts=4 et

import fontforge
from itertools import compress
import os
from os.path import basename, splitext, join
import subprocess
from features import update_features

SCRIPTS = os.path.dirname(os.path.realpath(__file__))

def mkdir_p(path):
    normalized = os.path.normpath(path)
    try:
        os.makedirs(normalized)
    except OSError:
        pass

# Builder
def style(name, does):
    if not isinstance(does, list):
        does = [does]
    option(name, name, [Variation(name)] + does)

    return name

def option(abrv, name, does):
    if not isinstance(does, list):
        does = [does]
    option.operations[abrv] = does
    option.abrvs.append(abrv)
    option.names[abrv] = name

    return abrv

# Initialize the operations map, abbreviation list, and name map
option.operations = {}
option.abrvs = []
option.names = {}

def conflicting(*abrvs):
    """Wrap the abbreviations as a tuple in the option abbreviation list"""
    # Assumes last #abrvs abbreviations are conflicting options
    option.abrvs = option.abrvs[:-len(abrvs)] + [tuple(abrvs)]

def _expand_options(bitmap):
    # Apply the bitmap to the options
    opts = compress(option.abrvs, bitmap)

    # Expand the permutations for all options
    expanded = [[]]
    for opt in opts:
        if isinstance(opt, tuple):
            expanded = [items + [prmtn] for items in expanded for prmtn in opt]
        else:
            expanded = [items + [opt] for items in expanded]

    return expanded

def permutations():
    """Yields all possible permutations from the options list"""
    count = len(option.abrvs)

    # Each option is a binary choice, so we use an int as a quick bitmap.
    # To iterate over every possible permutation, all we have to do is increment
    # up to the maximum value 2^(#options)
    bitmap_max = 1 << count

    # Iterate over all possible permutations
    for i in range(bitmap_max):
        # Map the iteration's permutations using a bitmap
        bitmap = [i >> n & 1 for n in range(count)]
        for opts in _expand_options(bitmap):
            yield(int(float(i)/bitmap_max*100), opts)

def _build(dstdir, font, permutations):
    for prcnt, opts in permutations:
        # Open the original font
        fnt = fontforge.open(font)

        # Get the base name for the font
        name = splitext(basename(font))[0]

        # Build a variant name based on applied options
        variants = []

        for opt in opts:
            # Append this option to the font name
            variants.append(str(opt))
            # Run all the operations for this option
            for oper in option.operations[opt]:
                oper(fnt)

        # Update the automatic features (code ligatures)
        update_features(fnt)

        variant = '-'.join(variants) or 'Normal'
        variant_dir = join(dstdir, variant)

        print(('Generating ' + variant_dir))

        mkdir_p(join(variant_dir, 'TTF'))
        mkdir_p(join(variant_dir, 'OTF'))
        mkdir_p(join(variant_dir, 'Webfonts'))

        # Output the files and cleanup
        fnt.generate(join(variant_dir, 'TTF', name + '.ttf'), flags=("opentype", "dummy-dsig"))
        fnt.generate(join(variant_dir, 'OTF', name + '.otf'), flags=("opentype", "dummy-dsig"))
        fnt.generate(join(variant_dir, 'Webfonts', name + '.svg'))
        fnt.close()

        # Output other formats and the CSS declaration
        subprocess.check_call(
            [join(SCRIPTS, 'generate-other-formats'), font],
            cwd=variant_dir
        )
        subprocess.check_call(
            [join(SCRIPTS, 'generate-css-decl'), font],
            cwd=variant_dir
        )

def build(dstdir, font):
    _build(dstdir, font, permutations())

def build_batch(dstdir, font, total_nodes, node_number):
    # Starting at (i) node_number, build option every (n) total_nodes
    _build(dstdir, font, list(permutations())[node_number::total_nodes])

# Operations
## NOTE:
## All operations return a closure with the 1st argument being a fontforge.font
def Line(ascent, descent):
    """Sets the ascent and/or descent of the font's line"""
    def line_op(fnt):
        fnt.os2_winascent = fnt.os2_typoascent = fnt.hhea_ascent = ascent
        fnt.os2_windescent = descent
        fnt.os2_typodescent = fnt.hhea_descent = -descent
    return line_op

def Bearing(left=0, right=0):
    """Adjusts the left and/or right bearings of all glyphs"""
    def bearing_op(fnt):
        for glyph in fnt.glyphs():
            if left != 0:
                glyph.left_side_bearing += left
            if right != 0:
                glyph.right_side_bearing += right
    return bearing_op

def Swap(glyph1, glyph2):
    """Swaps the places of two glyphs"""
    def swap_op(fnt):
        # Unlike selections, glyph layer data is returned as a copy
        swp = fnt[glyph1].foreground
        fnt[glyph1].foreground = fnt[glyph2].foreground
        fnt[glyph2].foreground = swp
    return swap_op

def SwapLookup(target_lookup):
    """Swaps the places of glyphs based on an OpenType lookup table"""
    def swaplookup_op(fnt):
        # Get every subtable for every matching lookup
        lookups = [i for i in fnt.gsub_lookups if fnt.getLookupInfo(i)[2][0][0] == target_lookup]
        subtables = []
        for lookup in lookups:
            for subtable in fnt.getLookupSubtables(lookup):
                subtables.append(subtable)

        for glyph in fnt.glyphs():
            subbed = False

            for subtable in subtables:
                posSub = glyph.getPosSub(subtable)
                if not subbed and posSub and posSub[0][1] == "Substitution":
                    subbed = True # Don't double tap if there are duplicates

                    sub = posSub[0][2]
                    swp = glyph.foreground
                    glyph.foreground = fnt[sub].foreground
                    fnt[sub].foreground = swp

    return swaplookup_op

def DropCAltAndLiga():
    """Removes Contextual Alternates and Ligatures"""
    def dropcaltandliga_op(fnt):
        for lookup in fnt.gsub_lookups:
            if fnt.getLookupInfo(lookup)[0] in ['gsub_ligature', 'gsub_contextchain']:
                fnt.removeLookup(lookup)

    return dropcaltandliga_op

def Variation(name):
    """Changes the subfamily/variation of the font"""
    def variation_op(fnt):
        # Get the SFNT information as dictionary {property: value}
        # where English (US) is the language... Here be dragons.
        #
        #                                      o
        #                                     /\
        #                                    /::\
        #                                   /::::\
        #                     ,a_a         /\::::/\
        #                    {/ ''\_      /\ \::/\ \
        #                    {\ ,_oo)    /\ \ \/\ \ \
        #                    {/  (_^____/  \ \ \ \ \ \
        #          .=.      {/ \___)))*)    \ \ \ \ \/
        #         (.=.`\   {/   /=;  ~/      \ \ \ \/
        #             \ `\{/(   \/\  /        \ \ \/
        #              \  `. `\  ) )           \ \/
        #               \    // /_/_            \/
        #                '==''---))))
        sfnt_dict = {sfnt[1]: sfnt[2] for sfnt in fnt.sfnt_names if sfnt[0] == 'English (US)'}

        fnt.familyname = sfnt_dict['Family'] + ' ' + name
        fnt.fullname = fnt.familyname + ' ' + sfnt_dict['SubFamily']
        fnt.fontname = fnt.fullname.replace(' ', '-')

        fnt.appendSFNTName('English (US)', 'Family', fnt.familyname)
        fnt.appendSFNTName('English (US)', 'Fullname', fnt.fullname)
        fnt.appendSFNTName('English (US)', 'PostScriptName', fnt.fontname)
        fnt.appendSFNTName('English (US)', 'SubFamily', sfnt_dict['SubFamily'])
        fnt.appendSFNTName('English (US)', 'UniqueID', sfnt_dict['UniqueID'] + ' : ' + name)
    return variation_op