Improve perfs of the font renderer

Some SVG paths are generated from the font and used in the main thread
to render the glyphs.
This commit is contained in:
Calixte Denizet 2024-12-06 23:16:16 +01:00
parent 23c42f891b
commit 2b05924504
4 changed files with 88 additions and 149 deletions

View File

@ -16,14 +16,13 @@
import { import {
bytesToString, bytesToString,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError, FormatError,
unreachable, unreachable,
Util,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { CFFParser } from "./cff_parser.js"; import { CFFParser } from "./cff_parser.js";
import { getGlyphsUnicode } from "./glyphlist.js"; import { getGlyphsUnicode } from "./glyphlist.js";
import { isNumberArray } from "./core_utils.js";
import { StandardEncoding } from "./encodings.js"; import { StandardEncoding } from "./encodings.js";
import { Stream } from "./stream.js"; import { Stream } from "./stream.js";
@ -182,13 +181,13 @@ function lookupCmap(ranges, unicode) {
function compileGlyf(code, cmds, font) { function compileGlyf(code, cmds, font) {
function moveTo(x, y) { function moveTo(x, y) {
cmds.add(FontRenderOps.MOVE_TO, [x, y]); cmds.add("M", [x, y]);
} }
function lineTo(x, y) { function lineTo(x, y) {
cmds.add(FontRenderOps.LINE_TO, [x, y]); cmds.add("L", [x, y]);
} }
function quadraticCurveTo(xa, ya, 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; let i = 0;
@ -249,22 +248,15 @@ function compileGlyf(code, cmds, font) {
if (subglyph) { if (subglyph) {
// TODO: the transform should be applied only if there is a scale: // TODO: the transform should be applied only if there is a scale:
// https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1205 // https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1205
cmds.add(FontRenderOps.SAVE); cmds.save();
cmds.add(FontRenderOps.TRANSFORM, [ cmds.transform([scaleX, scale01, scale10, scaleY, x, y]);
scaleX,
scale01,
scale10,
scaleY,
x,
y,
]);
if (!(flags & 0x02)) { if (!(flags & 0x02)) {
// TODO: we must use arg1 and arg2 to make something similar to: // TODO: we must use arg1 and arg2 to make something similar to:
// https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1209 // https://github.com/freetype/freetype/blob/edd4fedc5427cf1cf1f4b045e53ff91eb282e9d4/src/truetype/ttgload.c#L1209
} }
compileGlyf(subglyph, cmds, font); compileGlyf(subglyph, cmds, font);
cmds.add(FontRenderOps.RESTORE); cmds.restore();
} }
} while (flags & 0x20); } while (flags & 0x20);
} else { } else {
@ -369,13 +361,13 @@ function compileGlyf(code, cmds, font) {
function compileCharString(charStringCode, cmds, font, glyphId) { function compileCharString(charStringCode, cmds, font, glyphId) {
function moveTo(x, y) { function moveTo(x, y) {
cmds.add(FontRenderOps.MOVE_TO, [x, y]); cmds.add("M", [x, y]);
} }
function lineTo(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) { 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 = []; const stack = [];
@ -548,8 +540,8 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
const bchar = stack.pop(); const bchar = stack.pop();
y = stack.pop(); y = stack.pop();
x = stack.pop(); x = stack.pop();
cmds.add(FontRenderOps.SAVE); cmds.save();
cmds.add(FontRenderOps.TRANSLATE, [x, y]); cmds.translate(x, y);
let cmap = lookupCmap( let cmap = lookupCmap(
font.cmap, font.cmap,
String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]]) String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]])
@ -560,7 +552,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
font, font,
cmap.glyphId cmap.glyphId
); );
cmds.add(FontRenderOps.RESTORE); cmds.restore();
cmap = lookupCmap( cmap = lookupCmap(
font.cmap, font.cmap,
@ -744,27 +736,49 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
parse(charStringCode); parse(charStringCode);
} }
const NOOP = []; const NOOP = "";
class Commands { class Commands {
cmds = []; cmds = [];
transformStack = [];
currentTransform = [1, 0, 0, 1, 0, 0];
add(cmd, args) { add(cmd, args) {
if (args) { if (args) {
if (!isNumberArray(args, null)) { const [a, b, c, d, e, f] = this.currentTransform;
warn( for (let i = 0, ii = args.length; i < ii; i += 2) {
`Commands.add - "${cmd}" has at least one non-number arg: "${args}".` const x = args[i];
); const y = args[i + 1];
// "Fix" the wrong args by replacing them with 0. args[i] = a * x + c * y + e;
const newArgs = args.map(arg => (typeof arg === "number" ? arg : 0)); args[i + 1] = b * x + d * y + f;
this.cmds.push(cmd, ...newArgs);
} else {
this.cmds.push(cmd, ...args);
} }
this.cmds.push(`${cmd}${args.join(" ")}`);
} else { } else {
this.cmds.push(cmd); 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 { class CompiledFont {
@ -785,7 +799,7 @@ class CompiledFont {
const { charCode, glyphId } = lookupCmap(this.cmap, unicode); const { charCode, glyphId } = lookupCmap(this.cmap, unicode);
let fn = this.compiledGlyphs[glyphId], let fn = this.compiledGlyphs[glyphId],
compileEx; compileEx;
if (!fn) { if (fn === undefined) {
try { try {
fn = this.compileGlyph(this.glyphs[glyphId], glyphId); fn = this.compileGlyph(this.glyphs[glyphId], glyphId);
} catch (ex) { } catch (ex) {
@ -822,13 +836,11 @@ class CompiledFont {
} }
const cmds = new Commands(); const cmds = new Commands();
cmds.add(FontRenderOps.SAVE); cmds.transform(fontMatrix.slice());
cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice());
cmds.add(FontRenderOps.SCALE);
this.compileGlyphImpl(code, cmds, glyphId); this.compileGlyphImpl(code, cmds, glyphId);
cmds.add(FontRenderOps.RESTORE); cmds.add("Z");
return cmds.cmds; return cmds.getSVG();
} }
compileGlyphImpl() { compileGlyphImpl() {

View File

@ -1885,15 +1885,19 @@ class CanvasGraphics {
return; return;
} }
ctx.save(); const newPath = new Path2D();
ctx.beginPath(); const invTransf = ctx.getTransform().invertSelf();
for (const path of paths) { for (const { transform, x, y, fontSize, path } of paths) {
ctx.setTransform(...path.transform); newPath.addPath(
ctx.translate(path.x, path.y); path,
path.addToPath(ctx, path.fontSize); new DOMMatrix(transform)
.preMultiplySelf(invTransf)
.translate(x, y)
.scale(fontSize, -fontSize)
);
} }
ctx.restore();
ctx.clip(); ctx.clip(newPath);
ctx.beginPath(); ctx.beginPath();
delete this.pendingTextPaths; delete this.pendingTextPaths;
} }
@ -2002,6 +2006,15 @@ class CanvasGraphics {
this.moveText(0, this.current.leading); 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) { paintChar(character, x, y, patternFillTransform, patternStrokeTransform) {
const ctx = this.ctx; const ctx = this.ctx;
const current = this.current; const current = this.current;
@ -2016,38 +2029,48 @@ class CanvasGraphics {
const patternFill = current.patternFill && !font.missingFile; const patternFill = current.patternFill && !font.missingFile;
const patternStroke = current.patternStroke && !font.missingFile; const patternStroke = current.patternStroke && !font.missingFile;
let addToPath; let path;
if ( if (
font.disableFontFace || font.disableFontFace ||
isAddToPathSet || isAddToPathSet ||
patternFill || patternFill ||
patternStroke patternStroke
) { ) {
addToPath = font.getPathGenerator(this.commonObjs, character); path = font.getPathGenerator(this.commonObjs, character);
} }
if (font.disableFontFace || patternFill || patternStroke) { if (font.disableFontFace || patternFill || patternStroke) {
ctx.save(); ctx.save();
ctx.translate(x, y); ctx.translate(x, y);
ctx.beginPath(); ctx.scale(fontSize, -fontSize);
addToPath(ctx, fontSize);
if ( if (
fillStrokeMode === TextRenderingMode.FILL || fillStrokeMode === TextRenderingMode.FILL ||
fillStrokeMode === TextRenderingMode.FILL_STROKE fillStrokeMode === TextRenderingMode.FILL_STROKE
) { ) {
if (patternFillTransform) { if (patternFillTransform) {
const currentTransform = ctx.getTransform();
ctx.setTransform(...patternFillTransform); ctx.setTransform(...patternFillTransform);
ctx.fill(
this.#getScaledPath(path, currentTransform, patternFillTransform)
);
} else {
ctx.fill(path);
} }
ctx.fill();
} }
if ( if (
fillStrokeMode === TextRenderingMode.STROKE || fillStrokeMode === TextRenderingMode.STROKE ||
fillStrokeMode === TextRenderingMode.FILL_STROKE fillStrokeMode === TextRenderingMode.FILL_STROKE
) { ) {
if (patternStrokeTransform) { if (patternStrokeTransform) {
const currentTransform = ctx.getTransform();
ctx.setTransform(...patternStrokeTransform); ctx.setTransform(...patternStrokeTransform);
ctx.stroke(
this.#getScaledPath(path, currentTransform, patternStrokeTransform)
);
} else {
ctx.lineWidth /= fontSize;
ctx.stroke(path);
} }
ctx.stroke();
} }
ctx.restore(); ctx.restore();
} else { } else {
@ -2072,7 +2095,7 @@ class CanvasGraphics {
x, x,
y, y,
fontSize, fontSize,
addToPath, path,
}); });
} }
} }

View File

@ -15,7 +15,6 @@
import { import {
assert, assert,
FontRenderOps,
isNodeJS, isNodeJS,
shadow, shadow,
string32, string32,
@ -427,89 +426,7 @@ class FontFaceObject {
} catch (ex) { } catch (ex) {
warn(`getPathGenerator - ignoring character: "${ex}".`); warn(`getPathGenerator - ignoring character: "${ex}".`);
} }
return (this.compiledGlyphs[character] = new Path2D(cmds || ""));
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);
}
});
} }
} }

View File

@ -1087,18 +1087,6 @@ function getUuid() {
const AnnotationPrefix = "pdfjs_internal_id_"; const AnnotationPrefix = "pdfjs_internal_id_";
const FontRenderOps = {
BEZIER_CURVE_TO: 0,
MOVE_TO: 1,
LINE_TO: 2,
QUADRATIC_CURVE_TO: 3,
RESTORE: 4,
SAVE: 5,
SCALE: 6,
TRANSFORM: 7,
TRANSLATE: 8,
};
// TODO: Remove this once `Uint8Array.prototype.toHex` is generally available. // TODO: Remove this once `Uint8Array.prototype.toHex` is generally available.
function toHexUtil(arr) { function toHexUtil(arr) {
if (Uint8Array.prototype.toHex) { if (Uint8Array.prototype.toHex) {
@ -1158,7 +1146,6 @@ export {
DocumentActionEventType, DocumentActionEventType,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError, FormatError,
fromBase64Util, fromBase64Util,
getModificationDate, getModificationDate,