diff options
Diffstat (limited to 'src/silfont/scripts/psfcreateinstances.py')
-rw-r--r-- | src/silfont/scripts/psfcreateinstances.py | 228 |
1 files changed, 228 insertions, 0 deletions
diff --git a/src/silfont/scripts/psfcreateinstances.py b/src/silfont/scripts/psfcreateinstances.py new file mode 100644 index 0000000..d623390 --- /dev/null +++ b/src/silfont/scripts/psfcreateinstances.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +__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__ = 'https://github.com/silnrsi/pysilfont' +__copyright__ = 'Copyright (c) 2018 SIL International (https://www.sil.org)' +__license__ = 'Released under the MIT License (https://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): |