• 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 fontMgr = CanvasKit.FontMgr.RefDefault();
68        const notoSerif = fontMgr.MakeTypefaceFromData(notoSerifFontBuffer);
69
70        const paint = new CanvasKit.Paint();
71        paint.setAntiAlias(true);
72        paint.setStyle(CanvasKit.PaintStyle.Stroke);
73
74        const font = new CanvasKit.Font(notoSerif, 24);
75        const fontPaint = new CanvasKit.Paint();
76        fontPaint.setAntiAlias(true);
77        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
78
79        const arc = new CanvasKit.Path();
80        arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
81        arc.lineTo(210, 140);
82        arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
83
84        const str = 'This téxt should follow the curve across contours...';
85        const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font, 60.5);
86
87        canvas.drawPath(arc, paint);
88        canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
89
90        textBlob.delete();
91        arc.delete();
92        paint.delete();
93        notoSerif.delete();
94        font.delete();
95        fontPaint.delete();
96        fontMgr.delete();
97    });
98
99    // https://bugs.chromium.org/p/skia/issues/detail?id=9314
100    gm('nullterminators_skbug_9314', (canvas) => {
101        const fontMgr = CanvasKit.FontMgr.RefDefault();
102        const bungee = fontMgr.MakeTypefaceFromData(bungeeFontBuffer);
103
104        // yellow, to make sure tofu is plainly visible
105        canvas.clear(CanvasKit.Color(255, 255, 0, 1));
106
107        const font = new CanvasKit.Font(bungee, 24);
108        const fontPaint = new CanvasKit.Paint();
109        fontPaint.setAntiAlias(true);
110        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
111
112
113        const str = 'This is téxt';
114        const textBlob = CanvasKit.TextBlob.MakeFromText(str + ' text blob', font);
115
116        canvas.drawTextBlob(textBlob, 10, 50, fontPaint);
117
118        canvas.drawText(str + ' normal', 10, 100, fontPaint, font);
119
120        canvas.drawText('null terminator ->\u0000<- on purpose', 10, 150, fontPaint, font);
121
122        textBlob.delete();
123        bungee.delete();
124        font.delete();
125        fontPaint.delete();
126        fontMgr.delete();
127    });
128
129    gm('textblobs_with_glyphs', (canvas) => {
130        canvas.clear(CanvasKit.WHITE);
131        const fontMgr = CanvasKit.FontMgr.RefDefault();
132        const notoSerif = fontMgr.MakeTypefaceFromData(notoSerifFontBuffer);
133
134        const font = new CanvasKit.Font(notoSerif, 24);
135        const bluePaint = new CanvasKit.Paint();
136        bluePaint.setColor(CanvasKit.parseColorString('#04083f')); // arbitrary deep blue
137        bluePaint.setAntiAlias(true);
138        bluePaint.setStyle(CanvasKit.PaintStyle.Fill);
139
140        const redPaint = new CanvasKit.Paint();
141        redPaint.setColor(CanvasKit.parseColorString('#770b1e')); // arbitrary deep red
142
143        const ids = font.getGlyphIDs('AEGIS ægis');
144        expect(ids.length).toEqual(10); // one glyph id per glyph
145        expect(ids[0]).toEqual(36); // spot check this, should be consistent as long as the font is.
146
147        const bounds = font.getGlyphBounds(ids, bluePaint);
148        expect(bounds.length).toEqual(40); // 4 measurements per glyph
149        expect(bounds[0]).toEqual(0); // again, spot check the measurements for the first glyph.
150        expect(bounds[1]).toEqual(-17);
151        expect(bounds[2]).toEqual(17);
152        expect(bounds[3]).toEqual(0);
153
154        const widths = font.getGlyphWidths(ids, bluePaint);
155        expect(widths.length).toEqual(10); // 1 width per glyph
156        expect(widths[0]).toEqual(17);
157
158        const topBlob = CanvasKit.TextBlob.MakeFromGlyphs(ids, font);
159        canvas.drawTextBlob(topBlob, 5, 30, bluePaint);
160        canvas.drawTextBlob(topBlob, 5, 60, redPaint);
161        topBlob.delete();
162
163        const mIDs = CanvasKit.MallocGlyphIDs(ids.length);
164        const mArr = mIDs.toTypedArray();
165        mArr.set(ids);
166
167        const mXforms = CanvasKit.Malloc(Float32Array, ids.length * 4);
168        const mXformsArr = mXforms.toTypedArray();
169        // Draw each glyph rotated slightly and slightly lower than the glyph before it.
170        let currX = 0;
171        for (let i = 0; i < ids.length; i++) {
172            mXformsArr[i * 4] = Math.cos(-Math.PI / 16); // scos
173            mXformsArr[i * 4 + 1] = Math.sin(-Math.PI / 16); // ssin
174            mXformsArr[i * 4 + 2] = currX; // tx
175            mXformsArr[i * 4 + 3] = i*2; // ty
176            currX += widths[i];
177        }
178
179        const bottomBlob = CanvasKit.TextBlob.MakeFromRSXformGlyphs(mIDs, mXforms, font);
180        canvas.drawTextBlob(bottomBlob, 5, 110, bluePaint);
181        canvas.drawTextBlob(bottomBlob, 5, 140, redPaint);
182        bottomBlob.delete();
183
184        CanvasKit.Free(mIDs);
185        CanvasKit.Free(mXforms);
186        bluePaint.delete();
187        redPaint.delete();
188        notoSerif.delete();
189        font.delete();
190        fontMgr.delete();
191    });
192
193    it('can make a font mgr with passed in fonts', () => {
194        // CanvasKit.FontMgr.FromData([bungeeFontBuffer, notoSerifFontBuffer]) also works
195        const fontMgr = CanvasKit.FontMgr.FromData(bungeeFontBuffer, notoSerifFontBuffer);
196        expect(fontMgr).toBeTruthy();
197        expect(fontMgr.countFamilies()).toBe(2);
198        // in debug mode, let's list them.
199        if (fontMgr.dumpFamilies) {
200            fontMgr.dumpFamilies();
201        }
202        fontMgr.delete();
203    });
204
205    it('can make a font provider with passed in fonts and aliases', () => {
206        const fontProvider = CanvasKit.TypefaceFontProvider.Make();
207        fontProvider.registerFont(bungeeFontBuffer, "My Bungee Alias");
208        fontProvider.registerFont(notoSerifFontBuffer, "My Noto Serif Alias");
209        expect(fontProvider).toBeTruthy();
210        expect(fontProvider.countFamilies()).toBe(2);
211        // in debug mode, let's list them.
212        if (fontProvider.dumpFamilies) {
213            fontProvider.dumpFamilies();
214        }
215        fontProvider.delete();
216    });
217
218    gm('various_font_formats', (canvas, fetchedByteBuffers) => {
219        const fontMgr = CanvasKit.FontMgr.RefDefault();
220        const fontPaint = new CanvasKit.Paint();
221        fontPaint.setAntiAlias(true);
222        fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
223        const inputs = [{
224            type: '.ttf font',
225            buffer: bungeeFontBuffer,
226            y: 60,
227        },{
228            type: '.otf font',
229            buffer: fetchedByteBuffers[0],
230            y: 90,
231        },{
232            type: '.woff font',
233            buffer: fetchedByteBuffers[1],
234            y: 120,
235        },{
236            type: '.woff2 font',
237            buffer: fetchedByteBuffers[2],
238            y: 150,
239        }];
240
241        const defaultFont = new CanvasKit.Font(null, 24);
242        canvas.drawText(`The following should be ${inputs.length + 1} lines of text:`, 5, 30, fontPaint, defaultFont);
243
244        for (const fontType of inputs) {
245            // smoke test that the font bytes loaded.
246            expect(fontType.buffer).toBeTruthy(fontType.type + ' did not load');
247
248            const typeface = fontMgr.MakeTypefaceFromData(fontType.buffer);
249            const font = new CanvasKit.Font(typeface, 24);
250
251            if (font && typeface) {
252                canvas.drawText(fontType.type + ' loaded', 5, fontType.y, fontPaint, font);
253            } else {
254                canvas.drawText(fontType.type + ' *not* loaded', 5, fontType.y, fontPaint, defaultFont);
255            }
256            font && font.delete();
257            typeface && typeface.delete();
258        }
259
260        // The only ttc font I could find was 14 MB big, so I'm using the smaller test font,
261        // which doesn't have very many glyphs in it, so we just check that we got a non-zero
262        // typeface for it. I was able to load NotoSansCJK-Regular.ttc just fine in a
263        // manual test.
264        const typeface = fontMgr.MakeTypefaceFromData(fetchedByteBuffers[3]);
265        expect(typeface).toBeTruthy('.ttc font');
266        if (typeface) {
267            canvas.drawText('.ttc loaded', 5, 180, fontPaint, defaultFont);
268            typeface.delete();
269        } else {
270            canvas.drawText('.ttc *not* loaded', 5, 180, fontPaint, defaultFont);
271        }
272
273        defaultFont.delete();
274        fontPaint.delete();
275        fontMgr.delete();
276    }, '/assets/Roboto-Regular.otf', '/assets/Roboto-Regular.woff', '/assets/Roboto-Regular.woff2', '/assets/test.ttc');
277
278    it('can measure text very precisely with proper settings', () => {
279        const fontMgr = CanvasKit.FontMgr.RefDefault();
280        const typeface = fontMgr.MakeTypefaceFromData(notoSerifFontBuffer);
281        const fontSizes = [257, 100, 11];
282        // The point of these values is to let us know 1) we can measure to sub-pixel levels
283        // and 2) that measurements don't drastically change. If these change a little bit,
284        // just update them with the new values. For super-accurate readings, one could
285        // run a C++ snippet of code and compare the values, but that is likely unnecessary
286        // unless we suspect a bug with the bindings.
287        const expectedSizes = [241.06299, 93.79883, 10.31787];
288        for (const idx in fontSizes) {
289            const font = new CanvasKit.Font(typeface, fontSizes[idx]);
290            font.setHinting(CanvasKit.FontHinting.None);
291            font.setLinearMetrics(true);
292            font.setSubpixel(true);
293
294            const ids = font.getGlyphIDs('M');
295            const widths = font.getGlyphWidths(ids);
296            expect(widths[0]).toBeCloseTo(expectedSizes[idx], 5);
297            font.delete();
298        }
299
300        typeface.delete();
301        fontMgr.delete();
302    });
303
304    gm('font_edging', (canvas) => {
305        // Draw a small font scaled up to see the aliasing artifacts.
306        canvas.scale(8, 8);
307        canvas.clear(CanvasKit.WHITE);
308        const fontMgr = CanvasKit.FontMgr.RefDefault();
309        const notoSerif = fontMgr.MakeTypefaceFromData(notoSerifFontBuffer);
310
311        const textPaint = new CanvasKit.Paint();
312        const annotationFont = new CanvasKit.Font(notoSerif, 6);
313
314        canvas.drawText('Default', 5, 5, textPaint, annotationFont);
315        canvas.drawText('Alias', 5, 25, textPaint, annotationFont);
316        canvas.drawText('AntiAlias', 5, 45, textPaint, annotationFont);
317        canvas.drawText('Subpixel', 5, 65, textPaint, annotationFont);
318
319        const testFont = new CanvasKit.Font(notoSerif, 20);
320
321        canvas.drawText('SEA', 35, 15, textPaint, testFont);
322        testFont.setEdging(CanvasKit.FontEdging.Alias);
323        canvas.drawText('SEA', 35, 35, textPaint, testFont);
324        testFont.setEdging(CanvasKit.FontEdging.AntiAlias);
325        canvas.drawText('SEA', 35, 55, textPaint, testFont);
326        testFont.setEdging(CanvasKit.FontEdging.SubpixelAntiAlias);
327        canvas.drawText('SEA', 35, 75, textPaint, testFont);
328
329        textPaint.delete();
330        annotationFont.delete();
331        testFont.delete();
332        notoSerif.delete();
333        fontMgr.delete();
334    });
335
336});
337