Skip to content

Commit

Permalink
WIP Build DesignSpace version 5 + add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
belluzj authored and madig committed Apr 25, 2022
1 parent 5b5a126 commit b8fed51
Show file tree
Hide file tree
Showing 117 changed files with 7,308 additions and 29 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ coverage.xml
*.cover
.hypothesis/
.pytest_cache/
tests/data/test.fea

# Translations
*.mo
Expand Down
30 changes: 28 additions & 2 deletions Lib/fontmake/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from argparse import ArgumentParser, FileType
from collections import namedtuple
from contextlib import contextmanager
from textwrap import dedent

from ufo2ft import CFFOptimization
from ufo2ft.featureWriters import loadFeatureWriterFromString
Expand Down Expand Up @@ -237,7 +238,7 @@ def main(args=None):
"--output-path",
default=None,
help="Output font file path. Only valid when the output is a single "
"file (e.g. input is a single UFO or output is variable font)",
"file (e.g. input is a single UFO or output is a single variable font)",
)
outputSubGroup.add_argument(
"--output-dir",
Expand All @@ -259,6 +260,24 @@ def main(args=None):
'E.g.: -i "Noto Sans Bold"; or -i ".* UI Condensed". '
"(for Glyphs or MutatorMath sources only). ",
)
outputGroup.add_argument(
"--variable-fonts",
nargs="?",
default=True,
const=True,
metavar="VARIABLE_FONT_FILENAME",
help=dedent(
"""\
Filter the list of variable fonts produced from the input
Designspace file. By default all listed variable fonts are
generated. To generate a specific variable font (or variable fonts)
that match a given "filename" attribute, you can pass as argument
the full filename or a regular expression. E.g.: --variable-fonts
"MyFontVF_WeightOnly.ttf"; or --variable-fonts
"MyFontVFItalic_.*.ttf".
"""
),
)
outputGroup.add_argument(
"--use-mutatormath",
action="store_true",
Expand Down Expand Up @@ -558,7 +577,13 @@ def main(args=None):
"variable output",
)
else:
exclude_args(parser, args, ["optimize_gvar"], "static output", positive=False)
exclude_args(
parser,
args,
["variable_fonts", "optimize_gvar"],
"static output",
positive=False,
)

if args.get("use_mutatormath"):
for module in ("defcon", "mutatorMath"):
Expand Down Expand Up @@ -607,6 +632,7 @@ def main(args=None):
args,
[
"interpolate",
"variable_fonts",
"use_mutatormath",
"interpolate_binary_layout",
"round_instances",
Expand Down
121 changes: 95 additions & 26 deletions Lib/fontmake/font_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,15 @@
from collections import OrderedDict
from contextlib import contextmanager
from functools import partial
from pathlib import Path
from re import fullmatch

import attr
import ufo2ft
import ufo2ft.errors
import ufoLib2
from fontTools import designspaceLib
from fontTools.designspaceLib.split import splitInterpolable
from fontTools.misc.loggingTools import Timer, configLogger
from fontTools.misc.plistlib import load as readPlist
from fontTools.ttLib import TTFont
Expand Down Expand Up @@ -306,9 +308,10 @@ def build_interpolatable_otfs(self, designspace, **kwargs):
"""
return self._build_interpolatable_masters(designspace, ttf=False, **kwargs)

def build_variable_font(
def build_variable_fonts(
self,
designspace,
designspace: designspaceLib.DesignSpaceDocument,
variable_fonts=True,
output_path=None,
output_dir=None,
ttf=True,
Expand All @@ -324,22 +327,55 @@ def build_variable_font(
filters=None,
**kwargs,
):
"""Build OpenType variable font from masters in a designspace."""
"""Build OpenType variable fonts from masters in a designspace."""
assert not (output_path and output_dir), "mutually exclusive args"

if output_path is None:
output_path = (
os.path.splitext(os.path.basename(designspace.path))[0] + "-VF"
vfs_in_document = designspace.getVariableFonts()
if not vfs_in_document:
logger.warning(
"No variable fonts in given designspace %s", designspace.path
)
ext = "ttf" if ttf else "otf"
output_path = self._output_path(
output_path, ext, is_variable=True, output_dir=output_dir
return {}

vfs_to_build = []
for vf in vfs_in_document:
# Skip variable fonts that do not match the user's inclusion regex if given.
if isinstance(variable_fonts, str) and not fullmatch(
variable_fonts, vf.name
):
continue
vfs_to_build.append(vf)

if not vfs_to_build:
logger.warning("No variable fonts matching %s", variable_fonts)
return {}

if len(vfs_to_build) > 1 and output_path:
raise FontmakeError(
"can't specify output path because there are several VFs to build",
designspace,
)

logger.info("Building variable font " + output_path)
vf_name_to_output_path = {}
if len(vfs_to_build) == 1 and output_path is not None:
vf_name_to_output_path[vfs_to_build[0].name] = output_path
else:
for vf in vfs_to_build:
ext = "ttf" if ttf else "otf"
font_name = vf.name
if vf.filename is not None:
font_name = Path(vf.filename).stem
output_path = self._output_path(
font_name, ext, is_variable=True, output_dir=output_dir
)
vf_name_to_output_path[vf.name] = output_path

logger.info(
"Building variable fonts " + ", ".join(vf_name_to_output_path.values())
)

if ttf:
font = ufo2ft.compileVariableTTF(
fonts = ufo2ft.compileVariableTTFs(
designspace,
featureWriters=feature_writers,
useProductionNames=use_production_names,
Expand All @@ -350,9 +386,10 @@ def build_variable_font(
debugFeatureFile=debug_feature_file,
filters=filters,
inplace=True,
variableFontNames=list(vf_name_to_output_path),
)
else:
font = ufo2ft.compileVariableCFF2(
fonts = ufo2ft.compileVariableCFF2s(
designspace,
featureWriters=feature_writers,
useProductionNames=use_production_names,
Expand All @@ -361,9 +398,11 @@ def build_variable_font(
optimizeCFF=optimize_cff,
filters=filters,
inplace=True,
variableFontNames=list(vf_name_to_output_path),
)

font.save(output_path)
for name, font in fonts.items():
font.save(vf_name_to_output_path[name])

def _iter_compile(self, ufos, ttf=False, debugFeatureFile=None, **kwargs):
# generator function that calls ufo2ft compiler for each ufo and
Expand Down Expand Up @@ -771,6 +810,8 @@ def interpolate_instance_ufos(
interpolation failed.
ValueError: an instance descriptor did not have a filename attribute set.
"""
# TODO: (Jany) for each instance, figure out in which "interpolatable sub-space" it is, and give that to mutatormath/the other one
# Maybe easier to make 1 designspace per interpolatable sub-space, and call this function X times?
from glyphsLib.interpolation import apply_instance_data_to_ufo

logger.info("Interpolating master UFOs from designspace")
Expand Down Expand Up @@ -859,6 +900,8 @@ def interpolate_instance_ufos_mutatormath(
ValueError: "expand_features_to_instances" is True but no source in the
designspace document is designated with '<features copy="1"/>'.
"""
# TODO: (Jany) for each instance, figure out in which "interpolatable sub-space" it is, and give that to mutatormath/the other one
# Maybe easier to make 1 designspace per interpolatable sub-space, and call this function X times?
from glyphsLib.interpolation import apply_instance_data
from mutatorMath.ufo.document import DesignSpaceDocumentReader

Expand Down Expand Up @@ -910,6 +953,7 @@ def run_from_designspace(
designspace_path,
output=(),
interpolate=False,
variable_fonts=True,
masters_as_instances=False,
interpolate_binary_layout=False,
round_instances=False,
Expand All @@ -930,6 +974,11 @@ def run_from_designspace(
match given name. The string is compiled into a regular
expression and matched against the "name" attribute of
designspace instances using `re.fullmatch`.
variable_fonts: if True output all variable fonts, otherwise if the
value is a string, only build variable fonts that match the
given filename. As above, the string is compiled into a regular
expression and matched against the "filename" attribute of
designspace variable fonts using `re.fullmatch`.
masters_as_instances: If True, output master fonts as instances.
interpolate_binary_layout: Interpolate layout tables from compiled
master binaries.
Expand Down Expand Up @@ -974,15 +1023,22 @@ def run_from_designspace(
preFilters, postFilters = loadFilters(designspace)
filters = preFilters + postFilters

source_fonts = [source.font for source in designspace.sources]
# glyphsLib currently stores this custom parameter on the fonts,
# not the designspace, so we check if it exists in any font's lib.
explicit_check = any(
font.lib.get(COMPAT_CHECK_KEY, False) for font in source_fonts
)
if interp_outputs or check_compatibility or explicit_check:
if not CompatibilityChecker(source_fonts).check():
raise FontmakeError("Compatibility check failed", designspace.path)
# Since Designspace version 5, one designspace file can have discrete
# axes (that do not interpolate) and thus only some sub-spaces are
# actually compatible for interpolation.
for discrete_location, subDoc in splitInterpolable(designspace):
# glyphsLib currently stores this custom parameter on the fonts,
# not the designspace, so we check if it exists in any font's lib.
source_fonts = [source.font for source in subDoc.sources]
explicit_check = any(
font.lib.get(COMPAT_CHECK_KEY, False) for font in source_fonts
)
if interp_outputs or check_compatibility or explicit_check:
if not CompatibilityChecker(source_fonts).check():
message = "Compatibility check failed"
if discrete_location:
message += f" in interpolable sub-space at {discrete_location}"
raise FontmakeError(message, designspace.path)

try:
if static_outputs:
Expand All @@ -1003,6 +1059,7 @@ def run_from_designspace(
self._run_from_designspace_interpolatable(
designspace,
outputs=interp_outputs,
variable_fonts=variable_fonts,
feature_writers=feature_writers,
filters=filters,
**kwargs,
Expand Down Expand Up @@ -1078,22 +1135,33 @@ def _run_from_designspace_static(
)

def _run_from_designspace_interpolatable(
self, designspace, outputs, output_path=None, output_dir=None, **kwargs
self,
designspace,
outputs,
variable_fonts=True,
output_path=None,
output_dir=None,
**kwargs,
):
ttf_designspace = otf_designspace = None

if "variable" in outputs:
self.build_variable_font(
designspace, output_path=output_path, output_dir=output_dir, **kwargs
self.build_variable_fonts(
designspace,
variable_fonts=variable_fonts,
output_path=output_path,
output_dir=output_dir,
**kwargs,
)

if "ttf-interpolatable" in outputs:
ttf_designspace = self.build_interpolatable_ttfs(designspace, **kwargs)
self._save_interpolatable_fonts(ttf_designspace, output_dir, ttf=True)

if "variable-cff2" in outputs:
self.build_variable_font(
self.build_variable_fonts(
designspace,
variable_fonts=variable_fonts,
output_path=output_path,
output_dir=output_dir,
ttf=False,
Expand Down Expand Up @@ -1281,3 +1349,4 @@ def _varLib_finder(source, directory="", ext="ttf"):

def _normpath(fname):
return os.path.normcase(os.path.normpath(fname))

3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ fontMath==0.9.1
defcon[lxml]==0.10.0; platform_python_implementation == 'CPython'
defcon==0.10.0; platform_python_implementation != 'CPython'
booleanOperations==0.9.0
ufoLib2==0.13.1
# ufoLib2==0.13.1
git+https://github.com/googlefonts/ufoLib2@build-ds5
attrs==21.4.0
cffsubr==0.2.9.post1
compreffor==0.5.1.post1
Expand Down
21 changes: 21 additions & 0 deletions tests/data/MutatorSansLite/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2017 Erik van Blokland

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Loading

0 comments on commit b8fed51

Please sign in to comment.