xref: /aosp_15_r20/external/skia/modules/canvaskit/tests/font_test.js (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1describe('Font Behavior', () => {
2    let container;
3
4    const assetLoadingPromises = [];
5    let notoSerifFontBuffer = null;
6    // This font is known to support kerning
7    assetLoadingPromises.push(fetch('/assets/NotoSerif-Regular.ttf').then(
8        (response) => response.arrayBuffer()).then(
9        (buffer) => {
10            notoSerifFontBuffer = buffer;
11        }));
12
13    let bungeeFontBuffer = null;
14    // This font has tofu for incorrect null terminators
15    // see https://bugs.chromium.org/p/skia/issues/detail?id=9314
16    assetLoadingPromises.push(fetch('/assets/Bungee-Regular.ttf').then(
17        (response) => response.arrayBuffer()).then(
18        (buffer) => {
19            bungeeFontBuffer = buffer;
20        }));
21
22    let colrv1FontBuffer = null;
23    // This font has glyphs for COLRv1. Also used in gms/colrv1.cpp
24    assetLoadingPromises.push(fetch('/assets/test_glyphs-glyf_colr_1.ttf').then(
25        (response) => response.arrayBuffer()).then(
26        (buffer) => {
27            colrv1FontBuffer = buffer;
28        }));
29
30    beforeEach(async () => {
31        await EverythingLoaded;
32        await Promise.all(assetLoadingPromises);
33        container = document.createElement('div');
34        container.innerHTML = `
35            <canvas width=600 height=600 id=test></canvas>
36            <canvas width=600 height=600 id=report></canvas>`;
37        document.body.appendChild(container);
38    });
39
40    afterEach(() => {
41        document.body.removeChild(container);
42    });
43
44    gm('monospace_text_on_path', (canvas) => {
45        const paint = new CanvasKit.Paint();
46        paint.setAntiAlias(true);
47        paint.setStyle(CanvasKit.PaintStyle.Stroke);
48
49        const font = new CanvasKit.Font(CanvasKit.Typeface.GetDefault(), 24);
50        const fontPaint = new CanvasKit.Paint();
51        fontPaint.setAntiAlias(true);
52        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
53
54
55        const arc = new CanvasKit.Path();
56        arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
57        arc.lineTo(210, 140);
58        arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
59
60        // Only 1 dot should show up in the image, because we run out of path.
61        const str = 'This téxt should follow the curve across contours...';
62        const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font);
63
64        canvas.drawPath(arc, paint);
65        canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
66
67        textBlob.delete();
68        arc.delete();
69        paint.delete();
70        font.delete();
71        fontPaint.delete();
72    });
73
74    gm('serif_text_on_path', (canvas) => {
75        const notoSerif = CanvasKit.Typeface.MakeTypefaceFromData(notoSerifFontBuffer);
76
77        const paint = new CanvasKit.Paint();
78        paint.setAntiAlias(true);
79        paint.setStyle(CanvasKit.PaintStyle.Stroke);
80
81        const font = new CanvasKit.Font(notoSerif, 24);
82        const fontPaint = new CanvasKit.Paint();
83        fontPaint.setAntiAlias(true);
84        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
85
86        const arc = new CanvasKit.Path();
87        arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
88        arc.lineTo(210, 140);
89        arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
90
91        const str = 'This téxt should follow the curve across contours...';
92        const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font, 60.5);
93
94        canvas.drawPath(arc, paint);
95        canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
96
97        textBlob.delete();
98        arc.delete();
99        paint.delete();
100        notoSerif.delete();
101        font.delete();
102        fontPaint.delete();
103    });
104
105    // https://bugs.chromium.org/p/skia/issues/detail?id=9314
106    gm('nullterminators_skbug_9314', (canvas) => {
107        const bungee = CanvasKit.Typeface.MakeTypefaceFromData(bungeeFontBuffer);
108
109        // yellow, to make sure tofu is plainly visible
110        canvas.clear(CanvasKit.Color(255, 255, 0, 1));
111
112        const font = new CanvasKit.Font(bungee, 24);
113        const fontPaint = new CanvasKit.Paint();
114        fontPaint.setAntiAlias(true);
115        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
116
117
118        const str = 'This is téxt';
119        const textBlob = CanvasKit.TextBlob.MakeFromText(str + ' text blob', font);
120
121        canvas.drawTextBlob(textBlob, 10, 50, fontPaint);
122
123        canvas.drawText(str + ' normal', 10, 100, fontPaint, font);
124
125        canvas.drawText('null terminator ->\u0000<- on purpose', 10, 150, fontPaint, font);
126
127        textBlob.delete();
128        bungee.delete();
129        font.delete();
130        fontPaint.delete();
131    });
132
133    gm('textblobs_with_glyphs', (canvas) => {
134        const notoSerif = CanvasKit.Typeface.MakeTypefaceFromData(notoSerifFontBuffer);
135
136        const font = new CanvasKit.Font(notoSerif, 24);
137        const bluePaint = new CanvasKit.Paint();
138        bluePaint.setColor(CanvasKit.parseColorString('#04083f')); // arbitrary deep blue
139        bluePaint.setAntiAlias(true);
140        bluePaint.setStyle(CanvasKit.PaintStyle.Fill);
141
142        const redPaint = new CanvasKit.Paint();
143        redPaint.setColor(CanvasKit.parseColorString('#770b1e')); // arbitrary deep red
144
145        const ids = notoSerif.getGlyphIDs('AEGIS ægis');
146        expect(ids.length).toEqual(10); // one glyph id per glyph
147        expect(ids[0]).toEqual(36); // spot check this, should be consistent as long as the font is.
148
149        const bounds = font.getGlyphBounds(ids, bluePaint);
150        expect(bounds.length).toEqual(40); // 4 measurements per glyph
151        expect(bounds[0]).toEqual(0); // again, spot check the measurements for the first glyph.
152        expect(bounds[1]).toEqual(-17);
153        expect(bounds[2]).toEqual(17);
154        expect(bounds[3]).toEqual(0);
155
156        const widths = font.getGlyphWidths(ids, bluePaint);
157        expect(widths.length).toEqual(10); // 1 width per glyph
158        expect(widths[0]).toEqual(17);
159
160        const topBlob = CanvasKit.TextBlob.MakeFromGlyphs(ids, font);
161        canvas.drawTextBlob(topBlob, 5, 30, bluePaint);
162        canvas.drawTextBlob(topBlob, 5, 60, redPaint);
163        topBlob.delete();
164
165        const mIDs = CanvasKit.MallocGlyphIDs(ids.length);
166        const mArr = mIDs.toTypedArray();
167        mArr.set(ids);
168
169        const mXforms = CanvasKit.Malloc(Float32Array, ids.length * 4);
170        const mXformsArr = mXforms.toTypedArray();
171        // Draw each glyph rotated slightly and slightly lower than the glyph before it.
172        let currX = 0;
173        for (let i = 0; i < ids.length; i++) {
174            mXformsArr[i * 4] = Math.cos(-Math.PI / 16); // scos
175            mXformsArr[i * 4 + 1] = Math.sin(-Math.PI / 16); // ssin
176            mXformsArr[i * 4 + 2] = currX; // tx
177            mXformsArr[i * 4 + 3] = i*2; // ty
178            currX += widths[i];
179        }
180
181        const bottomBlob = CanvasKit.TextBlob.MakeFromRSXformGlyphs(mIDs, mXforms, font);
182        canvas.drawTextBlob(bottomBlob, 5, 110, bluePaint);
183        canvas.drawTextBlob(bottomBlob, 5, 140, redPaint);
184        bottomBlob.delete();
185
186        CanvasKit.Free(mIDs);
187        CanvasKit.Free(mXforms);
188        bluePaint.delete();
189        redPaint.delete();
190        notoSerif.delete();
191        font.delete();
192    });
193
194    it('can make a font mgr with passed in fonts', () => {
195        // CanvasKit.FontMgr.FromData([bungeeFontBuffer, notoSerifFontBuffer]) also works
196        const fontMgr = CanvasKit.FontMgr.FromData(bungeeFontBuffer, notoSerifFontBuffer);
197        expect(fontMgr).toBeTruthy();
198        expect(fontMgr.countFamilies()).toBe(2);
199        // in debug mode, let's list them.
200        if (fontMgr.dumpFamilies) {
201            fontMgr.dumpFamilies();
202        }
203
204        const font1 = fontMgr.matchFamilyStyle(fontMgr.getFamilyName(0), {});
205        expect(font1).toBeTruthy();
206
207        const font2 = fontMgr.matchFamilyStyle(fontMgr.getFamilyName(1), { width: 5, weight: 400 });
208        expect(font2).toBeTruthy();
209
210        font1.delete();
211        font2.delete();
212        fontMgr.delete();
213    });
214
215    it('can make a font provider with passed in fonts and aliases', () => {
216        const fontProvider = CanvasKit.TypefaceFontProvider.Make();
217        fontProvider.registerFont(bungeeFontBuffer, "My Bungee Alias");
218        fontProvider.registerFont(notoSerifFontBuffer, "My Noto Serif Alias");
219        expect(fontProvider).toBeTruthy();
220        expect(fontProvider.countFamilies()).toBe(2);
221        // in debug mode, let's list them.
222        if (fontProvider.dumpFamilies) {
223            fontProvider.dumpFamilies();
224        }
225        fontProvider.delete();
226    });
227
228    gm('various_font_formats', (canvas, fetchedByteBuffers) => {
229        const fontPaint = new CanvasKit.Paint();
230        fontPaint.setAntiAlias(true);
231        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
232        const inputs = [{
233            type: '.ttf font',
234            buffer: bungeeFontBuffer,
235            y: 60,
236        },{
237            type: '.otf font',
238            buffer: fetchedByteBuffers[0],
239            y: 90,
240        },{
241            type: '.woff font',
242            buffer: fetchedByteBuffers[1],
243            y: 120,
244        },{
245            type: '.woff2 font',
246            buffer: fetchedByteBuffers[2],
247            y: 150,
248        }];
249
250        const defaultFont = new CanvasKit.Font(CanvasKit.Typeface.GetDefault(), 24);
251        canvas.drawText(`The following should be ${inputs.length + 1} lines of text:`, 5, 30, fontPaint, defaultFont);
252
253        for (const fontType of inputs) {
254            // smoke test that the font bytes loaded.
255            expect(fontType.buffer).toBeTruthy(fontType.type + ' did not load');
256
257            const typeface = CanvasKit.Typeface.MakeTypefaceFromData(fontType.buffer);
258            const font = new CanvasKit.Font(typeface, 24);
259
260            if (font && typeface) {
261                canvas.drawText(fontType.type + ' loaded', 5, fontType.y, fontPaint, font);
262            } else {
263                canvas.drawText(fontType.type + ' *not* loaded', 5, fontType.y, fontPaint, defaultFont);
264            }
265            font && font.delete();
266            typeface && typeface.delete();
267        }
268
269        // The only ttc font I could find was 14 MB big, so I'm using the smaller test font,
270        // which doesn't have very many glyphs in it, so we just check that we got a non-zero
271        // typeface for it. I was able to load NotoSansCJK-Regular.ttc just fine in a
272        // manual test.
273        const typeface = CanvasKit.Typeface.MakeTypefaceFromData(fetchedByteBuffers[3]);
274        expect(typeface).toBeTruthy('.ttc font');
275        if (typeface) {
276            canvas.drawText('.ttc loaded', 5, 180, fontPaint, defaultFont);
277            typeface.delete();
278        } else {
279            canvas.drawText('.ttc *not* loaded', 5, 180, fontPaint, defaultFont);
280        }
281
282        defaultFont.delete();
283        fontPaint.delete();
284    }, '/assets/Roboto-Regular.otf', '/assets/Roboto-Regular.woff', '/assets/Roboto-Regular.woff2', '/assets/test.ttc');
285
286    it('can measure text very precisely with proper settings', () => {
287        const typeface = CanvasKit.Typeface.MakeTypefaceFromData(notoSerifFontBuffer);
288        const fontSizes = [257, 100, 11];
289        // The point of these values is to let us know 1) we can measure to sub-pixel levels
290        // and 2) that measurements don't drastically change. If these change a little bit,
291        // just update them with the new values. For super-accurate readings, one could
292        // run a C++ snippet of code and compare the values, but that is likely unnecessary
293        // unless we suspect a bug with the bindings.
294        const expectedSizes = [241.06299, 93.79883, 10.31787];
295        for (const idx in fontSizes) {
296            const font = new CanvasKit.Font(typeface, fontSizes[idx]);
297            font.setHinting(CanvasKit.FontHinting.None);
298            font.setLinearMetrics(true);
299            font.setSubpixel(true);
300
301            const ids = font.getGlyphIDs('M');
302            const widths = font.getGlyphWidths(ids);
303            expect(widths[0]).toBeCloseTo(expectedSizes[idx], 5);
304            font.delete();
305        }
306
307        typeface.delete();
308    });
309
310    gm('font_edging', (canvas) => {
311        // Draw a small font scaled up to see the aliasing artifacts.
312        canvas.scale(8, 8);
313        const notoSerif = CanvasKit.Typeface.MakeTypefaceFromData(notoSerifFontBuffer);
314
315        const textPaint = new CanvasKit.Paint();
316        const annotationFont = new CanvasKit.Font(notoSerif, 6);
317
318        canvas.drawText('Default', 5, 5, textPaint, annotationFont);
319        canvas.drawText('Alias', 5, 25, textPaint, annotationFont);
320        canvas.drawText('AntiAlias', 5, 45, textPaint, annotationFont);
321        canvas.drawText('Subpixel', 5, 65, textPaint, annotationFont);
322
323        const testFont = new CanvasKit.Font(notoSerif, 20);
324
325        canvas.drawText('SEA', 35, 15, textPaint, testFont);
326        testFont.setEdging(CanvasKit.FontEdging.Alias);
327        canvas.drawText('SEA', 35, 35, textPaint, testFont);
328        testFont.setEdging(CanvasKit.FontEdging.AntiAlias);
329        canvas.drawText('SEA', 35, 55, textPaint, testFont);
330        testFont.setEdging(CanvasKit.FontEdging.SubpixelAntiAlias);
331        canvas.drawText('SEA', 35, 75, textPaint, testFont);
332
333        textPaint.delete();
334        annotationFont.delete();
335        testFont.delete();
336        notoSerif.delete();
337    });
338
339    it('can get the intercepts of glyphs', () => {
340        const font = new CanvasKit.Font(CanvasKit.Typeface.GetDefault(), 100);
341        const ids = font.getGlyphIDs('I');
342        expect(ids.length).toEqual(1);
343
344        // aim for the middle of the I at 100 point, expecting a hit
345        let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
346        expect(sects.length).toEqual(2, "expected one pair of intercepts");
347        expect(sects[0]).toBeCloseTo(25.39063, 5);
348        expect(sects[1]).toBeCloseTo(34.52148, 5);
349
350        // aim below the baseline where we expect no intercepts
351        sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
352        expect(sects.length).toEqual(0, "expected no intercepts");
353        font.delete();
354    });
355
356    it('can use mallocd and normal arrays', () => {
357        const font = new CanvasKit.Font(CanvasKit.Typeface.GetDefault(), 100);
358        const ids = font.getGlyphIDs('I');
359        expect(ids.length).toEqual(1);
360        const glyphID = ids[0];
361
362        // aim for the middle of the I at 100 point, expecting a hit
363        const sects = font.getGlyphIntercepts(Array.of(glyphID), Float32Array.of(0, 0), -60, -40);
364        expect(sects.length).toEqual(2);
365        expect(sects[0]).toBeLessThan(sects[1]);
366        // these values were recorded from the first time it was run
367        expect(sects[0]).toBeCloseTo(25.39063, 5);
368        expect(sects[1]).toBeCloseTo(34.52148, 5);
369
370        const free_list = [];   // will free CanvasKit.Malloc objects at the end
371
372        // Want to exercise 4 different ways we can receive an array:
373        //  1. normal array
374        //  2. typed-array
375        //  3. CanvasKit.Malloc typeed-array
376        //  4. CavnasKit.Malloc (raw)
377
378        const id_makers = [
379            (id) => [ id ],
380            (id) => new Uint16Array([ id ]),
381            (id) => {
382                const a = CanvasKit.Malloc(Uint16Array, 1);
383                free_list.push(a);
384                const ta = a.toTypedArray();
385                ta[0] = id;
386                return ta;  // return typed-array
387            },
388            (id) => {
389                const a = CanvasKit.Malloc(Uint16Array, 1);
390                free_list.push(a);
391                a.toTypedArray()[0] = id;
392                return a;   // return raw obj
393            },
394        ];
395        const pos_makers = [
396            (x, y) => [ x, y ],
397            (x, y) => new Float32Array([ x, y ]),
398            (x, y) => {
399                const a = CanvasKit.Malloc(Float32Array, 2);
400                free_list.push(a);
401                const ta = a.toTypedArray();
402                ta[0] = x;
403                ta[1] = y;
404                return ta;  // return typed-array
405            },
406            (x, y) => {
407                const a = CanvasKit.Malloc(Float32Array, 2);
408                free_list.push(a);
409                const ta = a.toTypedArray();
410                ta[0] = x;
411                ta[1] = y;
412                return a;   // return raw obj
413            },
414        ];
415
416        for (const idm of id_makers) {
417            for (const posm of pos_makers) {
418                const s = font.getGlyphIntercepts(idm(glyphID), posm(0, 0), -60, -40);
419                expect(s.length).toEqual(sects.length);
420                for (let i = 0; i < s.length; ++i) {
421                    expect(s[i]).toEqual(sects[i]);
422                }
423            }
424
425        }
426
427        free_list.forEach(obj => CanvasKit.Free(obj));
428        font.delete();
429    });
430
431    gm('colrv1_gradients', (canvas) => {
432        // Inspired by gm/colrv1.cpp, specifically the kColorFontsRepoGradients one.
433        const colrFace = CanvasKit.Typeface.MakeTypefaceFromData(colrv1FontBuffer);
434
435        const textPaint = new CanvasKit.Paint();
436        const annotationFont = new CanvasKit.Font(CanvasKit.Typeface.GetDefault(), 20);
437
438        canvas.drawText('You should see 4 lines of gradient glyphs below',
439            5, 25, textPaint, annotationFont);
440
441        // These glyphs show off gradients in the COLRv1 font.
442        // See https://github.com/googlefonts/color-fonts/blob/main/glyph_descriptions.md for
443        // the list of available test glyphs and their codepoints.
444        const testCodepoints = "\u{F0200} \u{F0100} \u{F0101} \u{F0102} \u{F0103} \u{F0D00}";
445        const testFont = new CanvasKit.Font(colrFace);
446        const sizes = [12, 18, 30, 100];
447        let y = 30;
448        for (let i = 0; i < sizes.length; i++) {
449            const size = sizes[i];
450            testFont.setSize(size);
451            const metrics = testFont.getMetrics();
452            y -= metrics.ascent;
453            canvas.drawText(testCodepoints, 5, y, textPaint, testFont);
454            y += metrics.descent + metrics.leading;
455        }
456
457        textPaint.delete();
458        annotationFont.delete();
459        testFont.delete();
460        colrFace.delete();
461    });
462});
463