def interpolate_layout(designspace_filename, loc, finder): masters, instances = designspace.load(designspace_filename) base_idx = None for i, m in enumerate(masters): if 'info' in m and m['info']['copy']: assert base_idx is None base_idx = i assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document." from pprint import pprint print("Index of base master:", base_idx) print("Building GX") print("Loading TTF masters") basedir = os.path.dirname(designspace_filename) master_ttfs = [ finder(os.path.join(basedir, m['filename'])) for m in masters ] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] #font = master_fonts[base_idx] font = TTFont(master_ttfs[base_idx]) master_locs = [o['location'] for o in masters] axis_tags = set(master_locs[0].keys()) assert all(axis_tags == set(m.keys()) for m in master_locs) # Set up axes axes = {} for tag in axis_tags: default = master_locs[base_idx][tag] lower = min(m[tag] for m in master_locs) upper = max(m[tag] for m in master_locs) axes[tag] = (lower, default, upper) print("Axes:") pprint(axes) print("Location:", loc) print("Master locations:") pprint(master_locs) # Normalize locations loc = models.normalizeLocation(loc, axes) master_locs = [models.normalizeLocation(m, axes) for m in master_locs] print("Normalized location:", loc) print("Normalized master locations:") pprint(master_locs) # Assume single-model for now. model = models.VariationModel(master_locs) assert 0 == model.mapping[base_idx] merger = InstancerMerger(model, loc) print("Building variations tables") merge_tables(font, merger, master_fonts, axes, base_idx, ['GPOS']) return font
def interpolate_layout(designspace_filename, loc, finder): masters, instances = designspace.load(designspace_filename) base_idx = None for i,m in enumerate(masters): if 'info' in m and m['info']['copy']: assert base_idx is None base_idx = i assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document." from pprint import pprint print("Index of base master:", base_idx) print("Building GX") print("Loading TTF masters") basedir = os.path.dirname(designspace_filename) master_ttfs = [finder(os.path.join(basedir, m['filename'])) for m in masters] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] #font = master_fonts[base_idx] font = TTFont(master_ttfs[base_idx]) master_locs = [o['location'] for o in masters] axis_tags = set(master_locs[0].keys()) assert all(axis_tags == set(m.keys()) for m in master_locs) # Set up axes axes = {} for tag in axis_tags: default = master_locs[base_idx][tag] lower = min(m[tag] for m in master_locs) upper = max(m[tag] for m in master_locs) axes[tag] = (lower, default, upper) print("Axes:") pprint(axes) print("Location:", loc) print("Master locations:") pprint(master_locs) # Normalize locations loc = models.normalizeLocation(loc, axes) master_locs = [models.normalizeLocation(m, axes) for m in master_locs] print("Normalized location:", loc) print("Normalized master locations:") pprint(master_locs) # Assume single-model for now. model = models.VariationModel(master_locs) assert 0 == model.mapping[base_idx] merger = InstancerMerger(font, model, loc) print("Building variations tables") merge_tables(font, merger, master_fonts, axes, base_idx, ['GPOS']) return font
def interpolateValues(self, glyphName, location): """Interpolate the glyph from masters and answer the list of (x,y) points tuples, component transformation and metrics. The axis location in world values is normalized to (-1..0..1).""" nLocation = normalizeLocation(location, self.ds.tripleAxes) interpolatedPoints = [] interpolatedComponents = [] interpolatedMetrics = [] mvx, mvy, mvCx, mvCy, mMt = self.getMasterValues(glyphName) for pIndex, _ in enumerate(mvx): interpolatedPoints.append(( self.vm.interpolateFromMasters(nLocation, mvx[pIndex]), self.vm.interpolateFromMasters(nLocation, mvy[pIndex]) )) for cIndex, _ in enumerate(mvCx): interpolatedComponents.append(( self.vm.interpolateFromMasters(nLocation, mvCx[cIndex]), self.vm.interpolateFromMasters(nLocation, mvCy[cIndex]) )) for mIndex, _ in enumerate(mMt): interpolatedMetrics.append( self.vm.interpolateFromMasters(nLocation, mMt[mIndex]) ) return interpolatedPoints, interpolatedComponents, interpolatedMetrics
def _get_normalizedLocations(self): """Answers the list of locations from all masters, normalized to the normalized axes. The list of masters includes all the ones known to the designspace, combining the origin (all default values), axis masters (all defaults except for one) and space masters (two or more location values are not default). >>> ds = DesignSpace() >>> a1 = ds.newAxis('wght') >>> a2 = ds.newAxis('XTRA') # Condensed primary axes, keeping stems equal, vary counters. >>> regular = ds.newMaster('MyFamily', 'Regular', location=ds.defaultLocation) >>> loc = ds.newLocation(wght=a1.maximum) >>> bold = ds.newMaster('MyFamily', 'Bold', location=loc) >>> loc = ds.newLocation(XTRA=a2.maximum) >>> cond = ds.newMaster('MyFamily', 'Condensed', location=loc) >>> loc = ds.newLocation(wght=a1.maximum, XTRA=a2.maximum) >>> condBold = ds.newMaster('MyFamily', 'Condensed Bold', location=loc) >>> ds.normalizedLocations [{'wght': 0.0, 'XTRA': 0.0}, {'wght': 1.0, 'XTRA': 0.0}, {'wght': 0.0, 'XTRA': 1.0}, {'wght': 1.0, 'XTRA': 1.0}] """ normalizedLocations = [] axes = self.tripleAxes for location in self.masterLocationList: if location is not None: nl = normalizeLocation(location, axes) normalizedLocations.append(nl) else: print('Cannot normalize location of "%s" with axes %s"' % (location, axes)) return normalizedLocations
def rcjkGlyphToVarCoGlyph(rcjkGlyph, glyph, renameTable, componentSourceGlyphSet): copyMarkColor(rcjkGlyph, glyph) pen = glyph.getPointPen() rcjkGlyph.drawPoints(pen) compoVarInfo = [] for compo in rcjkGlyph.components: transform = compo.transform x, y = transform["x"], transform["y"] pen.addComponent(renameTable.get(compo.name, compo.name), (1, 0, 0, 1, x, y)) # the transformation center goes into varco data varCoTransform = dict( # TODO: We could skip values that are default (0, or 1 for scale values) rotation=transform["rotation"], scalex=transform["scalex"], scaley=transform["scaley"], tcenterx=transform["tcenterx"], tcentery=transform["tcentery"], ) if compo.name not in componentSourceGlyphSet: coord = {} else: baseGlyph = componentSourceGlyphSet.getGlyph(compo.name) axisNameMapping = _makeAxisNameMapping(baseGlyph.axes) coord = normalizeLocation(compo.coord, baseGlyph.axes) coord = { axisNameMapping[k]: v for k, v in coord.items() if k in axisNameMapping } info = dict(coord=coord, transform=varCoTransform) compoVarInfo.append(info) if compoVarInfo: glyph.lib["varco.components"] = compoVarInfo
def _get_normalizedLocations(self): u"""Answer the list of locations from all masters, normalized to the normalized axes.""" normalizedLocations = [] axes = self.tripleAxes for location in self.locations: nl = normalizeLocation(location, axes) normalizedLocations.append(nl) return normalizedLocations
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag, val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading variable font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varfont, glyphname) for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue # TODO Do IUP / handle None items coordinates += GlyphCoordinates(var.coordinates) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def normalize(self, location): """Return a normalized location based on the designspace. Args: location: A dictionary mapping axis names to user space coordinates. Example:: >>> location = { "Width": 75, "Weight": 170 } >>> f.normalize(location) {'Weight': 0.8947368421052632, 'Width': -0.35714285714285715} """ return models.normalizeLocation(location, self.internal_axis_supports)
def interpolate_layout(designspace_filename, loc, master_finder=lambda s: s, mapped=False): """ Interpolate GPOS from a designspace file and location. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). If mapped is False (default), then location is mapped using the map element of the axes in designspace file. If mapped is True, it is assumed that location is in designspace's internal space and no mapping is performed. """ axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances = load_designspace( designspace_filename) log.info("Building interpolated font") log.info("Loading master fonts") basedir = os.path.dirname(designspace_filename) master_ttfs = [ master_finder(os.path.join(basedir, m['filename'])) for m in masters ] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] #font = master_fonts[base_idx] font = TTFont(master_ttfs[base_idx]) log.info("Location: %s", pformat(loc)) if not mapped: loc = {name: axes[name].map_forward(v) for name, v in loc.items()} log.info("Internal location: %s", pformat(loc)) loc = models.normalizeLocation(loc, internal_axis_supports) log.info("Normalized location: %s", pformat(loc)) # Assume single-model for now. model = models.VariationModel(normalized_master_locs) assert 0 == model.mapping[base_idx] merger = InstancerMerger(font, model, loc) log.info("Building interpolated tables") merger.mergeTables(font, master_fonts, ['GPOS']) return font
def interpolate_layout(designspace, loc, master_finder=lambda s: s, mapped=False): """ Interpolate GPOS from a designspace file and location. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). If mapped is False (default), then location is mapped using the map element of the axes in designspace file. If mapped is True, it is assumed that location is in designspace's internal space and no mapping is performed. """ if hasattr(designspace, "sources"): # Assume a DesignspaceDocument pass else: # Assume a file path from fontTools.designspaceLib import DesignSpaceDocument designspace = DesignSpaceDocument.fromfile(designspace) ds = load_designspace(designspace) log.info("Building interpolated font") log.info("Loading master fonts") master_fonts = load_masters(designspace, master_finder) font = deepcopy(master_fonts[ds.base_idx]) log.info("Location: %s", pformat(loc)) if not mapped: loc = {name: ds.axes[name].map_forward(v) for name, v in loc.items()} log.info("Internal location: %s", pformat(loc)) loc = models.normalizeLocation(loc, ds.internal_axis_supports) log.info("Normalized location: %s", pformat(loc)) # Assume single-model for now. model = models.VariationModel(ds.normalized_master_locs) assert 0 == model.mapping[ds.base_idx] merger = InstancerMerger(font, model, loc) log.info("Building interpolated tables") # TODO GSUB/GDEF merger.mergeTables(font, master_fonts, ['GPOS']) return font
def _make_model(self): masters = self.designspace.sources internal_master_locs = [o.location for o in masters] self.internal_axis_supports = {} for axis in self.designspace.axes: triple = (axis.minimum, axis.default, axis.maximum) self.internal_axis_supports[axis.name] = [ axis.map_forward(v) for v in triple ] normalized_master_locs = [ models.normalizeLocation(m, self.internal_axis_supports) for m in internal_master_locs ] axis_tags = [axis.name for axis in self.designspace.axes] self.variation_model = models.VariationModel(normalized_master_locs, axisOrder=axis_tags)
def addRCJKGlyphToVarCoUFO( ufo, rcjkGlyphSet, srcGlyphName, dstGlyphName, unicodes, renameTable, componentSourceGlyphSet, globalAxisNames, ): if renameTable is None: renameTable = {} rcjkGlyph = rcjkGlyphSet.getGlyph(srcGlyphName) if rcjkGlyph.components and not rcjkGlyph.outline.isEmpty(): logger.warning( f"glyph {srcGlyphName} has both outlines and components") glyph = UGlyph(dstGlyphName) glyph.unicodes = unicodes glyph.width = max(0, rcjkGlyph.width) # width can't be negative rcjkGlyphToVarCoGlyph(rcjkGlyph, glyph, renameTable, componentSourceGlyphSet) if globalAxisNames is None: axisNameMapping = _makeAxisNameMapping(rcjkGlyph.axes) axisNames = set(axisNameMapping.values()) else: axisNames = globalAxisNames for varIndex, rcjkVarGlyph in enumerate(rcjkGlyph.variations): location = rcjkVarGlyph.location location = normalizeLocation(location, rcjkGlyph.axes) if globalAxisNames is None: location = {axisNameMapping[k]: v for k, v in location.items()} sparseLocation = {k: v for k, v in location.items() if v != 0} layerName = layerNameFromLocation(sparseLocation, axisNames) assert layerName, (srcGlyphName, varIndex, location, rcjkGlyph.axes) layer = getUFOLayer(ufo, layerName) varGlyph = UGlyph(dstGlyphName) varGlyph.width = max(0, rcjkVarGlyph.width) # width can't be negative rcjkGlyphToVarCoGlyph(rcjkVarGlyph, varGlyph, renameTable, componentSourceGlyphSet) layer[dstGlyphName] = varGlyph ufo[dstGlyphName] = glyph
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag,val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading GX font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] for glyphname,variations in gvar.variations.items(): coordinates,_ = _GetCoordinates(varfont, glyphname) for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue # TODO Do IUP / handle None items coordinates += GlyphCoordinates(var.coordinates) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing GX tables") for tag in ('fvar','avar','gvar'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def __init__(self, font, location, normalized=False): from fontTools.varLib.models import normalizeLocation, piecewiseLinearMap self._ttFont = font if not normalized: axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in font['fvar'].axes} location = normalizeLocation(location, axes) if 'avar' in font: avar = font['avar'] avarSegments = avar.segments new_location = {} for axis_tag,value in location.items(): avarMapping = avarSegments.get(axis_tag, None) if avarMapping is not None: value = piecewiseLinearMap(value, avarMapping) new_location[axis_tag] = value location = new_location del new_location self.location = location
def interpolate_layout(designspace_filename, loc, master_finder=lambda s:s, mapped=False): """ Interpolate GPOS from a designspace file and location. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). If mapped is False (default), then location is mapped using the map element of the axes in designspace file. If mapped is True, it is assumed that location is in designspace's internal space and no mapping is performed. """ ds = load_designspace(designspace_filename) log.info("Building interpolated font") log.info("Loading master fonts") basedir = os.path.dirname(designspace_filename) master_ttfs = [ master_finder(os.path.join(basedir, m.filename)) for m in ds.masters ] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] #font = master_fonts[ds.base_idx] font = TTFont(master_ttfs[ds.base_idx]) log.info("Location: %s", pformat(loc)) if not mapped: loc = {name: ds.axes[name].map_forward(v) for name,v in loc.items()} log.info("Internal location: %s", pformat(loc)) loc = models.normalizeLocation(loc, ds.internal_axis_supports) log.info("Normalized location: %s", pformat(loc)) # Assume single-model for now. model = models.VariationModel(ds.normalized_master_locs) assert 0 == model.mapping[ds.base_idx] merger = InstancerMerger(font, model, loc) log.info("Building interpolated tables") # TODO GSUB/GDEF merger.mergeTables(font, master_fonts, ['GPOS']) return font
def _makeWarpFromList(self, axisName, mapData): # check for the extremes, add if necessary minimum, default, maximum = self.axes[axisName] if not sum([a == minimum for a, b in mapData]): mapData = [(minimum, minimum)] + mapData if not sum([a == maximum for a, b in mapData]): mapData.append((maximum, maximum)) if not (default, default) in mapData: mapData.append((default, default)) mapLocations = [] mapValues = [] for x, y in mapData: l = normalizeLocation(dict(w=x), dict(w=[minimum, default, maximum])) mapLocations.append(l) mapValues.append(y) self.models[axisName] = VariationModel(mapLocations, axisOrder=['w']) self.values[axisName] = mapValues
def interpolateGlyph(self, glyph, location): """Interpolate the glyph from the masters. If glyph is not compatible with the masters, then first copy one of the masters into glyphs. Location is normalized to (-1..0..1).""" # If there are components, make sure to interpolate them first, then # interpolate (dx, dy). Check if the referenced glyph exists. # Otherwise copy it from one of the masters into the parent of glyph. mvx, mvy, mvCx, mvCy, mMt = self.getMasterValues(glyph.name) points = getPoints(glyph) components = getComponents(glyph) if components: font = glyph.getParent() for component in components: if component.baseGlyph in font: self.interpolateGlyph(font[component.baseGlyph], location) if len(points) != len(mvx): # Glyph may not exist, or not compatible. # Clear the glyph and copy from the first master in the list, so it always interpolates. glyph.clear() masterGlyph = self.masterList[0][glyph.name] masterGlyph.draw(glyph.getPen()) points = getPoints(glyph) # Get new set of points # Normalize to location (-1..0..1) nLocation = normalizeLocation(location, self.ds.tripleAxes) # Get the point of the glyph, and set their (x,y) to the calculated variable points. for pIndex, _ in enumerate(mvx): p = points[pIndex] p.x = self.vm.interpolateFromMasters(nLocation, mvx[pIndex]) p.y = self.vm.interpolateFromMasters(nLocation, mvy[pIndex]) for cIndex, _ in enumerate(mvCx): c = components[cIndex] t = list(c.transformation) t[-2] = self.vm.interpolateFromMasters(nLocation, mvCx[cIndex]) t[-1] = self.vm.interpolateFromMasters(nLocation, mvCy[cIndex]) c.transformation = t glyph.width = self.vm.interpolateFromMasters(nLocation, mMt[0])
def unpackDesignSpace(doc): axisTagMapping = {axis.name: axis.tag for axis in doc.axes} axes = {axis.tag: (axis.minimum, axis.default, axis.maximum) for axis in doc.axes} # We want the default source to be the first in the list; the rest of # the order is not important sources = sorted(doc.sources, key=lambda src: src != doc.default) ufos = [] locations = [] _loaded = {} for src in sources: loc = src.location loc = { axisTagMapping[axisName]: axisValue for axisName, axisValue in loc.items() } loc = normalizeLocation(loc, axes) loc = { axisName: axisValue for axisName, axisValue in loc.items() if axisValue != 0 } locations.append(loc) ufo = _loaded.get(src.path) if ufo is None: ufo = UFont(src.path) _loaded[src.path] = ufo if src.layerName is None: ufo.layers.defaultLayer else: ufo = ufo.layers[src.layerName] ufos.append(ufo) userAxes = { axis.tag: (axis.minimum, axis.default, axis.maximum) for axis in doc.axes if not axis.hidden } return userAxes, ufos, locations
def drawGlyph(self, pen, glyphName, location): normLocation = normalizeLocation(location, self.axes) fvarTable = self.ttFont["fvar"] glyfTable = self.ttFont["glyf"] varcTable = self.ttFont.get("VarC") if varcTable is not None: glyphData = varcTable.GlyphData else: glyphData = {} g = glyfTable[glyphName] varComponents = glyphData.get(glyphName) if g.isComposite(): componentOffsets = instantiateComponentOffsets( self.ttFont, glyphName, normLocation ) if varComponents is not None: assert len(g.components) == len(varComponents) varcInstancer = VarStoreInstancer( varcTable.VarStore, fvarTable.axes, normLocation ) for (x, y), gc, vc in zip( componentOffsets, g.components, varComponents ): componentLocation = unpackComponentLocation(vc.coord, varcInstancer) transform = unpackComponentTransform( vc.transform, varcInstancer, vc.numIntBitsForScale ) tPen = TransformPen(pen, _makeTransform(x, y, transform)) self.drawGlyph(tPen, gc.glyphName, componentLocation) else: for (x, y), gc in zip(componentOffsets, g.components): tPen = TransformPen(pen, (1, 0, 0, 1, x, y)) self.drawGlyph(tPen, gc.glyphName, {}) else: glyphID = self.ttFont.getGlyphID(glyphName) self.hbFont.set_variations(location) self.hbFont.draw_glyph_with_pen(glyphID, pen)
def load_designspace(designspace_filename): ds = designspace.load(designspace_filename) axes = ds.get('axes') masters = ds.get('sources') if not masters: raise VarLibError("no sources found in .designspace") instances = ds.get('instances', []) standard_axis_map = OrderedDict([ ('weight', ('wght', {'en':'Weight'})), ('width', ('wdth', {'en':'Width'})), ('slant', ('slnt', {'en':'Slant'})), ('optical', ('opsz', {'en':'Optical Size'})), ]) # Setup axes class DesignspaceAxis(object): def __repr__(self): return repr(self.__dict__) @staticmethod def _map(v, map): keys = map.keys() if not keys: return v if v in keys: return map[v] k = min(keys) if v < k: return v + map[k] - k k = max(keys) if v > k: return v + map[k] - k # Interpolate a = max(k for k in keys if k < v) b = min(k for k in keys if k > v) va = map[a] vb = map[b] return va + (vb - va) * (v - a) / (b - a) def map_forward(self, v): if self.map is None: return v return self._map(v, self.map) def map_backward(self, v): if self.map is None: return v map = {v:k for k,v in self.map.items()} return self._map(v, map) axis_objects = OrderedDict() if axes is not None: for axis_dict in axes: axis_name = axis_dict.get('name') if not axis_name: axis_name = axis_dict['name'] = axis_dict['tag'] if 'map' not in axis_dict: axis_dict['map'] = None else: axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']} if axis_name in standard_axis_map: if 'tag' not in axis_dict: axis_dict['tag'] = standard_axis_map[axis_name][0] if 'labelname' not in axis_dict: axis_dict['labelname'] = standard_axis_map[axis_name][1].copy() axis = DesignspaceAxis() for item in ['name', 'tag', 'labelname', 'minimum', 'default', 'maximum', 'map']: assert item in axis_dict, 'Axis does not have "%s"' % item axis.__dict__ = axis_dict axis_objects[axis_name] = axis else: # No <axes> element. Guess things... base_idx = None for i,m in enumerate(masters): if 'info' in m and m['info']['copy']: assert base_idx is None base_idx = i assert base_idx is not None, "Cannot find 'base' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document." master_locs = [o['location'] for o in masters] base_loc = master_locs[base_idx] axis_names = set(base_loc.keys()) assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no <axes> element." for name,(tag,labelname) in standard_axis_map.items(): if name not in axis_names: continue axis = DesignspaceAxis() axis.name = name axis.tag = tag axis.labelname = labelname.copy() axis.default = base_loc[name] axis.minimum = min(m[name] for m in master_locs if name in m) axis.maximum = max(m[name] for m in master_locs if name in m) axis.map = None # TODO Fill in weight / width mapping from OS/2 table? Need loading fonts... axis_objects[name] = axis del base_idx, base_loc, axis_names, master_locs axes = axis_objects del axis_objects log.info("Axes:\n%s", pformat(axes)) # Check all master and instance locations are valid and fill in defaults for obj in masters+instances: obj_name = obj.get('name', obj.get('stylename', '')) loc = obj['location'] for axis_name in loc.keys(): assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name) for axis_name,axis in axes.items(): if axis_name not in loc: loc[axis_name] = axis.default else: v = axis.map_backward(loc[axis_name]) assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum) # Normalize master locations normalized_master_locs = [o['location'] for o in masters] log.info("Internal master locations:\n%s", pformat(normalized_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar_avar internal_axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in normalized_master_locs] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) # Find base master base_idx = None for i,m in enumerate(normalized_master_locs): if all(v == 0 for v in m.values()): assert base_idx is None base_idx = i assert base_idx is not None, "Base master not found; no master at default location?" log.info("Index of base master: %s", base_idx) return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances
def load_designspace(designspace_filename): ds = DesignSpaceDocument.fromfile(designspace_filename) masters = ds.sources if not masters: raise VarLibError("no sources found in .designspace") instances = ds.instances standard_axis_map = OrderedDict([ ('weight', ('wght', {'en':'Weight'})), ('width', ('wdth', {'en':'Width'})), ('slant', ('slnt', {'en':'Slant'})), ('optical', ('opsz', {'en':'Optical Size'})), ]) # Setup axes axes = OrderedDict() for axis in ds.axes: axis_name = axis.name if not axis_name: assert axis.tag is not None axis_name = axis.name = axis.tag if axis_name in standard_axis_map: if axis.tag is None: axis.tag = standard_axis_map[axis_name][0] if not axis.labelNames: axis.labelNames.update(standard_axis_map[axis_name][1]) else: assert axis.tag is not None if not axis.labelNames: axis.labelNames["en"] = axis_name axes[axis_name] = axis log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()])) # Check all master and instance locations are valid and fill in defaults for obj in masters+instances: obj_name = obj.name or obj.styleName or '' loc = obj.location for axis_name in loc.keys(): assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name) for axis_name,axis in axes.items(): if axis_name not in loc: loc[axis_name] = axis.default else: v = axis.map_backward(loc[axis_name]) assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum) # Normalize master locations internal_master_locs = [o.location for o in masters] log.info("Internal master locations:\n%s", pformat(internal_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) # Find base master base_idx = None for i,m in enumerate(normalized_master_locs): if all(v == 0 for v in m.values()): assert base_idx is None base_idx = i assert base_idx is not None, "Base master not found; no master at default location?" log.info("Index of base master: %s", base_idx) return _DesignSpaceData( axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances, ds.rules, )
def normalize(name, value): return models.normalizeLocation( {name: value}, internal_axis_supports )[name]
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) if 'gvar' in varfont: log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) else: glyf = None if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'CFF2' in varfont: log.info("Mutating CFF2 table") glyphOrder = varfont.getGlyphOrder() CFF2 = varfont['CFF2'] topDict = CFF2.cff.topDictIndex[0] vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) interpolateFromDeltas = vsInstancer.interpolateFromDeltas interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) CFF2.desubroutinize() interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) del topDict.rawDict['VarStore'] del topDict.VarStore if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) log.info("Mutating FeatureVariations") for tableTag in 'GSUB','GPOS': if not tableTag in varfont: continue table = varfont[tableTag].table if not hasattr(table, 'FeatureVariations'): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: applies = True for condition in record.ConditionSet.ConditionTable: if condition.Format == 1: axisIdx = condition.AxisIndex axisTag = fvar.axes[axisIdx].axisTag Min = condition.FilterRangeMinValue Max = condition.FilterRangeMaxValue v = loc[axisTag] if not (Min <= v <= Max): applies = False else: applies = False if not applies: break if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature break del table.FeatureVariations if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003: log.info("Mutating GDEF/GPOS/GSUB tables") gdef = varfont['GDEF'].table instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) merger = MutatorMerger(varfont, loc) merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS']) # Downgrade GDEF. del gdef.VarStore gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef is None: del gdef.MarkGlyphSetsDef gdef.Version = 0x00010000 if not (gdef.LigCaretList or gdef.MarkAttachClassDef or gdef.GlyphClassDef or gdef.AttachList or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): del varfont['GDEF'] addidef = False if glyf: for glyph in glyf.glyphs.values(): if hasattr(glyph, "program"): instructions = glyph.program.getAssembly() # If GETVARIATION opcode is used in bytecode of any glyph add IDEF addidef = any(op.startswith("GETVARIATION") for op in instructions) if addidef: break if addidef: log.info("Adding IDEF to fpgm table for GETVARIATION opcode") asm = [] if 'fpgm' in varfont: fpgm = varfont['fpgm'] asm = fpgm.program.getAssembly() else: fpgm = newTable('fpgm') fpgm.program = ttProgram.Program() varfont['fpgm'] = fpgm asm.append("PUSHB[000] 145") asm.append("IDEF[ ]") args = [str(len(loc))] for a in fvar.axes: args.append(str(floatToFixed(loc[a.axisTag], 14))) asm.append("NPUSHW[ ] " + ' '.join(args)) asm.append("ENDF[ ]") fpgm.program.fromAssembly(asm) # Change maxp attributes as IDEF is added if 'maxp' in varfont: maxp = varfont['maxp'] if hasattr(maxp, "maxInstructionDefs"): maxp.maxInstructionDefs += 1 else: setattr(maxp, "maxInstructionDefs", 1) if hasattr(maxp, "maxStackElements"): maxp.maxStackElements = max(len(loc), maxp.maxStackElements) else: setattr(maxp, "maxInstructionDefs", len(loc)) if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) if 'ltag' in varfont: # Drop the whole 'ltag' table if all its language tags are referenced by # name records to be pruned. # TODO: prune unused ltag tags and re-enumerate langIDs accordingly excludedUnicodeLangIDs = [ n.langID for n in varfont['name'].names if n.nameID in exclude and n.platformID == 0 and n.langID != 0xFFFF ] if set(excludedUnicodeLangIDs) == set(range(len((varfont['ltag'].tags)))): del varfont['ltag'] varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k: piecewiseLinearMap(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) if 'gvar' in varfont: log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) else: glyf = None if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'CFF2' in varfont: log.info("Mutating CFF2 table") glyphOrder = varfont.getGlyphOrder() CFF2 = varfont['CFF2'] topDict = CFF2.cff.topDictIndex[0] vsInstancer = VarStoreInstancer(topDict.VarStore.otVarStore, fvar.axes, loc) interpolateFromDeltas = vsInstancer.interpolateFromDeltas interpolate_cff2_PrivateDict(topDict, interpolateFromDeltas) CFF2.desubroutinize(varfont) interpolate_cff2_charstrings(topDict, interpolateFromDeltas, glyphOrder) interpolate_cff2_metrics(varfont, topDict, glyphOrder, loc) del topDict.rawDict['VarStore'] del topDict.VarStore if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) log.info("Mutating FeatureVariations") for tableTag in 'GSUB','GPOS': if not tableTag in varfont: continue table = varfont[tableTag].table if not hasattr(table, 'FeatureVariations'): continue variations = table.FeatureVariations for record in variations.FeatureVariationRecord: applies = True for condition in record.ConditionSet.ConditionTable: if condition.Format == 1: axisIdx = condition.AxisIndex axisTag = fvar.axes[axisIdx].axisTag Min = condition.FilterRangeMinValue Max = condition.FilterRangeMaxValue v = loc[axisTag] if not (Min <= v <= Max): applies = False else: applies = False if not applies: break if applies: assert record.FeatureTableSubstitution.Version == 0x00010000 for rec in record.FeatureTableSubstitution.SubstitutionRecord: table.FeatureList.FeatureRecord[rec.FeatureIndex].Feature = rec.Feature break del table.FeatureVariations if 'GDEF' in varfont and varfont['GDEF'].table.Version >= 0x00010003: log.info("Mutating GDEF/GPOS/GSUB tables") gdef = varfont['GDEF'].table instancer = VarStoreInstancer(gdef.VarStore, fvar.axes, loc) merger = MutatorMerger(varfont, loc) merger.mergeTables(varfont, [varfont], ['GDEF', 'GPOS']) # Downgrade GDEF. del gdef.VarStore gdef.Version = 0x00010002 if gdef.MarkGlyphSetsDef is None: del gdef.MarkGlyphSetsDef gdef.Version = 0x00010000 if not (gdef.LigCaretList or gdef.MarkAttachClassDef or gdef.GlyphClassDef or gdef.AttachList or (gdef.Version >= 0x00010002 and gdef.MarkGlyphSetsDef)): del varfont['GDEF'] addidef = False if glyf: for glyph in glyf.glyphs.values(): if hasattr(glyph, "program"): instructions = glyph.program.getAssembly() # If GETVARIATION opcode is used in bytecode of any glyph add IDEF addidef = any(op.startswith("GETVARIATION") for op in instructions) if addidef: break if addidef: log.info("Adding IDEF to fpgm table for GETVARIATION opcode") asm = [] if 'fpgm' in varfont: fpgm = varfont['fpgm'] asm = fpgm.program.getAssembly() else: fpgm = newTable('fpgm') fpgm.program = ttProgram.Program() varfont['fpgm'] = fpgm asm.append("PUSHB[000] 145") asm.append("IDEF[ ]") args = [str(len(loc))] for a in fvar.axes: args.append(str(floatToFixed(loc[a.axisTag], 14))) asm.append("NPUSHW[ ] " + ' '.join(args)) asm.append("ENDF[ ]") fpgm.program.fromAssembly(asm) # Change maxp attributes as IDEF is added if 'maxp' in varfont: maxp = varfont['maxp'] if hasattr(maxp, "maxInstructionDefs"): maxp.maxInstructionDefs += 1 else: setattr(maxp, "maxInstructionDefs", 1) if hasattr(maxp, "maxStackElements"): maxp.maxStackElements = max(len(loc), maxp.maxStackElements) else: setattr(maxp, "maxInstructionDefs", len(loc)) if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def instantiateVariableFont(varfont, location, inplace=False): """ Generate a static instance from a variable TTFont and a dictionary defining the desired location along the variable font's axes. The location values must be specified as user-space coordinates, e.g.: {'wght': 400, 'wdth': 100} By default, a new TTFont object is returned. If ``inplace`` is True, the input varfont is modified and reduced to a static font. """ if not inplace: # make a copy to leave input varfont unmodified stream = BytesIO() varfont.save(stream) stream.seek(0) varfont = TTFont(stream) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} loc = normalizeLocation(location, axes) if 'avar' in varfont: maps = varfont['avar'].segments loc = {k:_DesignspaceAxis._map(v, maps[k]) for k,v in loc.items()} # Quantize to F2Dot14, to avoid surprise interpolations. loc = {k:floatToFixedToFloat(v, 14) for k,v in loc.items()} # Location is normalized now log.info("Normalized location: %s", loc) log.info("Mutating glyf/gvar tables") gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) if 'cvar' in varfont: log.info("Mutating cvt/cvar tables") cvar = varfont['cvar'] cvt = varfont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += otRound(delta) if 'MVAR' in varfont: log.info("Mutating MVAR table") mvar = varfont['MVAR'].table varStoreInstancer = VarStoreInstancer(mvar.VarStore, fvar.axes, loc) records = mvar.ValueRecord for rec in records: mvarTag = rec.ValueTag if mvarTag not in MVAR_ENTRIES: continue tableTag, itemName = MVAR_ENTRIES[mvarTag] delta = otRound(varStoreInstancer[rec.VarIdx]) if not delta: continue setattr(varfont[tableTag], itemName, getattr(varfont[tableTag], itemName) + delta) if 'GDEF' in varfont: log.info("Mutating GDEF/GPOS/GSUB tables") merger = MutatorMerger(varfont, loc) log.info("Building interpolated tables") merger.instantiate() if 'name' in varfont: log.info("Pruning name table") exclude = {a.axisNameID for a in fvar.axes} for i in fvar.instances: exclude.add(i.subfamilyNameID) exclude.add(i.postscriptNameID) varfont['name'].names[:] = [ n for n in varfont['name'].names if n.nameID not in exclude ] if "wght" in location and "OS/2" in varfont: varfont["OS/2"].usWeightClass = otRound( max(1, min(location["wght"], 1000)) ) if "wdth" in location: wdth = location["wdth"] for percent, widthClass in sorted(OS2_WIDTH_CLASS_VALUES.items()): if wdth < percent: varfont["OS/2"].usWidthClass = widthClass break else: varfont["OS/2"].usWidthClass = 9 if "slnt" in location and "post" in varfont: varfont["post"].italicAngle = max(-90, min(location["slnt"], 90)) log.info("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] return varfont
def load_designspace(designspace): # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument, # never a file path, as that's already handled by caller if hasattr(designspace, "sources"): # Assume a DesignspaceDocument ds = designspace else: # Assume a file path ds = DesignSpaceDocument.fromfile(designspace) masters = ds.sources if not masters: raise VarLibValidationError( "Designspace must have at least one source.") instances = ds.instances # TODO: Use fontTools.designspaceLib.tagForAxisName instead. standard_axis_map = OrderedDict([ ('weight', ('wght', { 'en': u'Weight' })), ('width', ('wdth', { 'en': u'Width' })), ('slant', ('slnt', { 'en': u'Slant' })), ('optical', ('opsz', { 'en': u'Optical Size' })), ('italic', ('ital', { 'en': u'Italic' })), ]) # Setup axes if not ds.axes: raise VarLibValidationError( f"Designspace must have at least one axis.") axes = OrderedDict() for axis_index, axis in enumerate(ds.axes): axis_name = axis.name if not axis_name: if not axis.tag: raise VarLibValidationError( f"Axis at index {axis_index} needs a tag.") axis_name = axis.name = axis.tag if axis_name in standard_axis_map: if axis.tag is None: axis.tag = standard_axis_map[axis_name][0] if not axis.labelNames: axis.labelNames.update(standard_axis_map[axis_name][1]) else: if not axis.tag: raise VarLibValidationError( f"Axis at index {axis_index} needs a tag.") if not axis.labelNames: axis.labelNames["en"] = tounicode(axis_name) axes[axis_name] = axis log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()])) # Check all master and instance locations are valid and fill in defaults for obj in masters + instances: obj_name = obj.name or obj.styleName or '' loc = obj.location if loc is None: raise VarLibValidationError( f"Source or instance '{obj_name}' has no location.") for axis_name in loc.keys(): if axis_name not in axes: raise VarLibValidationError( f"Location axis '{axis_name}' unknown for '{obj_name}'.") for axis_name, axis in axes.items(): if axis_name not in loc: # NOTE: `axis.default` is always user-space, but `obj.location` always design-space. loc[axis_name] = axis.map_forward(axis.default) else: v = axis.map_backward(loc[axis_name]) if not (axis.minimum <= v <= axis.maximum): raise VarLibValidationError( f"Source or instance '{obj_name}' has out-of-range location " f"for axis '{axis_name}': is mapped to {v} but must be in " f"mapped range [{axis.minimum}..{axis.maximum}] (NOTE: all " "values are in user-space).") # Normalize master locations internal_master_locs = [o.location for o in masters] log.info("Internal master locations:\n%s", pformat(internal_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) internal_axis_supports[axis.name] = [ axis.map_forward(v) for v in triple ] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) normalized_master_locs = [ models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs ] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) # Find base master base_idx = None for i, m in enumerate(normalized_master_locs): if all(v == 0 for v in m.values()): if base_idx is not None: raise VarLibValidationError( "More than one base master found in Designspace.") base_idx = i if base_idx is None: raise VarLibValidationError( "Base master not found; no master at default location?") log.info("Index of base master: %s", base_idx) return _DesignSpaceData( axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances, ds.rules, ds.rulesProcessingLast, ds.lib, )
def test_normalizeLocation(): axes = {"wght": (100, 400, 900)} assert normalizeLocation({"wght": 400}, axes) == {'wght': 0.0} assert normalizeLocation({"wght": 100}, axes) == {'wght': -1.0} assert normalizeLocation({"wght": 900}, axes) == {'wght': 1.0} assert normalizeLocation({"wght": 650}, axes) == {'wght': 0.5} assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0} assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0} axes = {"wght": (0, 0, 1000)} assert normalizeLocation({"wght": 0}, axes) == {'wght': 0.0} assert normalizeLocation({"wght": -1}, axes) == {'wght': 0.0} assert normalizeLocation({"wght": 1000}, axes) == {'wght': 1.0} assert normalizeLocation({"wght": 500}, axes) == {'wght': 0.5} assert normalizeLocation({"wght": 1001}, axes) == {'wght': 1.0} axes = {"wght": (0, 1000, 1000)} assert normalizeLocation({"wght": 0}, axes) == {'wght': -1.0} assert normalizeLocation({"wght": -1}, axes) == {'wght': -1.0} assert normalizeLocation({"wght": 500}, axes) == {'wght': -0.5} assert normalizeLocation({"wght": 1000}, axes) == {'wght': 0.0} assert normalizeLocation({"wght": 1001}, axes) == {'wght': 0.0}
def setLocation(self, location): self.location = normalizeLocation(location, self._axes)
def _normalize(self, location): return normalizeLocation(location, self.axes)
def _normalize(self, location): new = {} for axisName in location.keys(): new[axisName] = normalizeLocation(dict(w=location[axisName]), dict(w=self.axes[axisName])) return new
def load_designspace(designspace): # TODO: remove this and always assume 'designspace' is a DesignSpaceDocument, # never a file path, as that's already handled by caller if hasattr(designspace, "sources"): # Assume a DesignspaceDocument ds = designspace else: # Assume a file path ds = DesignSpaceDocument.fromfile(designspace) masters = ds.sources if not masters: raise VarLibError("no sources found in .designspace") instances = ds.instances standard_axis_map = OrderedDict([ ('weight', ('wght', { 'en': u'Weight' })), ('width', ('wdth', { 'en': u'Width' })), ('slant', ('slnt', { 'en': u'Slant' })), ('optical', ('opsz', { 'en': u'Optical Size' })), ('italic', ('ital', { 'en': u'Italic' })), ]) # Setup axes axes = OrderedDict() for axis in ds.axes: axis_name = axis.name if not axis_name: assert axis.tag is not None axis_name = axis.name = axis.tag if axis_name in standard_axis_map: if axis.tag is None: axis.tag = standard_axis_map[axis_name][0] if not axis.labelNames: axis.labelNames.update(standard_axis_map[axis_name][1]) else: assert axis.tag is not None if not axis.labelNames: axis.labelNames["en"] = tounicode(axis_name) axes[axis_name] = axis log.info("Axes:\n%s", pformat([axis.asdict() for axis in axes.values()])) # Check all master and instance locations are valid and fill in defaults for obj in masters + instances: obj_name = obj.name or obj.styleName or '' loc = obj.location for axis_name in loc.keys(): assert axis_name in axes, "Location axis '%s' unknown for '%s'." % ( axis_name, obj_name) for axis_name, axis in axes.items(): if axis_name not in loc: loc[axis_name] = axis.default else: v = axis.map_backward(loc[axis_name]) assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % ( axis_name, v, obj_name, axis.minimum, axis.maximum) # Normalize master locations internal_master_locs = [o.location for o in masters] log.info("Internal master locations:\n%s", pformat(internal_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) internal_axis_supports[axis.name] = [ axis.map_forward(v) for v in triple ] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) normalized_master_locs = [ models.normalizeLocation(m, internal_axis_supports) for m in internal_master_locs ] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) # Find base master base_idx = None for i, m in enumerate(normalized_master_locs): if all(v == 0 for v in m.values()): assert base_idx is None base_idx = i assert base_idx is not None, "Base master not found; no master at default location?" log.info("Index of base master: %s", base_idx) return _DesignSpaceData( axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances, ds.rules, )
def build(designspace_filename, master_finder=lambda s: s, axisMap=None): """ Build variation font from a designspace file. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). If axisMap is set, it should be dictionary mapping axis-id to (axis-tag, axis-name). """ masters, instances = designspace.load(designspace_filename) base_idx = None for i, m in enumerate(masters): if 'info' in m and m['info']['copy']: assert base_idx is None base_idx = i assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document." from pprint import pprint print("Index of base master:", base_idx) print("Building GX") print("Loading TTF masters") basedir = os.path.dirname(designspace_filename) master_ttfs = [ master_finder(os.path.join(basedir, m['filename'])) for m in masters ] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] standard_axis_map = { 'weight': ('wght', 'Weight'), 'width': ('wdth', 'Width'), 'slant': ('slnt', 'Slant'), 'optical': ('opsz', 'Optical Size'), 'custom': ('xxxx', 'Custom'), } axis_map = standard_axis_map if axisMap: axis_map = axis_map.copy() axis_map.update(axisMap) # TODO: For weight & width, use OS/2 values and setup 'avar' mapping. master_locs = [o['location'] for o in masters] axis_tags = set(master_locs[0].keys()) assert all(axis_tags == set(m.keys()) for m in master_locs) # Set up axes axes = {} for tag in axis_tags: default = master_locs[base_idx][tag] lower = min(m[tag] for m in master_locs) upper = max(m[tag] for m in master_locs) axes[tag] = (lower, default, upper) print("Axes:") pprint(axes) print("Master locations:") pprint(master_locs) # We can use the base font straight, but it's faster to load it again since # then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables. #gx = master_fonts[base_idx] gx = TTFont(master_ttfs[base_idx]) # TODO append masters as named-instances as well; needs .designspace change. fvar = _add_fvar(gx, axes, instances, axis_map) # Normalize master locations master_locs = [models.normalizeLocation(m, axes) for m in master_locs] print("Normalized master locations:") pprint(master_locs) # TODO Clean this up. del instances del axes master_locs = [{axis_map[k][0]: v for k, v in loc.items()} for loc in master_locs] #instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs] axisTags = [axis.axisTag for axis in fvar.axes] # Assume single-model for now. model = models.VariationModel(master_locs) assert 0 == model.mapping[base_idx] print("Building variations tables") if 'glyf' in gx: _add_gvar(gx, model, master_fonts) _add_HVAR(gx, model, master_fonts, axisTags) _merge_OTL(gx, model, master_fonts, axisTags, base_idx) return gx, model, master_ttfs
def makeInstance(pathOrVarFont, location, dstPath=None, normalize=True, cached=True, lazy=True, kerning=None): """Instantiate an instance of a variable font at the specified location. Keyword arguments: - varfilename -- a variable font file path - location -- a dictionary of axis tag and value {"wght": 0.75, "wdth": -0.5} >>> vf = findFont('RobotoDelta-VF') >>> print(vf) <Font RobotoDelta-VF> >>> print(len(vf)) 188 >>> instance = makeInstance(vf.path, dict(opsz=8), cached=False) >>> instance <Font RobotoDelta-VF-opsz8> >>> len(instance) 241 >>> len(instance['H'].points) 12 >>> instance['Egrave'] <PageBot Glyph Egrave Pts:0/Cnt:0/Cmp:2> >>> len(instance['Egrave'].components) 2 """ # make a custom file name from the location e.g. # VariableFont-wghtXXX-wdthXXX.ttf instanceName = "" if isinstance(pathOrVarFont, Font): pathOrVarFont = pathOrVarFont.path varFont = Font(pathOrVarFont, lazy=lazy) ttFont = varFont.ttFont for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) if dstPath is None: targetFileName = '.'.join(varFont.path.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' targetDirectory = getInstancePath() if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) dstPath = targetDirectory + targetFileName # Instance does not exist as file. Create it. if not cached or not os.path.exists(dstPath): # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = ttFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") ttFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name ttFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name ttFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix fvar = ttFont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc) gvar = ttFont['gvar'] glyf = ttFont['glyf'] # get list of glyph names in gvar sorted by component depth glyphNames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphName in glyphNames: variations = gvar.variations[glyphName] coordinates, _ = _GetCoordinates(ttFont, glyphName) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( ttFont, glyphName) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(ttFont, glyphName, coordinates) # Interpolate cvt if 'cvar' in ttFont: cvar = ttFont['cvar'] cvt = ttFont['cvt '] deltas = {} for var in cvar.variations: scalar = supportScalar(loc, var.axes) if not scalar: continue for i, c in enumerate(var.coordinates): if c is not None: deltas[i] = deltas.get(i, 0) + scalar * c for i, delta in deltas.items(): cvt[i] += int(round(delta)) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in ttFont: del ttFont[tag] if kerning is not None: for pair, value in kerning.items(): varFont.kerning[pair] = value #print("Saving instance font", outFile) varFont.save(dstPath) # Answer instance. return Font(dstPath, lazy=lazy)
def main(args=None): if args is None: import sys args = sys.argv[1:] varfilename = args[0] locargs = args[1:] outfile = os.path.splitext(varfilename)[0] + '-instance.ttf' loc = {} for arg in locargs: tag,val = arg.split('=') assert len(tag) <= 4 loc[tag.ljust(4)] = float(val) print("Location:", loc) print("Loading variable font") varfont = TTFont(varfilename) fvar = varfont['fvar'] axes = {a.axisTag:(a.minValue,a.defaultValue,a.maxValue) for a in fvar.axes} # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(loc, axes) # Location is normalized now print("Normalized location:", loc) gvar = varfont['gvar'] glyf = varfont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: ( glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates,_ = _GetCoordinates(varfont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords,control = _GetCoordinates(varfont, glyphname) endPts = control[1] if control[0] >= 1 else list(range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) # TODO Do IUP / handle None items coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varfont, glyphname, coordinates) print("Removing variable tables") for tag in ('avar','cvar','fvar','gvar','HVAR','MVAR','VVAR','STAT'): if tag in varfont: del varfont[tag] print("Saving instance font", outfile) varfont.save(outfile)
def normalize(name, value): return models.normalizeLocation({name: value}, internal_axis_supports)[name]
def generateInstance(variableFontPath, location, targetDirectory, normalize=False, force=False): instanceName = "" for k, v in sorted(location.items()): # TODO better way to normalize the location name to (0, 1000) v = min(v, 1000) v = max(v, 0) instanceName += "-%s%s" % (k, v) targetFileName = '.'.join(variableFontPath.split('/')[-1].split('.') [:-1]) + instanceName + '.ttf' if not targetDirectory.endswith('/'): targetDirectory += '/' if not os.path.exists(targetDirectory): os.makedirs(targetDirectory) outFile = targetDirectory + targetFileName if force or not os.path.exists(outFile): #print location #print("Loading variable font") varFont = TTFont(variableFontPath) fvar = varFont['fvar'] axes = { a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes } # TODO Apply avar # TODO Round to F2Dot14? loc = normalizeLocation(location, axes) # Location is normalized now #print("Normalized location:", loc, 'from', location) # Set the instance name IDs in the name table platforms = ((1, 0, 0), (3, 1, 0x409)) # Macintosh and Windows for platformID, platEncID, langID in platforms: familyName = varFont['name'].getName(1, platformID, platEncID, langID) # 1 Font Family name if not familyName: continue familyName = familyName.toUnicode() # NameRecord to unicode string styleName = unicode( instanceName) # TODO make sure this works in any case fullFontName = " ".join([familyName, styleName]) postscriptName = fullFontName.replace(" ", "-") varFont['name'].setName(styleName, 2, platformID, platEncID, langID) # 2 Font Subfamily name varFont['name'].setName(fullFontName, 4, platformID, platEncID, langID) # 4 Full font name varFont['name'].setName(postscriptName, 6, platformID, platEncID, langID) # 6 Postscript name for the font # Other important name IDs # 3 Unique font identifier (e.g. Version 0.000;NONE;Promise Bold Regular) # 25 Variables PostScript Name Prefix gvar = varFont['gvar'] glyf = varFont['glyf'] # get list of glyph names in gvar sorted by component depth glyphnames = sorted( gvar.variations.keys(), key=lambda name: (glyf[name].getCompositeMaxpValues(glyf).maxComponentDepth if glyf[name].isComposite() else 0, name)) for glyphname in glyphnames: variations = gvar.variations[glyphname] coordinates, _ = _GetCoordinates(varFont, glyphname) origCoords, endPts = None, None for var in variations: scalar = supportScalar(loc, var.axes) #, ot=True) if not scalar: continue delta = var.coordinates if None in delta: if origCoords is None: origCoords, control = _GetCoordinates( varFont, glyphname) endPts = control[1] if control[0] >= 1 else list( range(len(control[1]))) delta = _iup_delta(delta, origCoords, endPts) coordinates += GlyphCoordinates(delta) * scalar _SetCoordinates(varFont, glyphname, coordinates) #print("Removing variable tables") for tag in ('avar', 'cvar', 'fvar', 'gvar', 'HVAR', 'MVAR', 'VVAR', 'STAT'): if tag in varFont: del varFont[tag] #print("Saving instance font", outFile) varFont.save(outFile) # Installing the font in DrawBot. Answer font name and path. return c.installFont(outFile), outFile
def load_designspace(designspace_filename): ds = designspace.load(designspace_filename) axes = ds.get('axes') masters = ds.get('sources') if not masters: raise VarLibError("no sources found in .designspace") instances = ds.get('instances', []) standard_axis_map = OrderedDict([ ('weight', ('wght', {'en':'Weight'})), ('width', ('wdth', {'en':'Width'})), ('slant', ('slnt', {'en':'Slant'})), ('optical', ('opsz', {'en':'Optical Size'})), ]) # Setup axes class DesignspaceAxis(object): def __repr__(self): return repr(self.__dict__) @staticmethod def _map(v, map): keys = map.keys() if not keys: return v if v in keys: return map[v] k = min(keys) if v < k: return v + map[k] - k k = max(keys) if v > k: return v + map[k] - k # Interpolate a = max(k for k in keys if k < v) b = min(k for k in keys if k > v) va = map[a] vb = map[b] return va + (vb - va) * (v - a) / (b - a) def map_forward(self, v): if self.map is None: return v return self._map(v, self.map) def map_backward(self, v): if self.map is None: return v map = {v:k for k,v in self.map.items()} return self._map(v, map) axis_objects = OrderedDict() if axes is not None: for axis_dict in axes: axis_name = axis_dict.get('name') if not axis_name: axis_name = axis_dict['name'] = axis_dict['tag'] if 'map' not in axis_dict: axis_dict['map'] = None else: axis_dict['map'] = {m['input']:m['output'] for m in axis_dict['map']} if axis_name in standard_axis_map: if 'tag' not in axis_dict: axis_dict['tag'] = standard_axis_map[axis_name][0] if 'labelname' not in axis_dict: axis_dict['labelname'] = standard_axis_map[axis_name][1].copy() axis = DesignspaceAxis() for item in ['name', 'tag', 'minimum', 'default', 'maximum', 'map']: assert item in axis_dict, 'Axis does not have "%s"' % item if 'labelname' not in axis_dict: axis_dict['labelname'] = {'en': axis_name} axis.__dict__ = axis_dict axis_objects[axis_name] = axis else: # No <axes> element. Guess things... base_idx = None for i,m in enumerate(masters): if 'info' in m and m['info']['copy']: assert base_idx is None base_idx = i assert base_idx is not None, "Cannot find 'base' master; Either add <axes> element to .designspace document, or add <info> element to one of the sources in the .designspace document." master_locs = [o['location'] for o in masters] base_loc = master_locs[base_idx] axis_names = set(base_loc.keys()) assert all(name in standard_axis_map for name in axis_names), "Non-standard axis found and there exist no <axes> element." for name,(tag,labelname) in standard_axis_map.items(): if name not in axis_names: continue axis = DesignspaceAxis() axis.name = name axis.tag = tag axis.labelname = labelname.copy() axis.default = base_loc[name] axis.minimum = min(m[name] for m in master_locs if name in m) axis.maximum = max(m[name] for m in master_locs if name in m) axis.map = None # TODO Fill in weight / width mapping from OS/2 table? Need loading fonts... axis_objects[name] = axis del base_idx, base_loc, axis_names, master_locs axes = axis_objects del axis_objects log.info("Axes:\n%s", pformat(axes)) # Check all master and instance locations are valid and fill in defaults for obj in masters+instances: obj_name = obj.get('name', obj.get('stylename', '')) loc = obj['location'] for axis_name in loc.keys(): assert axis_name in axes, "Location axis '%s' unknown for '%s'." % (axis_name, obj_name) for axis_name,axis in axes.items(): if axis_name not in loc: loc[axis_name] = axis.default else: v = axis.map_backward(loc[axis_name]) assert axis.minimum <= v <= axis.maximum, "Location for axis '%s' (mapped to %s) out of range for '%s' [%s..%s]" % (axis_name, v, obj_name, axis.minimum, axis.maximum) # Normalize master locations normalized_master_locs = [o['location'] for o in masters] log.info("Internal master locations:\n%s", pformat(normalized_master_locs)) # TODO This mapping should ideally be moved closer to logic in _add_fvar/avar internal_axis_supports = {} for axis in axes.values(): triple = (axis.minimum, axis.default, axis.maximum) internal_axis_supports[axis.name] = [axis.map_forward(v) for v in triple] log.info("Internal axis supports:\n%s", pformat(internal_axis_supports)) normalized_master_locs = [models.normalizeLocation(m, internal_axis_supports) for m in normalized_master_locs] log.info("Normalized master locations:\n%s", pformat(normalized_master_locs)) # Find base master base_idx = None for i,m in enumerate(normalized_master_locs): if all(v == 0 for v in m.values()): assert base_idx is None base_idx = i assert base_idx is not None, "Base master not found; no master at default location?" log.info("Index of base master: %s", base_idx) return axes, internal_axis_supports, base_idx, normalized_master_locs, masters, instances
def build(designspace_filename, master_finder=lambda s:s, axisMap=None): """ Build variation font from a designspace file. If master_finder is set, it should be a callable that takes master filename as found in designspace file and map it to master font binary as to be opened (eg. .ttf or .otf). If axisMap is set, it should be dictionary mapping axis-id to (axis-tag, axis-name). """ masters, instances = designspace.load(designspace_filename) base_idx = None for i,m in enumerate(masters): if 'info' in m and m['info']['copy']: assert base_idx is None base_idx = i assert base_idx is not None, "Cannot find 'base' master; Add <info> element to one of the masters in the .designspace document." log.info("Index of base master: %s", base_idx) log.info("Building variable font") log.info("Loading TTF masters") basedir = os.path.dirname(designspace_filename) master_ttfs = [master_finder(os.path.join(basedir, m['filename'])) for m in masters] master_fonts = [TTFont(ttf_path) for ttf_path in master_ttfs] standard_axis_map = { 'weight': ('wght', 'Weight'), 'width': ('wdth', 'Width'), 'slant': ('slnt', 'Slant'), 'optical': ('opsz', 'Optical Size'), 'custom': ('xxxx', 'Custom'), } axis_map = standard_axis_map if axisMap: axis_map = axis_map.copy() axis_map.update(axisMap) # TODO: For weight & width, use OS/2 values and setup 'avar' mapping. master_locs = [o['location'] for o in masters] axis_tags = set(master_locs[0].keys()) assert all(axis_tags == set(m.keys()) for m in master_locs) # Set up axes axes = {} for tag in axis_tags: default = master_locs[base_idx][tag] lower = min(m[tag] for m in master_locs) upper = max(m[tag] for m in master_locs) if default == lower == upper: continue axes[tag] = (lower, default, upper) log.info("Axes:\n%s", pformat(axes)) log.info("Master locations:\n%s", pformat(master_locs)) # We can use the base font straight, but it's faster to load it again since # then we won't be recompiling the existing ('glyf', 'hmtx', ...) tables. #gx = master_fonts[base_idx] gx = TTFont(master_ttfs[base_idx]) # TODO append masters as named-instances as well; needs .designspace change. fvar = _add_fvar(gx, axes, instances, axis_map) # Normalize master locations master_locs = [models.normalizeLocation(m, axes) for m in master_locs] log.info("Normalized master locations:\n%s", pformat(master_locs)) # TODO Clean this up. del instances del axes master_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in master_locs] #instance_locs = [{axis_map[k][0]:v for k,v in loc.items()} for loc in instance_locs] axisTags = [axis.axisTag for axis in fvar.axes] # Assume single-model for now. model = models.VariationModel(master_locs) assert 0 == model.mapping[base_idx] log.info("Building variations tables") if 'glyf' in gx: _add_gvar(gx, model, master_fonts) _add_HVAR(gx, model, master_fonts, axisTags) _merge_OTL(gx, model, master_fonts, axisTags, base_idx) return gx, model, master_ttfs