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):