diff --git a/Lib/fontmake/__main__.py b/Lib/fontmake/__main__.py
index 6801f065..5e1395ba 100644
--- a/Lib/fontmake/__main__.py
+++ b/Lib/fontmake/__main__.py
@@ -15,7 +15,7 @@
import logging
import os
import sys
-from argparse import ArgumentParser, FileType
+from argparse import SUPPRESS, ArgumentParser, FileType
from collections import namedtuple
from contextlib import contextmanager
from textwrap import dedent
@@ -288,7 +288,7 @@ def main(args=None):
'match a given "name" attribute, you can pass as argument '
"the full instance name or a regular expression. "
'E.g.: -i "Noto Sans Bold"; or -i ".* UI Condensed". '
- "(for Glyphs or MutatorMath sources only). ",
+ "(for Glyphs or DesignSpace sources only). ",
)
outputGroup.add_argument(
"--variable-fonts",
@@ -308,14 +308,8 @@ def main(args=None):
"""
),
)
- outputGroup.add_argument(
- "--use-mutatormath",
- action="store_true",
- help=(
- "Use MutatorMath to generate instances (supports extrapolation and "
- "anisotropic locations)."
- ),
- )
+ # no longer show option in --help but keep to produce nice error message
+ outputGroup.add_argument("--use-mutatormath", action="store_true", help=SUPPRESS)
outputGroup.add_argument(
"-M",
"--masters-as-instances",
@@ -536,7 +530,7 @@ def main(args=None):
const=True,
metavar="MASTER_DIR",
help="Interpolate layout tables from compiled master binaries. "
- "Requires Glyphs or MutatorMath source.",
+ "Requires Glyphs or DesignSpace source.",
)
layoutGroup.add_argument(
"--feature-writer",
@@ -620,6 +614,13 @@ def main(args=None):
args = vars(parser.parse_args(args))
+ use_mutatormath = args.pop("use_mutatormath")
+ if use_mutatormath:
+ parser.error(
+ "MutatorMath is no longer supported by fontmake. "
+ "Try to use ufoProcessor: https://github.com/LettError/ufoProcessor"
+ )
+
level = args.pop("verbose")
_configure_logging(level, timing=args.pop("timing"))
@@ -643,7 +644,6 @@ def main(args=None):
"interpolate",
"masters_as_instances",
"interpolate_binary_layout",
- "use_mutatormath",
],
"variable output",
)
@@ -656,16 +656,6 @@ def main(args=None):
positive=False,
)
- if args.get("use_mutatormath"):
- for module in ("defcon", "mutatorMath"):
- try:
- __import__(module)
- except ImportError:
- parser.error(
- f"{module} module not found; reinstall fontmake with the "
- "[mutatormath] extra"
- )
-
PRINT_TRACEBACK = level == "DEBUG"
try:
project = FontProject(validate_ufo=args.pop("validate_ufo"))
@@ -705,7 +695,6 @@ def main(args=None):
[
"interpolate",
"variable_fonts",
- "use_mutatormath",
"interpolate_binary_layout",
"round_instances",
"expand_features_to_instances",
diff --git a/Lib/fontmake/font_project.py b/Lib/fontmake/font_project.py
index ea1037c0..94061d9d 100644
--- a/Lib/fontmake/font_project.py
+++ b/Lib/fontmake/font_project.py
@@ -21,7 +21,6 @@
import shutil
import tempfile
from collections import OrderedDict
-from contextlib import contextmanager
from functools import partial
from pathlib import Path
from re import fullmatch
@@ -32,6 +31,7 @@
import ufoLib2
from fontTools import designspaceLib
from fontTools.designspaceLib.split import splitInterpolable
+from fontTools.misc.cliTools import makeOutputFileName
from fontTools.misc.loggingTools import Timer
from fontTools.misc.plistlib import load as readPlist
from fontTools.ttLib import TTFont
@@ -73,47 +73,9 @@
GLYPHS_PREFIX + "customParameter.InstanceDescriptorAsGSInstance.TTFAutohint options"
)
-
-@contextmanager
-def temporarily_disabling_axis_maps(designspace_path):
- """Context manager to prevent MutatorMath from warping designspace locations.
-
- MutatorMath assumes that the masters and instances' locations are in
- user-space coordinates -- whereas they actually are in internal design-space
- coordinates, and thus they do not need any 'bending'. To work around this we
- we create a temporary designspace document without the axis maps, and with the
- min/default/max triplet mapped "forward" from user-space coordinates (input)
- to internal designspace coordinates (output).
-
- Args:
- designspace_path: A path to a designspace document.
-
- Yields:
- A temporary path string to the thus modified designspace document.
- After the context is exited, it removes the temporary file.
-
- Related issues:
- https://github.com/LettError/designSpaceDocument/issues/16
- https://github.com/fonttools/fonttools/pull/1395
- """
- try:
- designspace = designspaceLib.DesignSpaceDocument.fromfile(designspace_path)
- except Exception as e:
- raise FontmakeError("Reading Designspace failed", designspace_path) from e
-
- for axis in designspace.axes:
- axis.minimum = axis.map_forward(axis.minimum)
- axis.default = axis.map_forward(axis.default)
- axis.maximum = axis.map_forward(axis.maximum)
- del axis.map[:]
-
- fd, temp_designspace_path = tempfile.mkstemp()
- os.close(fd)
- try:
- designspace.write(temp_designspace_path)
- yield temp_designspace_path
- finally:
- os.remove(temp_designspace_path)
+# tempLib keys for fontmake's internal use only, won't be saved to disk
+INSTANCE_LOCATION_KEY = "com.github.googlefonts.fontmake.instance_location"
+INSTANCE_FILENAME_KEY = "com.github.googlefonts.fontmake.instance_filename"
def needs_subsetting(ufo):
@@ -172,7 +134,7 @@ def open_ufo(self, path):
def save_ufo_as(self, font, path, ufo_structure="package"):
try:
font.save(
- path,
+ _ensure_parent_dir(path),
overwrite=True,
validate=self.validate_ufo,
structure=ufo_structure,
@@ -193,14 +155,13 @@ def build_master_ufos(
generate_GDEF=True,
ufo_structure="package",
glyph_data=None,
+ save_ufos=True,
):
- """Build UFOs and MutatorMath designspace from Glyphs source."""
+ """Build UFOs and designspace from Glyphs source."""
import glyphsLib
if master_dir is None:
master_dir = self._output_dir("ufo")
- if not os.path.isdir(master_dir):
- os.mkdir(master_dir)
if instance_dir is None:
instance_dir = self._output_dir("ufo", is_instance=True)
@@ -242,25 +203,35 @@ def build_master_ufos(
# multiple sources can have the same font/filename (but different layer),
# we want to save a font only once
for source in designspace.sources:
- if source.path in masters:
- assert source.font is masters[source.path]
- continue
ufo_path = os.path.join(master_dir, source.filename)
- # no need to also set the relative 'filename' attribute as that
- # will be auto-updated on writing the designspace document
source.path = ufo_path
+ # relative 'filename' would be auto-updated upon writing the designspace
+ # but we may not always write one out, thus we keep it up-to-date
+ source.filename = os.path.relpath(ufo_path, designspace_dir)
+ if ufo_path in masters:
+ assert source.font is masters[ufo_path]
+ continue
masters[ufo_path] = source.font
- if designspace_path is None:
- designspace_path = os.path.join(master_dir, designspace.filename)
- designspace.write(designspace_path)
if mti_source:
self.add_mti_features_to_master_ufos(mti_source, masters)
- for ufo_path, ufo in masters.items():
- self.save_ufo_as(ufo, ufo_path, ufo_structure)
+ save_ds = designspace_path is not None or save_ufos
+ if designspace_path is None:
+ designspace_path = os.path.join(master_dir, designspace.filename)
+
+ if save_ds:
+ logger.info("Saving %s", designspace_path)
+ designspace.write(_ensure_parent_dir(designspace_path))
+ else:
+ designspace.path = designspace_path
- return designspace_path
+ if save_ufos:
+ for ufo_path, ufo in masters.items():
+ logger.info("Saving %s", ufo_path)
+ self.save_ufo_as(ufo, ufo_path, ufo_structure)
+
+ return designspace
@timer()
def add_mti_features_to_master_ufos(self, mti_source, masters):
@@ -289,23 +260,6 @@ def build_ttfs(self, ufos, **kwargs):
"""Build OpenType binaries with TrueType outlines."""
self.save_otfs(ufos, ttf=True, **kwargs)
- def _load_designspace_sources(self, designspace):
- if isinstance(designspace, (str, os.PathLike)):
- ds_path = os.fspath(designspace)
- else:
- # reload designspace from its path so we have a new copy
- # that can be modified in-place.
- ds_path = designspace.path
- if ds_path is not None:
- try:
- designspace = designspaceLib.DesignSpaceDocument.fromfile(ds_path)
- except Exception as e:
- raise FontmakeError("Reading Designspace failed", ds_path) from e
-
- designspace.loadSourceFonts(opener=self.open_ufo)
-
- return designspace
-
def _build_interpolatable_masters(
self,
designspace,
@@ -459,7 +413,9 @@ def build_variable_fonts(
)
for name, font in fonts.items():
- font.save(vf_name_to_output_path[name])
+ output_path = vf_name_to_output_path[name]
+ logger.info("Saving %s", output_path)
+ font.save(_ensure_parent_dir(output_path))
def _iter_compile(self, ufos, ttf=False, debugFeatureFile=None, **kwargs):
# generator function that calls ufo2ft compiler for each ufo and
@@ -641,12 +597,16 @@ def save_otfs(
inplace=True, # avoid extra copy
)
+ if interpolate_layout_from is not None:
+ master_locations = self._designspace_full_source_locations(
+ interpolate_layout_from
+ )
for font, ufo in zip(fonts, ufos):
- if interpolate_layout_from is not None:
- master_locations, instance_locations = self._designspace_locations(
- interpolate_layout_from
- )
- loc = instance_locations[_normpath(ufo.path)]
+ if (
+ interpolate_layout_from is not None
+ and INSTANCE_LOCATION_KEY in ufo.tempLib
+ ):
+ loc = ufo.tempLib[INSTANCE_LOCATION_KEY]
gpos_src = interpolate_layout(
interpolate_layout_from, loc, finder, mapped=True
)
@@ -685,7 +645,7 @@ def save_otfs(
otf_path = output_path
logger.info("Saving %s", otf_path)
- font.save(otf_path)
+ font.save(_ensure_parent_dir(otf_path))
# 'subset' is an Optional[bool], can be None, True or False.
# When False, we never subset; when True, we always do; when
@@ -706,7 +666,11 @@ def save_otfs(
)
try:
logger.info("Autohinting %s", hinted_otf_path)
- ttfautohint(otf_path, hinted_otf_path, args=autohint_thisfont)
+ ttfautohint(
+ otf_path,
+ _ensure_parent_dir(hinted_otf_path),
+ args=autohint_thisfont,
+ )
except TTFAError:
# copy unhinted font to destination before re-raising error
shutil.copyfile(otf_path, hinted_otf_path)
@@ -728,7 +692,7 @@ def _save_interpolatable_fonts(self, designspace, output_dir, ttf):
suffix=source.layerName,
)
logger.info("Saving %s", otf_path)
- source.font.save(otf_path)
+ source.font.save(_ensure_parent_dir(otf_path))
source.path = otf_path
source.layerName = None
for instance in designspace.instances:
@@ -824,6 +788,9 @@ def run_from_glyphs(
write_skipexportglyphs=True,
generate_GDEF=True,
glyph_data=None,
+ output=(),
+ output_dir=None,
+ interpolate=False,
**kwargs,
):
"""Run toolchain from Glyphs source.
@@ -844,9 +811,21 @@ def run_from_glyphs(
glyph_data: A list of GlyphData XML file paths.
kwargs: Arguments passed along to run_from_designspace.
"""
+ # only save *master* UFOs when explicitly requested: i.e. outputs contain
+ # 'ufo' and the -i/--interpolate option was not passed (that's for *instances*)
+ # or a --master-dir was set
+ save_ufos = "ufo" in output and (not interpolate or master_dir is not None)
+ # take --output-dir to mean same as --master-dir when -o ufo and not -i
+ if (
+ set(output) == {"ufo"}
+ and not interpolate
+ and master_dir is None
+ and output_dir is not None
+ ):
+ master_dir = output_dir
logger.info("Building master UFOs and designspace from Glyphs source")
- designspace_path = self.build_master_ufos(
+ designspace = self.build_master_ufos(
glyphs_path,
designspace_path=designspace_path,
master_dir=master_dir,
@@ -857,6 +836,7 @@ def run_from_glyphs(
generate_GDEF=generate_GDEF,
ufo_structure=kwargs.get("ufo_structure"),
glyph_data=glyph_data,
+ save_ufos=save_ufos,
)
# 'include' statements in features.fea should be resolved relative to
# the input .glyphs path, like Glyphs.app would do, and not relative
@@ -864,12 +844,44 @@ def run_from_glyphs(
fea_include_dir = os.path.dirname(glyphs_path)
try:
self.run_from_designspace(
- designspace_path, fea_include_dir=fea_include_dir, **kwargs
+ designspace,
+ output=output,
+ fea_include_dir=fea_include_dir,
+ output_dir=output_dir,
+ interpolate=interpolate,
+ **kwargs,
)
except FontmakeError as e:
e.source_trail.append(glyphs_path)
raise
+ def _instance_ufo_path(
+ self, instance, designspace_path, output_dir=None, ext=".ufo"
+ ):
+ """Return an instance path, optionally overriding output dir or extension"""
+ # prefer absolute instance.path over relative instance.filename
+ instance_path = instance.path
+ if instance_path is not None:
+ instance_dir = Path(instance_path).parent
+ elif instance.filename is not None:
+ instance_path = Path(designspace_path).parent / instance.filename
+ instance_dir = instance_path.parent
+ else:
+ # if neither is set, make one up from UFO family/style names
+ instance_path = self._font_name(instance.font)
+ instance_dir = None
+
+ # let --output-dir override the instance UFO directory
+ if output_dir is not None:
+ instance_dir = output_dir
+
+ # fall back to 'instance_ufo/' if we can't find a suitable directory
+ if instance_dir is None:
+ instance_dir = self._output_dir("ufo", is_instance=True)
+
+ instance_path = Path(instance_dir) / f"{Path(instance_path).stem}{ext}"
+ return os.path.normpath(instance_path)
+
def interpolate_instance_ufos(
self,
designspace,
@@ -878,6 +890,9 @@ def interpolate_instance_ufos(
expand_features_to_instances=False,
fea_include_dir=None,
ufo_structure="package",
+ save_ufos=True,
+ output_path=None,
+ output_dir=None,
):
"""Interpolate master UFOs with Instantiator and return instance UFOs.
@@ -902,14 +917,9 @@ def interpolate_instance_ufos(
"""
from glyphsLib.interpolation import apply_instance_data_to_ufo
- logger.info("Interpolating master UFOs from designspace")
- try:
- designspace = designspaceLib.DesignSpaceDocument.fromfile(designspace.path)
- except Exception as e:
- raise FontmakeError("Reading Designspace failed", designspace.path) from e
-
- designspace.loadSourceFonts(opener=self.open_ufo)
+ assert not (output_path and output_dir), "mutually exclusive args"
+ logger.info("Interpolating master UFOs from designspace")
for _location, subDoc in splitInterpolable(designspace):
try:
generator = instantiator.Instantiator.from_designspace(
@@ -952,102 +962,41 @@ def interpolate_instance_ufos(
apply_instance_data_to_ufo(instance.font, instance, subDoc)
- # TODO: Making filenames up on the spot is complicated, ideally don't save
- # anything if filename is not set, but make something up when "ufo" is in
- # output formats, but also consider output_path.
- if instance.filename is None:
- raise ValueError(
- "It is currently required that instances have filenames set."
- )
- ufo_path = os.path.join(
- os.path.dirname(designspace.path), instance.filename
- )
- os.makedirs(os.path.dirname(ufo_path), exist_ok=True)
- self.save_ufo_as(instance.font, ufo_path, ufo_structure)
+ if save_ufos:
+ ext = ".ufoz" if ufo_structure == "zip" else ".ufo"
+ if output_path is not None:
+ # we don't know in advance how many instances we will generate
+ # (depends on splitInterpolable and include filter); if we
+ # overwrite or stop in the middle of the build it'd be worse,
+ # so we make the output_path unique using #1, #2, etc. suffix
+ instance_path = makeOutputFileName(output_path, extension=ext)
+ else:
+ instance_path = self._instance_ufo_path(
+ instance, designspace.path, output_dir, ext
+ )
+ logger.info("Saving %s", instance_path)
+ self.save_ufo_as(instance.font, instance_path, ufo_structure)
+ elif instance.filename is not None:
+ # saving a UFO sets its path attribute; when saving the binary font
+ # compiled from this UFO, self._output_path() uses the ufo.path to
+ # make up the output path. Since we aren't saving the UFO in this
+ # case, the ufo.path is not set (it's a read-only attribute).
+ # Therefore we resort to this temporary lib key to pass down the DS
+ # instance filename to self._output_path() method and make sure the
+ # font is named correctly whether or not the UFO is saved to disk.
+ instance.font.tempLib[INSTANCE_FILENAME_KEY] = instance.filename
+
+ # --interpolate-binary-layout needs to know the location of each
+ # instance UFO; the previous code relied on matching instance.path and
+ # ufo.path, but we no longer necessarily save the UFO thus the ufo.path
+ # may not be set. Instead store the location directly in the ufo.tempLib
+ instance.font.tempLib[INSTANCE_LOCATION_KEY] = instance.location
yield instance.font
- def interpolate_instance_ufos_mutatormath(
- self,
- designspace,
- include=None,
- round_instances=False,
- expand_features_to_instances=False,
- fea_include_dir=None,
- ):
- """Interpolate master UFOs with MutatorMath and return instance UFOs.
-
- Args:
- designspace: a DesignSpaceDocument object containing sources and
- instances.
- include (str): optional regular expression pattern to match the
- DS instance 'name' attribute and only interpolate the matching
- instances.
- round_instances (bool): round instances' coordinates to integer.
- expand_features_to_instances: parses the master feature file, expands all
- include()s and writes the resulting full feature file to all instance
- UFOs. Use this if you share feature files among masters in external
- files. Otherwise, the relative include paths can break as instances
- may end up elsewhere. Only done on interpolation.
- Returns:
- list of defcon.Font objects corresponding to the UFO instances.
- Raises:
- FontmakeError: if any of the sources defines a custom 'layer', for
- this is not supported by MutatorMath.
- ValueError: "expand_features_to_instances" is True but no source in the
- designspace document is designated with ''.
- """
- from glyphsLib.interpolation import apply_instance_data
- from mutatorMath.ufo.document import DesignSpaceDocumentReader
-
- if any(source.layerName is not None for source in designspace.sources):
- raise FontmakeError(
- "MutatorMath doesn't support DesignSpace sources with 'layer' "
- "attribute",
- None,
- )
-
- with temporarily_disabling_axis_maps(designspace.path) as temp_designspace_path:
- builder = DesignSpaceDocumentReader(
- temp_designspace_path,
- ufoVersion=3,
- roundGeometry=round_instances,
- verbose=True,
- )
- logger.info("Interpolating master UFOs from designspace")
- if include is not None:
- instances = self._search_instances(designspace, pattern=include)
- for instance_name in instances:
- builder.readInstance(("name", instance_name))
- filenames = set(instances.values())
- else:
- builder.readInstances()
- filenames = None # will include all instances
-
- logger.info("Applying instance data from designspace")
- instance_ufos = apply_instance_data(designspace, include_filenames=filenames)
-
- if expand_features_to_instances:
- logger.debug("Expanding features to instance UFOs")
- master_source = next(
- (s for s in designspace.sources if s.copyFeatures), None
- )
- if not master_source:
- raise ValueError("No source is designated as the master for features.")
- else:
- master_source_font = builder.sources[master_source.name][0]
- master_source_features = parseLayoutFeatures(
- master_source_font, includeDir=fea_include_dir
- ).asFea()
- for instance_ufo in instance_ufos:
- instance_ufo.features.text = master_source_features
- instance_ufo.save()
-
- return instance_ufos
-
def run_from_designspace(
self,
- designspace_path,
+ designspace,
output=(),
interpolate=False,
variable_fonts: str = ".*",
@@ -1057,7 +1006,6 @@ def run_from_designspace(
feature_writers=None,
filters=None,
expand_features_to_instances=False,
- use_mutatormath=False,
check_compatibility=None,
**kwargs,
):
@@ -1065,7 +1013,7 @@ def run_from_designspace(
instance fonts (ttf or otf), interpolatable or variable fonts.
Args:
- designspace_path: Path to designspace document.
+ designspace: Path to designspace or DesignSpaceDocument object.
interpolate: If True output all instance fonts, otherwise just
masters. If the value is a string, only build instance(s) that
match given name. The string is compiled into a regular
@@ -1079,8 +1027,8 @@ def run_from_designspace(
masters_as_instances: If True, output master fonts as instances.
interpolate_binary_layout: Interpolate layout tables from compiled
master binaries.
- round_instances: apply integer rounding when interpolating with
- MutatorMath.
+ round_instances: apply integer rounding when interpolating static
+ instance UFOs.
kwargs: Arguments passed along to run_from_ufos.
Raises:
@@ -1103,12 +1051,25 @@ def run_from_designspace(
% (argname, ", ".join(sorted(interp_outputs)))
)
- try:
- designspace = designspaceLib.DesignSpaceDocument.fromfile(designspace_path)
- except Exception as e:
- raise FontmakeError("Reading Designspace failed", designspace_path) from e
+ if isinstance(designspace, (str, os.PathLike)):
+ designspace_path = os.fspath(designspace)
+ try:
+ designspace = designspaceLib.DesignSpaceDocument.fromfile(
+ designspace_path
+ )
+ except Exception as e:
+ raise FontmakeError(
+ "Reading Designspace failed", designspace_path
+ ) from e
+ elif isinstance(designspace, designspaceLib.DesignSpaceDocument):
+ # get our own DS copy so we can modify in-place
+ designspace = designspace.deepcopyExceptFonts()
+ else:
+ raise TypeError(
+ f"expected path or DesignSpaceDocument, found {type(designspace.__name__)}"
+ )
- designspace = self._load_designspace_sources(designspace)
+ designspace.loadSourceFonts(opener=self.open_ufo)
# if no --feature-writers option was passed, check in the designspace's
# element if user supplied a custom featureWriters configuration;
@@ -1150,7 +1111,6 @@ def run_from_designspace(
round_instances=round_instances,
feature_writers=feature_writers,
expand_features_to_instances=expand_features_to_instances,
- use_mutatormath=use_mutatormath,
filters=filters,
**kwargs,
)
@@ -1185,36 +1145,38 @@ def _run_from_designspace_static(
feature_writers=None,
expand_features_to_instances=False,
fea_include_dir=None,
- use_mutatormath=False,
ufo_structure="package",
+ output_path=None,
+ output_dir=None,
**kwargs,
):
+ save_ufos = "ufo" in outputs
ufos = []
if not interpolate or masters_as_instances:
- ufos.extend(s.path for s in designspace.sources if s.path)
+ unique_srcs = {id(s.font): s.font for s in designspace.sources}
+ ufos.extend(unique_srcs.values())
if interpolate:
pattern = interpolate if isinstance(interpolate, str) else None
- if use_mutatormath:
- ufos.extend(
- self.interpolate_instance_ufos_mutatormath(
- designspace,
- include=pattern,
- round_instances=round_instances,
- expand_features_to_instances=expand_features_to_instances,
- fea_include_dir=fea_include_dir,
- )
- )
- else:
- ufos.extend(
- self.interpolate_instance_ufos(
- designspace,
- include=pattern,
- round_instances=round_instances,
- expand_features_to_instances=expand_features_to_instances,
- fea_include_dir=fea_include_dir,
- ufo_structure=ufo_structure,
- )
+ # use --output-{path,dir} options for instance UFOs if 'ufo' is the only -o
+ ufo_output_path = ufo_output_dir = None
+ if set(outputs) == {"ufo"}:
+ if output_path is not None:
+ ufo_output_path = output_path
+ if output_dir is not None:
+ ufo_output_dir = output_dir
+ ufos.extend(
+ self.interpolate_instance_ufos(
+ designspace,
+ include=pattern,
+ round_instances=round_instances,
+ expand_features_to_instances=expand_features_to_instances,
+ fea_include_dir=fea_include_dir,
+ ufo_structure=ufo_structure,
+ save_ufos=save_ufos,
+ output_path=ufo_output_path,
+ output_dir=ufo_output_dir,
)
+ )
if interpolate_binary_layout is False:
interpolate_layout_from = interpolate_layout_dir = None
@@ -1236,6 +1198,8 @@ def _run_from_designspace_static(
interpolate_layout_dir=interpolate_layout_dir,
feature_writers=feature_writers,
fea_include_dir=fea_include_dir,
+ output_path=output_path,
+ output_dir=output_dir,
**kwargs,
)
@@ -1294,31 +1258,27 @@ def run_from_ufos(self, ufos, output=(), **kwargs):
# the `ufos` parameter can be a list of UFO objects
# or it can be a path (string) with a glob syntax
- ufo_paths = []
if isinstance(ufos, str):
- ufo_paths = glob.glob(ufos)
- ufos = [self.open_ufo(x) for x in ufo_paths]
+ ufos = [self.open_ufo(x) for x in glob.glob(ufos)]
elif isinstance(ufos, list):
# ufos can be either paths or open Font objects, so normalize them
ufos = [self.open_ufo(x) if isinstance(x, str) else x for x in ufos]
- ufo_paths = [x.path for x in ufos]
else:
raise TypeError(
- "UFOs parameter is neither a defcon.Font object, a path or a glob, "
+ "UFOs parameter is neither a ufoLib2.Font object, a path or a glob, "
f"nor a list of any of these: {ufos:r}."
)
- need_reload = False
cff_version = 1 if "otf" in output else 2 if "otf-cff2" in output else None
+
+ # if building both OTF & TTF we must tell ufo2ft to compile with inplace=False
+ inplace = not (cff_version is not None and "ttf" in output)
+
if cff_version is not None:
- self.build_otfs(ufos, cff_version=cff_version, **kwargs)
- need_reload = True
+ self.build_otfs(ufos, cff_version=cff_version, inplace=inplace, **kwargs)
if "ttf" in output:
- if need_reload:
- ufos = [self.open_ufo(path) for path in ufo_paths]
- self.build_ttfs(ufos, **kwargs)
- need_reload = True
+ self.build_ttfs(ufos, inplace=inplace, **kwargs)
@staticmethod
def _search_instances(designspace, pattern):
@@ -1394,36 +1354,39 @@ def _output_path(
if isinstance(ufo_or_font_name, str):
font_name = ufo_or_font_name
- elif ufo_or_font_name.path:
- font_name = os.path.splitext(
- os.path.basename(os.path.normpath(ufo_or_font_name.path))
- )[0]
else:
- font_name = self._font_name(ufo_or_font_name)
+ ufo = ufo_or_font_name
+ if ufo.path:
+ font_name = os.path.splitext(
+ os.path.basename(os.path.normpath(ufo.path))
+ )[0]
+ elif INSTANCE_FILENAME_KEY in ufo.tempLib:
+ font_name = Path(ufo.tempLib[INSTANCE_FILENAME_KEY]).stem
+ else:
+ font_name = self._font_name(ufo)
if output_dir is None:
output_dir = self._output_dir(
ext, is_instance, interpolatable, autohinted, is_variable
)
- if not os.path.exists(output_dir):
- os.makedirs(output_dir)
if suffix:
return os.path.join(output_dir, f"{font_name}-{suffix}.{ext}")
else:
return os.path.join(output_dir, f"{font_name}.{ext}")
- def _designspace_locations(self, designspace):
- """Map font filenames to their locations in a designspace."""
+ def _designspace_full_source_locations(self, designspace):
+ """Map "full" sources' paths to their locations in a designspace.
- maps = []
- for elements in (designspace.sources, designspace.instances):
- location_map = {}
- for element in elements:
- path = _normpath(element.path)
- location_map[path] = element.location
- maps.append(location_map)
- return maps
+ 'Sparse layer' sources only contributing glyph outlines but no
+ info/kerning/features are ignored.
+ """
+ location_map = {}
+ default_source = designspace.findDefault()
+ for source in designspace.sources:
+ if source is default_source or source.layerName is not None:
+ location_map[_normpath(source.path)] = source.location
+ return location_map
def _closest_location(self, location_map, target):
"""Return path of font whose location is closest to target."""
@@ -1454,3 +1417,8 @@ def _varLib_finder(source, directory="", ext="ttf"):
def _normpath(fname):
return os.path.normcase(os.path.normpath(fname))
+
+
+def _ensure_parent_dir(path):
+ Path(path).parent.mkdir(parents=True, exist_ok=True)
+ return path
diff --git a/requirements.txt b/requirements.txt
index ff8082a3..c589709c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,13 +1,10 @@
-fonttools[unicode,ufo,lxml]==4.41.0; platform_python_implementation == 'CPython'
-fonttools[unicode,ufo]==4.41.0; platform_python_implementation != 'CPython'
+fonttools[unicode,ufo,lxml]==4.41.1; platform_python_implementation == 'CPython'
+fonttools[unicode,ufo]==4.41.1; platform_python_implementation != 'CPython'
glyphsLib==6.2.5
ufo2ft==2.32.0
-MutatorMath==3.0.1
fontMath==0.9.3
-defcon[lxml]==0.10.2; platform_python_implementation == 'CPython'
-defcon==0.10.2; platform_python_implementation != 'CPython'
booleanOperations==0.9.0
-ufoLib2==0.14.0
+ufoLib2==0.16.0
attrs==23.1.0
cffsubr==0.2.9.post1
compreffor==0.5.3
diff --git a/setup.py b/setup.py
index a896b2c6..c64cbac3 100644
--- a/setup.py
+++ b/setup.py
@@ -33,7 +33,9 @@
"lxml": [
# "lxml>=4.2.4",
],
- "mutatormath": ["MutatorMath>=2.1.2"],
+ # MutatorMath is no longer supported but a dummy extras is kept below
+ # to avoid fontmake installation failing if requested
+ "mutatormath": [],
"autohint": ["ttfautohint-py>=0.5.0"],
}
# use a special 'all' key as shorthand to includes all the extra dependencies
@@ -55,12 +57,12 @@
setup_requires=wheel + ["setuptools_scm"],
python_requires=">=3.8",
install_requires=[
- "fonttools[ufo,lxml,unicode]>=4.40.0 ; implementation_name == 'cpython'",
- "fonttools[ufo,unicode]>=4.40.0 ; implementation_name != 'cpython'",
+ "fonttools[ufo,lxml,unicode]>=4.41.1 ; implementation_name == 'cpython'",
+ "fonttools[ufo,unicode]>=4.41.1 ; implementation_name != 'cpython'",
"glyphsLib>=6.2.5",
"ufo2ft[compreffor]>=2.32.0",
"fontMath>=0.9.3",
- "ufoLib2>=0.14.0",
+ "ufoLib2>=0.16.0",
"attrs>=19",
],
extras_require=extras_require,
diff --git a/tests/test_main.py b/tests/test_main.py
index 9c9d1f3f..4a8bdcc2 100644
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -71,57 +71,6 @@ def test_interpolation_designspace_5(data_dir, tmp_path):
}
-def test_interpolation_mutatormath(data_dir, tmp_path):
- shutil.copytree(data_dir / "DesignspaceTest", tmp_path / "sources")
-
- fontmake.__main__.main(
- [
- "-m",
- str(tmp_path / "sources" / "DesignspaceTest.designspace"),
- "-i",
- "--use-mutatormath",
- "--output-dir",
- str(tmp_path),
- ]
- )
-
- assert {p.name for p in tmp_path.glob("*.*")} == {
- "MyFont-Regular.ttf",
- "MyFont-Regular.otf",
- }
-
- test_output_ttf = fontTools.ttLib.TTFont(tmp_path / "MyFont-Regular.ttf")
- assert test_output_ttf["OS/2"].usWeightClass == 400
- glyph = test_output_ttf["glyf"]["l"]
- assert glyph.xMin == 50
- assert glyph.xMax == 170
-
- test_output_otf = fontTools.ttLib.TTFont(tmp_path / "MyFont-Regular.otf")
- assert test_output_otf["OS/2"].usWeightClass == 400
- glyph_set = test_output_otf.getGlyphSet()
- charstrings = list(test_output_otf["CFF "].cff.values())[0].CharStrings
- glyph = charstrings["l"]
- x_min, _, x_max, _ = glyph.calcBounds(glyph_set)
- assert x_min == 50
- assert x_max == 170
-
-
-def test_interpolation_mutatormath_source_layer(data_dir, tmp_path):
- shutil.copytree(data_dir / "MutatorSans", tmp_path / "layertest")
-
- with pytest.raises(SystemExit, match="sources with 'layer'"):
- fontmake.__main__.main(
- [
- "-m",
- str(tmp_path / "layertest" / "MutatorSans.designspace"),
- "-i",
- "--use-mutatormath",
- "--output-dir",
- str(tmp_path),
- ]
- )
-
-
def test_interpolation_and_masters_as_instances(data_dir, tmp_path):
shutil.copytree(data_dir / "DesignspaceTest", tmp_path / "sources")
@@ -414,15 +363,13 @@ def test_shared_features_expansion(data_dir, tmp_path):
"-i",
"--expand-features-to-instances",
"-o",
- "ttf",
+ "ufo",
"--output-dir",
str(tmp_path),
]
)
- test_feature_file = (
- tmp_path / "sources/instance_ufo/DesignspaceTest-Light.ufo/features.fea"
- )
+ test_feature_file = tmp_path / "DesignspaceTest-Light.ufo/features.fea"
assert test_feature_file.read_text() == "# test"
@@ -1100,11 +1047,12 @@ def test_main_designspace_v5_can_use_output_path_with_1_vf(data_dir, tmp_path):
"--variable-fonts",
"MutatorSansVariable_Width",
"--output-path",
- str(tmp_path / "MySingleVF.ttf"),
+ str(tmp_path / "output" / "MySingleVF.ttf"),
]
)
- assert (tmp_path / "MySingleVF.ttf").exists()
+ # 'output' subfolder was created automatically
+ assert (tmp_path / "output" / "MySingleVF.ttf").exists()
def test_main_designspace_v5_dont_interpolate_discrete_axis(data_dir, tmp_path):