Initial commit
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("fontTools.varLib.avar")
|
||||
|
||||
|
||||
def main(args=None):
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools import configLogger
|
||||
import argparse
|
||||
import sys
|
||||
|
||||
print(
|
||||
"WARNING: This script is deprecated. Use `fonttools varLib.avar.build` "
|
||||
"or `fonttools varLib.avar.unbuild` instead.\n",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools varLib.avar",
|
||||
description="Add `avar` table from designspace file to variable font.",
|
||||
)
|
||||
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
|
||||
parser.add_argument(
|
||||
"designspace",
|
||||
metavar="family.designspace",
|
||||
help="Designspace file.",
|
||||
nargs="?",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
type=str,
|
||||
help="Output font file name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
configLogger(level=("INFO" if options.verbose else "WARNING"))
|
||||
|
||||
font = TTFont(options.font)
|
||||
|
||||
if options.designspace is None:
|
||||
from .unbuild import unbuild
|
||||
|
||||
unbuild(font)
|
||||
return 0
|
||||
|
||||
from .build import build
|
||||
|
||||
build(font, options.designspace)
|
||||
|
||||
if options.output_file is None:
|
||||
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar")
|
||||
else:
|
||||
outfile = options.output_file
|
||||
if outfile:
|
||||
log.info("Saving %s", outfile)
|
||||
font.save(outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main())
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,79 @@
|
||||
from fontTools.varLib import _add_fvar, _add_avar, load_designspace
|
||||
from fontTools.ttLib import newTable
|
||||
import logging
|
||||
|
||||
log = logging.getLogger("fontTools.varLib.avar")
|
||||
|
||||
|
||||
def build(font, designspace_file):
|
||||
ds = load_designspace(designspace_file, require_sources=False)
|
||||
|
||||
if not "fvar" in font:
|
||||
# if "name" not in font:
|
||||
font["name"] = newTable("name")
|
||||
_add_fvar(font, ds.axes, ds.instances)
|
||||
|
||||
axisTags = [a.axisTag for a in font["fvar"].axes]
|
||||
|
||||
if "avar" in font:
|
||||
log.warning("avar table already present, overwriting.")
|
||||
del font["avar"]
|
||||
|
||||
_add_avar(font, ds.axes, ds.axisMappings, axisTags)
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Add `avar` table from designspace file to variable font."""
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
from fontTools.misc.cliTools import makeOutputFileName
|
||||
from fontTools import configLogger
|
||||
import argparse
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools varLib.avar.build",
|
||||
description="Add `avar` table from designspace file to variable font.",
|
||||
)
|
||||
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
|
||||
parser.add_argument(
|
||||
"designspace",
|
||||
metavar="family.designspace",
|
||||
help="Designspace file.",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output-file",
|
||||
type=str,
|
||||
help="Output font file name.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-v", "--verbose", action="store_true", help="Run more verbosely."
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
configLogger(level=("INFO" if options.verbose else "WARNING"))
|
||||
|
||||
font = TTFont(options.font)
|
||||
|
||||
build(font, options.designspace)
|
||||
|
||||
if options.output_file is None:
|
||||
outfile = makeOutputFileName(options.font, overWrite=True, suffix=".avar")
|
||||
else:
|
||||
outfile = options.output_file
|
||||
if outfile:
|
||||
log.info("Saving %s", outfile)
|
||||
font.save(outfile)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main())
|
||||
108
venv/lib/python3.10/site-packages/fontTools/varLib/avar/map.py
Normal file
108
venv/lib/python3.10/site-packages/fontTools/varLib/avar/map.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from fontTools.varLib.models import normalizeValue
|
||||
|
||||
|
||||
def _denormalize(v, triplet):
|
||||
if v >= 0:
|
||||
return triplet[1] + v * (triplet[2] - triplet[1])
|
||||
else:
|
||||
return triplet[1] + v * (triplet[1] - triplet[0])
|
||||
|
||||
|
||||
def map(
|
||||
font, location, *, inputNormalized=False, outputNormalized=False, dropZeroes=False
|
||||
):
|
||||
if "fvar" not in font:
|
||||
return None
|
||||
|
||||
fvar = font["fvar"]
|
||||
axes = {a.axisTag: (a.minValue, a.defaultValue, a.maxValue) for a in fvar.axes}
|
||||
|
||||
if not inputNormalized:
|
||||
location = {
|
||||
tag: normalizeValue(value, axes[tag]) for tag, value in location.items()
|
||||
}
|
||||
|
||||
if "avar" in font:
|
||||
location = font["avar"].renormalizeLocation(location, font, dropZeroes)
|
||||
|
||||
if not outputNormalized:
|
||||
location = {
|
||||
tag: _denormalize(value, axes[tag]) for tag, value in location.items()
|
||||
}
|
||||
|
||||
return location
|
||||
|
||||
|
||||
def main(args=None):
|
||||
"""Map variation coordinates through the `avar` table."""
|
||||
|
||||
from fontTools.ttLib import TTFont
|
||||
import argparse
|
||||
|
||||
if args is None:
|
||||
import sys
|
||||
|
||||
args = sys.argv[1:]
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
"fonttools varLib.avar.map",
|
||||
description="Map variation coordinates through the `avar` table.",
|
||||
)
|
||||
parser.add_argument("font", metavar="varfont.ttf", help="Variable-font file.")
|
||||
parser.add_argument(
|
||||
"coords",
|
||||
metavar="[AXIS=value...]",
|
||||
help="Coordinates to map, e.g. 'wght=700 wdth=75'.",
|
||||
nargs="*",
|
||||
default=None,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f", action="store_true", help="Do not omit axes at default location."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-i", action="store_true", help="Input coordinates are normalized (-1..1)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o", action="store_true", help="Output coordinates as normalized (-1..1)."
|
||||
)
|
||||
|
||||
options = parser.parse_args(args)
|
||||
|
||||
if not options.coords:
|
||||
parser.error(
|
||||
"No coordinates provided. Please specify at least one axis coordinate (e.g., wght=500)"
|
||||
)
|
||||
|
||||
if options.font.endswith(".designspace"):
|
||||
from .build import build
|
||||
|
||||
font = TTFont()
|
||||
build(font, options.font)
|
||||
else:
|
||||
font = TTFont(options.font)
|
||||
if "fvar" not in font:
|
||||
parser.error(f"Font '{options.font}' does not contain an 'fvar' table.")
|
||||
|
||||
location = {
|
||||
tag: float(value) for tag, value in (item.split("=") for item in options.coords)
|
||||
}
|
||||
|
||||
mapped = map(
|
||||
font,
|
||||
location,
|
||||
inputNormalized=options.i,
|
||||
outputNormalized=options.o,
|
||||
dropZeroes=not options.f,
|
||||
)
|
||||
assert mapped is not None
|
||||
|
||||
for tag in mapped:
|
||||
v = mapped[tag]
|
||||
v = int(v) if v == int(v) else v
|
||||
print(f"{tag}={v:g}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
sys.exit(main())
|
||||
1004
venv/lib/python3.10/site-packages/fontTools/varLib/avar/plan.py
Normal file
1004
venv/lib/python3.10/site-packages/fontTools/varLib/avar/plan.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,271 @@
|
||||
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())
|
||||
Reference in New Issue
Block a user