Skip to content

Instantly share code, notes, and snippets.

@arrowtype
Last active June 27, 2023 16:48
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save arrowtype/18fb1b009ad09d34f8c081792bf526e4 to your computer and use it in GitHub Desktop.
Save arrowtype/18fb1b009ad09d34f8c081792bf526e4 to your computer and use it in GitHub Desktop.
Example script that helps keep support sources up to date between full sources of a designspace.
"""
A script to make it slightly less painful to work with support sources.
Checks the spacing of glyphs in support sources, by re-interpolating
those glyphs from main sources, and checking the support glyphs against that.
Also places interpolated versions in the background, for visual reference.
Fixes small spacing discrepancies, but leaves bigger ones, as they might be intentional.
Can be run either in RoboFont or as a standalone script, if you adjust 'mainDSpath'
and 'generatorDSpath' variables.
Assumptions / prequesites:
- You have two designspaces: one that is a "generator" with only main (full) sources,
and another that is a "full" designspace that includes sparse sources.
- The generator designspace has instances at intended support locations
- Support sources include the substring "sparse" or "support" in their filenames
- Full sources *don’t* include the substring "sparse" or "support" in their filenames
- You want to clear glyph color marks in support sources, and use red marks to
indicate glyphs to check (this can be configured below)
TODO:
- [ ] round dimensions for glyph2?
- [ ] generate "generator" DS on the fly
- [ ] update to latest UFOprocessor for designspace5 support
"""
from fontTools.designspaceLib import DesignSpaceDocument
from fontParts.fontshell import RFont as Font
import ufoProcessor
import os
from ufonormalizer import normalizeUFO
import subprocess
# -------------------------------------------------------------------
# CONFIGURATION
## main DS, with sparse/support sources – update path as needed. Switch the comment in the next two lines to instead use RoboFont.
mainDSpath = "/Users/stephennixon/type-repos/name-sans/source/masters/name_sans-wght_1_1000-opsz_12_96--w_sparse_supports.designspace"
# mainDSpath = "/Users/stephennixon/type-repos/name-sans/source/masters/italic/name_sans_italic-wght_1_1000-opsz_12_96--w_sparse_supports.designspace"
## "generator" DS, with main sources only – update path as needed. Switch the comment in the next two lines to instead use RoboFont.
generatorDSpath = "/Users/stephennixon/type-repos/name-sans/source/masters/name_sans-wght_1_1000-opsz_12_96--generator.designspace"
# generatorDSpath = "/Users/stephennixon/type-repos/name-sans/source/masters/italic/name_sans_italic-wght_1_1000-opsz_12_96--generator.designspace"
# set to True if you want to unmark all glyphs in support sources, then add a certain color to glyphs to check
markGlyphsToFix = True
markColor = (1.0, 0.65, 0.65, 1) # (1.0, 0.65, 0.65, 1) is a nice pinkish red
# set to True if you intend to run this in RoboFont rather than in a terminal
runInRobofont = True
# CONFIGURATION
# -------------------------------------------------------------------
# open & clear output window if running in robofont
if runInRobofont:
from vanilla.dialogs import getFile
from mojo.UI import OutputWindow
OutputWindow().show()
OutputWindow().clear()
mainDSpath = getFile("Select Designspace with sparse/support sources", allowsMultipleSelection=False, fileTypes=["designspace"])[0]
generatorDSpath = getFile("Select Designspace WITHOUT sparse/support sources", allowsMultipleSelection=False, fileTypes=["designspace"])[0]
# set up a place to store information to print out later
report = {}
# open main DS & fonts with ufoProcessor
mainDS = DesignSpaceDocument.fromfile(mainDSpath)
openedMainDS = ufoProcessor.DesignSpaceProcessor()
openedMainDS.read(mainDSpath)
# opens generator DS with FontTools
generatorDS = DesignSpaceDocument.fromfile(generatorDSpath)
# open generator DS & fonts with ufoProcessor
openedGeneratorDS = ufoProcessor.DesignSpaceProcessor()
openedGeneratorDS.read(generatorDSpath)
openedGeneratorDS.loadFonts()
def computeItalicOffset(font, offsetBasisGlyph="H", roundOffset=True):
"""
https://robofont.com/RF4.1/documentation/tutorials/making-italic-fonts/#applying-the-italic-slant-offset-after-drawing
"""
if offsetBasisGlyph not in font.keys():
if "o" in font.keys():
offsetBasisGlyph = "o"
elif "e" in font.keys():
offsetBasisGlyph = "e"
elif "period" in font.keys():
offsetBasisGlyph = "period"
else:
print(f"Can’t correct italic offset of {font.path}")
return
# calculate offset value with offsetBasisGlyph
baseLeftMargin = (font[offsetBasisGlyph].angledLeftMargin + font[offsetBasisGlyph].angledRightMargin) / 2.0
offset = -font[offsetBasisGlyph].angledLeftMargin + baseLeftMargin
# round offset value
if roundOffset and offset != 0:
offset = round(offset)
return offset
def fixAnchors(glyph1, glyph2):
"""
Clears anchors in the support glyph, and copies over new ones with rounded positioning
"""
# clear any existing anchors
glyph1.clearAnchors()
# # attempt to position anchors better
# glyph2.moveBy((italicOffset, 0))
# copy anchors (may need a loop instead...?)
if len(glyph2.anchors) > 0:
for a in glyph2.anchors:
glyph1.appendAnchor(a.name, (a.x, a.y))
# apply italic offset from interpolated font
# for anchor in glyph1.anchors:
# anchor.x += -1 * glyph2.font.info.italicOffset
def copyG2toG1bg(glyph1, glyph2):
"""
Copies the outline of glyph2 to the background layer of glyph1. Also places guidelines in the glyph1 foreground to show margins of glyph2.
Args:
- glyph1: a manually edited glyph in the "suppoort" source of a buildable designspace
- glyph2: a newly-generated glyph, interpolated from the main/non-support sources of that designspace
"""
supportSourceFont = glyph1.font
italicOffset = supportSourceFont.lib["com.typemytype.robofont.italicSlantOffset"]
# let’s also add a single guide for the interpolated width (TODO: check if italic angle is working)
glyph1.clearGuidelines()
glyph1.appendGuideline((glyph2.width + italicOffset, 0), 90 + supportSourceFont.info.italicAngle)
# also just add a guideline at the left edge, to make the point more obvious
glyph1.appendGuideline((0 + italicOffset, 0), 90 + supportSourceFont.info.italicAngle)
# get background layer name
supportBg = supportSourceFont.layers[1]
supportFontBgLayerName = supportBg.name
# check if g1 has background layer (really, if layer has g1); if not, add it
if glyph1.name not in supportBg:
supportBg.newGlyph(glyph1.name)
# get glyph1 bg, then clear it
glyph1Bg = supportSourceFont[glyph1.name].getLayer(supportFontBgLayerName)
glyph1Bg.clear()
# # move glyph2 so we can position it better in background
# glyph2.moveBy((italicOffset, 0))
# get the point pen of the layer glyph
penToDrawWith = glyph1Bg.getPointPen()
# draw the points of the imported glyph into the layered glyph
glyph2.drawPoints(penToDrawWith)
def fixSpacing(glyph1, glyph2):
"""
- if glyph is empty (like /space), just update the width
- if g1 with g2.leftMargin and g2.rightMargin would equal g2.width, update g2.leftMargin and g2.width to match
- if not, add the glyph diffs to the report
- in practice, no glyphs were auto-fixed with this... it will be more important to flag what issues are
"""
if glyph1.isEmpty():
glyph1.width = round(glyph2.width)
else:
# # get width of drawn glyph
# glyph1BoundsWidth = glyph1.bounds[0] - glyph1.bounds[2]
# # if g1 with g2.leftMargin and g2.rightMargin would equal g2.width, update g2.leftMargin and g2.width to match
# if round(glyph1BoundsWidth + glyph2.angledLeftMargin + glyph2.angledRightMargin) == round(glyph2.width):
# glyph1.angledLeftMargin = glyph2.angledLeftMargin
# glyph1.width = glyph2.width
# TODO? figure out a better heuristic to not mess up spacing in dot glyphs...
dotGlyphs = "dotbelowcmb quoteleft quoteright quotedblleft quotedblright quotesinglbase quotedblbase period comma colon semicolon exclam exclamdown question questiondown dotcomb ldotcomb comma.brut semicolon.brut exclam.brut exclamdown.brut colon.tnum semicolon.tnum exclamdown.case_brut exclamdown.case questiondown.case".split(" ")
if glyph1.name not in dotGlyphs:
glyph1.angledLeftMargin = glyph2.angledLeftMargin
glyph1.width = glyph2.width
def checkForSpacingDiffs(glyph1, glyph2):
"""
Fixes spacing discrepancies in glyph1 to match glyph2, so long as that can be done without modifying the contours of glyph1.
Args:
- glyph1: a manually edited glyph in the "suppoort" source of a buildable designspace
- glyph2: a newly-generated glyph, interpolated from the main/non-support sources of that designspace
"""
# get basic dimensions of g1
supportGlyphDimensions = (round(glyph1.width), round(glyph1.leftMargin), round(glyph1.rightMargin))
# get basic dimensions of g2
generatedGlyphDimensions = (round(glyph2.width), round(glyph2.leftMargin), round(glyph2.rightMargin))
spacing = (supportGlyphDimensions, generatedGlyphDimensions)
# save differences to logger (TODO? save as file? put in markdown todo list format?)
if supportGlyphDimensions != generatedGlyphDimensions:
return spacing
def fuzzyReport(glyph1, spacingDiff, marginOfError=2):
"""
Check spacing differences. If they exceed margin of error, add them to the report for fixing.
Spacing diffs arg is a tuple of (glyph1dimensions, glyph2dimensions) if they don’t
have exactly matched (width, leftMargin, rightMargin):
> (
> (589, 164, 189), # from support source
> (502, 159, 189) # from generated interpolation
> ),
"""
reportSection = ""
widthDiff = abs(spacingDiff[1][0] - spacingDiff[0][0]) >= marginOfError
leftMarginDiff = abs(spacingDiff[1][1] - spacingDiff[0][1]) >= marginOfError
rightMarginDiff = abs(spacingDiff[1][2] - spacingDiff[0][2]) >= marginOfError
if widthDiff or leftMarginDiff or rightMarginDiff:
reportSection += "\n" + "- [ ] " + glyph1.name + "\n"
# defined in top configuration
if markGlyphsToFix:
glyph1.markColor = markColor
if widthDiff:
reportSection += " - width difference is " + str(abs(spacingDiff[0][0] - spacingDiff[1][0])) + "\n"
if leftMarginDiff:
reportSection += " - leftMargin difference is " + str(abs(spacingDiff[0][1] - spacingDiff[1][1])) + "\n"
if rightMarginDiff:
reportSection += " - rightMargin difference is " + str(abs(spacingDiff[0][2] - spacingDiff[1][2])) + "\n"
report[glyph1.font.path] += reportSection
# a list of locations to check
supportLocations = {}
# first, we make a dict of sparse sources and their locations
for dsSource in openedMainDS.sources:
if "sparse" in dsSource.filename or "support" in dsSource.filename:
supportLocations[dsSource] = dsSource.location
# then we loop through the instances of the "generator" designspace
for dsInstance in generatorDS.instances:
# and check if it matches a support source
for supportSource, location in supportLocations.items():
if dsInstance.location == location:
report[supportSource.path] = ""
# # open support source as RFont ... should we use RF’s OpenFont() to access angledLeftMargin, etc?
supportSourceFont = OpenFont(supportSource.path, showInterface=False)
print(supportSourceFont)
# save, then open generated font
generatedInstance = openedGeneratorDS.makeInstance(dsInstance)
## open font with robofont
generatedFont = OpenFont(generatedInstance, showInterface=False)
# sort generatedFont
newGlyphOrder = generatedFont.naked().unicodeData.sortGlyphNames(generatedFont.glyphOrder, sortDescriptors=[
dict(type="cannedDesign", ascending=True, allowPseudoUnicode=True)])
generatedFont.glyphOrder = newGlyphOrder
# sort supportSourceFont
newGlyphOrder = supportSourceFont.naked().unicodeData.sortGlyphNames(supportSourceFont.glyphOrder, sortDescriptors=[
dict(type="cannedDesign", ascending=True, allowPseudoUnicode=True)])
supportSourceFont.glyphOrder = newGlyphOrder
# compute italic offset, then apply to both fonts
# TODO? should this instead be computed simply from the italic offset of fonts, using a factor derived from the designspace? Probably...
italicOffset = computeItalicOffset(generatedFont, offsetBasisGlyph="H", roundOffset=True)
# generatedFont.info.italicOffset = italicOffset
# supportSourceFont.info.italicOffset = italicOffset
generatedFont.lib["com.typemytype.robofont.italicSlantOffset"] = -italicOffset
supportSourceFont.lib["com.typemytype.robofont.italicSlantOffset"] = -italicOffset
# go through each glyph in manually edited support source
for g1 in supportSourceFont:
g2 = generatedFont[g1.name]
# copy new glyphs to background of main support glyphs, for reference
copyG2toG1bg(g1, generatedFont[g1.name])
# fix some things automatically if possible
fixSpacing(g1, g2)
# clear existing anchors from support, then add new ones
fixAnchors(g1, g2)
# compare glyphs and report differences in spacing
spacingDiffs = checkForSpacingDiffs(g1, g2)
# if spacingDiffs not "None"
if spacingDiffs:
if markGlyphsToFix:
# set mark to None, to overwrite it with fuzzy report
g1.markColor = None
# make report if things are more than marginOfError off
fuzzyReport(g1, spacingDiffs, marginOfError=2)
supportSourceFont.save()
normalizeUFO(supportSourceFont.path, writeModTimes=False)
print(".", end=" ")
continue
finalReport = ""
# print the spacing report in a readable way
for fontpath, reportSection in report.items():
finalReport += "\n"
finalReport += "<details><summary><b>"
finalReport += f"{os.path.split(fontpath)[1]}"
finalReport += "</b> (Click to expand)</summary>"
finalReport += "\n"
finalReport += reportSection
finalReport += "\n"
finalReport += "</details>"
finalReport += "\n"
print(finalReport)
subprocess.run("pbcopy", text=True, input=finalReport)
print("GitHub-ready markdown report copied to clipboard!")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment