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