opentypejs / opentype.js

Read and write OpenType fonts using JavaScript.
https://opentype.js.org/
MIT License
4.45k stars 476 forks source link

Ligatures #194

Closed casperno closed 8 years ago

casperno commented 8 years ago

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

fpirsch commented 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.

casperno commented 8 years ago

Fantastic! Looking forward to it.

fdb commented 8 years ago

Hey @fpirsch that's great news! Let me know if I can help.

fpirsch commented 8 years ago

200 Adds full GSUB parsing. Writing simple cases coming soon.

Harbs commented 8 years ago

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?

fdb commented 8 years ago

@fpirsch amazing work! I'll look into this tomorrow.

fpirsch commented 8 years ago

@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.

Harbs commented 8 years ago

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.

fdb commented 8 years ago

@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).

Harbs commented 8 years ago

@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.

fpirsch commented 8 years ago

@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.

Harbs commented 8 years ago

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?

fpirsch commented 8 years ago

@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".

Harbs commented 8 years ago

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! :-)

fpirsch commented 8 years ago

My idea was to stay low-level and simply append new substitutions. This leaves the problem of ordering to the application.

Harbs commented 8 years ago

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).

davelab6 commented 8 years ago

I wonder if CSS3 feature strings could be helpful, https://www.w3.org/TR/css-fonts-3/#propdef-font-variant

casperno commented 8 years ago

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;

            }
Harbs commented 8 years ago

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.

casperno commented 8 years ago

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?

fpirsch commented 8 years ago

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.

Harbs commented 8 years ago

I did not quite get exactly what expandCoverage does. (I was actually trying to figure out the logic inside substitutions today.) Can you explain?

Harbs commented 8 years ago

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.

casperno commented 8 years ago

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.

Harbs commented 8 years ago

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.

Harbs commented 8 years ago

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.

Harbs commented 8 years ago

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.

fpirsch commented 8 years ago

@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.

Jolg42 commented 8 years ago

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 😉