1*e1fe3e4aSElliott Hughes"""Allows building all the variable fonts of a DesignSpace version 5 by 2*e1fe3e4aSElliott Hughessplitting the document into interpolable sub-space, then into each VF. 3*e1fe3e4aSElliott Hughes""" 4*e1fe3e4aSElliott Hughes 5*e1fe3e4aSElliott Hughesfrom __future__ import annotations 6*e1fe3e4aSElliott Hughes 7*e1fe3e4aSElliott Hughesimport itertools 8*e1fe3e4aSElliott Hughesimport logging 9*e1fe3e4aSElliott Hughesimport math 10*e1fe3e4aSElliott Hughesfrom typing import Any, Callable, Dict, Iterator, List, Tuple, cast 11*e1fe3e4aSElliott Hughes 12*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib import ( 13*e1fe3e4aSElliott Hughes AxisDescriptor, 14*e1fe3e4aSElliott Hughes AxisMappingDescriptor, 15*e1fe3e4aSElliott Hughes DesignSpaceDocument, 16*e1fe3e4aSElliott Hughes DiscreteAxisDescriptor, 17*e1fe3e4aSElliott Hughes InstanceDescriptor, 18*e1fe3e4aSElliott Hughes RuleDescriptor, 19*e1fe3e4aSElliott Hughes SimpleLocationDict, 20*e1fe3e4aSElliott Hughes SourceDescriptor, 21*e1fe3e4aSElliott Hughes VariableFontDescriptor, 22*e1fe3e4aSElliott Hughes) 23*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib.statNames import StatNames, getStatNames 24*e1fe3e4aSElliott Hughesfrom fontTools.designspaceLib.types import ( 25*e1fe3e4aSElliott Hughes ConditionSet, 26*e1fe3e4aSElliott Hughes Range, 27*e1fe3e4aSElliott Hughes Region, 28*e1fe3e4aSElliott Hughes getVFUserRegion, 29*e1fe3e4aSElliott Hughes locationInRegion, 30*e1fe3e4aSElliott Hughes regionInRegion, 31*e1fe3e4aSElliott Hughes userRegionToDesignRegion, 32*e1fe3e4aSElliott Hughes) 33*e1fe3e4aSElliott Hughes 34*e1fe3e4aSElliott HughesLOGGER = logging.getLogger(__name__) 35*e1fe3e4aSElliott Hughes 36*e1fe3e4aSElliott HughesMakeInstanceFilenameCallable = Callable[ 37*e1fe3e4aSElliott Hughes [DesignSpaceDocument, InstanceDescriptor, StatNames], str 38*e1fe3e4aSElliott Hughes] 39*e1fe3e4aSElliott Hughes 40*e1fe3e4aSElliott Hughes 41*e1fe3e4aSElliott Hughesdef defaultMakeInstanceFilename( 42*e1fe3e4aSElliott Hughes doc: DesignSpaceDocument, instance: InstanceDescriptor, statNames: StatNames 43*e1fe3e4aSElliott Hughes) -> str: 44*e1fe3e4aSElliott Hughes """Default callable to synthesize an instance filename 45*e1fe3e4aSElliott Hughes when makeNames=True, for instances that don't specify an instance name 46*e1fe3e4aSElliott Hughes in the designspace. This part of the name generation can be overriden 47*e1fe3e4aSElliott Hughes because it's not specified by the STAT table. 48*e1fe3e4aSElliott Hughes """ 49*e1fe3e4aSElliott Hughes familyName = instance.familyName or statNames.familyNames.get("en") 50*e1fe3e4aSElliott Hughes styleName = instance.styleName or statNames.styleNames.get("en") 51*e1fe3e4aSElliott Hughes return f"{familyName}-{styleName}.ttf" 52*e1fe3e4aSElliott Hughes 53*e1fe3e4aSElliott Hughes 54*e1fe3e4aSElliott Hughesdef splitInterpolable( 55*e1fe3e4aSElliott Hughes doc: DesignSpaceDocument, 56*e1fe3e4aSElliott Hughes makeNames: bool = True, 57*e1fe3e4aSElliott Hughes expandLocations: bool = True, 58*e1fe3e4aSElliott Hughes makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename, 59*e1fe3e4aSElliott Hughes) -> Iterator[Tuple[SimpleLocationDict, DesignSpaceDocument]]: 60*e1fe3e4aSElliott Hughes """Split the given DS5 into several interpolable sub-designspaces. 61*e1fe3e4aSElliott Hughes There are as many interpolable sub-spaces as there are combinations of 62*e1fe3e4aSElliott Hughes discrete axis values. 63*e1fe3e4aSElliott Hughes 64*e1fe3e4aSElliott Hughes E.g. with axes: 65*e1fe3e4aSElliott Hughes - italic (discrete) Upright or Italic 66*e1fe3e4aSElliott Hughes - style (discrete) Sans or Serif 67*e1fe3e4aSElliott Hughes - weight (continuous) 100 to 900 68*e1fe3e4aSElliott Hughes 69*e1fe3e4aSElliott Hughes There are 4 sub-spaces in which the Weight axis should interpolate: 70*e1fe3e4aSElliott Hughes (Upright, Sans), (Upright, Serif), (Italic, Sans) and (Italic, Serif). 71*e1fe3e4aSElliott Hughes 72*e1fe3e4aSElliott Hughes The sub-designspaces still include the full axis definitions and STAT data, 73*e1fe3e4aSElliott Hughes but the rules, sources, variable fonts, instances are trimmed down to only 74*e1fe3e4aSElliott Hughes keep what falls within the interpolable sub-space. 75*e1fe3e4aSElliott Hughes 76*e1fe3e4aSElliott Hughes Args: 77*e1fe3e4aSElliott Hughes - ``makeNames``: Whether to compute the instance family and style 78*e1fe3e4aSElliott Hughes names using the STAT data. 79*e1fe3e4aSElliott Hughes - ``expandLocations``: Whether to turn all locations into "full" 80*e1fe3e4aSElliott Hughes locations, including implicit default axis values where missing. 81*e1fe3e4aSElliott Hughes - ``makeInstanceFilename``: Callable to synthesize an instance filename 82*e1fe3e4aSElliott Hughes when makeNames=True, for instances that don't specify an instance name 83*e1fe3e4aSElliott Hughes in the designspace. This part of the name generation can be overridden 84*e1fe3e4aSElliott Hughes because it's not specified by the STAT table. 85*e1fe3e4aSElliott Hughes 86*e1fe3e4aSElliott Hughes .. versionadded:: 5.0 87*e1fe3e4aSElliott Hughes """ 88*e1fe3e4aSElliott Hughes discreteAxes = [] 89*e1fe3e4aSElliott Hughes interpolableUserRegion: Region = {} 90*e1fe3e4aSElliott Hughes for axis in doc.axes: 91*e1fe3e4aSElliott Hughes if hasattr(axis, "values"): 92*e1fe3e4aSElliott Hughes # Mypy doesn't support narrowing union types via hasattr() 93*e1fe3e4aSElliott Hughes # TODO(Python 3.10): use TypeGuard 94*e1fe3e4aSElliott Hughes # https://mypy.readthedocs.io/en/stable/type_narrowing.html 95*e1fe3e4aSElliott Hughes axis = cast(DiscreteAxisDescriptor, axis) 96*e1fe3e4aSElliott Hughes discreteAxes.append(axis) 97*e1fe3e4aSElliott Hughes else: 98*e1fe3e4aSElliott Hughes axis = cast(AxisDescriptor, axis) 99*e1fe3e4aSElliott Hughes interpolableUserRegion[axis.name] = Range( 100*e1fe3e4aSElliott Hughes axis.minimum, 101*e1fe3e4aSElliott Hughes axis.maximum, 102*e1fe3e4aSElliott Hughes axis.default, 103*e1fe3e4aSElliott Hughes ) 104*e1fe3e4aSElliott Hughes valueCombinations = itertools.product(*[axis.values for axis in discreteAxes]) 105*e1fe3e4aSElliott Hughes for values in valueCombinations: 106*e1fe3e4aSElliott Hughes discreteUserLocation = { 107*e1fe3e4aSElliott Hughes discreteAxis.name: value 108*e1fe3e4aSElliott Hughes for discreteAxis, value in zip(discreteAxes, values) 109*e1fe3e4aSElliott Hughes } 110*e1fe3e4aSElliott Hughes subDoc = _extractSubSpace( 111*e1fe3e4aSElliott Hughes doc, 112*e1fe3e4aSElliott Hughes {**interpolableUserRegion, **discreteUserLocation}, 113*e1fe3e4aSElliott Hughes keepVFs=True, 114*e1fe3e4aSElliott Hughes makeNames=makeNames, 115*e1fe3e4aSElliott Hughes expandLocations=expandLocations, 116*e1fe3e4aSElliott Hughes makeInstanceFilename=makeInstanceFilename, 117*e1fe3e4aSElliott Hughes ) 118*e1fe3e4aSElliott Hughes yield discreteUserLocation, subDoc 119*e1fe3e4aSElliott Hughes 120*e1fe3e4aSElliott Hughes 121*e1fe3e4aSElliott Hughesdef splitVariableFonts( 122*e1fe3e4aSElliott Hughes doc: DesignSpaceDocument, 123*e1fe3e4aSElliott Hughes makeNames: bool = False, 124*e1fe3e4aSElliott Hughes expandLocations: bool = False, 125*e1fe3e4aSElliott Hughes makeInstanceFilename: MakeInstanceFilenameCallable = defaultMakeInstanceFilename, 126*e1fe3e4aSElliott Hughes) -> Iterator[Tuple[str, DesignSpaceDocument]]: 127*e1fe3e4aSElliott Hughes """Convert each variable font listed in this document into a standalone 128*e1fe3e4aSElliott Hughes designspace. This can be used to compile all the variable fonts from a 129*e1fe3e4aSElliott Hughes format 5 designspace using tools that can only deal with 1 VF at a time. 130*e1fe3e4aSElliott Hughes 131*e1fe3e4aSElliott Hughes Args: 132*e1fe3e4aSElliott Hughes - ``makeNames``: Whether to compute the instance family and style 133*e1fe3e4aSElliott Hughes names using the STAT data. 134*e1fe3e4aSElliott Hughes - ``expandLocations``: Whether to turn all locations into "full" 135*e1fe3e4aSElliott Hughes locations, including implicit default axis values where missing. 136*e1fe3e4aSElliott Hughes - ``makeInstanceFilename``: Callable to synthesize an instance filename 137*e1fe3e4aSElliott Hughes when makeNames=True, for instances that don't specify an instance name 138*e1fe3e4aSElliott Hughes in the designspace. This part of the name generation can be overridden 139*e1fe3e4aSElliott Hughes because it's not specified by the STAT table. 140*e1fe3e4aSElliott Hughes 141*e1fe3e4aSElliott Hughes .. versionadded:: 5.0 142*e1fe3e4aSElliott Hughes """ 143*e1fe3e4aSElliott Hughes # Make one DesignspaceDoc v5 for each variable font 144*e1fe3e4aSElliott Hughes for vf in doc.getVariableFonts(): 145*e1fe3e4aSElliott Hughes vfUserRegion = getVFUserRegion(doc, vf) 146*e1fe3e4aSElliott Hughes vfDoc = _extractSubSpace( 147*e1fe3e4aSElliott Hughes doc, 148*e1fe3e4aSElliott Hughes vfUserRegion, 149*e1fe3e4aSElliott Hughes keepVFs=False, 150*e1fe3e4aSElliott Hughes makeNames=makeNames, 151*e1fe3e4aSElliott Hughes expandLocations=expandLocations, 152*e1fe3e4aSElliott Hughes makeInstanceFilename=makeInstanceFilename, 153*e1fe3e4aSElliott Hughes ) 154*e1fe3e4aSElliott Hughes vfDoc.lib = {**vfDoc.lib, **vf.lib} 155*e1fe3e4aSElliott Hughes yield vf.name, vfDoc 156*e1fe3e4aSElliott Hughes 157*e1fe3e4aSElliott Hughes 158*e1fe3e4aSElliott Hughesdef convert5to4( 159*e1fe3e4aSElliott Hughes doc: DesignSpaceDocument, 160*e1fe3e4aSElliott Hughes) -> Dict[str, DesignSpaceDocument]: 161*e1fe3e4aSElliott Hughes """Convert each variable font listed in this document into a standalone 162*e1fe3e4aSElliott Hughes format 4 designspace. This can be used to compile all the variable fonts 163*e1fe3e4aSElliott Hughes from a format 5 designspace using tools that only know about format 4. 164*e1fe3e4aSElliott Hughes 165*e1fe3e4aSElliott Hughes .. versionadded:: 5.0 166*e1fe3e4aSElliott Hughes """ 167*e1fe3e4aSElliott Hughes vfs = {} 168*e1fe3e4aSElliott Hughes for _location, subDoc in splitInterpolable(doc): 169*e1fe3e4aSElliott Hughes for vfName, vfDoc in splitVariableFonts(subDoc): 170*e1fe3e4aSElliott Hughes vfDoc.formatVersion = "4.1" 171*e1fe3e4aSElliott Hughes vfs[vfName] = vfDoc 172*e1fe3e4aSElliott Hughes return vfs 173*e1fe3e4aSElliott Hughes 174*e1fe3e4aSElliott Hughes 175*e1fe3e4aSElliott Hughesdef _extractSubSpace( 176*e1fe3e4aSElliott Hughes doc: DesignSpaceDocument, 177*e1fe3e4aSElliott Hughes userRegion: Region, 178*e1fe3e4aSElliott Hughes *, 179*e1fe3e4aSElliott Hughes keepVFs: bool, 180*e1fe3e4aSElliott Hughes makeNames: bool, 181*e1fe3e4aSElliott Hughes expandLocations: bool, 182*e1fe3e4aSElliott Hughes makeInstanceFilename: MakeInstanceFilenameCallable, 183*e1fe3e4aSElliott Hughes) -> DesignSpaceDocument: 184*e1fe3e4aSElliott Hughes subDoc = DesignSpaceDocument() 185*e1fe3e4aSElliott Hughes # Don't include STAT info 186*e1fe3e4aSElliott Hughes # FIXME: (Jany) let's think about it. Not include = OK because the point of 187*e1fe3e4aSElliott Hughes # the splitting is to build VFs and we'll use the STAT data of the full 188*e1fe3e4aSElliott Hughes # document to generate the STAT of the VFs, so "no need" to have STAT data 189*e1fe3e4aSElliott Hughes # in sub-docs. Counterpoint: what if someone wants to split this DS for 190*e1fe3e4aSElliott Hughes # other purposes? Maybe for that it would be useful to also subset the STAT 191*e1fe3e4aSElliott Hughes # data? 192*e1fe3e4aSElliott Hughes # subDoc.elidedFallbackName = doc.elidedFallbackName 193*e1fe3e4aSElliott Hughes 194*e1fe3e4aSElliott Hughes def maybeExpandDesignLocation(object): 195*e1fe3e4aSElliott Hughes if expandLocations: 196*e1fe3e4aSElliott Hughes return object.getFullDesignLocation(doc) 197*e1fe3e4aSElliott Hughes else: 198*e1fe3e4aSElliott Hughes return object.designLocation 199*e1fe3e4aSElliott Hughes 200*e1fe3e4aSElliott Hughes for axis in doc.axes: 201*e1fe3e4aSElliott Hughes range = userRegion[axis.name] 202*e1fe3e4aSElliott Hughes if isinstance(range, Range) and hasattr(axis, "minimum"): 203*e1fe3e4aSElliott Hughes # Mypy doesn't support narrowing union types via hasattr() 204*e1fe3e4aSElliott Hughes # TODO(Python 3.10): use TypeGuard 205*e1fe3e4aSElliott Hughes # https://mypy.readthedocs.io/en/stable/type_narrowing.html 206*e1fe3e4aSElliott Hughes axis = cast(AxisDescriptor, axis) 207*e1fe3e4aSElliott Hughes subDoc.addAxis( 208*e1fe3e4aSElliott Hughes AxisDescriptor( 209*e1fe3e4aSElliott Hughes # Same info 210*e1fe3e4aSElliott Hughes tag=axis.tag, 211*e1fe3e4aSElliott Hughes name=axis.name, 212*e1fe3e4aSElliott Hughes labelNames=axis.labelNames, 213*e1fe3e4aSElliott Hughes hidden=axis.hidden, 214*e1fe3e4aSElliott Hughes # Subset range 215*e1fe3e4aSElliott Hughes minimum=max(range.minimum, axis.minimum), 216*e1fe3e4aSElliott Hughes default=range.default or axis.default, 217*e1fe3e4aSElliott Hughes maximum=min(range.maximum, axis.maximum), 218*e1fe3e4aSElliott Hughes map=[ 219*e1fe3e4aSElliott Hughes (user, design) 220*e1fe3e4aSElliott Hughes for user, design in axis.map 221*e1fe3e4aSElliott Hughes if range.minimum <= user <= range.maximum 222*e1fe3e4aSElliott Hughes ], 223*e1fe3e4aSElliott Hughes # Don't include STAT info 224*e1fe3e4aSElliott Hughes axisOrdering=None, 225*e1fe3e4aSElliott Hughes axisLabels=None, 226*e1fe3e4aSElliott Hughes ) 227*e1fe3e4aSElliott Hughes ) 228*e1fe3e4aSElliott Hughes 229*e1fe3e4aSElliott Hughes subDoc.axisMappings = mappings = [] 230*e1fe3e4aSElliott Hughes subDocAxes = {axis.name for axis in subDoc.axes} 231*e1fe3e4aSElliott Hughes for mapping in doc.axisMappings: 232*e1fe3e4aSElliott Hughes if not all(axis in subDocAxes for axis in mapping.inputLocation.keys()): 233*e1fe3e4aSElliott Hughes continue 234*e1fe3e4aSElliott Hughes if not all(axis in subDocAxes for axis in mapping.outputLocation.keys()): 235*e1fe3e4aSElliott Hughes LOGGER.error( 236*e1fe3e4aSElliott Hughes "In axis mapping from input %s, some output axes are not in the variable-font: %s", 237*e1fe3e4aSElliott Hughes mapping.inputLocation, 238*e1fe3e4aSElliott Hughes mapping.outputLocation, 239*e1fe3e4aSElliott Hughes ) 240*e1fe3e4aSElliott Hughes continue 241*e1fe3e4aSElliott Hughes 242*e1fe3e4aSElliott Hughes mappingAxes = set() 243*e1fe3e4aSElliott Hughes mappingAxes.update(mapping.inputLocation.keys()) 244*e1fe3e4aSElliott Hughes mappingAxes.update(mapping.outputLocation.keys()) 245*e1fe3e4aSElliott Hughes for axis in doc.axes: 246*e1fe3e4aSElliott Hughes if axis.name not in mappingAxes: 247*e1fe3e4aSElliott Hughes continue 248*e1fe3e4aSElliott Hughes range = userRegion[axis.name] 249*e1fe3e4aSElliott Hughes if ( 250*e1fe3e4aSElliott Hughes range.minimum != axis.minimum 251*e1fe3e4aSElliott Hughes or (range.default is not None and range.default != axis.default) 252*e1fe3e4aSElliott Hughes or range.maximum != axis.maximum 253*e1fe3e4aSElliott Hughes ): 254*e1fe3e4aSElliott Hughes LOGGER.error( 255*e1fe3e4aSElliott Hughes "Limiting axis ranges used in <mapping> elements not supported: %s", 256*e1fe3e4aSElliott Hughes axis.name, 257*e1fe3e4aSElliott Hughes ) 258*e1fe3e4aSElliott Hughes continue 259*e1fe3e4aSElliott Hughes 260*e1fe3e4aSElliott Hughes mappings.append( 261*e1fe3e4aSElliott Hughes AxisMappingDescriptor( 262*e1fe3e4aSElliott Hughes inputLocation=mapping.inputLocation, 263*e1fe3e4aSElliott Hughes outputLocation=mapping.outputLocation, 264*e1fe3e4aSElliott Hughes ) 265*e1fe3e4aSElliott Hughes ) 266*e1fe3e4aSElliott Hughes 267*e1fe3e4aSElliott Hughes # Don't include STAT info 268*e1fe3e4aSElliott Hughes # subDoc.locationLabels = doc.locationLabels 269*e1fe3e4aSElliott Hughes 270*e1fe3e4aSElliott Hughes # Rules: subset them based on conditions 271*e1fe3e4aSElliott Hughes designRegion = userRegionToDesignRegion(doc, userRegion) 272*e1fe3e4aSElliott Hughes subDoc.rules = _subsetRulesBasedOnConditions(doc.rules, designRegion) 273*e1fe3e4aSElliott Hughes subDoc.rulesProcessingLast = doc.rulesProcessingLast 274*e1fe3e4aSElliott Hughes 275*e1fe3e4aSElliott Hughes # Sources: keep only the ones that fall within the kept axis ranges 276*e1fe3e4aSElliott Hughes for source in doc.sources: 277*e1fe3e4aSElliott Hughes if not locationInRegion(doc.map_backward(source.designLocation), userRegion): 278*e1fe3e4aSElliott Hughes continue 279*e1fe3e4aSElliott Hughes 280*e1fe3e4aSElliott Hughes subDoc.addSource( 281*e1fe3e4aSElliott Hughes SourceDescriptor( 282*e1fe3e4aSElliott Hughes filename=source.filename, 283*e1fe3e4aSElliott Hughes path=source.path, 284*e1fe3e4aSElliott Hughes font=source.font, 285*e1fe3e4aSElliott Hughes name=source.name, 286*e1fe3e4aSElliott Hughes designLocation=_filterLocation( 287*e1fe3e4aSElliott Hughes userRegion, maybeExpandDesignLocation(source) 288*e1fe3e4aSElliott Hughes ), 289*e1fe3e4aSElliott Hughes layerName=source.layerName, 290*e1fe3e4aSElliott Hughes familyName=source.familyName, 291*e1fe3e4aSElliott Hughes styleName=source.styleName, 292*e1fe3e4aSElliott Hughes muteKerning=source.muteKerning, 293*e1fe3e4aSElliott Hughes muteInfo=source.muteInfo, 294*e1fe3e4aSElliott Hughes mutedGlyphNames=source.mutedGlyphNames, 295*e1fe3e4aSElliott Hughes ) 296*e1fe3e4aSElliott Hughes ) 297*e1fe3e4aSElliott Hughes 298*e1fe3e4aSElliott Hughes # Copy family name translations from the old default source to the new default 299*e1fe3e4aSElliott Hughes vfDefault = subDoc.findDefault() 300*e1fe3e4aSElliott Hughes oldDefault = doc.findDefault() 301*e1fe3e4aSElliott Hughes if vfDefault is not None and oldDefault is not None: 302*e1fe3e4aSElliott Hughes vfDefault.localisedFamilyName = oldDefault.localisedFamilyName 303*e1fe3e4aSElliott Hughes 304*e1fe3e4aSElliott Hughes # Variable fonts: keep only the ones that fall within the kept axis ranges 305*e1fe3e4aSElliott Hughes if keepVFs: 306*e1fe3e4aSElliott Hughes # Note: call getVariableFont() to make the implicit VFs explicit 307*e1fe3e4aSElliott Hughes for vf in doc.getVariableFonts(): 308*e1fe3e4aSElliott Hughes vfUserRegion = getVFUserRegion(doc, vf) 309*e1fe3e4aSElliott Hughes if regionInRegion(vfUserRegion, userRegion): 310*e1fe3e4aSElliott Hughes subDoc.addVariableFont( 311*e1fe3e4aSElliott Hughes VariableFontDescriptor( 312*e1fe3e4aSElliott Hughes name=vf.name, 313*e1fe3e4aSElliott Hughes filename=vf.filename, 314*e1fe3e4aSElliott Hughes axisSubsets=[ 315*e1fe3e4aSElliott Hughes axisSubset 316*e1fe3e4aSElliott Hughes for axisSubset in vf.axisSubsets 317*e1fe3e4aSElliott Hughes if isinstance(userRegion[axisSubset.name], Range) 318*e1fe3e4aSElliott Hughes ], 319*e1fe3e4aSElliott Hughes lib=vf.lib, 320*e1fe3e4aSElliott Hughes ) 321*e1fe3e4aSElliott Hughes ) 322*e1fe3e4aSElliott Hughes 323*e1fe3e4aSElliott Hughes # Instances: same as Sources + compute missing names 324*e1fe3e4aSElliott Hughes for instance in doc.instances: 325*e1fe3e4aSElliott Hughes if not locationInRegion(instance.getFullUserLocation(doc), userRegion): 326*e1fe3e4aSElliott Hughes continue 327*e1fe3e4aSElliott Hughes 328*e1fe3e4aSElliott Hughes if makeNames: 329*e1fe3e4aSElliott Hughes statNames = getStatNames(doc, instance.getFullUserLocation(doc)) 330*e1fe3e4aSElliott Hughes familyName = instance.familyName or statNames.familyNames.get("en") 331*e1fe3e4aSElliott Hughes styleName = instance.styleName or statNames.styleNames.get("en") 332*e1fe3e4aSElliott Hughes subDoc.addInstance( 333*e1fe3e4aSElliott Hughes InstanceDescriptor( 334*e1fe3e4aSElliott Hughes filename=instance.filename 335*e1fe3e4aSElliott Hughes or makeInstanceFilename(doc, instance, statNames), 336*e1fe3e4aSElliott Hughes path=instance.path, 337*e1fe3e4aSElliott Hughes font=instance.font, 338*e1fe3e4aSElliott Hughes name=instance.name or f"{familyName} {styleName}", 339*e1fe3e4aSElliott Hughes userLocation={} if expandLocations else instance.userLocation, 340*e1fe3e4aSElliott Hughes designLocation=_filterLocation( 341*e1fe3e4aSElliott Hughes userRegion, maybeExpandDesignLocation(instance) 342*e1fe3e4aSElliott Hughes ), 343*e1fe3e4aSElliott Hughes familyName=familyName, 344*e1fe3e4aSElliott Hughes styleName=styleName, 345*e1fe3e4aSElliott Hughes postScriptFontName=instance.postScriptFontName 346*e1fe3e4aSElliott Hughes or statNames.postScriptFontName, 347*e1fe3e4aSElliott Hughes styleMapFamilyName=instance.styleMapFamilyName 348*e1fe3e4aSElliott Hughes or statNames.styleMapFamilyNames.get("en"), 349*e1fe3e4aSElliott Hughes styleMapStyleName=instance.styleMapStyleName 350*e1fe3e4aSElliott Hughes or statNames.styleMapStyleName, 351*e1fe3e4aSElliott Hughes localisedFamilyName=instance.localisedFamilyName 352*e1fe3e4aSElliott Hughes or statNames.familyNames, 353*e1fe3e4aSElliott Hughes localisedStyleName=instance.localisedStyleName 354*e1fe3e4aSElliott Hughes or statNames.styleNames, 355*e1fe3e4aSElliott Hughes localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName 356*e1fe3e4aSElliott Hughes or statNames.styleMapFamilyNames, 357*e1fe3e4aSElliott Hughes localisedStyleMapStyleName=instance.localisedStyleMapStyleName 358*e1fe3e4aSElliott Hughes or {}, 359*e1fe3e4aSElliott Hughes lib=instance.lib, 360*e1fe3e4aSElliott Hughes ) 361*e1fe3e4aSElliott Hughes ) 362*e1fe3e4aSElliott Hughes else: 363*e1fe3e4aSElliott Hughes subDoc.addInstance( 364*e1fe3e4aSElliott Hughes InstanceDescriptor( 365*e1fe3e4aSElliott Hughes filename=instance.filename, 366*e1fe3e4aSElliott Hughes path=instance.path, 367*e1fe3e4aSElliott Hughes font=instance.font, 368*e1fe3e4aSElliott Hughes name=instance.name, 369*e1fe3e4aSElliott Hughes userLocation={} if expandLocations else instance.userLocation, 370*e1fe3e4aSElliott Hughes designLocation=_filterLocation( 371*e1fe3e4aSElliott Hughes userRegion, maybeExpandDesignLocation(instance) 372*e1fe3e4aSElliott Hughes ), 373*e1fe3e4aSElliott Hughes familyName=instance.familyName, 374*e1fe3e4aSElliott Hughes styleName=instance.styleName, 375*e1fe3e4aSElliott Hughes postScriptFontName=instance.postScriptFontName, 376*e1fe3e4aSElliott Hughes styleMapFamilyName=instance.styleMapFamilyName, 377*e1fe3e4aSElliott Hughes styleMapStyleName=instance.styleMapStyleName, 378*e1fe3e4aSElliott Hughes localisedFamilyName=instance.localisedFamilyName, 379*e1fe3e4aSElliott Hughes localisedStyleName=instance.localisedStyleName, 380*e1fe3e4aSElliott Hughes localisedStyleMapFamilyName=instance.localisedStyleMapFamilyName, 381*e1fe3e4aSElliott Hughes localisedStyleMapStyleName=instance.localisedStyleMapStyleName, 382*e1fe3e4aSElliott Hughes lib=instance.lib, 383*e1fe3e4aSElliott Hughes ) 384*e1fe3e4aSElliott Hughes ) 385*e1fe3e4aSElliott Hughes 386*e1fe3e4aSElliott Hughes subDoc.lib = doc.lib 387*e1fe3e4aSElliott Hughes 388*e1fe3e4aSElliott Hughes return subDoc 389*e1fe3e4aSElliott Hughes 390*e1fe3e4aSElliott Hughes 391*e1fe3e4aSElliott Hughesdef _conditionSetFrom(conditionSet: List[Dict[str, Any]]) -> ConditionSet: 392*e1fe3e4aSElliott Hughes c: Dict[str, Range] = {} 393*e1fe3e4aSElliott Hughes for condition in conditionSet: 394*e1fe3e4aSElliott Hughes minimum, maximum = condition.get("minimum"), condition.get("maximum") 395*e1fe3e4aSElliott Hughes c[condition["name"]] = Range( 396*e1fe3e4aSElliott Hughes minimum if minimum is not None else -math.inf, 397*e1fe3e4aSElliott Hughes maximum if maximum is not None else math.inf, 398*e1fe3e4aSElliott Hughes ) 399*e1fe3e4aSElliott Hughes return c 400*e1fe3e4aSElliott Hughes 401*e1fe3e4aSElliott Hughes 402*e1fe3e4aSElliott Hughesdef _subsetRulesBasedOnConditions( 403*e1fe3e4aSElliott Hughes rules: List[RuleDescriptor], designRegion: Region 404*e1fe3e4aSElliott Hughes) -> List[RuleDescriptor]: 405*e1fe3e4aSElliott Hughes # What rules to keep: 406*e1fe3e4aSElliott Hughes # - Keep the rule if any conditionset is relevant. 407*e1fe3e4aSElliott Hughes # - A conditionset is relevant if all conditions are relevant or it is empty. 408*e1fe3e4aSElliott Hughes # - A condition is relevant if 409*e1fe3e4aSElliott Hughes # - axis is point (C-AP), 410*e1fe3e4aSElliott Hughes # - and point in condition's range (C-AP-in) 411*e1fe3e4aSElliott Hughes # (in this case remove the condition because it's always true) 412*e1fe3e4aSElliott Hughes # - else (C-AP-out) whole conditionset can be discarded (condition false 413*e1fe3e4aSElliott Hughes # => conditionset false) 414*e1fe3e4aSElliott Hughes # - axis is range (C-AR), 415*e1fe3e4aSElliott Hughes # - (C-AR-all) and axis range fully contained in condition range: we can 416*e1fe3e4aSElliott Hughes # scrap the condition because it's always true 417*e1fe3e4aSElliott Hughes # - (C-AR-inter) and intersection(axis range, condition range) not empty: 418*e1fe3e4aSElliott Hughes # keep the condition with the smaller range (= intersection) 419*e1fe3e4aSElliott Hughes # - (C-AR-none) else, whole conditionset can be discarded 420*e1fe3e4aSElliott Hughes newRules: List[RuleDescriptor] = [] 421*e1fe3e4aSElliott Hughes for rule in rules: 422*e1fe3e4aSElliott Hughes newRule: RuleDescriptor = RuleDescriptor( 423*e1fe3e4aSElliott Hughes name=rule.name, conditionSets=[], subs=rule.subs 424*e1fe3e4aSElliott Hughes ) 425*e1fe3e4aSElliott Hughes for conditionset in rule.conditionSets: 426*e1fe3e4aSElliott Hughes cs = _conditionSetFrom(conditionset) 427*e1fe3e4aSElliott Hughes newConditionset: List[Dict[str, Any]] = [] 428*e1fe3e4aSElliott Hughes discardConditionset = False 429*e1fe3e4aSElliott Hughes for selectionName, selectionValue in designRegion.items(): 430*e1fe3e4aSElliott Hughes # TODO: Ensure that all(key in conditionset for key in region.keys())? 431*e1fe3e4aSElliott Hughes if selectionName not in cs: 432*e1fe3e4aSElliott Hughes # raise Exception("Selection has different axes than the rules") 433*e1fe3e4aSElliott Hughes continue 434*e1fe3e4aSElliott Hughes if isinstance(selectionValue, (float, int)): # is point 435*e1fe3e4aSElliott Hughes # Case C-AP-in 436*e1fe3e4aSElliott Hughes if selectionValue in cs[selectionName]: 437*e1fe3e4aSElliott Hughes pass # always matches, conditionset can stay empty for this one. 438*e1fe3e4aSElliott Hughes # Case C-AP-out 439*e1fe3e4aSElliott Hughes else: 440*e1fe3e4aSElliott Hughes discardConditionset = True 441*e1fe3e4aSElliott Hughes else: # is range 442*e1fe3e4aSElliott Hughes # Case C-AR-all 443*e1fe3e4aSElliott Hughes if selectionValue in cs[selectionName]: 444*e1fe3e4aSElliott Hughes pass # always matches, conditionset can stay empty for this one. 445*e1fe3e4aSElliott Hughes else: 446*e1fe3e4aSElliott Hughes intersection = cs[selectionName].intersection(selectionValue) 447*e1fe3e4aSElliott Hughes # Case C-AR-inter 448*e1fe3e4aSElliott Hughes if intersection is not None: 449*e1fe3e4aSElliott Hughes newConditionset.append( 450*e1fe3e4aSElliott Hughes { 451*e1fe3e4aSElliott Hughes "name": selectionName, 452*e1fe3e4aSElliott Hughes "minimum": intersection.minimum, 453*e1fe3e4aSElliott Hughes "maximum": intersection.maximum, 454*e1fe3e4aSElliott Hughes } 455*e1fe3e4aSElliott Hughes ) 456*e1fe3e4aSElliott Hughes # Case C-AR-none 457*e1fe3e4aSElliott Hughes else: 458*e1fe3e4aSElliott Hughes discardConditionset = True 459*e1fe3e4aSElliott Hughes if not discardConditionset: 460*e1fe3e4aSElliott Hughes newRule.conditionSets.append(newConditionset) 461*e1fe3e4aSElliott Hughes if newRule.conditionSets: 462*e1fe3e4aSElliott Hughes newRules.append(newRule) 463*e1fe3e4aSElliott Hughes 464*e1fe3e4aSElliott Hughes return newRules 465*e1fe3e4aSElliott Hughes 466*e1fe3e4aSElliott Hughes 467*e1fe3e4aSElliott Hughesdef _filterLocation( 468*e1fe3e4aSElliott Hughes userRegion: Region, 469*e1fe3e4aSElliott Hughes location: Dict[str, float], 470*e1fe3e4aSElliott Hughes) -> Dict[str, float]: 471*e1fe3e4aSElliott Hughes return { 472*e1fe3e4aSElliott Hughes name: value 473*e1fe3e4aSElliott Hughes for name, value in location.items() 474*e1fe3e4aSElliott Hughes if name in userRegion and isinstance(userRegion[name], Range) 475*e1fe3e4aSElliott Hughes } 476