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