def _getInstanceGlyph(self, location, masters): I = self._getInstance(location, masters) if I is not None: return I.extractGlyph(RGlyph()) else: errorMessage = self.mutatorErrors[-1]['error'] return ErrorGlyph('Interpolation', errorMessage)
class MutatorScaleEngine: """ This object is built to handle the interpolated scaling of glyphs using MutatorMath. It requires a list of fonts (at least two) from which it determines which kind of interpolation it can achieve. Maybe I should state the obvious: the whole process is based on the assumption that the provided fonts are compatible for interpolation. With existing masters, the object is then set to certain parameters that allow for specific glyph scaling operations, while scaling, a MutatorScaleEngine attempts to obtain specified weight and contrast for the scaled glyph by interpolating accordingly and to the best possible result with available masters. Each master in a MutatorScaleEngine is an instance of a MutatorScaleFont for which stem values are defined. If not specifically provided, these stem values are measured on capital letters I and H for vertical and horizontal stems respectively. The stem values obtained are only meant to be reference value and do not reflect the stem values of all glyphs but only of I and H. While scaling, if you ask for a scaled glyph with stem values (80, 60), you’re effectively asking for a scaledGlyph interpolated as to have the vertical stem of a I equal to 80 and the horizontal stem of a H equal to 60. It is not akin to ask that these stem values are applied to the exact glyph you asked for, that’s not how interpolation works. When a MutatorScaleEngine is asked for a scaled glyph with specific horizontal and vertical stem values, here’s what happens: – it collects glyphs corresponding to the glyphName passed to .getScaledGlyph() in the available masters; – it scales all the master glyphs to the proportions to which the MutatorScaleEngine is set; – it then builds a MutatorMath space in which masters are placed according to their horizontal and vertical stem values scaled down; – finally, it returns a scaled down (as all masters are) interpolated glyph with the asked for stem values. ##### Here’s how it goes: >>> scaler = MutatorScaleEngine(ListOfFonts) >>> scaler.set({ 'scale': (1.03, 0.85) }) >>> scaler.getScaledGlyph('a', ()) """ errorGlyph = ErrorGlyph() def __init__(self, masterFonts=[], stemsWithSlantedSection=False): self.masters = {} self._currentScale = None self._workingStems = None self.stemsWithSlantedSection = stemsWithSlantedSection self._availableGlyphs = [] for font in masterFonts: self.addMaster(font) self.mutatorErrors = [] def __repr__(self): return 'MutatorScaleEngine w/ {0} masters\n- {1}\n'.format( len(self.masters), '\n- '.join([repr(master) for master in self.masters.values()])) def __getitem__(self, key): if key in self.masters.keys(): return self.masters[key] else: raise KeyError(key) def __iter__(self): for master in self.masters.values(): yield master def __len__(self): return len(self.masters) def __contains__(self, fontName): return fontName in self.masters def getMaster(self, font): """Returning a master by parsing a fonts name and returning it if it’s among masters.""" name = makeListFontName(font) if name in self.masters: return self.masters[name] return def getMasterByName(self, familyName, styleName): name = joinFontName(familyName, styleName) if name in self: return self[name] def getCurrentStemBase(self): return self._workingStems def hasTwoAxes(self): if self._workingStems == 'both': return True else: return False def hasGlyph(self, glyphName): """Checking for glyph availability in all masters.""" return glyphName in self._availableGlyphs def getReferenceGlyphNames(self): """Returning a list of glyphNames for valid reference glyphs, i.e., glyphs that are not empty so they can serve as height reference. """ masters = self.masters.values() glyphNames = self._availableGlyphs validGlyphs_names = reduce(lambda a, b: list(set(a) & set(b)), [[ glyphName for glyphName in glyphNames if len(master.glyphSet[glyphName]) ] for master in masters]) return validGlyphs_names def set(self, scalingParameters): """Define scaling parameters. Collect relevant data in the various forms it can be input, produce a scale definition relevant to a ScaleFont object. """ scale = (1, 1) width = 1 if 'width' in scalingParameters: width = scalingParameters['width'] scale = (width, 1) if 'scale' in scalingParameters: scale = scalingParameters['scale'] if isinstance(scale, (float, int)): scale = (scale, scale) elif 'targetHeight' in scalingParameters and 'referenceHeight' in scalingParameters: targetHeight = scalingParameters['targetHeight'] referenceHeight = scalingParameters['referenceHeight'] scale = (width, targetHeight, referenceHeight) for master in self.masters.values(): master.setScale(scale) self._currentScale = scale def update(self): self._determineWorkingStems() def _parseStemsInput(self, stems): if stems is None: vstem, hstem = None, None else: try: vstem, hstem = stems except: vstem, hstem = stems, None return vstem, hstem def _makeMaster(self, font, vstem, hstem): """Return a MutatorScaleFont.""" name = makeListFontName(font) master = MutatorScaleFont( font, vstem=vstem, hstem=hstem, stemsWithSlantedSection=self.stemsWithSlantedSection) return name, master def addMaster(self, font, stems=None): """Add a MutatorScaleFont to masters.""" vstem, hstem = self._parseStemsInput(stems) if (vstem is None) and ('I' not in font): vstem = len(self.masters) * 100 name, master = self._makeMaster(font, vstem, hstem) if not len(self._availableGlyphs): self._availableGlyphs = master.keys() elif len(self._availableGlyphs): self._availableGlyphs = list( set(self._availableGlyphs) & set(master.keys())) if self._currentScale is not None: master.setScale(self._currentScale) self.masters[name] = master self.update() def removeMaster(self, font): """Remove a MutatorScaleFont from masters.""" name = makeListFontName(font) if name in self.masters: self.masters.pop(name, 0) self.update() def getScaledGlyph(self, glyphName, stemTarget, slantCorrection=True, attributes=None): """Return an interpolated & scaled glyph according to set parameters and given masters.""" masters = self.masters.values() workingStems = self._workingStems mutatorMasters = [] yScales = [] angles = [] """ Gather master glyphs for interpolation: each master glyph is scaled down according to set parameter, it is then inserted in a mutator design space with scaled down stem values. Asking for the initial stem values of a scaled down glyphName will result in an scaled glyph which will retain specified stem widths. """ if len(masters) > 1 and workingStems is not None: medianYscale = 1 medianAngle = 0 for master in masters: xScale, yScale = master.getScale() vstem, hstem = master.getStems() yScales.append(yScale) if glyphName in master and vstem is not None and hstem is not None: masterGlyph = master[glyphName] if workingStems == 'both': axis = { 'vstem': vstem * xScale, 'hstem': hstem * yScale } else: if workingStems == 'vstem': stem = vstem elif workingStems == 'hstem': stem = hstem if slantCorrection == True: # if interpolation is an/isotropic # skew master glyphs to upright angle to minimize deformations angle = master.italicAngle if angle: masterGlyph.skewX(angle) angles.append(angle) axis = {'stem': stem * xScale} mutatorMasters.append((Location(**axis), masterGlyph)) if len(angles) and slantCorrection == True: # calculate a median slant angle # in case there are variations among masters # shouldn’t happen, most of the time medianAngle = sum(angles) / len(angles) medianYscale = sum(yScales) / len(yScales) targetLocation = self._getTargetLocation(stemTarget, masters, workingStems, (xScale, medianYscale)) instanceGlyph = self._getInstanceGlyph(targetLocation, mutatorMasters) if instanceGlyph.name == '_error_': if self.hasGlyph(glyphName): instanceGlyph.unicodes = masters[0][glyphName].unicodes self.mutatorErrors[-1]['glyph'] = glyphName self.mutatorErrors[-1]['masters'] = mutatorMasters if medianAngle and slantCorrection == True: # if masters were skewed to upright position # skew instance back to probable slant angle instanceGlyph.skew(-medianAngle) instanceGlyph.round() if attributes is not None: for attributeName in attributes: value = attributes[attributeName] setattr(instanceGlyph, attributeName, value) return instanceGlyph return ErrorGlyph('None') def _getInstanceGlyph(self, location, masters): I = self._getInstance(location, masters) if I is not None: return I.extractGlyph(RGlyph()) else: errorMessage = self.mutatorErrors[-1]['error'] return ErrorGlyph('Interpolation', errorMessage) def _getInstance(self, location, masters): try: b, m = buildMutator(masters) if m is not None: instance = m.makeInstance(location) return instance except Exception as e: self.mutatorErrors.append({'error': e.message}) return None def _getTargetLocation(self, stemTarget, masters, workingStems, scale): """ Return a proper Location object for a scaled glyph instance, the essential part lies in the conversion of stem values. so that in anisotropic mode, a MutatorScaleEngine can attempt to produce a glyph with proper stem widths without requiring two-axes interpolation. """ xScale, yScale = scale targetVstem, targetHstem = None, None try: targetVstem, targetHstem = stemTarget except: pass if targetVstem is not None and targetHstem is not None: if workingStems == 'both': return Location(vstem=targetVstem, hstem=targetHstem) elif workingStems == 'vstem': vStems = [master.vstem * xScale for master in masters] hStems = [master.hstem * yScale for master in masters] (minVStem, minStemIndex), (maxVStem, maxStemIndex) = self._getExtremes(vStems) vStemSpan = (minVStem, maxVStem) hStemSpan = hStems[minStemIndex], hStems[maxStemIndex] newHstem = mapValue(targetHstem, hStemSpan, vStemSpan) return Location(stem=(targetVstem, newHstem)) elif workingStems == 'hstem': return Location(stem=targetHstem) else: return Location(stem=stemTarget) def _getExtremes(self, values): """ Return the minimum and maximum in a list of values with indices, this implementation was necessary to distinguish indices when min and max value happen to be equal (without being the same value per se). """ if len(values) > 1: baseValue = (values[0], 0) smallest, largest = baseValue, baseValue for i, value in enumerate(values[1:]): if value >= largest[0]: largest = (value, (i + 1)) elif value < smallest[0]: smallest = (value, (i + 1)) return smallest, largest return def _determineWorkingStems(self): """ Check conditions are met for two-axis interpolation in MutatorMath: 1. At least two identical values (to bind a new axis to the first axis) 2. At least a third and different value (to be able to have a differential on second axis) """ masters = self.masters.values() twoAxes = False stemMode = None stems = { 'vstem': [master.vstem for master in masters], 'hstem': [master.hstem for master in masters] } if len(masters) > 2: twoAxes = self._checkForTwoAxes(stems) if twoAxes == True: stemMode = 'both' elif twoAxes == False: for stemName in stems: stemValues = stems[stemName] diff = self._numbersHaveDifferential(stemValues) if diff == True: stemMode = stemName break self._workingStems = stemMode def _checkForTwoAxes(self, stemsList): """ Check conditions are met for two-axis interpolation in MutatorMath: 1. At least two identical values (to bind a new axis to the first axis) 2. At least a third and different value (to be able to have a differential on second axis) """ twoAxes = [] vstems = stemsList['vstem'] hstems = stemsList['hstem'] twoAxes.append(self._numbersHaveDifferential(vstems)) twoAxes.append(self._numbersHaveSplitDifferential(hstems)) return bool(reduce(lambda a, b: a * b, twoAxes)) def _numbersHaveSplitDifferential(self, values): """Looking for at least two similar values and one differing from the others.""" length = len(values) values.sort() if length > 1: identicalValues = 0 differentValues = 0 for i, value in enumerate(values): if i < length - 1: nextValue = values[i + 1] if value is not None: if nextValue == value: identicalValues += 1 if nextValue != value: differentValues += 1 return bool(identicalValues) and bool(differentValues) return False def _numbersHaveDifferential(self, values): """Looking for at least two different values in a bunch.""" length = len(values) values.sort() differential = False if length > 1: differentValues = 0 for i, value in enumerate(values): if i < length - 1: nextValue = values[i + 1] if nextValue != value and value is not None: differential = True break return differential def getMutatorReport(self): return self.mutatorErrors
def getScaledGlyph(self, glyphName, stemTarget, slantCorrection=True, attributes=None): """Return an interpolated & scaled glyph according to set parameters and given masters.""" masters = self.masters.values() workingStems = self._workingStems mutatorMasters = [] yScales = [] angles = [] """ Gather master glyphs for interpolation: each master glyph is scaled down according to set parameter, it is then inserted in a mutator design space with scaled down stem values. Asking for the initial stem values of a scaled down glyphName will result in an scaled glyph which will retain specified stem widths. """ if len(masters) > 1 and workingStems is not None: medianYscale = 1 medianAngle = 0 for master in masters: xScale, yScale = master.getScale() vstem, hstem = master.getStems() yScales.append(yScale) if glyphName in master and vstem is not None and hstem is not None: masterGlyph = master[glyphName] if workingStems == 'both': axis = { 'vstem': vstem * xScale, 'hstem': hstem * yScale } else: if workingStems == 'vstem': stem = vstem elif workingStems == 'hstem': stem = hstem if slantCorrection == True: # if interpolation is an/isotropic # skew master glyphs to upright angle to minimize deformations angle = master.italicAngle if angle: masterGlyph.skewX(angle) angles.append(angle) axis = {'stem': stem * xScale} mutatorMasters.append((Location(**axis), masterGlyph)) if len(angles) and slantCorrection == True: # calculate a median slant angle # in case there are variations among masters # shouldn’t happen, most of the time medianAngle = sum(angles) / len(angles) medianYscale = sum(yScales) / len(yScales) targetLocation = self._getTargetLocation(stemTarget, masters, workingStems, (xScale, medianYscale)) instanceGlyph = self._getInstanceGlyph(targetLocation, mutatorMasters) if instanceGlyph.name == '_error_': if self.hasGlyph(glyphName): instanceGlyph.unicodes = masters[0][glyphName].unicodes self.mutatorErrors[-1]['glyph'] = glyphName self.mutatorErrors[-1]['masters'] = mutatorMasters if medianAngle and slantCorrection == True: # if masters were skewed to upright position # skew instance back to probable slant angle instanceGlyph.skew(-medianAngle) instanceGlyph.round() if attributes is not None: for attributeName in attributes: value = attributes[attributeName] setattr(instanceGlyph, attributeName, value) return instanceGlyph return ErrorGlyph('None')