mirror of
https://github.com/rsms/inter.git
synced 2024-11-25 07:45:18 +03:00
279 lines
8.9 KiB
Python
279 lines
8.9 KiB
Python
import sys, os, os.path, re, argparse
|
|
import defcon
|
|
from multiprocessing import Pool
|
|
from fontTools.designspaceLib import DesignSpaceDocument
|
|
from ufo2ft.filters import loadFilters
|
|
from datetime import datetime
|
|
|
|
sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), 'tools')))
|
|
from common import getGitHash, getVersion
|
|
|
|
|
|
OPT_EDITABLE = False # --editable
|
|
|
|
|
|
def update_version(ufo):
|
|
version = getVersion()
|
|
buildtag, buildtagErrs = getGitHash()
|
|
now = datetime.utcnow()
|
|
if buildtag == "" or len(buildtagErrs) > 0:
|
|
buildtag = "src"
|
|
print("warning: getGitHash() failed: %r" % buildtagErrs, file=sys.stderr)
|
|
versionMajor, versionMinor = [int(num) for num in version.split(".")]
|
|
ufo.info.versionMajor = versionMajor
|
|
ufo.info.versionMinor = versionMinor
|
|
ufo.info.year = now.year
|
|
ufo.info.openTypeNameVersion = "Version %d.%03d;git-%s" % (versionMajor, versionMinor, buildtag)
|
|
psFamily = re.sub(r'\s', '', ufo.info.familyName)
|
|
psStyle = re.sub(r'\s', '', ufo.info.styleName)
|
|
#
|
|
# id format:
|
|
# version ";" "git-" git-tag ";" foundry-tag ";" ps_family "-" ps_style
|
|
# E.g.
|
|
# "4.001;git-4de559246;RSMS;Inter-DisplayThinItalic"
|
|
# Note: this should match what generated by fontmake.
|
|
# fix-static-display-names.py depends on this format being consistent for all fonts.
|
|
#
|
|
if buildtag != "src":
|
|
buildtag = "git-" + buildtag
|
|
ufo.info.openTypeNameUniqueID = "%d.%03d;%s;%s;%s-%s" % (
|
|
versionMajor, versionMinor,
|
|
buildtag,
|
|
ufo.info.openTypeOS2VendorID,
|
|
psFamily, psStyle)
|
|
ufo.info.openTypeHeadCreated = now.strftime("%Y/%m/%d %H:%M:%S")
|
|
|
|
|
|
def ufo_set_wws(ufo):
|
|
# Fix missing WWS entries for Display fonts:
|
|
# See https://github.com/googlefonts/glyphsLib/issues/820
|
|
subfamily = ufo.info.styleName
|
|
if subfamily.find("Display") == -1:
|
|
return
|
|
subfamily = subfamily[len("Display"):].strip()
|
|
if subfamily == "":
|
|
# "Display" -> "Regular"
|
|
subfamily = "Regular"
|
|
ufo.info.openTypeNameWWSFamilyName = "Inter Display"
|
|
ufo.info.openTypeNameWWSSubfamilyName = subfamily
|
|
|
|
|
|
def fix_opsz_range(designspace):
|
|
opsz_min = 1000000
|
|
opsz_max = 0
|
|
opsz_name = ''
|
|
opsz_axis = None
|
|
|
|
for opsz_axis in designspace.axes:
|
|
if opsz_axis.tag == "opsz":
|
|
opsz_name = opsz_axis.name
|
|
break
|
|
|
|
for instance in designspace.instances:
|
|
opsz_value = instance.location[opsz_name]
|
|
if opsz_value < opsz_min:
|
|
opsz_min = opsz_value
|
|
if opsz_value > opsz_max:
|
|
opsz_max = opsz_value
|
|
|
|
opsz_axis.minimum = opsz_min
|
|
opsz_axis.maximum = opsz_max
|
|
|
|
return designspace
|
|
|
|
|
|
def fix_wght_range(designspace):
|
|
for a in designspace.axes:
|
|
if a.tag == "wght":
|
|
a.minimum = 100
|
|
a.maximum = 900
|
|
break
|
|
return designspace
|
|
|
|
|
|
def should_decompose_glyph(g):
|
|
if not g.components or len(g.components) == 0:
|
|
return False
|
|
|
|
# Does the component have non-trivial transformation? (i.e. scaled or skewed)
|
|
# Example of no transformation: (identity matrix)
|
|
# (1, 0, 0, 1, 0, 0) no scale or offset
|
|
# Example of simple offset transformation matrix:
|
|
# (1, 0, 0, 1, 20, 30) 20 x offset, 30 y offset
|
|
# Example of scaled transformation matrix:
|
|
# (-1.0, 0, 0.3311, 1, 1464.0, 0) flipped x axis, sheered and offset
|
|
# Matrix order:
|
|
# (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
|
|
for cn in g.components:
|
|
# if g.name == 'dotmacron.lc':
|
|
# print(f"{g.name} cn {cn.baseGlyph}", cn.transformation)
|
|
# Check if transformation is not identity (ignoring x & y offset)
|
|
m = cn.transformation
|
|
if m[0] + m[1] + m[2] + m[3] != 2.0:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def copy_component_anchors(font, g):
|
|
# do nothing if there are no components or if g has anchors already
|
|
if not g.components or len(g.anchors) > 0:
|
|
return
|
|
|
|
anchor_names = set()
|
|
for cn in g.components:
|
|
if cn.transformation[1] != 0.0 or cn.transformation[2] != 0.0:
|
|
print(f"TODO: support transformations with skew ({g.name})")
|
|
return
|
|
cn_g = font[cn.baseGlyph]
|
|
# copy_component_anchors(font, cn_g) # depth first
|
|
for a in cn_g.anchors:
|
|
# Check if there are multiple components with achors with the same name.
|
|
# Don't copy any anchors if there are duplicate "_..." anchors
|
|
if a.name in anchor_names and len(a.name) > 1 and a.name[0] == '_':
|
|
return
|
|
anchor_names.add(a.name)
|
|
|
|
if len(anchor_names) == 0:
|
|
return
|
|
|
|
anchor_names.clear()
|
|
for cn in g.components:
|
|
for a in font[cn.baseGlyph].anchors:
|
|
if a.name in anchor_names:
|
|
continue
|
|
anchor_names.add(a.name)
|
|
a2 = defcon.Anchor(glyph=g, anchorDict=a.copy())
|
|
m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
|
|
a2.x += m[4] * m[0]
|
|
a2.y += m[5] * m[3]
|
|
g.appendAnchor(a2)
|
|
|
|
|
|
def copy_anchors_from_components(font, g):
|
|
# We use two passes here, to deduplicate anchors which appear in several components.
|
|
# Two assumptions are made:
|
|
# 1. Insertion order of Python dict() is retained (true for Python >=3.7)
|
|
# 2. Base components are listed first, mark/accent comonents later.
|
|
# e.g. in /Ecircumflex, /E comes before /circumflexcomb, so we use "top"
|
|
# anchor from /circumflexcomb rather than /E
|
|
|
|
# skip certain glyphs
|
|
if len(g.unicodes) == 0 or len(g.anchors) > 0 or not g.components:
|
|
return
|
|
|
|
add_anchors = dict()
|
|
names_to_copy = set(('top', 'bottom'))
|
|
|
|
for cn in g.components:
|
|
checked_cn_skew = False
|
|
#print(f" [{g.name}] cn {cn.baseGlyph}")
|
|
cn_g = font[cn.baseGlyph]
|
|
copy_anchors_from_components(font, cn_g) # depth first
|
|
for a in cn_g.anchors:
|
|
if a.name not in names_to_copy:
|
|
continue
|
|
#print(f" [{g.name}] use anchor {a.name}")
|
|
m = cn.transformation # (x_scale, x_skew, y_skew, y_scale, x_offs, y_offs)
|
|
if not checked_cn_skew:
|
|
checked_cn_skew = True
|
|
if m[1] != 0.0 or m[2] != 0.0:
|
|
#print(f"TODO: skewed components ({cn_g.name} used by {g.name})")
|
|
return
|
|
a2 = defcon.Anchor(glyph=g, anchorDict=a.copy())
|
|
a2.x += m[4] * m[0]
|
|
a2.y += m[5] * m[3]
|
|
add_anchors[a.name] = a2
|
|
|
|
for a in add_anchors.values():
|
|
#print(f" [{g.name}] append anchor {a.name}")
|
|
g.appendAnchor(a)
|
|
|
|
|
|
def find_glyphs_to_decompose(designspace_source):
|
|
glyph_names = set()
|
|
# print("find_glyphs_to_decompose inspecting %r" % designspace_source.name)
|
|
font = defcon.Font(designspace_source.path)
|
|
for g in font:
|
|
# copy_anchors_from_components(font, g)
|
|
if should_decompose_glyph(g):
|
|
glyph_names.add(g.name)
|
|
font.save(designspace_source.path)
|
|
return list(glyph_names)
|
|
|
|
|
|
def set_ufo_filter(ufo, **filter_dict):
|
|
filters = ufo.lib.setdefault("com.github.googlei18n.ufo2ft.filters", [])
|
|
for i in range(len(filters)):
|
|
if filters[i].get("name") == filter_dict["name"]:
|
|
filters[i] = filter_dict
|
|
return
|
|
filters.append(filter_dict)
|
|
|
|
|
|
def del_ufo_filter(ufo, name):
|
|
filters = ufo.lib.get("com.github.googlei18n.ufo2ft.filters")
|
|
if not filters:
|
|
return
|
|
for i in range(len(filters)):
|
|
if filters[i].get("name") == name:
|
|
filters.pop(i)
|
|
return
|
|
|
|
|
|
def update_source_ufo(ufo_file, glyphs_to_decompose):
|
|
print(f"update {os.path.basename(ufo_file)}")
|
|
|
|
ufo = defcon.Font(ufo_file)
|
|
update_version(ufo)
|
|
|
|
set_ufo_filter(ufo, name="decomposeComponents", include=glyphs_to_decompose)
|
|
|
|
# decompose now, up front, instead of later when compiling fonts
|
|
if not OPT_EDITABLE:
|
|
preFilters, postFilters = loadFilters(ufo)
|
|
for filter in preFilters:
|
|
filter(ufo)
|
|
for filter in postFilters:
|
|
filter(ufo)
|
|
# del_ufo_filter(ufo, "decomposeComponents")
|
|
del ufo.lib["com.github.googlei18n.ufo2ft.filters"]
|
|
|
|
ufo_set_wws(ufo) # Fix missing WWS entries for Display fonts
|
|
ufo.save(ufo_file)
|
|
|
|
|
|
def update_sources(designspace):
|
|
with Pool() as p:
|
|
sources = [source for source in designspace.sources]
|
|
# sources = [s for s in sources if s.name == "Inter Thin"] # DEBUG
|
|
glyphs_to_decompose = set()
|
|
for glyph_names in p.map(find_glyphs_to_decompose, sources):
|
|
glyphs_to_decompose.update(glyph_names)
|
|
glyphs_to_decompose = list(glyphs_to_decompose)
|
|
# print("glyphs marked to be decomposed: %s" % ', '.join(glyphs_to_decompose))
|
|
source_files = list(set([s.path for s in sources]))
|
|
p.starmap(update_source_ufo, [(path, glyphs_to_decompose) for path in source_files])
|
|
return designspace
|
|
|
|
|
|
def main(argv):
|
|
ap = argparse.ArgumentParser(description=
|
|
'Fixup designspace and source UFOs after they are generated by fontmake from Glyphs source')
|
|
ap.add_argument('--editable', action='store_true',
|
|
help="Generate UFOs suitable for further editing (don't apply filters)")
|
|
ap.add_argument("designspace", help="Path to designspace file")
|
|
|
|
args = ap.parse_args()
|
|
OPT_EDITABLE = args.editable
|
|
|
|
designspace = DesignSpaceDocument.fromfile(args.designspace)
|
|
designspace = fix_opsz_range(designspace)
|
|
designspace = fix_wght_range(designspace)
|
|
designspace = update_sources(designspace)
|
|
designspace.write(args.designspace)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main(sys.argv)
|