Skip to content

Commit

Permalink
Improve perfs of the font renderer
Browse files Browse the repository at this point in the history
Some SVG paths are generated from the font and used in the main thread
to render the glyphs.
  • Loading branch information
calixteman committed Dec 8, 2024
1 parent 23c42f8 commit 2b05924
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 149 deletions.
86 changes: 49 additions & 37 deletions src/core/font_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,13 @@
import {
bytesToString,
FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError,
unreachable,
Util,
warn,
} from "../shared/util.js";
import { CFFParser } from "./cff_parser.js";
import { getGlyphsUnicode } from "./glyphlist.js";
import { isNumberArray } from "./core_utils.js";
import { StandardEncoding } from "./encodings.js";
import { Stream } from "./stream.js";

Expand Down Expand Up @@ -182,13 +181,13 @@ function lookupCmap(ranges, unicode) {

function compileGlyf(code, cmds, font) {
function moveTo(x, y) {
cmds.add(FontRenderOps.MOVE_TO, [x, y]);
cmds.add("M", [x, y]);
}
function lineTo(x, y) {
cmds.add(FontRenderOps.LINE_TO, [x, y]);
cmds.add("L", [x, y]);
}
function quadraticCurveTo(xa, ya, x, y) {
cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]);
cmds.add("Q", [xa, ya, x, y]);
}

let i = 0;
Expand Down Expand Up @@ -249,22 +248,15 @@ function compileGlyf(code, cmds, font) {
if (subglyph) {
// TODO: the transform should be applied only if there is a scale:
// https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1205
cmds.add(FontRenderOps.SAVE);
cmds.add(FontRenderOps.TRANSFORM, [
scaleX,
scale01,
scale10,
scaleY,
x,
y,
]);
cmds.save();
cmds.transform([scaleX, scale01, scale10, scaleY, x, y]);

if (!(flags & 0x02)) {
// TODO: we must use arg1 and arg2 to make something similar to:
// https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1209
}
compileGlyf(subglyph, cmds, font);
cmds.add(FontRenderOps.RESTORE);
cmds.restore();
}
} while (flags & 0x20);
} else {
Expand Down Expand Up @@ -369,13 +361,13 @@ function compileGlyf(code, cmds, font) {

function compileCharString(charStringCode, cmds, font, glyphId) {
function moveTo(x, y) {
cmds.add(FontRenderOps.MOVE_TO, [x, y]);
cmds.add("M", [x, y]);
}
function lineTo(x, y) {
cmds.add(FontRenderOps.LINE_TO, [x, y]);
cmds.add("L", [x, y]);
}
function bezierCurveTo(x1, y1, x2, y2, x, y) {
cmds.add(FontRenderOps.BEZIER_CURVE_TO, [x1, y1, x2, y2, x, y]);
cmds.add("C", [x1, y1, x2, y2, x, y]);
}

const stack = [];
Expand Down Expand Up @@ -548,8 +540,8 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
const bchar = stack.pop();
y = stack.pop();
x = stack.pop();
cmds.add(FontRenderOps.SAVE);
cmds.add(FontRenderOps.TRANSLATE, [x, y]);
cmds.save();
cmds.translate(x, y);
let cmap = lookupCmap(
font.cmap,
String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]])
Expand All @@ -560,7 +552,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
font,
cmap.glyphId
);
cmds.add(FontRenderOps.RESTORE);
cmds.restore();

cmap = lookupCmap(
font.cmap,
Expand Down Expand Up @@ -744,27 +736,49 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
parse(charStringCode);
}

const NOOP = [];
const NOOP = "";

class Commands {
cmds = [];

transformStack = [];

currentTransform = [1, 0, 0, 1, 0, 0];

add(cmd, args) {
if (args) {
if (!isNumberArray(args, null)) {
warn(
`Commands.add - "${cmd}" has at least one non-number arg: "${args}".`
);
// "Fix" the wrong args by replacing them with 0.
const newArgs = args.map(arg => (typeof arg === "number" ? arg : 0));
this.cmds.push(cmd, ...newArgs);
} else {
this.cmds.push(cmd, ...args);
const [a, b, c, d, e, f] = this.currentTransform;
for (let i = 0, ii = args.length; i < ii; i += 2) {
const x = args[i];
const y = args[i + 1];
args[i] = a * x + c * y + e;
args[i + 1] = b * x + d * y + f;
}
this.cmds.push(`${cmd}${args.join(" ")}`);
} else {
this.cmds.push(cmd);
}
}

transform(transf) {
this.currentTransform = Util.transform(this.currentTransform, transf);
}

translate(x, y) {
this.transform([1, 0, 0, 1, x, y]);
}

save() {
this.transformStack.push(this.currentTransform.slice());
}

restore() {
this.currentTransform = this.transformStack.pop() || [1, 0, 0, 1, 0, 0];
}

getSVG() {
return this.cmds.join("");
}
}

class CompiledFont {
Expand All @@ -785,7 +799,7 @@ class CompiledFont {
const { charCode, glyphId } = lookupCmap(this.cmap, unicode);
let fn = this.compiledGlyphs[glyphId],
compileEx;
if (!fn) {
if (fn === undefined) {
try {
fn = this.compileGlyph(this.glyphs[glyphId], glyphId);
} catch (ex) {
Expand Down Expand Up @@ -822,13 +836,11 @@ class CompiledFont {
}

const cmds = new Commands();
cmds.add(FontRenderOps.SAVE);
cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice());
cmds.add(FontRenderOps.SCALE);
cmds.transform(fontMatrix.slice());
this.compileGlyphImpl(code, cmds, glyphId);
cmds.add(FontRenderOps.RESTORE);
cmds.add("Z");

return cmds.cmds;
return cmds.getSVG();
}

compileGlyphImpl() {
Expand Down
53 changes: 38 additions & 15 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -1885,15 +1885,19 @@ class CanvasGraphics {
return;
}

ctx.save();
ctx.beginPath();
for (const path of paths) {
ctx.setTransform(...path.transform);
ctx.translate(path.x, path.y);
path.addToPath(ctx, path.fontSize);
const newPath = new Path2D();
const invTransf = ctx.getTransform().invertSelf();
for (const { transform, x, y, fontSize, path } of paths) {
newPath.addPath(
path,
new DOMMatrix(transform)
.preMultiplySelf(invTransf)
.translate(x, y)
.scale(fontSize, -fontSize)
);
}
ctx.restore();
ctx.clip();

ctx.clip(newPath);
ctx.beginPath();
delete this.pendingTextPaths;
}
Expand Down Expand Up @@ -2002,6 +2006,15 @@ class CanvasGraphics {
this.moveText(0, this.current.leading);
}

#getScaledPath(path, currentTransform, transform) {
const newPath = new Path2D();
newPath.addPath(
path,
new DOMMatrix(transform).invertSelf().multiplySelf(currentTransform)
);
return newPath;
}

paintChar(character, x, y, patternFillTransform, patternStrokeTransform) {
const ctx = this.ctx;
const current = this.current;
Expand All @@ -2016,38 +2029,48 @@ class CanvasGraphics {
const patternFill = current.patternFill && !font.missingFile;
const patternStroke = current.patternStroke && !font.missingFile;

let addToPath;
let path;
if (
font.disableFontFace ||
isAddToPathSet ||
patternFill ||
patternStroke
) {
addToPath = font.getPathGenerator(this.commonObjs, character);
path = font.getPathGenerator(this.commonObjs, character);
}

if (font.disableFontFace || patternFill || patternStroke) {
ctx.save();
ctx.translate(x, y);
ctx.beginPath();
addToPath(ctx, fontSize);
ctx.scale(fontSize, -fontSize);
if (
fillStrokeMode === TextRenderingMode.FILL ||
fillStrokeMode === TextRenderingMode.FILL_STROKE
) {
if (patternFillTransform) {
const currentTransform = ctx.getTransform();
ctx.setTransform(...patternFillTransform);
ctx.fill(
this.#getScaledPath(path, currentTransform, patternFillTransform)
);
} else {
ctx.fill(path);
}
ctx.fill();
}
if (
fillStrokeMode === TextRenderingMode.STROKE ||
fillStrokeMode === TextRenderingMode.FILL_STROKE
) {
if (patternStrokeTransform) {
const currentTransform = ctx.getTransform();
ctx.setTransform(...patternStrokeTransform);
ctx.stroke(
this.#getScaledPath(path, currentTransform, patternStrokeTransform)
);
} else {
ctx.lineWidth /= fontSize;
ctx.stroke(path);
}
ctx.stroke();
}
ctx.restore();
} else {
Expand All @@ -2072,7 +2095,7 @@ class CanvasGraphics {
x,
y,
fontSize,
addToPath,
path,
});
}
}
Expand Down
85 changes: 1 addition & 84 deletions src/display/font_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@

import {
assert,
FontRenderOps,
isNodeJS,
shadow,
string32,
Expand Down Expand Up @@ -427,89 +426,7 @@ class FontFaceObject {
} catch (ex) {
warn(`getPathGenerator - ignoring character: "${ex}".`);
}

if (!Array.isArray(cmds) || cmds.length === 0) {
return (this.compiledGlyphs[character] = function (c, size) {
// No-op function, to allow rendering to continue.
});
}

const commands = [];
for (let i = 0, ii = cmds.length; i < ii; ) {
switch (cmds[i++]) {
case FontRenderOps.BEZIER_CURVE_TO:
{
const [a, b, c, d, e, f] = cmds.slice(i, i + 6);
commands.push(ctx => ctx.bezierCurveTo(a, b, c, d, e, f));
i += 6;
}
break;
case FontRenderOps.MOVE_TO:
{
const [a, b] = cmds.slice(i, i + 2);
commands.push(ctx => ctx.moveTo(a, b));
i += 2;
}
break;
case FontRenderOps.LINE_TO:
{
const [a, b] = cmds.slice(i, i + 2);
commands.push(ctx => ctx.lineTo(a, b));
i += 2;
}
break;
case FontRenderOps.QUADRATIC_CURVE_TO:
{
const [a, b, c, d] = cmds.slice(i, i + 4);
commands.push(ctx => ctx.quadraticCurveTo(a, b, c, d));
i += 4;
}
break;
case FontRenderOps.RESTORE:
commands.push(ctx => ctx.restore());
break;
case FontRenderOps.SAVE:
commands.push(ctx => ctx.save());
break;
case FontRenderOps.SCALE:
// The scale command must be at the third position, after save and
// transform (for the font matrix) commands (see also
// font_renderer.js).
// The goal is to just scale the canvas and then run the commands loop
// without the need to pass the size parameter to each command.
assert(
commands.length === 2,
"Scale command is only valid at the third position."
);
break;
case FontRenderOps.TRANSFORM:
{
const [a, b, c, d, e, f] = cmds.slice(i, i + 6);
commands.push(ctx => ctx.transform(a, b, c, d, e, f));
i += 6;
}
break;
case FontRenderOps.TRANSLATE:
{
const [a, b] = cmds.slice(i, i + 2);
commands.push(ctx => ctx.translate(a, b));
i += 2;
}
break;
}
}
// From https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#paths
// All contours must be closed with a lineto operation.
commands.push(ctx => ctx.closePath());

return (this.compiledGlyphs[character] = function glyphDrawer(ctx, size) {
commands[0](ctx);
commands[1](ctx);
ctx.scale(size, -size);
for (let i = 2, ii = commands.length; i < ii; i++) {
commands[i](ctx);
}
});
return (this.compiledGlyphs[character] = new Path2D(cmds || ""));
}
}

Expand Down
Loading

0 comments on commit 2b05924

Please sign in to comment.