Closed casperno closed 8 years ago
Hi @casperno, I wrote the GPOS code, and I am currently working on the GSUB table which stores ligatures and other interesting features. I will submit some pull requests before the end of the month to support GSUB reading and (partial) writing.
Fantastic! Looking forward to it.
Hey @fpirsch that's great news! Let me know if I can help.
This is amazing! How would you go about using the tables for specific features (like rlig, liga, onum, salt, etc.)? Is that implemented yet? If not, what are your thoughts on implementation?
@fpirsch amazing work! I'll look into this tomorrow.
@Harbs The GSUB data is exposed through the font.tables.gsub
object. I'm now working on a Substitution
object, made available as font.substitution
, carrying all the methods needed to list, search and edit this data (for the most common features).
Of course all thoughts and ideas are welcome.
I'm not sure what other APIs look like. The FTE model is one that makes sense to me. http://help.adobe.com/en_US/ActionScript/3.0_ProgrammingAS3/WS56773E36-AC97-4047-B9C1-D9DFA9A51F3F.html http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/text/engine/TextElement.html http://help.adobe.com/en_US/FlashPlatform/reference/actionscript/3/flash/text/engine/ElementFormat.html Some similar API seems ideal. I like that opentype.js is finer grained than FTE.
@fpirsch just merged the table parsing, thanks again!
Here are my thoughts for the API. It seems at the highest level glyph subsitution could happen in the stringToGlyphs
call.
font.stringToGlyphs('affinity', {'liga': true})
//=> [{glyph a}, {glyph f_f_i}, {glyph n}, {glyph i}, {glyph t}, {glyph y}]
One level down we could pass in a list of glyphs to be (potentially) substituted by another set of glyphs given the required features:
font.substituteGlyphs([{glyph 5}, {glyph slash}, {glyph 3}], {'frac': true})
//=> [{glyph five.numr}, {glyph fraction}, {glyph three.dnom}]
Then we could query a single feature to get something similar to Adobe's feature descriptions:
font.gsub.getFeature('liga')
//=> {'sub': ['f', 'f', 'i'], 'by': ['f_f_i']}
These APIs might need some more thinking :-)
For reference, I took examples from An Introduction to OpenType Substitution Features.
I have no clue how much work it might be to actually do the substitutions instead of just parsing them.
@Harbs I think FTE might be too high-level. The scope of this library is to provide easy access to the font data (and — as a courtesy — very simple LTR formatting). The Flash Text Engine, or a similar API, could be layered on top of OpenType.js (e.g. see opentype-layout).
@fdb To be clear, I was not suggesting a full FTE-type implementation. I'd love for someone to do my work for me, but I expect to have to do that myself... ;-)
I was rather suggesting some type of object similar to ElementFormat where you could specify features for a run of text and get back Glyph objects which match the feature set.
Something like: Font.stringToGlyphs(string,format)
instead of Font.stringToGlyphs(string)
It would make a lot of sense for at least this functionality to be in the core library.
@fdb I'm about to submit a new PR where the API looks like
font.substitution.getLigatures()
font.substitution.addLigature({sub: [42, 42, 45], by: 167})
Glyphs are referenced by id. Using their names would require some more work in the GlyphSet I suppose.
font.gsub
could be confusing with the actual GSUB data font.tables.gsub
which does not always exist, so I went for font.substitution
.
This is a start, it could use some polish later.
I'd rather change the API a bit. Instead of font.substitution.getLigatures()
I think font.substitution.getFeature('liga')
makes more sense. There's at least three features related to ligatures alone (i.e. rlig, liga, dlig) not to mention the plethora of other GSUB features. It might also make sense to have font.substitution.getFeatures(['liga','rlig'])
which would aggregate the subsitutions of multiple features.
Likewise, font.substitution.addLigature({sub: [42, 42, 45], by: 167})
should probably be font.substitution.addSub("liga",{sub: [42, 42, 45], by: 167})
Does this make sense?
@Harbs You're right. I was focused on standard ligatures, but a more generic approach by feature name make more sense. So let's stick to font.substitution.getFeature('liga')
.
And maybe font.substitution.add("liga",{sub: [42, 42, 45], by: 167})
could do the job ? font.substitution.addSub("liga",{sub: [42, 42, 45], by: 167})
feels like there's a lot of "sub".
Sure. "add" sounds better.
One thing that's going to be important to think about is the order of substitutions. There's lots of complex fonts which have really complex tables where the order is critical for correct rendering of text. I'm not sure the best way to handle that, and "one thing at a time", but I figured it does not hurt to mention the issue... This is really awesome stuff! :-)
My idea was to stay low-level and simply append new substitutions. This leaves the problem of ordering to the application.
The order is programmed into the font. The application has no way of knowing the correct order unless the info is presented in an informative way.
One possible way to solve this problem would be to allow font.substitution.getFeatures(['liga','rlig'])
like I mentioned above and the list of substitutions would reflect the table order in the font (not the order of features as requested).
I wonder if CSS3 feature strings could be helpful, https://www.w3.org/TR/css-fonts-3/#propdef-font-variant
I've updated opentype.js in my project and implemented support for ligatures. THANK YOU! As a typography nerd, this makes me very happy.
The getLigatures
does what it says, but returns a result that isn't very fast to use for lookups. So I've wrote a new function that returns a lookup table, structured much like the opentype font.
Is this something you'd feel comfortable adding to the main project?
Usage:
var ligLookup = fnt.substitution.getLigatureLookup('liga');
var fiGlyph = ligLookup['f']['f].glyph
Code:
Substitution.prototype.ligLookup = null;
Substitution.prototype.getLigatureLookup = function(feature, script, language) {
script = script || 'DFLT';
language = language || 'DFLT';
if (this.ligLookup)
return this.ligLookup;
var ligLookup = {}
var lookupTable = this.getLookupTable(script, language, feature, 4);
if (!lookupTable) {
return ligLookup;
}
var subtables = lookupTable.subtables;
for (var i = 0; i < subtables.length; i++) {
var subtable = subtables[i];
var glyphs = this.expandCoverage(subtable.coverage);
var ligatureSets = subtable.ligatureSets;
for (var j = 0; j < glyphs.length; j++) {
var startGlyph = glyphs[j];
var fontGlyphs = this.font.glyphs.glyphs;
var firstChr = fontGlyphs[startGlyph].name;
ligLookup[firstChr] = {};
var ligSet = ligatureSets[j];
for (var k = 0; k < ligSet.length; k++) {
var lig = ligSet[k];
var child = ligLookup[firstChr];
var ligCmps = lig.components;
for (var l = 0; l < ligCmps.length; l++) {
if (child[fontGlyphs[ligCmps[l]].name] == undefined)
child[fontGlyphs[ligCmps[l]].name] = {};
if (l < ligCmps.length - 1)
child = child[fontGlyphs[ligCmps[l]].name];
}
child[fontGlyphs[ligCmps[l - 1]].name].glyph = fontGlyphs[lig.ligGlyph];
}
}
}
this.ligLookup = ligLookup;
return ligLookup;
}
Good idea, but I'd take it one step further. Instead of getLigatureLookup, I'd make it "getFeatureLookup" to make it generalized, and it would have a hastable of the previously requested features.
Good thinking. I see that getFeature is implemented this way.
How do you feel about the results being cached in Substitution.prototype.ligLookup
?
Should it be cached by the code using it instead? Or is the additional memory footprint negligible?
Hi @Casperno, you're right the getLigatures
method is not intended for fast rendering lookups, but for font edition purposes.
IMHO the best lookup structure for this would be the raw font data in font.tables.gsub
. I don't think there is a need to build an additional structure. In particular, expandCoverage
is handy but not efficient. Instead, we need fast search methods in the raw data.
Of course the fastest way would be to search directly the binary data.
I did not quite get exactly what expandCoverage does. (I was actually trying to figure out the logic inside substitutions today.) Can you explain?
BTW: I'm not sure I'm reading the code correctly, so I might have missed it, but did not see where ligature lookup sorting was taking place.
According to the spec: sub f f by f_f; sub f i by f_i; sub f f i by f_f_i; sub o f f i by o_f_f_i;
will produce an identical representation in the font as:
sub o f f i by o_f_f_i; sub f f i by f_f_i; sub f f by f_f; sub f i by f_i;
For the substitutions to work correctly in all cases, someone needs to do the sorting. I'm not sure if the sorting should happen when the type 4 tables are initially parsed or at some later point. (It makes the most sense to me to do a one time lazy sorting, the first time the type 4 tables are accessed.)
Thanks.
You're quite right, the sorting is not done in the supplied code. Basically, if a char has ligatures at all, we look forward four characters, try first looking up with all four, then three, then two.
I kept that part out of opentypejs, as it seem more of an layout implementation problem than.
That seems pretty inefficient, but I'm not sure there's a better way.
I'm probably going to implement a caching mechanism for words so I only have to do the lookups on a particular word once.
Another question I'm having trouble figuring out:
Are glyph classes and glyph ranges supported? It seems they would need to be (for GPOS as well), but I couldn't find it.
Any pointers would be very welcome.
FYI: While brushing up on my OpenType, I came across this post: http://ansuz.sooke.bc.ca/entry/131
Very useful observations there.
I found that bit towards the end about named lookups very interesting as well. It's important to know where the named lookup is declared for implementation purposes.
@Harbs expandCoverage
unifies both coverage table types (list and range) in a single list. Useful for editors, and that's all.
Glyph classes and ranges are parsed, but not used in the rendering (btw, currently the GSUB table is not used at all in the rendering process). font.substitution.add
doesn't use them either. If you want to add a substitution with classes or ranges, you'll have to modify directly the font.tables.gsub
object.
There is no ordering in opentype. font.substitution.getFeature
lists substitutions in the same order they are in the file. And font.substitution.add
adds them one by one in the order you provide them. So YOU (the font designer) have to do the sorting you want.
I'm closing this issue as the first version with ligatures is now working. The examples are in the tests for now. If someone have ideas or more advanced need, just open a new issue 😉
Hi,
I've spent some time with opentype, using it to load fonts used in txt.js. It was surprisingly easy, but I miss ligatures.
From what I can see in the code, opentypejs uses GPOS tables for kerning values. Isn't the GPOS also the table for ligatures? Anyone working on this now? If not, any pointer for me if I find the time to try and implement it?
Thanks to all developers for an amazing feat - if it wasn't for the obvious proof that it's working, I wouldn't have believed that parsing of binary font data was feasible in javascript