1 describe('Font Behavior', () => {
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(
9 notoSerifFontBuffer = buffer;
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(
18 bungeeFontBuffer = buffer;
21 let colrv1FontBuffer = null;
22 // This font has glyphs for COLRv1. Also used in gms/colrv1.cpp
23 const colrv1FontLoaded = fetch('/assets/more_samples-glyf_colr_1.ttf').then(
24 (response) => response.arrayBuffer()).then(
26 colrv1FontBuffer = buffer;
29 beforeEach(async () => {
30 await EverythingLoaded;
31 await notoSerifFontLoaded;
32 await bungeeFontLoaded;
33 await colrv1FontLoaded;
34 container = document.createElement('div');
35 container.innerHTML = `
36 <canvas width=600 height=600 id=test></canvas>
37 <canvas width=600 height=600 id=report></canvas>`;
38 document.body.appendChild(container);
42 document.body.removeChild(container);
45 gm('monospace_text_on_path', (canvas) => {
46 const paint = new CanvasKit.Paint();
47 paint.setAntiAlias(true);
48 paint.setStyle(CanvasKit.PaintStyle.Stroke);
50 const font = new CanvasKit.Font(null, 24);
51 const fontPaint = new CanvasKit.Paint();
52 fontPaint.setAntiAlias(true);
53 fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
56 const arc = new CanvasKit.Path();
57 arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
59 arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
61 // Only 1 dot should show up in the image, because we run out of path.
62 const str = 'This téxt should follow the curve across contours...';
63 const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font);
65 canvas.drawPath(arc, paint);
66 canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
75 gm('serif_text_on_path', (canvas) => {
76 const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
78 const paint = new CanvasKit.Paint();
79 paint.setAntiAlias(true);
80 paint.setStyle(CanvasKit.PaintStyle.Stroke);
82 const font = new CanvasKit.Font(notoSerif, 24);
83 const fontPaint = new CanvasKit.Paint();
84 fontPaint.setAntiAlias(true);
85 fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
87 const arc = new CanvasKit.Path();
88 arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
90 arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
92 const str = 'This téxt should follow the curve across contours...';
93 const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font, 60.5);
95 canvas.drawPath(arc, paint);
96 canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
106 // https://bugs.chromium.org/p/skia/issues/detail?id=9314
107 gm('nullterminators_skbug_9314', (canvas) => {
108 const bungee = CanvasKit.Typeface.MakeFreeTypeFaceFromData(bungeeFontBuffer);
110 // yellow, to make sure tofu is plainly visible
111 canvas.clear(CanvasKit.Color(255, 255, 0, 1));
113 const font = new CanvasKit.Font(bungee, 24);
114 const fontPaint = new CanvasKit.Paint();
115 fontPaint.setAntiAlias(true);
116 fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
119 const str = 'This is téxt';
120 const textBlob = CanvasKit.TextBlob.MakeFromText(str + ' text blob', font);
122 canvas.drawTextBlob(textBlob, 10, 50, fontPaint);
124 canvas.drawText(str + ' normal', 10, 100, fontPaint, font);
126 canvas.drawText('null terminator ->\u0000<- on purpose', 10, 150, fontPaint, font);
134 gm('textblobs_with_glyphs', (canvas) => {
135 canvas.clear(CanvasKit.WHITE);
136 const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
138 const font = new CanvasKit.Font(notoSerif, 24);
139 const bluePaint = new CanvasKit.Paint();
140 bluePaint.setColor(CanvasKit.parseColorString('#04083f')); // arbitrary deep blue
141 bluePaint.setAntiAlias(true);
142 bluePaint.setStyle(CanvasKit.PaintStyle.Fill);
144 const redPaint = new CanvasKit.Paint();
145 redPaint.setColor(CanvasKit.parseColorString('#770b1e')); // arbitrary deep red
147 const ids = notoSerif.getGlyphIDs('AEGIS ægis');
148 expect(ids.length).toEqual(10); // one glyph id per glyph
149 expect(ids[0]).toEqual(36); // spot check this, should be consistent as long as the font is.
151 const bounds = font.getGlyphBounds(ids, bluePaint);
152 expect(bounds.length).toEqual(40); // 4 measurements per glyph
153 expect(bounds[0]).toEqual(0); // again, spot check the measurements for the first glyph.
154 expect(bounds[1]).toEqual(-17);
155 expect(bounds[2]).toEqual(17);
156 expect(bounds[3]).toEqual(0);
158 const widths = font.getGlyphWidths(ids, bluePaint);
159 expect(widths.length).toEqual(10); // 1 width per glyph
160 expect(widths[0]).toEqual(17);
162 const topBlob = CanvasKit.TextBlob.MakeFromGlyphs(ids, font);
163 canvas.drawTextBlob(topBlob, 5, 30, bluePaint);
164 canvas.drawTextBlob(topBlob, 5, 60, redPaint);
167 const mIDs = CanvasKit.MallocGlyphIDs(ids.length);
168 const mArr = mIDs.toTypedArray();
171 const mXforms = CanvasKit.Malloc(Float32Array, ids.length * 4);
172 const mXformsArr = mXforms.toTypedArray();
173 // Draw each glyph rotated slightly and slightly lower than the glyph before it.
175 for (let i = 0; i < ids.length; i++) {
176 mXformsArr[i * 4] = Math.cos(-Math.PI / 16); // scos
177 mXformsArr[i * 4 + 1] = Math.sin(-Math.PI / 16); // ssin
178 mXformsArr[i * 4 + 2] = currX; // tx
179 mXformsArr[i * 4 + 3] = i*2; // ty
183 const bottomBlob = CanvasKit.TextBlob.MakeFromRSXformGlyphs(mIDs, mXforms, font);
184 canvas.drawTextBlob(bottomBlob, 5, 110, bluePaint);
185 canvas.drawTextBlob(bottomBlob, 5, 140, redPaint);
188 CanvasKit.Free(mIDs);
189 CanvasKit.Free(mXforms);
196 it('can make a font mgr with passed in fonts', () => {
197 // CanvasKit.FontMgr.FromData([bungeeFontBuffer, notoSerifFontBuffer]) also works
198 const fontMgr = CanvasKit.FontMgr.FromData(bungeeFontBuffer, notoSerifFontBuffer);
199 expect(fontMgr).toBeTruthy();
200 expect(fontMgr.countFamilies()).toBe(2);
201 // in debug mode, let's list them.
202 if (fontMgr.dumpFamilies) {
203 fontMgr.dumpFamilies();
208 it('can make a font provider with passed in fonts and aliases', () => {
209 const fontProvider = CanvasKit.TypefaceFontProvider.Make();
210 fontProvider.registerFont(bungeeFontBuffer, "My Bungee Alias");
211 fontProvider.registerFont(notoSerifFontBuffer, "My Noto Serif Alias");
212 expect(fontProvider).toBeTruthy();
213 expect(fontProvider.countFamilies()).toBe(2);
214 // in debug mode, let's list them.
215 if (fontProvider.dumpFamilies) {
216 fontProvider.dumpFamilies();
218 fontProvider.delete();
221 gm('various_font_formats', (canvas, fetchedByteBuffers) => {
222 const fontPaint = new CanvasKit.Paint();
223 fontPaint.setAntiAlias(true);
224 fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
227 buffer: bungeeFontBuffer,
231 buffer: fetchedByteBuffers[0],
235 buffer: fetchedByteBuffers[1],
239 buffer: fetchedByteBuffers[2],
243 const defaultFont = new CanvasKit.Font(null, 24);
244 canvas.drawText(`The following should be ${inputs.length + 1} lines of text:`, 5, 30, fontPaint, defaultFont);
246 for (const fontType of inputs) {
247 // smoke test that the font bytes loaded.
248 expect(fontType.buffer).toBeTruthy(fontType.type + ' did not load');
250 const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontType.buffer);
251 const font = new CanvasKit.Font(typeface, 24);
253 if (font && typeface) {
254 canvas.drawText(fontType.type + ' loaded', 5, fontType.y, fontPaint, font);
256 canvas.drawText(fontType.type + ' *not* loaded', 5, fontType.y, fontPaint, defaultFont);
258 font && font.delete();
259 typeface && typeface.delete();
262 // The only ttc font I could find was 14 MB big, so I'm using the smaller test font,
263 // which doesn't have very many glyphs in it, so we just check that we got a non-zero
264 // typeface for it. I was able to load NotoSansCJK-Regular.ttc just fine in a
266 const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fetchedByteBuffers[3]);
267 expect(typeface).toBeTruthy('.ttc font');
269 canvas.drawText('.ttc loaded', 5, 180, fontPaint, defaultFont);
272 canvas.drawText('.ttc *not* loaded', 5, 180, fontPaint, defaultFont);
275 defaultFont.delete();
277 }, '/assets/Roboto-Regular.otf', '/assets/Roboto-Regular.woff', '/assets/Roboto-Regular.woff2', '/assets/test.ttc');
279 it('can measure text very precisely with proper settings', () => {
280 const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(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);
294 const ids = font.getGlyphIDs('M');
295 const widths = font.getGlyphWidths(ids);
296 expect(widths[0]).toBeCloseTo(expectedSizes[idx], 5);
303 gm('font_edging', (canvas) => {
304 // Draw a small font scaled up to see the aliasing artifacts.
306 canvas.clear(CanvasKit.WHITE);
307 const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
309 const textPaint = new CanvasKit.Paint();
310 const annotationFont = new CanvasKit.Font(notoSerif, 6);
312 canvas.drawText('Default', 5, 5, textPaint, annotationFont);
313 canvas.drawText('Alias', 5, 25, textPaint, annotationFont);
314 canvas.drawText('AntiAlias', 5, 45, textPaint, annotationFont);
315 canvas.drawText('Subpixel', 5, 65, textPaint, annotationFont);
317 const testFont = new CanvasKit.Font(notoSerif, 20);
319 canvas.drawText('SEA', 35, 15, textPaint, testFont);
320 testFont.setEdging(CanvasKit.FontEdging.Alias);
321 canvas.drawText('SEA', 35, 35, textPaint, testFont);
322 testFont.setEdging(CanvasKit.FontEdging.AntiAlias);
323 canvas.drawText('SEA', 35, 55, textPaint, testFont);
324 testFont.setEdging(CanvasKit.FontEdging.SubpixelAntiAlias);
325 canvas.drawText('SEA', 35, 75, textPaint, testFont);
328 annotationFont.delete();
333 it('can get the intercepts of glyphs', () => {
334 const font = new CanvasKit.Font(null, 100);
335 const ids = font.getGlyphIDs('I');
336 expect(ids.length).toEqual(1);
338 // aim for the middle of the I at 100 point, expecting a hit
339 let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
340 expect(sects.length).toEqual(2, "expected one pair of intercepts");
341 expect(sects[0]).toBeCloseTo(25.39063, 5);
342 expect(sects[1]).toBeCloseTo(34.52148, 5);
344 // aim below the baseline where we expect no intercepts
345 sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
346 expect(sects.length).toEqual(0, "expected no intercepts");
350 it('can use mallocd and normal arrays', () => {
351 const font = new CanvasKit.Font(null, 100);
352 const ids = font.getGlyphIDs('I');
353 expect(ids.length).toEqual(1);
354 const glyphID = ids[0];
356 // aim for the middle of the I at 100 point, expecting a hit
357 const sects = font.getGlyphIntercepts(Array.of(glyphID), Float32Array.of(0, 0), -60, -40);
358 expect(sects.length).toEqual(2);
359 expect(sects[0]).toBeLessThan(sects[1]);
360 // these values were recorded from the first time it was run
361 expect(sects[0]).toBeCloseTo(25.39063, 5);
362 expect(sects[1]).toBeCloseTo(34.52148, 5);
364 const free_list = []; // will free CanvasKit.Malloc objects at the end
366 // Want to exercise 4 different ways we can receive an array:
369 // 3. CanvasKit.Malloc typeed-array
370 // 4. CavnasKit.Malloc (raw)
374 (id) => new Uint16Array([ id ]),
376 const a = CanvasKit.Malloc(Uint16Array, 1);
378 const ta = a.toTypedArray();
380 return ta; // return typed-array
383 const a = CanvasKit.Malloc(Uint16Array, 1);
385 a.toTypedArray()[0] = id;
386 return a; // return raw obj
391 (x, y) => new Float32Array([ x, y ]),
393 const a = CanvasKit.Malloc(Float32Array, 2);
395 const ta = a.toTypedArray();
398 return ta; // return typed-array
401 const a = CanvasKit.Malloc(Float32Array, 2);
403 const ta = a.toTypedArray();
406 return a; // return raw obj
410 for (const idm of id_makers) {
411 for (const posm of pos_makers) {
412 const s = font.getGlyphIntercepts(idm(glyphID), posm(0, 0), -60, -40);
413 expect(s.length).toEqual(sects.length);
414 for (let i = 0; i < s.length; ++i) {
415 expect(s[i]).toEqual(sects[i]);
421 free_list.forEach(obj => CanvasKit.Free(obj));
425 gm('colrv1_gradients', (canvas) => {
426 // Inspired by gm/colrv1.cpp, specifically the kColorFontsRepoGradients one.
427 canvas.clear(CanvasKit.WHITE);
428 const colrFace = CanvasKit.Typeface.MakeFreeTypeFaceFromData(colrv1FontBuffer);
430 const textPaint = new CanvasKit.Paint();
431 const annotationFont = new CanvasKit.Font(null, 20);
433 canvas.drawText('You should see 4 lines of gradient glyphs below',
434 5, 25, textPaint, annotationFont);
436 // These glyphIDs show off gradients in the COLRv1 font.
437 const glyphIDs = [2, 5, 6, 7, 8, 55];
438 const testFont = new CanvasKit.Font(colrFace);
439 const sizes = [12, 18, 30, 100];
441 for (let i = 0; i < sizes.length; i++) {
442 const size = sizes[i];
443 testFont.setSize(size);
444 const metrics = testFont.getMetrics();
446 const positions = calculateRun(testFont, glyphIDs)
447 canvas.drawGlyphs(glyphIDs, positions, 5, y, testFont, textPaint);
448 y += metrics.descent + metrics.leading;
452 annotationFont.delete();
457 function calculateRun(font, glyphIDs) {
458 const spacing = 5; // put 5 pixels between each glyph
459 const bounds = font.getGlyphBounds(glyphIDs);
460 const positions = [0, 0];
462 for (let i = 0; i < glyphIDs.length - 1; i++) {
463 // subtract the right bounds from the left bounds to get glyph width
464 const glyphWidth = bounds[2 + i*4] - bounds[i*4];
465 width += glyphWidth + spacing
466 positions.push(width, 0);