Skip to content

Commit 1990872

Browse files
calixtemanpull[bot]
authored andcommitted
Improve perfs of the font renderer
Some SVG paths are generated from the font and used in the main thread to render the glyphs.
1 parent 14df494 commit 1990872

File tree

4 files changed

+88
-149
lines changed

4 files changed

+88
-149
lines changed

src/core/font_renderer.js

Lines changed: 49 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,13 @@
1616
import {
1717
bytesToString,
1818
FONT_IDENTITY_MATRIX,
19-
FontRenderOps,
2019
FormatError,
2120
unreachable,
21+
Util,
2222
warn,
2323
} from "../shared/util.js";
2424
import { CFFParser } from "./cff_parser.js";
2525
import { getGlyphsUnicode } from "./glyphlist.js";
26-
import { isNumberArray } from "./core_utils.js";
2726
import { StandardEncoding } from "./encodings.js";
2827
import { Stream } from "./stream.js";
2928

@@ -182,13 +181,13 @@ function lookupCmap(ranges, unicode) {
182181

183182
function compileGlyf(code, cmds, font) {
184183
function moveTo(x, y) {
185-
cmds.add(FontRenderOps.MOVE_TO, [x, y]);
184+
cmds.add("M", [x, y]);
186185
}
187186
function lineTo(x, y) {
188-
cmds.add(FontRenderOps.LINE_TO, [x, y]);
187+
cmds.add("L", [x, y]);
189188
}
190189
function quadraticCurveTo(xa, ya, x, y) {
191-
cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]);
190+
cmds.add("Q", [xa, ya, x, y]);
192191
}
193192

194193
let i = 0;
@@ -249,22 +248,15 @@ function compileGlyf(code, cmds, font) {
249248
if (subglyph) {
250249
// TODO: the transform should be applied only if there is a scale:
251250
// https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1205
252-
cmds.add(FontRenderOps.SAVE);
253-
cmds.add(FontRenderOps.TRANSFORM, [
254-
scaleX,
255-
scale01,
256-
scale10,
257-
scaleY,
258-
x,
259-
y,
260-
]);
251+
cmds.save();
252+
cmds.transform([scaleX, scale01, scale10, scaleY, x, y]);
261253

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

370362
function compileCharString(charStringCode, cmds, font, glyphId) {
371363
function moveTo(x, y) {
372-
cmds.add(FontRenderOps.MOVE_TO, [x, y]);
364+
cmds.add("M", [x, y]);
373365
}
374366
function lineTo(x, y) {
375-
cmds.add(FontRenderOps.LINE_TO, [x, y]);
367+
cmds.add("L", [x, y]);
376368
}
377369
function bezierCurveTo(x1, y1, x2, y2, x, y) {
378-
cmds.add(FontRenderOps.BEZIER_CURVE_TO, [x1, y1, x2, y2, x, y]);
370+
cmds.add("C", [x1, y1, x2, y2, x, y]);
379371
}
380372

381373
const stack = [];
@@ -548,8 +540,8 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
548540
const bchar = stack.pop();
549541
y = stack.pop();
550542
x = stack.pop();
551-
cmds.add(FontRenderOps.SAVE);
552-
cmds.add(FontRenderOps.TRANSLATE, [x, y]);
543+
cmds.save();
544+
cmds.translate(x, y);
553545
let cmap = lookupCmap(
554546
font.cmap,
555547
String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]])
@@ -560,7 +552,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
560552
font,
561553
cmap.glyphId
562554
);
563-
cmds.add(FontRenderOps.RESTORE);
555+
cmds.restore();
564556

565557
cmap = lookupCmap(
566558
font.cmap,
@@ -744,27 +736,49 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
744736
parse(charStringCode);
745737
}
746738

747-
const NOOP = [];
739+
const NOOP = "";
748740

749741
class Commands {
750742
cmds = [];
751743

744+
transformStack = [];
745+
746+
currentTransform = [1, 0, 0, 1, 0, 0];
747+
752748
add(cmd, args) {
753749
if (args) {
754-
if (!isNumberArray(args, null)) {
755-
warn(
756-
`Commands.add - "${cmd}" has at least one non-number arg: "${args}".`
757-
);
758-
// "Fix" the wrong args by replacing them with 0.
759-
const newArgs = args.map(arg => (typeof arg === "number" ? arg : 0));
760-
this.cmds.push(cmd, ...newArgs);
761-
} else {
762-
this.cmds.push(cmd, ...args);
750+
const [a, b, c, d, e, f] = this.currentTransform;
751+
for (let i = 0, ii = args.length; i < ii; i += 2) {
752+
const x = args[i];
753+
const y = args[i + 1];
754+
args[i] = a * x + c * y + e;
755+
args[i + 1] = b * x + d * y + f;
763756
}
757+
this.cmds.push(`${cmd}${args.join(" ")}`);
764758
} else {
765759
this.cmds.push(cmd);
766760
}
767761
}
762+
763+
transform(transf) {
764+
this.currentTransform = Util.transform(this.currentTransform, transf);
765+
}
766+
767+
translate(x, y) {
768+
this.transform([1, 0, 0, 1, x, y]);
769+
}
770+
771+
save() {
772+
this.transformStack.push(this.currentTransform.slice());
773+
}
774+
775+
restore() {
776+
this.currentTransform = this.transformStack.pop() || [1, 0, 0, 1, 0, 0];
777+
}
778+
779+
getSVG() {
780+
return this.cmds.join("");
781+
}
768782
}
769783

770784
class CompiledFont {
@@ -785,7 +799,7 @@ class CompiledFont {
785799
const { charCode, glyphId } = lookupCmap(this.cmap, unicode);
786800
let fn = this.compiledGlyphs[glyphId],
787801
compileEx;
788-
if (!fn) {
802+
if (fn === undefined) {
789803
try {
790804
fn = this.compileGlyph(this.glyphs[glyphId], glyphId);
791805
} catch (ex) {
@@ -822,13 +836,11 @@ class CompiledFont {
822836
}
823837

824838
const cmds = new Commands();
825-
cmds.add(FontRenderOps.SAVE);
826-
cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice());
827-
cmds.add(FontRenderOps.SCALE);
839+
cmds.transform(fontMatrix.slice());
828840
this.compileGlyphImpl(code, cmds, glyphId);
829-
cmds.add(FontRenderOps.RESTORE);
841+
cmds.add("Z");
830842

831-
return cmds.cmds;
843+
return cmds.getSVG();
832844
}
833845

834846
compileGlyphImpl() {

src/display/canvas.js

Lines changed: 38 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1885,15 +1885,19 @@ class CanvasGraphics {
18851885
return;
18861886
}
18871887

1888-
ctx.save();
1889-
ctx.beginPath();
1890-
for (const path of paths) {
1891-
ctx.setTransform(...path.transform);
1892-
ctx.translate(path.x, path.y);
1893-
path.addToPath(ctx, path.fontSize);
1888+
const newPath = new Path2D();
1889+
const invTransf = ctx.getTransform().invertSelf();
1890+
for (const { transform, x, y, fontSize, path } of paths) {
1891+
newPath.addPath(
1892+
path,
1893+
new DOMMatrix(transform)
1894+
.preMultiplySelf(invTransf)
1895+
.translate(x, y)
1896+
.scale(fontSize, -fontSize)
1897+
);
18941898
}
1895-
ctx.restore();
1896-
ctx.clip();
1899+
1900+
ctx.clip(newPath);
18971901
ctx.beginPath();
18981902
delete this.pendingTextPaths;
18991903
}
@@ -2002,6 +2006,15 @@ class CanvasGraphics {
20022006
this.moveText(0, this.current.leading);
20032007
}
20042008

2009+
#getScaledPath(path, currentTransform, transform) {
2010+
const newPath = new Path2D();
2011+
newPath.addPath(
2012+
path,
2013+
new DOMMatrix(transform).invertSelf().multiplySelf(currentTransform)
2014+
);
2015+
return newPath;
2016+
}
2017+
20052018
paintChar(character, x, y, patternFillTransform, patternStrokeTransform) {
20062019
const ctx = this.ctx;
20072020
const current = this.current;
@@ -2016,38 +2029,48 @@ class CanvasGraphics {
20162029
const patternFill = current.patternFill && !font.missingFile;
20172030
const patternStroke = current.patternStroke && !font.missingFile;
20182031

2019-
let addToPath;
2032+
let path;
20202033
if (
20212034
font.disableFontFace ||
20222035
isAddToPathSet ||
20232036
patternFill ||
20242037
patternStroke
20252038
) {
2026-
addToPath = font.getPathGenerator(this.commonObjs, character);
2039+
path = font.getPathGenerator(this.commonObjs, character);
20272040
}
20282041

20292042
if (font.disableFontFace || patternFill || patternStroke) {
20302043
ctx.save();
20312044
ctx.translate(x, y);
2032-
ctx.beginPath();
2033-
addToPath(ctx, fontSize);
2045+
ctx.scale(fontSize, -fontSize);
20342046
if (
20352047
fillStrokeMode === TextRenderingMode.FILL ||
20362048
fillStrokeMode === TextRenderingMode.FILL_STROKE
20372049
) {
20382050
if (patternFillTransform) {
2051+
const currentTransform = ctx.getTransform();
20392052
ctx.setTransform(...patternFillTransform);
2053+
ctx.fill(
2054+
this.#getScaledPath(path, currentTransform, patternFillTransform)
2055+
);
2056+
} else {
2057+
ctx.fill(path);
20402058
}
2041-
ctx.fill();
20422059
}
20432060
if (
20442061
fillStrokeMode === TextRenderingMode.STROKE ||
20452062
fillStrokeMode === TextRenderingMode.FILL_STROKE
20462063
) {
20472064
if (patternStrokeTransform) {
2065+
const currentTransform = ctx.getTransform();
20482066
ctx.setTransform(...patternStrokeTransform);
2067+
ctx.stroke(
2068+
this.#getScaledPath(path, currentTransform, patternStrokeTransform)
2069+
);
2070+
} else {
2071+
ctx.lineWidth /= fontSize;
2072+
ctx.stroke(path);
20492073
}
2050-
ctx.stroke();
20512074
}
20522075
ctx.restore();
20532076
} else {
@@ -2072,7 +2095,7 @@ class CanvasGraphics {
20722095
x,
20732096
y,
20742097
fontSize,
2075-
addToPath,
2098+
path,
20762099
});
20772100
}
20782101
}

src/display/font_loader.js

Lines changed: 1 addition & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515

1616
import {
1717
assert,
18-
FontRenderOps,
1918
isNodeJS,
2019
shadow,
2120
string32,
@@ -427,89 +426,7 @@ class FontFaceObject {
427426
} catch (ex) {
428427
warn(`getPathGenerator - ignoring character: "${ex}".`);
429428
}
430-
431-
if (!Array.isArray(cmds) || cmds.length === 0) {
432-
return (this.compiledGlyphs[character] = function (c, size) {
433-
// No-op function, to allow rendering to continue.
434-
});
435-
}
436-
437-
const commands = [];
438-
for (let i = 0, ii = cmds.length; i < ii; ) {
439-
switch (cmds[i++]) {
440-
case FontRenderOps.BEZIER_CURVE_TO:
441-
{
442-
const [a, b, c, d, e, f] = cmds.slice(i, i + 6);
443-
commands.push(ctx => ctx.bezierCurveTo(a, b, c, d, e, f));
444-
i += 6;
445-
}
446-
break;
447-
case FontRenderOps.MOVE_TO:
448-
{
449-
const [a, b] = cmds.slice(i, i + 2);
450-
commands.push(ctx => ctx.moveTo(a, b));
451-
i += 2;
452-
}
453-
break;
454-
case FontRenderOps.LINE_TO:
455-
{
456-
const [a, b] = cmds.slice(i, i + 2);
457-
commands.push(ctx => ctx.lineTo(a, b));
458-
i += 2;
459-
}
460-
break;
461-
case FontRenderOps.QUADRATIC_CURVE_TO:
462-
{
463-
const [a, b, c, d] = cmds.slice(i, i + 4);
464-
commands.push(ctx => ctx.quadraticCurveTo(a, b, c, d));
465-
i += 4;
466-
}
467-
break;
468-
case FontRenderOps.RESTORE:
469-
commands.push(ctx => ctx.restore());
470-
break;
471-
case FontRenderOps.SAVE:
472-
commands.push(ctx => ctx.save());
473-
break;
474-
case FontRenderOps.SCALE:
475-
// The scale command must be at the third position, after save and
476-
// transform (for the font matrix) commands (see also
477-
// font_renderer.js).
478-
// The goal is to just scale the canvas and then run the commands loop
479-
// without the need to pass the size parameter to each command.
480-
assert(
481-
commands.length === 2,
482-
"Scale command is only valid at the third position."
483-
);
484-
break;
485-
case FontRenderOps.TRANSFORM:
486-
{
487-
const [a, b, c, d, e, f] = cmds.slice(i, i + 6);
488-
commands.push(ctx => ctx.transform(a, b, c, d, e, f));
489-
i += 6;
490-
}
491-
break;
492-
case FontRenderOps.TRANSLATE:
493-
{
494-
const [a, b] = cmds.slice(i, i + 2);
495-
commands.push(ctx => ctx.translate(a, b));
496-
i += 2;
497-
}
498-
break;
499-
}
500-
}
501-
// From https://learn.microsoft.com/en-us/typography/opentype/spec/cff2#paths
502-
// All contours must be closed with a lineto operation.
503-
commands.push(ctx => ctx.closePath());
504-
505-
return (this.compiledGlyphs[character] = function glyphDrawer(ctx, size) {
506-
commands[0](ctx);
507-
commands[1](ctx);
508-
ctx.scale(size, -size);
509-
for (let i = 2, ii = commands.length; i < ii; i++) {
510-
commands[i](ctx);
511-
}
512-
});
429+
return (this.compiledGlyphs[character] = new Path2D(cmds || ""));
513430
}
514431
}
515432

0 commit comments

Comments
 (0)