describe('Font Behavior', () => {
let container;
let notoSerifFontBuffer = null;
// This font is known to support kerning
const notoSerifFontLoaded = fetch('/assets/NotoSerif-Regular.ttf').then(
(response) => response.arrayBuffer()).then(
(buffer) => {
notoSerifFontBuffer = buffer;
});
let bungeeFontBuffer = null;
// This font has tofu for incorrect null terminators
// see https://bugs.chromium.org/p/skia/issues/detail?id=9314
const bungeeFontLoaded = fetch('/assets/Bungee-Regular.ttf').then(
(response) => response.arrayBuffer()).then(
(buffer) => {
bungeeFontBuffer = buffer;
});
beforeEach(async () => {
await LoadCanvasKit;
await notoSerifFontLoaded;
await bungeeFontLoaded;
container = document.createElement('div');
container.innerHTML = `
`;
document.body.appendChild(container);
});
afterEach(() => {
document.body.removeChild(container);
});
gm('monospace_text_on_path', (canvas) => {
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const font = new CanvasKit.Font(null, 24);
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const arc = new CanvasKit.Path();
arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
arc.lineTo(210, 140);
arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
// Only 1 dot should show up in the image, because we run out of path.
const str = 'This téxt should follow the curve across contours...';
const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font);
canvas.drawPath(arc, paint);
canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
textBlob.delete();
arc.delete();
paint.delete();
font.delete();
fontPaint.delete();
});
gm('serif_text_on_path', (canvas) => {
const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const paint = new CanvasKit.Paint();
paint.setAntiAlias(true);
paint.setStyle(CanvasKit.PaintStyle.Stroke);
const font = new CanvasKit.Font(notoSerif, 24);
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const arc = new CanvasKit.Path();
arc.arcToOval(CanvasKit.LTRBRect(20, 40, 280, 300), -160, 140, true);
arc.lineTo(210, 140);
arc.arcToOval(CanvasKit.LTRBRect(20, 0, 280, 260), 160, -140, true);
const str = 'This téxt should follow the curve across contours...';
const textBlob = CanvasKit.TextBlob.MakeOnPath(str, arc, font, 60.5);
canvas.drawPath(arc, paint);
canvas.drawTextBlob(textBlob, 0, 0, fontPaint);
textBlob.delete();
arc.delete();
paint.delete();
notoSerif.delete();
font.delete();
fontPaint.delete();
});
// https://bugs.chromium.org/p/skia/issues/detail?id=9314
gm('nullterminators_skbug_9314', (canvas) => {
const bungee = CanvasKit.Typeface.MakeFreeTypeFaceFromData(bungeeFontBuffer);
// yellow, to make sure tofu is plainly visible
canvas.clear(CanvasKit.Color(255, 255, 0, 1));
const font = new CanvasKit.Font(bungee, 24);
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const str = 'This is téxt';
const textBlob = CanvasKit.TextBlob.MakeFromText(str + ' text blob', font);
canvas.drawTextBlob(textBlob, 10, 50, fontPaint);
canvas.drawText(str + ' normal', 10, 100, fontPaint, font);
canvas.drawText('null terminator ->\u0000<- on purpose', 10, 150, fontPaint, font);
textBlob.delete();
bungee.delete();
font.delete();
fontPaint.delete();
});
gm('textblobs_with_glyphs', (canvas) => {
canvas.clear(CanvasKit.WHITE);
const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const font = new CanvasKit.Font(notoSerif, 24);
const bluePaint = new CanvasKit.Paint();
bluePaint.setColor(CanvasKit.parseColorString('#04083f')); // arbitrary deep blue
bluePaint.setAntiAlias(true);
bluePaint.setStyle(CanvasKit.PaintStyle.Fill);
const redPaint = new CanvasKit.Paint();
redPaint.setColor(CanvasKit.parseColorString('#770b1e')); // arbitrary deep red
const ids = notoSerif.getGlyphIDs('AEGIS ægis');
expect(ids.length).toEqual(10); // one glyph id per glyph
expect(ids[0]).toEqual(36); // spot check this, should be consistent as long as the font is.
const bounds = font.getGlyphBounds(ids, bluePaint);
expect(bounds.length).toEqual(40); // 4 measurements per glyph
expect(bounds[0]).toEqual(0); // again, spot check the measurements for the first glyph.
expect(bounds[1]).toEqual(-17);
expect(bounds[2]).toEqual(17);
expect(bounds[3]).toEqual(0);
const widths = font.getGlyphWidths(ids, bluePaint);
expect(widths.length).toEqual(10); // 1 width per glyph
expect(widths[0]).toEqual(17);
const topBlob = CanvasKit.TextBlob.MakeFromGlyphs(ids, font);
canvas.drawTextBlob(topBlob, 5, 30, bluePaint);
canvas.drawTextBlob(topBlob, 5, 60, redPaint);
topBlob.delete();
const mIDs = CanvasKit.MallocGlyphIDs(ids.length);
const mArr = mIDs.toTypedArray();
mArr.set(ids);
const mXforms = CanvasKit.Malloc(Float32Array, ids.length * 4);
const mXformsArr = mXforms.toTypedArray();
// Draw each glyph rotated slightly and slightly lower than the glyph before it.
let currX = 0;
for (let i = 0; i < ids.length; i++) {
mXformsArr[i * 4] = Math.cos(-Math.PI / 16); // scos
mXformsArr[i * 4 + 1] = Math.sin(-Math.PI / 16); // ssin
mXformsArr[i * 4 + 2] = currX; // tx
mXformsArr[i * 4 + 3] = i*2; // ty
currX += widths[i];
}
const bottomBlob = CanvasKit.TextBlob.MakeFromRSXformGlyphs(mIDs, mXforms, font);
canvas.drawTextBlob(bottomBlob, 5, 110, bluePaint);
canvas.drawTextBlob(bottomBlob, 5, 140, redPaint);
bottomBlob.delete();
CanvasKit.Free(mIDs);
CanvasKit.Free(mXforms);
bluePaint.delete();
redPaint.delete();
notoSerif.delete();
font.delete();
});
it('can make a font mgr with passed in fonts', () => {
// CanvasKit.FontMgr.FromData([bungeeFontBuffer, notoSerifFontBuffer]) also works
const fontMgr = CanvasKit.FontMgr.FromData(bungeeFontBuffer, notoSerifFontBuffer);
expect(fontMgr).toBeTruthy();
expect(fontMgr.countFamilies()).toBe(2);
// in debug mode, let's list them.
if (fontMgr.dumpFamilies) {
fontMgr.dumpFamilies();
}
fontMgr.delete();
});
it('can make a font provider with passed in fonts and aliases', () => {
const fontProvider = CanvasKit.TypefaceFontProvider.Make();
fontProvider.registerFont(bungeeFontBuffer, "My Bungee Alias");
fontProvider.registerFont(notoSerifFontBuffer, "My Noto Serif Alias");
expect(fontProvider).toBeTruthy();
expect(fontProvider.countFamilies()).toBe(2);
// in debug mode, let's list them.
if (fontProvider.dumpFamilies) {
fontProvider.dumpFamilies();
}
fontProvider.delete();
});
gm('various_font_formats', (canvas, fetchedByteBuffers) => {
const fontPaint = new CanvasKit.Paint();
fontPaint.setAntiAlias(true);
fontPaint.setStyle(CanvasKit.PaintStyle.Fill);
const inputs = [{
type: '.ttf font',
buffer: bungeeFontBuffer,
y: 60,
},{
type: '.otf font',
buffer: fetchedByteBuffers[0],
y: 90,
},{
type: '.woff font',
buffer: fetchedByteBuffers[1],
y: 120,
},{
type: '.woff2 font',
buffer: fetchedByteBuffers[2],
y: 150,
}];
const defaultFont = new CanvasKit.Font(null, 24);
canvas.drawText(`The following should be ${inputs.length + 1} lines of text:`, 5, 30, fontPaint, defaultFont);
for (const fontType of inputs) {
// smoke test that the font bytes loaded.
expect(fontType.buffer).toBeTruthy(fontType.type + ' did not load');
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fontType.buffer);
const font = new CanvasKit.Font(typeface, 24);
if (font && typeface) {
canvas.drawText(fontType.type + ' loaded', 5, fontType.y, fontPaint, font);
} else {
canvas.drawText(fontType.type + ' *not* loaded', 5, fontType.y, fontPaint, defaultFont);
}
font && font.delete();
typeface && typeface.delete();
}
// The only ttc font I could find was 14 MB big, so I'm using the smaller test font,
// which doesn't have very many glyphs in it, so we just check that we got a non-zero
// typeface for it. I was able to load NotoSansCJK-Regular.ttc just fine in a
// manual test.
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(fetchedByteBuffers[3]);
expect(typeface).toBeTruthy('.ttc font');
if (typeface) {
canvas.drawText('.ttc loaded', 5, 180, fontPaint, defaultFont);
typeface.delete();
} else {
canvas.drawText('.ttc *not* loaded', 5, 180, fontPaint, defaultFont);
}
defaultFont.delete();
fontPaint.delete();
}, '/assets/Roboto-Regular.otf', '/assets/Roboto-Regular.woff', '/assets/Roboto-Regular.woff2', '/assets/test.ttc');
it('can measure text very precisely with proper settings', () => {
const typeface = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const fontSizes = [257, 100, 11];
// The point of these values is to let us know 1) we can measure to sub-pixel levels
// and 2) that measurements don't drastically change. If these change a little bit,
// just update them with the new values. For super-accurate readings, one could
// run a C++ snippet of code and compare the values, but that is likely unnecessary
// unless we suspect a bug with the bindings.
const expectedSizes = [241.06299, 93.79883, 10.31787];
for (const idx in fontSizes) {
const font = new CanvasKit.Font(typeface, fontSizes[idx]);
font.setHinting(CanvasKit.FontHinting.None);
font.setLinearMetrics(true);
font.setSubpixel(true);
const ids = font.getGlyphIDs('M');
const widths = font.getGlyphWidths(ids);
expect(widths[0]).toBeCloseTo(expectedSizes[idx], 5);
font.delete();
}
typeface.delete();
});
gm('font_edging', (canvas) => {
// Draw a small font scaled up to see the aliasing artifacts.
canvas.scale(8, 8);
canvas.clear(CanvasKit.WHITE);
const notoSerif = CanvasKit.Typeface.MakeFreeTypeFaceFromData(notoSerifFontBuffer);
const textPaint = new CanvasKit.Paint();
const annotationFont = new CanvasKit.Font(notoSerif, 6);
canvas.drawText('Default', 5, 5, textPaint, annotationFont);
canvas.drawText('Alias', 5, 25, textPaint, annotationFont);
canvas.drawText('AntiAlias', 5, 45, textPaint, annotationFont);
canvas.drawText('Subpixel', 5, 65, textPaint, annotationFont);
const testFont = new CanvasKit.Font(notoSerif, 20);
canvas.drawText('SEA', 35, 15, textPaint, testFont);
testFont.setEdging(CanvasKit.FontEdging.Alias);
canvas.drawText('SEA', 35, 35, textPaint, testFont);
testFont.setEdging(CanvasKit.FontEdging.AntiAlias);
canvas.drawText('SEA', 35, 55, textPaint, testFont);
testFont.setEdging(CanvasKit.FontEdging.SubpixelAntiAlias);
canvas.drawText('SEA', 35, 75, textPaint, testFont);
textPaint.delete();
annotationFont.delete();
testFont.delete();
notoSerif.delete();
});
it('can get the intercepts of glyphs', () => {
const font = new CanvasKit.Font(null, 100);
const ids = font.getGlyphIDs('I');
expect(ids.length).toEqual(1);
// aim for the middle of the I at 100 point, expecting a hit
let sects = font.getGlyphIntercepts(ids, [0, 0], -60, -40);
expect(sects.length).toEqual(2, "expected one pair of intercepts");
expect(sects[0]).toBeCloseTo(25.39063, 5);
expect(sects[1]).toBeCloseTo(34.52148, 5);
// aim below the baseline where we expect no intercepts
sects = font.getGlyphIntercepts(ids, [0, 0], 20, 30);
expect(sects.length).toEqual(0, "expected no intercepts");
font.delete();
});
it('can use mallocd and normal arrays', () => {
const font = new CanvasKit.Font(null, 100);
const ids = font.getGlyphIDs('I');
expect(ids.length).toEqual(1);
const glyphID = ids[0];
// aim for the middle of the I at 100 point, expecting a hit
const sects = font.getGlyphIntercepts(Array.of(glyphID), Float32Array.of(0, 0), -60, -40);
expect(sects.length).toEqual(2);
expect(sects[0]).toBeLessThan(sects[1]);
// these values were recorded from the first time it was run
expect(sects[0]).toBeCloseTo(25.39063, 5);
expect(sects[1]).toBeCloseTo(34.52148, 5);
const free_list = []; // will free CanvasKit.Malloc objects at the end
// Want to exercise 4 different ways we can receive an array:
// 1. normal array
// 2. typed-array
// 3. CanvasKit.Malloc typeed-array
// 4. CavnasKit.Malloc (raw)
const id_makers = [
(id) => [ id ],
(id) => new Uint16Array([ id ]),
(id) => {
const a = CanvasKit.Malloc(Uint16Array, 1);
free_list.push(a);
const ta = a.toTypedArray();
ta[0] = id;
return ta; // return typed-array
},
(id) => {
const a = CanvasKit.Malloc(Uint16Array, 1);
free_list.push(a);
a.toTypedArray()[0] = id;
return a; // return raw obj
},
];
const pos_makers = [
(x, y) => [ x, y ],
(x, y) => new Float32Array([ x, y ]),
(x, y) => {
const a = CanvasKit.Malloc(Float32Array, 2);
free_list.push(a);
const ta = a.toTypedArray();
ta[0] = x;
ta[1] = y;
return ta; // return typed-array
},
(x, y) => {
const a = CanvasKit.Malloc(Float32Array, 2);
free_list.push(a);
const ta = a.toTypedArray();
ta[0] = x;
ta[1] = y;
return a; // return raw obj
},
];
for (const idm of id_makers) {
for (const posm of pos_makers) {
const s = font.getGlyphIntercepts(idm(glyphID), posm(0, 0), -60, -40);
expect(s.length).toEqual(sects.length);
for (let i = 0; i < s.length; ++i) {
expect(s[i]).toEqual(sects[i]);
}
}
}
free_list.forEach(obj => CanvasKit.Free(obj));
font.delete();
});
});