Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Glyphs3 RTL kerning #778

Closed
wants to merge 56 commits into from
Closed
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
d8bda71
Attaching previously unattached attributes
yanone Feb 21, 2022
698cce4
When script isn't found for unencoded derivate glyphs (= hah-ar.init.…
yanone Feb 21, 2022
5cb0fa3
Correctly read newly sorted RTL kerning class values
yanone Feb 21, 2022
5d1f602
Correctly read newly sorted kerning classes by reading script from gl…
yanone Feb 21, 2022
e9c20e5
Script guessing test
yanone Feb 21, 2022
f09d578
Fixed someone else’s typo
yanone Feb 21, 2022
d9295ad
Added G2 and G3 test files with RTL kerning, making sure both generat…
yanone Feb 21, 2022
0cb3787
Formatting
yanone Feb 21, 2022
91dee17
Formatting
yanone Feb 21, 2022
9e7bc65
Formatting
yanone Feb 21, 2022
61a35d1
Formatting
yanone Feb 21, 2022
a6f2ace
Oops, forgot to remove print statement
yanone Feb 21, 2022
2fe2e9b
Formatting
yanone Feb 21, 2022
9e6af8e
Formatting
yanone Feb 21, 2022
877f862
Comment was erroneously copied from another test
yanone Feb 22, 2022
bb799de
Rewrote script construction to align with other glyph data constructi…
yanone Feb 22, 2022
8541e61
Formatting
yanone Feb 22, 2022
68537fe
Remap G3 width class to user value (#781)
simoncozens Feb 23, 2022
5fe3238
WIP update Glyphs Info algorithm
schriftgestalt Apr 20, 2022
9b1a409
add unicodeLegacy
schriftgestalt May 24, 2022
7c9fed3
chane glyph name
schriftgestalt May 24, 2022
433ef5a
WIP: computation of glyphInfo
schriftgestalt May 24, 2022
d3e8f29
Formatting, commented-out unused variables using # Never used
yanone Sep 5, 2022
52900c1
Renaming .production to ._production to avoid overlaps with the .prod…
yanone Sep 5, 2022
10c500b
Making both properties available. glyphsLib contains numerous referen…
yanone Sep 5, 2022
454dc60
Returning unpropagated/empty GlyphsInfo object as a fallback
yanone Sep 5, 2022
232aab1
Formatting, fixed imports
yanone Sep 5, 2022
2442b57
add some tests
schriftgestalt Sep 6, 2022
49258f3
make sure we get the order of arguments right
schriftgestalt Sep 6, 2022
ffd8714
handling of liga suffixes
schriftgestalt Sep 6, 2022
05f0196
debug
schriftgestalt Sep 6, 2022
e82e96f
I get a warning when committing in the Lib folder
schriftgestalt Sep 6, 2022
a4eab6c
adjust the name construction
schriftgestalt Sep 6, 2022
cf93d8f
more liga name fixed
schriftgestalt Sep 7, 2022
41ceb8d
Script guessing test
yanone Feb 21, 2022
ab76f97
Attaching previously unattached attributes
yanone Feb 21, 2022
41ea5dc
When script isn't found for unencoded derivate glyphs (= hah-ar.init.…
yanone Feb 21, 2022
5938aa1
Correctly read newly sorted RTL kerning class values
yanone Feb 21, 2022
6e48267
Correctly read newly sorted kerning classes by reading script from gl…
yanone Feb 21, 2022
ea8516b
Script guessing test
yanone Feb 21, 2022
5e36cc1
Fixed someone else’s typo
yanone Feb 21, 2022
0e66a83
Added G2 and G3 test files with RTL kerning, making sure both generat…
yanone Feb 21, 2022
3c35d9a
Formatting
yanone Feb 21, 2022
4356125
Formatting
yanone Feb 21, 2022
e2af04d
Formatting
yanone Feb 21, 2022
5372913
Formatting
yanone Feb 21, 2022
f866a6e
Oops, forgot to remove print statement
yanone Feb 21, 2022
76f3b2f
Formatting
yanone Feb 21, 2022
e51a087
Formatting
yanone Feb 21, 2022
4dc7328
Comment was erroneously copied from another test
yanone Feb 22, 2022
1820c6e
Rewrote script construction to align with other glyph data constructi…
yanone Feb 22, 2022
4bf75aa
Formatting
yanone Feb 22, 2022
00853c4
Script guessing test
yanone Feb 21, 2022
e95e8b5
Merge branch 'support_glyphs3_rtl_kerning' of https://github.com/goog…
yanone Sep 7, 2022
e43f4f8
Update glyphdata.py
yanone Sep 7, 2022
f012805
Update glyphdata.py
yanone Sep 7, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 22 additions & 6 deletions Lib/glyphsLib/builder/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
import os
import re

from glyphsLib import classes
from glyphsLib import classes, glyphdata
from .constants import GLYPHLIB_PREFIX
from .glyph import BRACKET_GLYPH_RE

Expand Down Expand Up @@ -140,12 +140,28 @@ def _to_glyphs_kerning_group(self, name, glyphs):

def _glyph_kerning_attr(glyph, side):
"""Return leftKerningGroup or rightKerningGroup depending on the UFO
group's side (1 or 2).
group's side (1 or 2) and writing script.
"""
if int(side) == 1:
return "rightKerningGroup"
else:
return "leftKerningGroup"
if glyph.parent.format_version > 2: # Glyphs 3
glyph_data = glyphdata.get_glyph(glyph.name)
if (
glyph_data.script in ("arabic", "hebrew")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why only "arabic" and "hebrew"?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are the two RTL scripts to my knowledge. This point is arguably the critical one, also about the numbers. I chose those to my best knowledge. Some algorithm needs to be in place to sort Glyphs’ kerning classes into the correct first/second pairs, and I didn’t know any better.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add the writing direction to the glyphdata.xml but there might be a way to query unicode to get the writing direction of each code. Then we don’t need to check for script == arabic and category != Number.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can get script direction from a function in ufo2ft.utils.

(I’m on the road this week so won’t be able to review.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can add the writing direction to the glyphdata.xml but there might be a way to query unicode to get the writing direction of each code. Then we don’t need to check for script == arabic and category != Number.

Yes, I could try this approach. @anthrotype Seems like a good idea?

and glyph_data.category != "Number"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

btw, there other characters belonging to RTL scripts (e.g. Arabic) that are not "Numbers" (in the unicode category sense of Nd, No, etc.) but which have a (weak) left-to-right bidirectional type: e.g. arabic decimal/thousand separators (Po: Punctuation other), or arabic number sign (Cf: Format), and some others.

In ufo2ft we don't check the category is Number, but we check if the bidiType (unicodedata.bidirectional) is either "L" (strong left to right), "AN" (Arabic numbers) or "EN" (extended arabic-indic digits)

Copy link
Member

@anthrotype anthrotype Apr 11, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note (to self, mostly) that in ufo2ft, the kerning pairs for the (LTR) arabic numbers are added to the same kern_rtl lookup where the rest of the (RTL) kern pairs between arabic non-numbers are; but they are treated as left-to-right, hence only the x-advance is modified (not also the x-origin):

E.g. see this test case:
https://github.com/googlefonts/ufo2ft/blob/ad28eea062e0dd48678309bd9ef86dfcc85fa85a/tests/featureWriters/kernFeatureWriter_test.py#L691-L695

Whereas here, it looks like the kerningRTL dict only contains the RTL kerning pairs.

So ufo2ft's kern_rtl lookup and Glyphs3 kerningRTL don't exactly overlap; the former is a superset of the latter.

):
if int(side) == 1:
return "leftKerningGroup"
else:
return "rightKerningGroup"
else:
if int(side) == 1:
return "rightKerningGroup"
else:
return "leftKerningGroup"
else: # Glyphs 2
if int(side) == 1:
return "rightKerningGroup"
else:
return "leftKerningGroup"


def _assert_groups_are_identical(self, reference_ufo, ufo):
Expand Down
19 changes: 14 additions & 5 deletions Lib/glyphsLib/builder/kerning.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,34 @@ def to_ufo_kerning(self):
for master in self.font.masters:
master_id = master.id
kerning_source = master.metricsSource.id # Maybe be a linked master
if kerning_source in self.font.kerning:
kerning = self.font.kerning[kerning_source]
if kerning_source in self.font.kerningLTR:
kerning = self.font.kerningLTR[kerning_source]
_to_ufo_kerning(self, self._sources[master_id].font, kerning)
if kerning_source in self.font.kerningRTL:
kerning = self.font.kerningRTL[kerning_source]
_to_ufo_kerning(self, self._sources[master_id].font, kerning, "RTL")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since both kerningLTR and kerningRTL are merged into the same ufo.kerning dictionary, is it ever possible that a clash might occur? I.e. the same kern pair defined in both kerningLTR and kerningRTL (potentially with different kern values), which would then mean that the RTL coming last would overwrite the LTR kern pair with the same (first, second) key?
I suppose this is possible in theory given Glypsh3 kerning is now defined in two split dictionaries, so glyphsLib should either raise some error or issue a warning about the ambiguous kern pair.



def _to_ufo_kerning(self, ufo, kerning_data):
def _to_ufo_kerning(self, ufo, kerning_data, direction="LTR"):
"""Add .glyphs kerning to an UFO."""

warning_msg = "Non-existent glyph class %s found in kerning rules."

for left, pairs in kerning_data.items():
match = re.match(r"@MMK_L_(.+)", left)
if direction == "LTR":
match = re.match(r"@MMK_L_(.+)", left)
elif direction == "RTL":
match = re.match(r"@MMK_R_(.+)", left)
Copy link
Member

@anthrotype anthrotype Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wait.. does Glyphs 3 really call the class on the left-hand side @MMK_R_ and the one on the right-hand side @MMK_L_ for RTL kerning?!

Copy link
Contributor Author

@yanone yanone Feb 22, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I’ll try to answer: The kerning pairs are still defined in logical order. So the code logic doesn’t change here. First stays first, second stays second. Additionally, for the new RTL-specific kerning table, Georg has switched the L and R to now be visual rather than logical, (as he did with kernLeft and kernRight classes) which I believe shouldn't have happened. We should have silently accepted that L and R were never appropriate names but are used to describe 1st and 2nd.

FYI, you can check the two added RTL_kerning_v2.glyphs and RTL_kerning_v3.glyphs files for reference, in particular the kerning pair reh-ar,hah-ar.init.swsh (in logical order) which is defined in both.

left_is_class = bool(match)
if left_is_class:
left = "public.kern1.%s" % match.group(1)
if left not in ufo.groups:
self.logger.warning(warning_msg % left)
for right, kerning_val in pairs.items():
match = re.match(r"@MMK_R_(.+)", right)
if direction == "LTR":
match = re.match(r"@MMK_R_(.+)", right)
elif direction == "RTL":
match = re.match(r"@MMK_L_(.+)", right)
right_is_class = bool(match)
if right_is_class:
right = "public.kern2.%s" % match.group(1)
Expand Down
4 changes: 2 additions & 2 deletions Lib/glyphsLib/classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -4643,8 +4643,8 @@ def settings(self):
{"plist_name": "features", "type": GSFeature},
{"plist_name": "fontMaster", "object_name": "masters", "type": GSFontMaster},
{"plist_name": "kerning", "object_name": "_kerningLTR", "type": OrderedDict},
{"plist_name": "kerningLTR", "type": OrderedDict},
{"plist_name": "kerningRTL", "type": OrderedDict},
{"plist_name": "kerningLTR", "object_name": "_kerningLTR", "type": OrderedDict},
{"plist_name": "kerningRTL", "object_name": "_kerningRTL", "type": OrderedDict},
{"plist_name": "kerningVertical", "type": OrderedDict},
{"plist_name": "date", "converter": parse_datetime},
{"plist_name": "disablesAutomaticAlignment", "converter": bool},
Expand Down
40 changes: 39 additions & 1 deletion Lib/glyphsLib/glyphdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,10 @@ def get_glyph(glyph_name, data=None, unicodes=None):
if category is None:
category, sub_category = _construct_category(glyph_name, data)

# TODO: Determine script in ligatures.
script = attributes.get("script")
if script is None:
script = _construct_script(glyph_name, data=data)

description = attributes.get("description")

return Glyph(
Expand Down Expand Up @@ -254,6 +256,42 @@ def _construct_category(glyph_name, data):
return None, None


def _construct_script(glyph_name, data):
"""Derive script of a glyph name."""
# Glyphs creates glyphs that start with an underscore as "non-exportable" glyphs or
# construction helpers without a category.
if glyph_name.startswith("_"):
return None

# Glyph variants (e.g. "fi.alt") don't have their own entry, so we strip e.g. the
# ".alt" and try a second lookup with just the base name. A variant is hopefully in
# the same category as its base glyph.
base_name = glyph_name.split(".", 1)[0]
base_attribute = data.names.get(base_name) or {}
if base_attribute:
script = base_attribute.get("script")
return script

# Detect ligatures.
if "_" in base_name:
base_names = base_name.split("_")
# The last name has a suffix, add it to all the names.
if "-" in base_names[-1]:
_, s = base_names[-1].rsplit("-", 1)
base_names = [
(n if n.endswith(f"-{s}") else f"{n}-{s}") for n in base_names
]
base_names_attributes = [_lookup_attributes(name, data) for name in base_names]
first_attribute = base_names_attributes[0]

# If the first part is a Letter...
if first_attribute.get("category") == "Letter":
script = first_attribute.get("script")
return script

return None


def _translate_category(glyph_name, unicode_category):
"""Return a translation from Unicode category letters to Glyphs
categories."""
Expand Down
183 changes: 183 additions & 0 deletions tests/data/RTL_kerning_v2.glyphs
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
{
.appVersion = "1352";
DisplayStrings = (
"/alef-hb/bet-hb",
"/reh-ar/hah-ar.init.swsh/hah-ar.init"
);
date = "2022-02-09 20:07:11 +0000";
familyName = "RTL kerning";
fontMaster = (
{
ascender = 800;
capHeight = 700;
descender = -200;
id = m01;
xHeight = 500;
}
);
glyphs = (
{
glyphname = A;
layers = (
{
layerId = m01;
width = 600;
}
);
unicode = 0041;
},
{
glyphname = B;
layers = (
{
layerId = m01;
width = 600;
}
);
unicode = 0042;
},
{
glyphname = C;
layers = (
{
layerId = m01;
width = 600;
}
);
unicode = 0043;
},
{
glyphname = "hah-ar.init";
lastChange = "2022-02-21 17:09:37 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
leftKerningGroup = "hah-ar.init";
unicode = FEA3;
},
{
glyphname = "hah-ar.init.swsh";
lastChange = "2022-02-21 17:25:40 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
leftKerningGroup = "hah-ar.init.swsh";
},
{
glyphname = "reh-ar";
lastChange = "2022-02-21 17:09:13 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
rightKerningGroup = "reh-ar";
unicode = 0631;
},
{
glyphname = "alef-hb";
lastChange = "2022-02-21 11:17:22 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
rightKerningGroup = leftAlef;
unicode = 05D0;
},
{
glyphname = "bet-hb";
lastChange = "2022-02-21 11:17:26 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
leftKerningGroup = rightBet;
unicode = 05D1;
},
{
glyphname = "he-hb";
lastChange = "2022-02-17 09:36:21 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
unicode = 05D4;
},
{
glyphname = "one-ar";
lastChange = "2022-02-21 14:41:56 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
leftKerningGroup = "left-one-ar";
unicode = 0661;
},
{
glyphname = "one-ar.wide.ss16.calt";
lastChange = "2022-02-21 14:59:56 +0000";
layers = (
{
layerId = m01;
width = 600;
}
);
leftKerningGroup = "left-one-ar";
},
{
glyphname = space;
layers = (
{
layerId = m01;
width = 600;
}
);
unicode = 0020;
}
);
instances = (
{
instanceInterpolations = {
m01 = 1;
};
name = Regular;
}
);
kerning = {
m01 = {
"@MMK_L_leftAlef" = {
"@MMK_R_rightBet" = -20;
"he-hb" = 4;
};
"@MMK_L_leftBet" = {
"@MMK_R_rightAlef" = 20;
};
"@MMK_L_reh-ar" = {
"@MMK_R_hah-ar.init" = -50;
"@MMK_R_hah-ar.init.swsh" = -50;
};
"he-hb" = {
"@MMK_R_rightAlef" = -2;
"he-hb" = -21;
};
};
};
unitsPerEm = 1000;
versionMajor = 1;
versionMinor = 0;
}
Loading