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