You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			272 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
			
		
		
	
	
			272 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Python
		
	
from fontTools.varLib.models import VariationModel
 | 
						|
from fontTools.varLib.varStore import VarStoreInstancer
 | 
						|
from fontTools.misc.fixedTools import fixedToFloat as fi2fl
 | 
						|
from itertools import product
 | 
						|
import sys
 | 
						|
 | 
						|
 | 
						|
def _denormalize(v, axis):
 | 
						|
    if v >= 0:
 | 
						|
        return axis.defaultValue + v * (axis.maxValue - axis.defaultValue)
 | 
						|
    else:
 | 
						|
        return axis.defaultValue + v * (axis.defaultValue - axis.minValue)
 | 
						|
 | 
						|
 | 
						|
def _pruneLocations(locations, poles, axisTags):
 | 
						|
    # Now we have all the input locations, find which ones are
 | 
						|
    # not needed and remove them.
 | 
						|
 | 
						|
    # Note: This algorithm is heavily tied to how VariationModel
 | 
						|
    # is implemented.  It assumes that input was extracted from
 | 
						|
    # VariationModel-generated object, like an ItemVariationStore
 | 
						|
    # created by fontmake using varLib.models.VariationModel.
 | 
						|
    # Some CoPilot blabbering:
 | 
						|
    # I *think* I can prove that this algorithm is correct, but
 | 
						|
    # I'm not 100% sure.  It's possible that there are edge cases
 | 
						|
    # where this algorithm will fail.  I'm not sure how to prove
 | 
						|
    # that it's correct, but I'm also not sure how to prove that
 | 
						|
    # it's incorrect.  I'm not sure how to write a test case that
 | 
						|
    # would prove that it's incorrect.  I'm not sure how to write
 | 
						|
    # a test case that would prove that it's correct.
 | 
						|
 | 
						|
    model = VariationModel(locations, axisTags)
 | 
						|
    modelMapping = model.mapping
 | 
						|
    modelSupports = model.supports
 | 
						|
    pins = {tuple(k.items()): None for k in poles}
 | 
						|
    for location in poles:
 | 
						|
        i = locations.index(location)
 | 
						|
        i = modelMapping[i]
 | 
						|
        support = modelSupports[i]
 | 
						|
        supportAxes = set(support.keys())
 | 
						|
        for axisTag, (minV, _, maxV) in support.items():
 | 
						|
            for v in (minV, maxV):
 | 
						|
                if v in (-1, 0, 1):
 | 
						|
                    continue
 | 
						|
                for pin in pins.keys():
 | 
						|
                    pinLocation = dict(pin)
 | 
						|
                    pinAxes = set(pinLocation.keys())
 | 
						|
                    if pinAxes != supportAxes:
 | 
						|
                        continue
 | 
						|
                    if axisTag not in pinAxes:
 | 
						|
                        continue
 | 
						|
                    if pinLocation[axisTag] == v:
 | 
						|
                        break
 | 
						|
                else:
 | 
						|
                    # No pin found. Go through the previous masters
 | 
						|
                    # and find a suitable pin.  Going backwards is
 | 
						|
                    # better because it can find a pin that is close
 | 
						|
                    # to the pole in more dimensions, and reducing
 | 
						|
                    # the total number of pins needed.
 | 
						|
                    for candidateIdx in range(i - 1, -1, -1):
 | 
						|
                        candidate = modelSupports[candidateIdx]
 | 
						|
                        candidateAxes = set(candidate.keys())
 | 
						|
                        if candidateAxes != supportAxes:
 | 
						|
                            continue
 | 
						|
                        if axisTag not in candidateAxes:
 | 
						|
                            continue
 | 
						|
                        candidate = {
 | 
						|
                            k: defaultV for k, (_, defaultV, _) in candidate.items()
 | 
						|
                        }
 | 
						|
                        if candidate[axisTag] == v:
 | 
						|
                            pins[tuple(candidate.items())] = None
 | 
						|
                            break
 | 
						|
                    else:
 | 
						|
                        assert False, "No pin found"
 | 
						|
    return [dict(t) for t in pins.keys()]
 | 
						|
 | 
						|
 | 
						|
def mappings_from_avar(font, denormalize=True):
 | 
						|
    fvarAxes = font["fvar"].axes
 | 
						|
    axisMap = {a.axisTag: a for a in fvarAxes}
 | 
						|
    axisTags = [a.axisTag for a in fvarAxes]
 | 
						|
    axisIndexes = {a.axisTag: i for i, a in enumerate(fvarAxes)}
 | 
						|
    if "avar" not in font:
 | 
						|
        return {}, {}
 | 
						|
    avar = font["avar"]
 | 
						|
    axisMaps = {
 | 
						|
        tag: seg
 | 
						|
        for tag, seg in avar.segments.items()
 | 
						|
        if seg and seg != {-1: -1, 0: 0, 1: 1}
 | 
						|
    }
 | 
						|
    mappings = []
 | 
						|
 | 
						|
    if getattr(avar, "majorVersion", 1) == 2:
 | 
						|
        varStore = avar.table.VarStore
 | 
						|
        regions = varStore.VarRegionList.Region
 | 
						|
 | 
						|
        # Find all the input locations; this finds "poles", that are
 | 
						|
        # locations of the peaks, and "corners", that are locations
 | 
						|
        # of the corners of the regions.  These two sets of locations
 | 
						|
        # together constitute inputLocations to consider.
 | 
						|
 | 
						|
        poles = {(): None}  # Just using it as an ordered set
 | 
						|
        inputLocations = set({()})
 | 
						|
        for varData in varStore.VarData:
 | 
						|
            regionIndices = varData.VarRegionIndex
 | 
						|
            for regionIndex in regionIndices:
 | 
						|
                peakLocation = []
 | 
						|
                corners = []
 | 
						|
                region = regions[regionIndex]
 | 
						|
                for axisIndex, axis in enumerate(region.VarRegionAxis):
 | 
						|
                    if axis.PeakCoord == 0:
 | 
						|
                        continue
 | 
						|
                    axisTag = axisTags[axisIndex]
 | 
						|
                    peakLocation.append((axisTag, axis.PeakCoord))
 | 
						|
                    corner = []
 | 
						|
                    if axis.StartCoord != 0:
 | 
						|
                        corner.append((axisTag, axis.StartCoord))
 | 
						|
                    if axis.EndCoord != 0:
 | 
						|
                        corner.append((axisTag, axis.EndCoord))
 | 
						|
                    corners.append(corner)
 | 
						|
                corners = set(product(*corners))
 | 
						|
                peakLocation = tuple(peakLocation)
 | 
						|
                poles[peakLocation] = None
 | 
						|
                inputLocations.add(peakLocation)
 | 
						|
                inputLocations.update(corners)
 | 
						|
 | 
						|
        # Sort them by number of axes, then by axis order
 | 
						|
        inputLocations = [
 | 
						|
            dict(t)
 | 
						|
            for t in sorted(
 | 
						|
                inputLocations,
 | 
						|
                key=lambda t: (len(t), tuple(axisIndexes[tag] for tag, _ in t)),
 | 
						|
            )
 | 
						|
        ]
 | 
						|
        poles = [dict(t) for t in poles.keys()]
 | 
						|
        inputLocations = _pruneLocations(inputLocations, list(poles), axisTags)
 | 
						|
 | 
						|
        # Find the output locations, at input locations
 | 
						|
        varIdxMap = avar.table.VarIdxMap
 | 
						|
        instancer = VarStoreInstancer(varStore, fvarAxes)
 | 
						|
        for location in inputLocations:
 | 
						|
            instancer.setLocation(location)
 | 
						|
            outputLocation = {}
 | 
						|
            for axisIndex, axisTag in enumerate(axisTags):
 | 
						|
                varIdx = axisIndex
 | 
						|
                if varIdxMap is not None:
 | 
						|
                    varIdx = varIdxMap[varIdx]
 | 
						|
                delta = instancer[varIdx]
 | 
						|
                if delta != 0:
 | 
						|
                    v = location.get(axisTag, 0)
 | 
						|
                    v = v + fi2fl(delta, 14)
 | 
						|
                    # See https://github.com/fonttools/fonttools/pull/3598#issuecomment-2266082009
 | 
						|
                    # v = max(-1, min(1, v))
 | 
						|
                    outputLocation[axisTag] = v
 | 
						|
            mappings.append((location, outputLocation))
 | 
						|
 | 
						|
        # Remove base master we added, if it maps to the default location
 | 
						|
        assert mappings[0][0] == {}
 | 
						|
        if mappings[0][1] == {}:
 | 
						|
            mappings.pop(0)
 | 
						|
 | 
						|
    if denormalize:
 | 
						|
        for tag, seg in axisMaps.items():
 | 
						|
            if tag not in axisMap:
 | 
						|
                raise ValueError(f"Unknown axis tag {tag}")
 | 
						|
            denorm = lambda v: _denormalize(v, axisMap[tag])
 | 
						|
            axisMaps[tag] = {denorm(k): denorm(v) for k, v in seg.items()}
 | 
						|
 | 
						|
        for i, (inputLoc, outputLoc) in enumerate(mappings):
 | 
						|
            inputLoc = {
 | 
						|
                tag: _denormalize(val, axisMap[tag]) for tag, val in inputLoc.items()
 | 
						|
            }
 | 
						|
            outputLoc = {
 | 
						|
                tag: _denormalize(val, axisMap[tag]) for tag, val in outputLoc.items()
 | 
						|
            }
 | 
						|
            mappings[i] = (inputLoc, outputLoc)
 | 
						|
 | 
						|
    return axisMaps, mappings
 | 
						|
 | 
						|
 | 
						|
def unbuild(font, f=sys.stdout):
 | 
						|
    fvar = font["fvar"]
 | 
						|
    axes = fvar.axes
 | 
						|
    segments, mappings = mappings_from_avar(font)
 | 
						|
 | 
						|
    if "name" in font:
 | 
						|
        name = font["name"]
 | 
						|
        axisNames = {axis.axisTag: name.getDebugName(axis.axisNameID) for axis in axes}
 | 
						|
    else:
 | 
						|
        axisNames = {a.axisTag: a.axisTag for a in axes}
 | 
						|
 | 
						|
    print("<?xml version='1.0' encoding='UTF-8'?>", file=f)
 | 
						|
    print('<designspace format="5.1">', file=f)
 | 
						|
    print("  <axes>", file=f)
 | 
						|
    for axis in axes:
 | 
						|
 | 
						|
        axisName = axisNames[axis.axisTag]
 | 
						|
 | 
						|
        triplet = (axis.minValue, axis.defaultValue, axis.maxValue)
 | 
						|
        triplet = [int(v) if v == int(v) else v for v in triplet]
 | 
						|
 | 
						|
        axisMap = segments.get(axis.axisTag)
 | 
						|
        closing = "/>" if axisMap is None else ">"
 | 
						|
 | 
						|
        print(
 | 
						|
            f'    <axis tag="{axis.axisTag}" name="{axisName}" minimum="{triplet[0]}" maximum="{triplet[2]}" default="{triplet[1]}"{closing}',
 | 
						|
            file=f,
 | 
						|
        )
 | 
						|
        if axisMap is not None:
 | 
						|
            for k in sorted(axisMap.keys()):
 | 
						|
                v = axisMap[k]
 | 
						|
                k = int(k) if k == int(k) else k
 | 
						|
                v = int(v) if v == int(v) else v
 | 
						|
                print(f'      <map input="{k}" output="{v}"/>', file=f)
 | 
						|
            print("    </axis>", file=f)
 | 
						|
    if mappings:
 | 
						|
        print("    <mappings>", file=f)
 | 
						|
        for inputLoc, outputLoc in mappings:
 | 
						|
            print("      <mapping>", file=f)
 | 
						|
            print("        <input>", file=f)
 | 
						|
            for tag in sorted(inputLoc.keys()):
 | 
						|
                v = inputLoc[tag]
 | 
						|
                v = int(v) if v == int(v) else v
 | 
						|
                print(
 | 
						|
                    f'          <dimension name="{axisNames[tag]}" xvalue="{v}"/>',
 | 
						|
                    file=f,
 | 
						|
                )
 | 
						|
            print("        </input>", file=f)
 | 
						|
            print("        <output>", file=f)
 | 
						|
            for tag in sorted(outputLoc.keys()):
 | 
						|
                v = outputLoc[tag]
 | 
						|
                v = int(v) if v == int(v) else v
 | 
						|
                print(
 | 
						|
                    f'          <dimension name="{axisNames[tag]}" xvalue="{v}"/>',
 | 
						|
                    file=f,
 | 
						|
                )
 | 
						|
            print("        </output>", file=f)
 | 
						|
            print("      </mapping>", file=f)
 | 
						|
        print("    </mappings>", file=f)
 | 
						|
    print("  </axes>", file=f)
 | 
						|
    print("</designspace>", file=f)
 | 
						|
 | 
						|
 | 
						|
def main(args=None):
 | 
						|
    """Print `avar` table as a designspace snippet."""
 | 
						|
 | 
						|
    if args is None:
 | 
						|
        args = sys.argv[1:]
 | 
						|
 | 
						|
    from fontTools.ttLib import TTFont
 | 
						|
    import argparse
 | 
						|
 | 
						|
    parser = argparse.ArgumentParser(
 | 
						|
        "fonttools varLib.avar.unbuild",
 | 
						|
        description="Print `avar` table as a designspace snippet.",
 | 
						|
    )
 | 
						|
    parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
 | 
						|
    options = parser.parse_args(args)
 | 
						|
 | 
						|
    font = TTFont(options.font)
 | 
						|
    if "fvar" not in font:
 | 
						|
        print("Not a variable font.", file=sys.stderr)
 | 
						|
        return 1
 | 
						|
 | 
						|
    unbuild(font)
 | 
						|
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    import sys
 | 
						|
 | 
						|
    sys.exit(main())
 |