Last active
February 14, 2020 11:32
-
-
Save ctrlcctrlv/0362099bb040aa45a84653ad9040e4d3 to your computer and use it in GitHub Desktop.
Guess Anchors for glyphs in FontForge (rough draft)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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