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