summaryrefslogtreecommitdiffstats
path: root/lib/silfont/scripts/psfcreateinstances.py
blob: 468435f2affabcb56ca805aa1316d4b054f7d1e3 (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
#!/usr/bin/env python
__doc__ = 'Generate instance UFOs from a designspace document and master UFOs'

# Python 2.7 script to build instance UFOs from a designspace document
# If a file is given, all instances are built
# A particular instance to build can be specified using the -i option
#  and the 'name' attribute value for an 'instance' element in the designspace file
#  Or it can be specified using the -a and -v options
#  to specify any attribute and value pair for an 'instance' in the designspace file
#  If more than one instances matches, all will be built
# A prefix for the output path can be specified (for smith processing)
# If the location of an instance UFO matches a master's location,
#  glyphs are copied instead of calculated
#  This allows instances to build with glyphs that are not interpolatable
#  An option exists to calculate glyphs instead of copying them
# If a folder is given using an option, all instances in all designspace files are built
#  Specifying an instance to build or an output path prefix is not supported with a folder
#  Also, all glyphs will be calculated

__url__ = 'http://github.com/silnrsi/pysilfont'
__copyright__ = 'Copyright (c) 2018 SIL International  (http://www.sil.org)'
__license__ = 'Released under the MIT License (http://opensource.org/licenses/MIT)'
__author__ = 'Alan Ward'

import os, re
from mutatorMath.ufo.document import DesignSpaceDocumentReader
from mutatorMath.ufo.instance import InstanceWriter
from fontMath.mathGlyph import MathGlyph
from mutatorMath.ufo import build as build_designspace
from silfont.core import execute

argspec = [
    ('designspace_path', {'help': 'Path to designspace document (or folder of them)'}, {}),
    ('-i', '--instanceName', {'help': 'Font name for instance to build'}, {}),
    ('-a', '--instanceAttr', {'help': 'Attribute used to specify instance to build'}, {}),
    ('-v', '--instanceVal', {'help': 'Value of attribute specifying instance to build'}, {}),
    ('-f', '--folder', {'help': 'Build all designspace files in a folder','action': 'store_true'}, {}),
    ('-o', '--output', {'help': 'Prepend path to all output paths'}, {}),
    ('--forceInterpolation', {'help': 'If an instance matches a master, calculate glyphs instead of copying them',
                               'action': 'store_true'}, {}),
    ('--roundInstances', {'help': 'Apply integer rounding to all geometry when interpolating',
                           'action': 'store_true'}, {}),
    ('-l','--log',{'help': 'Log file (default: *_createinstances.log)'}, {'type': 'outfile', 'def': '_createinstances.log'}),
    ('-W','--weightfix',{'help': 'Enable RIBBI style weight fixing', 'action': 'store_true'}, {}),
]

# Class factory to wrap a subclass in a closure to store values not defined in the original class
#  that our method overrides will utilize
# The class methods will fail unless the class is generated by the factory, which is enforced by scoping
# Using class attribs or global variables would violate encapsulation even more
#  and would only allow for one instance of the class

weightClasses = {
    'bold': 700
}

def InstanceWriterCF(output_path_prefix, calc_glyphs, fix_weight):

    class LocalInstanceWriter(InstanceWriter):
        fixWeight = fix_weight

        def __init__(self, path, *args, **kw):
            if output_path_prefix:
                path = os.path.join(output_path_prefix, path)
            return super(LocalInstanceWriter, self).__init__(path, *args, **kw)

        # Override the method used to calculate glyph geometry
        # If copy_glyphs is true and the glyph being processed is in the same location
        # (has all the same axes values) as a master UFO,
        # then extract the glyph geometry directly into the target glyph.
        # FYI, in the superclass method, m = buildMutator(); m.makeInstance() returns a MathGlyph
        def _calculateGlyph(self, targetGlyphObject, instanceLocationObject, glyphMasters):
            # Search for a glyphMaster with the same location as instanceLocationObject
            found = False
            if not calc_glyphs: # i.e. if copying glyphs
                for item in glyphMasters:
                    locationObject = item['location'] # mutatorMath Location
                    if locationObject.sameAs(instanceLocationObject) == 0:
                        found = True
                        fontObject = item['font'] # defcon Font
                        glyphName = item['glyphName'] # string
                        glyphObject = MathGlyph(fontObject[glyphName])
                        glyphObject.extractGlyph(targetGlyphObject, onlyGeometry=True)
                        break

            if not found: # includes case of calc_glyphs == True
                super(LocalInstanceWriter, self)._calculateGlyph(targetGlyphObject,
                                                                 instanceLocationObject,
                                                                 glyphMasters)

        def _copyFontInfo(self, targetInfo, sourceInfo):
            super(LocalInstanceWriter, self)._copyFontInfo(targetInfo, sourceInfo)

            if getattr(self, 'fixWeight', False):
                # fixWeight is True since the --weightfix (or -W) option was specified

                # This mode is used for RIBBI font builds,
                # therefore the weight class can be determined
                # by the style name
                if self.font.info.styleMapStyleName.lower().startswith("bold"):
                    weight_class = 700
                else:
                    weight_class = 400
            else:
                # fixWeight is False (or None)

                # This mode is used for non-RIBBI font builds,
                # therefore the weight class can be determined
                # by the weight axis map in the Designspace file
                foundmap = False
                weight = int(self.locationObject["weight"])
                for map_space in self.axes["weight"]["map"]:
                    userspace = int(map_space[0])  # called input in the Designspace file
                    designspace = int(map_space[1])  # called output in the Designspace file
                    if designspace == weight:
                        weight_class = userspace
                        foundmap = True
                if not foundmap:
                    weight_class = 399 # Dummy value designed to look non-standard
                    logger.log(f'No entry in designspace axis mapping for {weight}; set to 399', 'W')
            setattr(targetInfo, 'openTypeOS2WeightClass', weight_class)

            localinfo = {}
            for k in (('openTypeNameManufacturer', None),
                      ('styleMapFamilyName', 'familyName'),
                      ('styleMapStyleName', 'styleName')):
                localinfo[k[0]] = getattr(targetInfo, k[0], (getattr(targetInfo, k[1]) if k[1] is not None else ""))
            localinfo['styleMapStyleName'] = localinfo['styleMapStyleName'].title()
            localinfo['year'] = re.sub(r'^.*?([0-9]+)\s*$', r'\1', getattr(targetInfo, 'openTypeNameUniqueID')) 
            uniqueID = "{openTypeNameManufacturer}: {styleMapFamilyName} {styleMapStyleName} {year}".format(**localinfo)
            setattr(targetInfo, 'openTypeNameUniqueID', uniqueID)

    return LocalInstanceWriter

logger = None
severe_error = False
def progress_func(state="update", action=None, text=None, tick=0):
    global severe_error
    if logger:
        if state == 'error':
            if str(action) == 'unicodes':
                logger.log("%s: %s\n%s" % (state, str(action), str(text)), 'W')
            else:
                logger.log("%s: %s\n%s" % (state, str(action), str(text)), 'E')
                severe_error = True
        else:
            logger.log("%s: %s\n%s" % (state, str(action), str(text)), 'I')

def doit(args):
    global logger
    logger = args.logger

    designspace_path = args.designspace_path
    instance_font_name = args.instanceName
    instance_attr = args.instanceAttr
    instance_val = args.instanceVal
    output_path_prefix = args.output
    calc_glyphs = args.forceInterpolation
    build_folder = args.folder
    round_instances = args.roundInstances

    if instance_font_name and (instance_attr or instance_val):
        args.logger.log('--instanceName is mutually exclusive with --instanceAttr or --instanceVal','S')
    if (instance_attr and not instance_val) or (instance_val and not instance_attr):
        args.logger.log('--instanceAttr and --instanceVal must be used together', 'S')
    if (build_folder and (instance_font_name or instance_attr or instance_val
                          or output_path_prefix or calc_glyphs)):
        args.logger.log('--folder cannot be used with options: -i, -a, -v, -o, --forceInterpolation', 'S')

    args.logger.log('Interpolating master UFOs from designspace', 'P')
    if not build_folder:
        if not os.path.isfile(designspace_path):
            args.logger.log('A designspace file (not a folder) is required', 'S')
        reader = DesignSpaceDocumentReader(designspace_path, ufoVersion=3,
                                           roundGeometry=round_instances,
                                           progressFunc=progress_func)
        # assignment to an internal object variable is a kludge, probably should use subclassing instead
        reader._instanceWriterClass = InstanceWriterCF(output_path_prefix, calc_glyphs, args.weightfix)
        if calc_glyphs:
            args.logger.log('Interpolating glyphs where an instance font location matches a master', 'P')
        if instance_font_name or instance_attr:
            key_attr = instance_attr if instance_val else 'name'
            key_val = instance_val if instance_attr else instance_font_name
            reader.readInstance((key_attr, key_val))
        else:
            reader.readInstances()
    else:
        # The below uses a utility function that's part of mutatorMath
        #  It will accept a folder and processes all designspace files there
        args.logger.log('Interpolating glyphs where an instance font location matches a master', 'P')
        build_designspace(designspace_path,
                          outputUFOFormatVersion=3, roundGeometry=round_instances,
                          progressFunc=progress_func)

    if not severe_error:
        args.logger.log('Done', 'P')
    else:
        args.logger.log('Done with severe error', 'S')

def cmd(): execute(None, doit, argspec)
if __name__ == '__main__': cmd()

# Future development might use: fonttools\Lib\fontTools\designspaceLib to read
#  the designspace file (which is the most up-to-date approach)
#  then pass that object to mutatorMath, but there's no way to do that today.


# For reference:
# from mutatorMath/ufo/__init__.py:
# 	build() is a convenience function for reading and executing a designspace file.
# 		documentPath: 				filepath to the .designspace document
# 		outputUFOFormatVersion:		ufo format for output
# 		verbose:					True / False for lots or no feedback [to log file]
# 		logPath:					filepath to a log file
# 		progressFunc:				an optional callback to report progress.
# 									see mutatorMath.ufo.tokenProgressFunc
#
# class DesignSpaceDocumentReader(object):
# def __init__(self, documentPath,
#         ufoVersion,
#         roundGeometry=False,
#         verbose=False,
#         logPath=None,
#         progressFunc=None
#         ):
#
# def readInstance(self, key, makeGlyphs=True, makeKerning=True, makeInfo=True):
# def readInstances(self, makeGlyphs=True, makeKerning=True, makeInfo=True):