Skip to content

Instantly share code, notes, and snippets.

@ctrlcctrlv
Last active February 14, 2020 11:32
Show Gist options
  • Select an option

  • Save ctrlcctrlv/0362099bb040aa45a84653ad9040e4d3 to your computer and use it in GitHub Desktop.

Select an option

Save ctrlcctrlv/0362099bb040aa45a84653ad9040e4d3 to your computer and use it in GitHub Desktop.
Guess Anchors for glyphs in FontForge (rough draft)
import fontforge
import re
from functools import partial
# Only a list so we can change the contents of its elements at runtime. Don't change its length at runtime.
AnchorLocations = [
"Above",
"Below",
"Overstrike",
"Right",
"Top-right"
]
def PUA_slots():
# Supplementary PUAs are less likely to actually be in use.
yield from range(0x100000, 0x10FFFD+1) # Supplementary PUA B
yield from range(0xF0000, 0xFFFFD+1) # Supplementary PUA A
yield from range(0xE000, 0xF8FF+1) # BMP PUA
def valid_classname(cn):
# These rules came from http://opentypecookbook.com/syntax-introduction.html
return cn is not None and len(cn) <= 31 and re.match("[^0-9.][A-Za-z0-9._]+", cn)
class GuessAnchors(object):
TSPACING = BSPACING = 6
TRSPACING = RSPACING = TSPACING*3
DefaultAnchorsToBuild = tuple([True for i in range(len(AnchorLocations))])
AnchorsToBuild = DefaultAnchorsToBuild
# Spacing setters... (UI)
def set_spacing(self, data, w, sp="TSPACING"):
temp = fontforge.askString("Spacing", "How much space should there be, as a percent of em size? (Can be negative.)", str(getattr(self, sp))+'%')
setattr(self, sp, int(temp.replace('%', '')))
# Class name setter... (UI)
def set_classname(self, data, w, i=0):
temp = fontforge.askString("Class name", "What should the anchor class name be?", AnchorLocations[i])
if temp is not None:
AnchorLocations[i] = temp
print(AnchorLocations)
def set_anchorstobuild(self, data, font):
r=fontforge.askChoices("Anchor types", "What anchor types should we build?", tuple(AnchorLocations),
default=DefaultAnchorsToBuild, multiple=True)
self.AnchorsToBuild = r
print(data, font)
# TODO: Use class names
def guess_anchors(self, data, glyph):
# addAnchorPoint ==> (anchor-class-name, anchor-type, x,y [,ligature-index])
xmin, ymin, xmax, ymax = glyph.boundingBox()
xmin2, xmax2 = glyph.xBoundsAtY(ymax/2, ymax)
ymin2, ymax2 = glyph.yBoundsAtX(xmax/2, xmax)
xmin3, xmax3 = glyph.xBoundsAtY((ymin+ymax)/2)
ts = ((glyph.font.ascent + glyph.font.descent) * self.TSPACING) / 100
bs = ((glyph.font.ascent + glyph.font.descent) * self.BSPACING) / 100
rs = ((glyph.font.ascent + glyph.font.descent) * self.RSPACING) / 100
glyph.addAnchorPoint("top", "base", (xmin2+xmax2)/2, ymax+ts)
glyph.addAnchorPoint("bottom", "base", (xmin+xmax)/2, ymin-bs)
glyph.addAnchorPoint("right", "base", xmax3+rs, (ymin+ymax)/2)
def guess_marks(self, data, glyph):
xmin, ymin, xmax, ymax = glyph.boundingBox()
glyph.addAnchorPoint("top", "mark", (xmin+xmax)/2, ymin)
glyph.addAnchorPoint("bottom", "mark", (xmin+xmax)/2, ymax)
glyph.addAnchorPoint("right", "mark", (xmin+xmax)/2, (ymin+ymax)/2)
def f_guess_anchors(self, data, font):
for g in font.selection.byGlyphs:
self.guess_anchors(data, g)
def f_guess_marks(self, data, font):
for g in font.selection.byGlyphs:
self.guess_marks(data, g)
def ask_mark_relative(self, data, font):
rel = fontforge.askString("Add mark relative to…", "This function uses FontForge's internal \"Build Accented Glyph\" function, then adds anchors to both glyphs such that you will get exactly the same result with anchors as you would via the FontForge function. Sometimes this might give better results than the simpler heuristics in use by the other functions.")
try:
return font[rel]
except TypeError:
fontforge.postError("Error", "Glyph «{}» does not exist in this font".format(rel))
return None
def mark_relative(self, data, glyph, mk=None):
if mk is None:
mk = self.ask_mark_relative(data, glyph.font)
free_slot = None
slots = PUA_slots()
while free_slot is None:
s = next(slots)
e = glyph.font.findEncodingSlot(s)
if e == -1:
free_slot = s
temp = glyph.font.createChar(free_slot, "{}_{}_GA".format(glyph.glyphname, mk.glyphname))
temp.addReference(glyph.glyphname)
temp.appendAccent(name=mk.glyphname)
transform = None
base_transform = None
for n, t in temp.layerrefs[1]:
if n == mk.glyphname:
transform = t
elif n == glyph.glyphname:
base_transform = t
if transform is None or base_transform is None: # sanity test
fontforge.postError("Error", "I failed to find the transformations as expected; this is a bug, please report it.")
return None
btx, bty = base_transform[-2], base_transform[-1]
tx, ty = transform[-2], transform[-1]
minx, miny, maxx, maxy = mk.boundingBox()
accent_center = ((minx+maxx)/2, (miny+maxy)/2)
minx += tx + btx
miny += ty + bty
maxx += tx + btx
maxy += ty + bty
transformed_center = ((minx+maxx)/2, (miny+maxy)/2)
glyph.addAnchorPoint(mk.glyphname, "base", *transformed_center)
mk.addAnchorPoint(mk.glyphname, "mark", *accent_center)
glyph.font.removeGlyph(temp)
script = fontforge.scriptFromUnicode(glyph.unicode)
print("Script we got was", script)
lookup = script + "_relative_marks"
self.add_relative_marks_otdata(glyph.font, script, lookup, mk.glyphname)
def add_relative_marks_otdata(self, font, script, lookup, subtable):
try:
font.getLookupInfo(lookup)
except OSError:
font.addLookup(lookup, "gpos_mark2base", None, (("mark",((script,("dflt")),)),))
# Subtable names must be unique, even in different lookups.
# So, if latn_relative_marks contains `tilde`, then DFLT_relative_marks may not.
# Therefore, let's just call it `latn_tilde`, `DFLT_tilde`, &c.
ac = subtable
subtable = script + subtable
has_st = False
for t in font.getLookupSubtables(lookup):
has_st = t == subtable
if has_st:
break
if not has_st:
print(font, lookup, subtable)
font.addLookupSubtable(lookup, subtable)
try:
acs = font.getLookupSubtableAnchorClasses(lookup)
except OSError:
acs = tuple()
if ac not in acs:
try:
font.addAnchorClass(subtable, ac)
except OSError: # ac (e.g. `tilde`) exists in another subtable, no need to add it to this one.
pass
def f_mark_relative(self, data, font):
# Only ask once if multiple glyphs are in selection.
mk = self.ask_mark_relative(data, font)
for g in font.selection.byGlyphs:
self.mark_relative(data, g, mk=mk)
pr = {}
g = GuessAnchors()
fontforge.registerMenuItem(g.guess_anchors, None, pr, "Glyph", None, "Guess Anchors",
"Guess Appropriate Base Anchors")
fontforge.registerMenuItem(g.guess_marks, None, pr, "Glyph", None, "Guess Anchors",
"Guess Appropriate Mark Anchors")
fontforge.registerMenuItem(g.f_guess_anchors, None, pr, "Font", None, "Guess Anchors",
"Guess Appropriate Base Anchors")
fontforge.registerMenuItem(g.f_guess_marks, None, pr, "Font", None, "Guess Anchors",
"Guess Appropriate Mark Anchors")
fontforge.registerMenuItem(g.mark_relative, None, pr, "Glyph", None, "Guess Anchors",
"Add mark relative to…")
fontforge.registerMenuItem(g.f_mark_relative, None, pr, "Font", None, "Guess Anchors",
"Add mark relative to…")
fontforge.registerMenuItem(g.set_anchorstobuild, None, pr, ("Font", "Glyph"), None, "Guess Anchors",
"Change which anchors are built")
fontforge.registerMenuItem(partial(g.set_spacing, sp="RSPACING"), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set spacing…",
"Set Right Spacing")
fontforge.registerMenuItem(partial(g.set_spacing, sp="BSPACING"), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set spacing…",
"Set Bottom Spacing")
fontforge.registerMenuItem(g.set_spacing, None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set spacing…",
"Set Top Spacing")
fontforge.registerMenuItem(partial(g.set_spacing, sp="TRSPACING"), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set spacing…",
"Set Top Right Spacing")
fontforge.registerMenuItem(partial(g.set_classname, i=3), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set class names…",
"Set Right Class Name")
fontforge.registerMenuItem(partial(g.set_classname, i=1), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set class names…",
"Set Bottom Class Name")
fontforge.registerMenuItem(partial(g.set_classname, i=0), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set class names…",
"Set Top Class Name")
fontforge.registerMenuItem(partial(g.set_classname, i=4), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set class names…",
"Set Top Right Class Name")
fontforge.registerMenuItem(partial(g.set_classname, i=2), None, pr, ("Font", "Glyph"), None, "Guess Anchors", "Set class names…",
"Set Overstrike Class Name")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment