go-text / typesetting

High quality text shaping in pure Go.
Other
88 stars 11 forks source link

Lower memory usage #58

Closed benoitkugler closed 1 year ago

benoitkugler commented 1 year ago

The general approach used in the Opentype parser is to aggressively decode the binary format into Go types, which are clearer and simpler to use that the intricate, offset-based Opentype format.

However, the Opentype format is intricate precisely because it uses clever conventions to very efficiently compact glyph data : decoding this data yields an in-memory representation that is larger.

Recent issues regarding memory usage indicate that we should aim at a better balance : this PR reduces memory usage for the glyf and GPOS tables, leading to significant savings for large fonts. For instance, the font mentionned in this Fyne issue, 14Mb on disk, was taking roughly 70Mb in memory, now reduced to 24Mb !

The speed during shaping is affected in the following ways :

... but the memory savings seem to be worth it.

whereswaldon commented 1 year ago

I've taken the liberty of adding a benchmark commit prior to this changeset and measuring the impact of this branch. Here are my results:

goos: linux
goarch: amd64
pkg: github.com/go-text/typesetting/shaping
cpu: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
                                              │ main-branch.txt │        lowermem-branch.txt         │
                                              │     sec/op      │   sec/op     vs base               │
Shaping/10runes-arabic-0fontCache-8                 155.7µ ± 1%   160.8µ ± 1%   +3.28% (p=0.001 n=7)
Shaping/100runes-arabic-0fontCache-8                458.4µ ± 1%   468.2µ ± 1%   +2.15% (p=0.001 n=7)
Shaping/1000runes-arabic-0fontCache-8               3.369m ± 3%   3.461m ± 2%   +2.72% (p=0.011 n=7)
Shaping/10runes-latin-0fontCache-8                  2.671µ ± 1%   2.833µ ± 1%   +6.07% (p=0.001 n=7)
Shaping/100runes-latin-0fontCache-8                 17.16µ ± 1%   18.28µ ± 1%   +6.49% (p=0.001 n=7)
Shaping/1000runes-latin-0fontCache-8                160.9µ ± 1%   171.1µ ± 0%   +6.33% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-arabic-8         23.58µ ± 1%   30.07µ ± 2%  +27.54% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-arabic-8        224.9µ ± 1%   290.8µ ± 1%  +29.34% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-arabic-8       2.304m ± 1%   2.978m ± 1%  +29.23% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-latin-8          10.05µ ± 2%   12.63µ ± 8%  +25.68% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-latin-8         101.4µ ± 2%   128.0µ ± 1%  +26.31% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-latin-8        1.007m ± 1%   1.280m ± 1%  +27.03% (p=0.001 n=7)
geomean                                             109.4µ        121.9µ       +11.44%

                                              │ main-branch.txt │         lowermem-branch.txt          │
                                              │      B/op       │     B/op      vs base                │
Shaping/10runes-arabic-0fontCache-8                68.92Ki ± 0%   68.92Ki ± 0%       ~ (p=0.266 n=7)
Shaping/100runes-arabic-0fontCache-8               136.4Ki ± 0%   136.4Ki ± 0%       ~ (p=0.674 n=7)
Shaping/1000runes-arabic-0fontCache-8              858.1Ki ± 0%   858.1Ki ± 0%  +0.00% (p=0.017 n=7)
Shaping/10runes-latin-0fontCache-8                 2.234Ki ± 0%   2.234Ki ± 0%       ~ (p=1.000 n=7) ¹
Shaping/100runes-latin-0fontCache-8                7.984Ki ± 0%   7.984Ki ± 0%       ~ (p=1.000 n=7) ¹
Shaping/1000runes-latin-0fontCache-8               65.64Ki ± 0%   65.64Ki ± 0%       ~ (p=0.085 n=7)
ShapingGlyphInfoExtraction/10runes-arabic-8        54.85Ki ± 0%   57.71Ki ± 0%  +5.21% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-arabic-8       499.1Ki ± 0%   524.4Ki ± 0%  +5.08% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-arabic-8      4.916Mi ± 0%   5.166Mi ± 0%  +5.08% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-latin-8         19.20Ki ± 0%   20.26Ki ± 0%  +5.54% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-latin-8        196.4Ki ± 0%   207.0Ki ± 0%  +5.40% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-latin-8       1.897Mi ± 0%   2.000Mi ± 0%  +5.44% (p=0.001 n=7)
geomean                                            74.07Ki        75.35Ki       +1.73%
¹ all samples are equal

                                              │ main-branch.txt │         lowermem-branch.txt          │
                                              │    allocs/op    │  allocs/op   vs base                 │
Shaping/10runes-arabic-0fontCache-8                 1.363k ± 0%   1.363k ± 0%        ~ (p=1.000 n=7) ¹
Shaping/100runes-arabic-0fontCache-8                3.339k ± 0%   3.339k ± 0%        ~ (p=1.000 n=7) ¹
Shaping/1000runes-arabic-0fontCache-8               24.57k ± 0%   24.57k ± 0%   +0.01% (p=0.001 n=7)
Shaping/10runes-latin-0fontCache-8                   9.000 ± 0%    9.000 ± 0%        ~ (p=1.000 n=7) ¹
Shaping/100runes-latin-0fontCache-8                  9.000 ± 0%    9.000 ± 0%        ~ (p=1.000 n=7) ¹
Shaping/1000runes-latin-0fontCache-8                 9.000 ± 0%    9.000 ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/10runes-arabic-8          163.0 ± 0%    183.0 ± 0%  +12.27% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-arabic-8        1.625k ± 0%   1.825k ± 0%  +12.31% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-arabic-8       16.81k ± 0%   18.93k ± 0%  +12.59% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-latin-8           105.0 ± 0%    114.0 ± 0%   +8.57% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-latin-8         1.028k ± 0%   1.113k ± 0%   +8.27% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-latin-8        10.25k ± 0%   11.10k ± 0%   +8.30% (p=0.001 n=7)
geomean                                              315.4         325.9        +3.34%
¹ all samples are equal

Based on my results, this changeset appears to:

I can live with the slowdown on shaping, but I worry about the glyph data extraction penalty. 25-30% is quite steep. I also (as I was writing this) realized that we have no benchmark to capture the metric this changeset primarily addresses (font loading memory requirements). I'll try to add one for that to measure the benefit on that side a little more concretely.

whereswaldon commented 1 year ago

Okay, I've pushed a font parsing benchmark as well, though it only loads the three fonts available in the typesetting repo, and therefore may not be really representative of our performance across a wide swathe of fonts. Here are my results from that:

goos: linux
goarch: amd64
pkg: github.com/go-text/typesetting/shaping
cpu: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
                                        │ main-font-parse.txt │      lowermem-font-parse.txt       │
                                        │       sec/op        │   sec/op     vs base               │
ShapingFontLoad/arabic:amiri_regular-8            3.984m ± 2%   2.122m ± 4%  -46.74% (p=0.001 n=7)
ShapingFontLoad/lating:go_regular-8               399.5µ ± 3%   239.8µ ± 3%  -39.98% (p=0.001 n=7)
ShapingFontLoad/lating:roboto_regular-8          1224.5µ ± 3%   335.1µ ± 3%  -72.64% (p=0.001 n=7)
geomean                                           1.249m        554.4µ       -55.61%

                                        │ main-font-parse.txt │       lowermem-font-parse.txt       │
                                        │        B/op         │     B/op      vs base               │
ShapingFontLoad/arabic:amiri_regular-8           4.678Mi ± 0%   2.854Mi ± 0%  -38.98% (p=0.001 n=7)
ShapingFontLoad/lating:go_regular-8              366.7Ki ± 0%   273.6Ki ± 0%  -25.38% (p=0.001 n=7)
ShapingFontLoad/lating:roboto_regular-8         1857.0Ki ± 0%   392.2Ki ± 0%  -78.88% (p=0.001 n=7)
geomean                                          1.448Mi        679.5Ki       -54.18%

                                        │ main-font-parse.txt │      lowermem-font-parse.txt       │
                                        │      allocs/op      │  allocs/op   vs base               │
ShapingFontLoad/arabic:amiri_regular-8            69.11k ± 0%   30.63k ± 0%  -55.69% (p=0.001 n=7)
ShapingFontLoad/lating:go_regular-8               2.764k ± 0%   2.056k ± 0%  -25.62% (p=0.001 n=7)
ShapingFontLoad/lating:roboto_regular-8          13.503k ± 0%   2.599k ± 0%  -80.75% (p=0.001 n=7)
geomean                                           13.71k        5.470k       -60.12%

These results make sense. As I understand it, the changeset primarily just does less unpacking into go types, so the results reflect doing less work.

As we only parse a font once (hopefully), whereas we use it continuously, I do think I'm still concerned about the glyph data extraction slowdown. We cache the results of that operation in Gio, but it's still quite expensive.

@benoitkugler Naturally, this is all a balancing act. Reducing memory use will put pressure on something else. Maybe this is the best option we have. Do you see other options that seem worth exploring in terms of balancing our various resource uses, or is this the first logical step from our previous strategy (nothing in between worth exploring)?

benoitkugler commented 1 year ago

Thank you very much for these very useful benchmarks !

Well, I don't see other obvious changes. Adding a cache on the font object does not seems very useful since it is already done in the toolkits.

We may want to only apply the 'GPOS' changes, which won't impact glyph extraction speed, but still save some memory on very large font.

Besides, I'm personally totally OK with choosing speed over memory use, but this probably put more pressure on the toolkits to properly load and cache fonts to avoid memory use skyrocketing...

whereswaldon commented 1 year ago

@benoitkugler Would you mind posting another PR with just the GPOS changes? We can evaluate the benchmarks from just doing that and see what the tradeoffs are.

benoitkugler commented 1 year ago

@benoitkugler Would you mind posting another PR with just the GPOS changes? We can evaluate the benchmarks from just doing that and see what the tradeoffs are.

Done ! I've also added another measure of the heap size of the resulting font.Font, not taking into account intermediate allocations. This is actually the primary metric I wanted to handle.

andydotxyz commented 1 year ago

Thanks both of you for the hard work on this - it is certainly hard to balance the memory and CPU usage requirements.

One question that came up in the Fyne Community, which I was not qualified to answer, is: "Is it possible to lazy-load glyph data for only those items required to shape the input text"

I wasn't sure if this was really clever or just impossible, but it could be relevant here. The reason it was asked is that some people have started combining all languages into a single font file which is clearly massive and rarely needed in its completeness...

benoitkugler commented 1 year ago

One question that came up in the Fyne Community, which I was not qualified to answer, is: "Is it possible to lazy-load glyph data for only those items required to shape the input text"

It is hard to predict which glyph will be used. We could maybe use the notion of Script to do so, but I think it requires some user input : either a sample of text, or a list of scripts.

I wasn't sure if this was really clever or just impossible, but it could be relevant here. The reason it was asked is that some people have started combining all languages into a single font file which is clearly massive and rarely needed in its completeness...

Yup, this PR is actually motivated by the font in question ("go noto universal") ! And in this case I guess the user precisely does not want to restrict its app to a particular script, so the previous idea is not relevant.

dweymouth commented 1 year ago

Just thought I'd weigh in here since I am one of the Fyne app developers looking into using Go Noto (or a similar pan-Unicode font). My app is a music player, so depending on the artist/album/track names in the user's library it may have to render arbitrary Unicode characters, but most of the time it will probably only be rendering Latin script with a few diacritics here and there. For some users, it may be Latin + a good amount of Chinese, or Latin + Japanese, but almost certainly it won't be asked to render Latin+all of CJK+Cyrillic+Arabic+Hebrew+Thai+... all at once.

So I wonder, but again I have no domain knowledge here, if it's possible to "have our cake and eat it too" by caching glyph loads as they're needed? If this full PR saves a lot of memory but makes glyph loading expensive, could the "loaded" or "expanded" into go structs glyphs be stored in a cache that is either size limited, or periodically prunes itself down. So if an app rendered a bunch of text from several scripts at once, there would be a memory spike that would settle back down after the screen was static for a few mins (plus of course a slight time penalty as the glyphs are initially loaded into the cache, but hopefully less than 30%).

Also, if this is useful at all, I created a personal repo to benchmark Fyne's handling of large fonts so I can figure out when future perf improvements have made it suitable to bundle large fonts into my app: https://github.com/dweymouth/fyne-font-benchmarking

whereswaldon commented 1 year ago

@benoitkugler Would you mind posting another PR with just the GPOS changes? We can evaluate the benchmarks from just doing that and see what the tradeoffs are.

Done ! I've also added another measure of the heap size of the resulting font.Font, not taking into account intermediate allocations. This is actually the primary metric I wanted to handle.

I've taken benchmarks before/after on the other branch. I'll share them here to keep all of the discussion in one place:

goos: linux
goarch: amd64
pkg: github.com/go-text/typesetting/shaping
cpu: Intel(R) Core(TM) i7-6700K CPU @ 4.00GHz
                                              │ main-no-gpos.txt │          lowermem-gpos.txt          │
                                              │      sec/op      │    sec/op     vs base               │
Shaping/10runes-arabic-0fontCache-8                  155.4µ ± 3%   156.9µ ±  1%        ~ (p=0.383 n=7)
Shaping/100runes-arabic-0fontCache-8                 463.9µ ± 2%   463.5µ ±  1%        ~ (p=1.000 n=7)
Shaping/1000runes-arabic-0fontCache-8                3.391m ± 2%   3.395m ±  1%        ~ (p=0.383 n=7)
Shaping/10runes-latin-0fontCache-8                   2.657µ ± 1%   2.673µ ±  3%        ~ (p=0.119 n=7)
Shaping/100runes-latin-0fontCache-8                  17.02µ ± 2%   17.20µ ±  1%   +1.05% (p=0.012 n=7)
Shaping/1000runes-latin-0fontCache-8                 160.6µ ± 2%   163.4µ ±  1%        ~ (p=0.097 n=7)
ShapingFontLoad/arabic:amiri_regular-8               4.004m ± 2%   2.920m ±  1%  -27.07% (p=0.001 n=7)
ShapingFontLoad/lating:go_regular-8                  403.6µ ± 3%   398.5µ ±  1%        ~ (p=0.053 n=7)
ShapingFontLoad/lating:roboto_regular-8             1226.0µ ± 1%   534.0µ ±  1%  -56.44% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-arabic-8          23.50µ ± 2%   24.76µ ±  2%   +5.34% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-arabic-8         226.3µ ± 3%   237.9µ ±  3%   +5.15% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-arabic-8        2.302m ± 1%   2.438m ±  2%   +5.90% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-latin-8           10.03µ ± 3%   10.86µ ± 11%   +8.33% (p=0.001 n=7)
ShapingGlyphInfoExtraction/100runes-latin-8          101.4µ ± 6%   108.2µ ± 15%   +6.80% (p=0.004 n=7)
ShapingGlyphInfoExtraction/1000runes-latin-8         1.015m ± 6%   1.087m ±  3%   +7.17% (p=0.001 n=7)
geomean                                              28.03µ        27.26µ         -2.75%

                                              │ main-no-gpos.txt │           lowermem-gpos.txt           │
                                              │       B/op       │     B/op      vs base                 │
Shaping/10runes-arabic-0fontCache-8               68.92Ki ± 0%     68.92Ki ± 0%        ~ (p=1.000 n=7)
Shaping/100runes-arabic-0fontCache-8              136.4Ki ± 0%     136.4Ki ± 0%        ~ (p=0.592 n=7)
Shaping/1000runes-arabic-0fontCache-8             858.1Ki ± 0%     858.1Ki ± 0%   +0.00% (p=0.004 n=7)
Shaping/10runes-latin-0fontCache-8                2.234Ki ± 0%     2.234Ki ± 0%        ~ (p=1.000 n=7) ¹
Shaping/100runes-latin-0fontCache-8               7.984Ki ± 0%     7.984Ki ± 0%        ~ (p=1.000 n=7) ¹
Shaping/1000runes-latin-0fontCache-8              65.64Ki ± 0%     65.64Ki ± 0%        ~ (p=0.429 n=7)
ShapingFontLoad/arabic:amiri_regular-8            4.678Mi ± 0%     3.484Mi ± 0%  -25.52% (p=0.001 n=7)
ShapingFontLoad/lating:go_regular-8               366.7Ki ± 0%     366.7Ki ± 0%   +0.01% (p=0.001 n=7)
ShapingFontLoad/lating:roboto_regular-8          1857.0Ki ± 0%     503.7Ki ± 0%  -72.88% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-arabic-8       54.85Ki ± 0%     54.85Ki ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/100runes-arabic-8      499.1Ki ± 0%     499.1Ki ± 0%   +0.00% (p=0.001 n=7)
ShapingGlyphInfoExtraction/1000runes-arabic-8     4.916Mi ± 0%     4.916Mi ± 0%   +0.00% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-latin-8        19.20Ki ± 0%     19.20Ki ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/100runes-latin-8       196.4Ki ± 0%     196.4Ki ± 0%        ~ (p=0.070 n=7)
ShapingGlyphInfoExtraction/1000runes-latin-8      1.897Mi ± 0%     1.897Mi ± 0%   +0.00% (p=0.001 n=7)
geomean                                                        ²                  -3.42%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

                                              │ main-no-gpos.txt │          lowermem-gpos.txt           │
                                              │    allocs/op     │  allocs/op   vs base                 │
Shaping/10runes-arabic-0fontCache-8                1.363k ± 0%     1.363k ± 0%        ~ (p=1.000 n=7) ¹
Shaping/100runes-arabic-0fontCache-8               3.339k ± 0%     3.339k ± 0%        ~ (p=1.000 n=7) ¹
Shaping/1000runes-arabic-0fontCache-8              24.57k ± 0%     24.57k ± 0%   +0.01% (p=0.001 n=7)
Shaping/10runes-latin-0fontCache-8                  9.000 ± 0%      9.000 ± 0%        ~ (p=1.000 n=7) ¹
Shaping/100runes-latin-0fontCache-8                 9.000 ± 0%      9.000 ± 0%        ~ (p=1.000 n=7) ¹
Shaping/1000runes-latin-0fontCache-8                9.000 ± 0%      9.000 ± 0%        ~ (p=1.000 n=7) ¹
ShapingFontLoad/arabic:amiri_regular-8             69.11k ± 0%     43.76k ± 0%  -36.68% (p=0.001 n=7)
ShapingFontLoad/lating:go_regular-8                2.764k ± 0%     2.764k ± 0%        ~ (p=1.000 n=7) ¹
ShapingFontLoad/lating:roboto_regular-8           13.504k ± 0%     4.476k ± 0%  -66.85% (p=0.001 n=7)
ShapingGlyphInfoExtraction/10runes-arabic-8         163.0 ± 0%      163.0 ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/100runes-arabic-8       1.625k ± 0%     1.625k ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/1000runes-arabic-8      16.81k ± 0%     16.81k ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/10runes-latin-8          105.0 ± 0%      105.0 ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/100runes-latin-8        1.028k ± 0%     1.028k ± 0%        ~ (p=1.000 n=7) ¹
ShapingGlyphInfoExtraction/1000runes-latin-8       10.25k ± 0%     10.25k ± 0%        ~ (p=1.000 n=7) ¹
geomean                                                        ²                 -3.34%               ²
¹ all samples are equal
² summaries must be >0 to compute geomean

To sum up, #60 has the following impact relative to what we do now:

So on the whole, #60 reduces the footprint of some fonts significantly while imposing a slight 5-8% penalty on glyph info extraction. I think that sounds like a much better tradeoff. What do you all think?

dweymouth commented 1 year ago

Personally, I'm curious as to benchmarking the memory savings of #60 with the GoNotoCurrent ttf (or Arial Unicode MS if you're on Mac/Windows). But I have no direct stake in this so whatever you choose :)

benoitkugler commented 1 year ago

Personally, I'm curious as to benchmarking the memory savings of #60 with the GoNotoCurrent ttf (or Arial Unicode MS if you're on Mac/Windows). But I have no direct stake in this so whatever you choose :)

@dweymouth From my experiments, I went from 70Mb in memory to about 50Mb.

benoitkugler commented 1 year ago

So on the whole, #60 reduces the footprint of some fonts significantly while imposing a slight 5-8% penalty on glyph info extraction. I think that sounds like a much better tradeoff. What do you all think?

Thank you again for the benchmarks. It also seems to me that the best option is to only reduce the 'GPOS' table memory use. (And we can always re-open the issue later if needed.)

dweymouth commented 1 year ago

I don't want to make perfect the enemy of the good, and #60 is certainly an improvement, and I agree it's the best option for now, but that would still be 100 Mb for an app that needs both the regular and bold version of the font. Perhaps the best way to support Unicode in the future is not with monolithic .ttf/.otf, but with .ttc / .otc collections, with each individual font within the collection only loaded into memory the first time it is used.

whereswaldon commented 1 year ago

I don't want to make perfect the enemy of the good, and #60 is certainly an improvement, and I agree it's the best option for now, but that would still be 100 Mb for an app that needs both the regular and bold version of the font. Perhaps the best way to support Unicode in the future is not with monolithic .ttf/.otf, but with .ttc / .otc collections, with each individual font within the collection only loaded into memory the first time it is used.

Do you happen to know how any other software handles this? If you open this universal font in another application, does it manage to avoid loading so much data?

I imagine there are savings to be had here by using variable fonts, but I don't understand some of the mechanics involved sufficiently well to know how big the savings might be.

dweymouth commented 1 year ago

I can certainly investigate this with GTK/FLTK, though maybe not until later next week. And in the meantime this PR (edit: I mean #60) should be merged since it is still a nice improvement :) I don't know much about how web browsers and other Unicode-friendly software handles it, but I am hoping there is some path toward the UX of having font rendering "just work" for all common worldwide scripts, but not having a huge perf/mem penalty if the app is only needing to render Latin characters, or a few scripts.

dweymouth commented 1 year ago

Apparently web browsers use script mapping where different fonts are used to render different scripts. This is probably the best way to support pan-Unicode text, though I don't know if this is something that should be handled by go-text, or by Gio/Fyne, or even by the apps themselves if the toolkits expose APIs that are flexible enough for the app to set their own script mapping.

benoitkugler commented 1 year ago

Apparently web browsers use script mapping where different fonts are used to render different scripts. This is probably the best way to support pan-Unicode text, though I don't know if this is something that should be handled by go-text, or by Gio/Fyne, or even by the apps themselves if the toolkits expose APIs that are flexible enough for the app to set their own script mapping.

From what I know, this mapping is called a FontMap, and it indeed only load fonts on a per rune basis. This is something I need for my personal project, but it was a bit bigger than "basic" typesetting, so that it is not (yet) included in go-text. (See the font-scan branch for a possible implementation. The GUI toolkits would probably have to undergo rather large changes to leverage such a font map, but that could be the way forward indeed.)

benoitkugler commented 1 year ago

I'll close this PR in favor of #60, since we seems to all agree it provides a better trade-off.