Simplify the way to pass the glyph drawing instructions from the worker to the main thread

and remove the use of eval in the font loader.
This commit is contained in:
Calixte Denizet 2024-04-27 17:13:34 +02:00
parent 90d4b9c2c0
commit 551e63901c
5 changed files with 149 additions and 60 deletions

View File

@ -4391,6 +4391,15 @@ class PartialEvaluator {
} }
} }
let fontMatrix = dict.getArray("FontMatrix");
if (
!Array.isArray(fontMatrix) ||
fontMatrix.length !== 6 ||
fontMatrix.some(x => typeof x !== "number")
) {
fontMatrix = FONT_IDENTITY_MATRIX;
}
const properties = { const properties = {
type, type,
name: fontName.name, name: fontName.name,
@ -4403,7 +4412,7 @@ class PartialEvaluator {
loadedName: baseDict.loadedName, loadedName: baseDict.loadedName,
composite, composite,
fixedPitch: false, fixedPitch: false,
fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX, fontMatrix,
firstChar, firstChar,
lastChar, lastChar,
toUnicode, toUnicode,

View File

@ -16,6 +16,7 @@
import { import {
bytesToString, bytesToString,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError, FormatError,
unreachable, unreachable,
warn, warn,
@ -180,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.push({ cmd: "moveTo", args: [x, y] }); cmds.add(FontRenderOps.MOVE_TO, [x, y]);
} }
function lineTo(x, y) { function lineTo(x, y) {
cmds.push({ cmd: "lineTo", args: [x, y] }); cmds.add(FontRenderOps.LINE_TO, [x, y]);
} }
function quadraticCurveTo(xa, ya, x, y) { function quadraticCurveTo(xa, ya, x, y) {
cmds.push({ cmd: "quadraticCurveTo", args: [xa, ya, x, y] }); cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]);
} }
let i = 0; let i = 0;
@ -247,20 +248,22 @@ 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.push( cmds.add(FontRenderOps.SAVE);
{ cmd: "save" }, cmds.add(FontRenderOps.TRANSFORM, [
{ scaleX,
cmd: "transform", scale01,
args: [scaleX, scale01, scale10, scaleY, x, y], 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.push({ cmd: "restore" }); cmds.add(FontRenderOps.RESTORE);
} }
} while (flags & 0x20); } while (flags & 0x20);
} else { } else {
@ -365,13 +368,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.push({ cmd: "moveTo", args: [x, y] }); cmds.add(FontRenderOps.MOVE_TO, [x, y]);
} }
function lineTo(x, y) { function lineTo(x, y) {
cmds.push({ cmd: "lineTo", args: [x, y] }); cmds.add(FontRenderOps.LINE_TO, [x, y]);
} }
function bezierCurveTo(x1, y1, x2, y2, x, y) { function bezierCurveTo(x1, y1, x2, y2, x, y) {
cmds.push({ cmd: "bezierCurveTo", args: [x1, y1, x2, y2, x, y] }); cmds.add(FontRenderOps.BEZIER_CURVE_TO, [x1, y1, x2, y2, x, y]);
} }
const stack = []; const stack = [];
@ -544,7 +547,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.push({ cmd: "save" }, { cmd: "translate", args: [x, y] }); cmds.add(FontRenderOps.SAVE);
cmds.add(FontRenderOps.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]])
@ -555,7 +559,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
font, font,
cmap.glyphId cmap.glyphId
); );
cmds.push({ cmd: "restore" }); cmds.add(FontRenderOps.RESTORE);
cmap = lookupCmap( cmap = lookupCmap(
font.cmap, font.cmap,
@ -741,6 +745,27 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
const NOOP = []; const NOOP = [];
class Commands {
cmds = [];
add(cmd, args) {
if (args) {
if (args.some(arg => typeof arg !== "number")) {
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);
}
} else {
this.cmds.push(cmd);
}
}
}
class CompiledFont { class CompiledFont {
constructor(fontMatrix) { constructor(fontMatrix) {
if (this.constructor === CompiledFont) { if (this.constructor === CompiledFont) {
@ -757,8 +782,10 @@ class CompiledFont {
let fn = this.compiledGlyphs[glyphId]; let fn = this.compiledGlyphs[glyphId];
if (!fn) { if (!fn) {
try { try {
fn = this.compileGlyph(this.glyphs[glyphId], glyphId); fn = this.compiledGlyphs[glyphId] = this.compileGlyph(
this.compiledGlyphs[glyphId] = fn; this.glyphs[glyphId],
glyphId
);
} catch (ex) { } catch (ex) {
// Avoid attempting to re-compile a corrupt glyph. // Avoid attempting to re-compile a corrupt glyph.
this.compiledGlyphs[glyphId] = NOOP; this.compiledGlyphs[glyphId] = NOOP;
@ -793,16 +820,14 @@ class CompiledFont {
} }
} }
const cmds = [ const cmds = new Commands();
{ cmd: "save" }, cmds.add(FontRenderOps.SAVE);
{ cmd: "transform", args: fontMatrix.slice() }, cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice());
{ cmd: "scale", args: ["size", "-size"] }, cmds.add(FontRenderOps.SCALE);
];
this.compileGlyphImpl(code, cmds, glyphId); this.compileGlyphImpl(code, cmds, glyphId);
cmds.add(FontRenderOps.RESTORE);
cmds.push({ cmd: "restore" }); return cmds.cmds;
return cmds;
} }
compileGlyphImpl() { compileGlyphImpl() {

View File

@ -169,8 +169,8 @@ const DefaultStandardFontDataFactory =
* pixels, i.e. width * height. Images above this value will not be rendered. * pixels, i.e. width * height. Images above this value will not be rendered.
* Use -1 for no limit, which is also the default value. * Use -1 for no limit, which is also the default value.
* @property {boolean} [isEvalSupported] - Determines if we can evaluate strings * @property {boolean} [isEvalSupported] - Determines if we can evaluate strings
* as JavaScript. Primarily used to improve performance of font rendering, and * as JavaScript. Primarily used to improve performance of PDF functions.
* when parsing PDF functions. The default value is `true`. * The default value is `true`.
* @property {boolean} [isOffscreenCanvasSupported] - Determines if we can use * @property {boolean} [isOffscreenCanvasSupported] - Determines if we can use
* `OffscreenCanvas` in the worker. Primarily used to improve performance of * `OffscreenCanvas` in the worker. Primarily used to improve performance of
* image conversion/rendering. * image conversion/rendering.
@ -384,7 +384,6 @@ function getDocument(src) {
}; };
const transportParams = { const transportParams = {
ignoreErrors, ignoreErrors,
isEvalSupported,
disableFontFace, disableFontFace,
fontExtraProperties, fontExtraProperties,
enableXfa, enableXfa,
@ -2744,7 +2743,6 @@ class WorkerTransport {
? (font, url) => globalThis.FontInspector.fontAdded(font, url) ? (font, url) => globalThis.FontInspector.fontAdded(font, url)
: null; : null;
const font = new FontFaceObject(exportedData, { const font = new FontFaceObject(exportedData, {
isEvalSupported: params.isEvalSupported,
disableFontFace: params.disableFontFace, disableFontFace: params.disableFontFace,
ignoreErrors: params.ignoreErrors, ignoreErrors: params.ignoreErrors,
inspectFont, inspectFont,

View File

@ -16,7 +16,7 @@
import { import {
assert, assert,
bytesToString, bytesToString,
FeatureTest, FontRenderOps,
isNodeJS, isNodeJS,
shadow, shadow,
string32, string32,
@ -362,19 +362,13 @@ class FontLoader {
class FontFaceObject { class FontFaceObject {
constructor( constructor(
translatedData, translatedData,
{ { disableFontFace = false, ignoreErrors = false, inspectFont = null }
isEvalSupported = true,
disableFontFace = false,
ignoreErrors = false,
inspectFont = null,
}
) { ) {
this.compiledGlyphs = Object.create(null); this.compiledGlyphs = Object.create(null);
// importing translated data // importing translated data
for (const i in translatedData) { for (const i in translatedData) {
this[i] = translatedData[i]; this[i] = translatedData[i];
} }
this.isEvalSupported = isEvalSupported !== false;
this.disableFontFace = disableFontFace === true; this.disableFontFace = disableFontFace === true;
this.ignoreErrors = ignoreErrors === true; this.ignoreErrors = ignoreErrors === true;
this._inspectFont = inspectFont; this._inspectFont = inspectFont;
@ -440,35 +434,85 @@ class FontFaceObject {
throw ex; throw ex;
} }
warn(`getPathGenerator - ignoring character: "${ex}".`); warn(`getPathGenerator - ignoring character: "${ex}".`);
}
if (!Array.isArray(cmds) || cmds.length === 0) {
return (this.compiledGlyphs[character] = function (c, size) { return (this.compiledGlyphs[character] = function (c, size) {
// No-op function, to allow rendering to continue. // No-op function, to allow rendering to continue.
}); });
} }
// If we can, compile cmds into JS for MAXIMUM SPEED... const commands = [];
if (this.isEvalSupported && FeatureTest.isEvalSupported) { for (let i = 0, ii = cmds.length; i < ii; ) {
const jsBuf = []; switch (cmds[i++]) {
for (const current of cmds) { case FontRenderOps.BEZIER_CURVE_TO:
const args = current.args !== undefined ? current.args.join(",") : ""; {
jsBuf.push("c.", current.cmd, "(", args, ");\n"); 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;
} }
// eslint-disable-next-line no-new-func
return (this.compiledGlyphs[character] = new Function(
"c",
"size",
jsBuf.join("")
));
} }
// ... but fall back on using Function.prototype.apply() if we're
// blocked from using eval() for whatever reason (like CSP policies). return (this.compiledGlyphs[character] = function glyphDrawer(ctx, size) {
return (this.compiledGlyphs[character] = function (c, size) { commands[0](ctx);
for (const current of cmds) { commands[1](ctx);
if (current.cmd === "scale") { ctx.scale(size, -size);
current.args = [size, -size]; for (let i = 2, ii = commands.length; i < ii; i++) {
} commands[i](ctx);
// eslint-disable-next-line prefer-spread
c[current.cmd].apply(c, current.args);
} }
}); });
} }

View File

@ -1073,6 +1073,18 @@ 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,
};
export { export {
AbortException, AbortException,
AnnotationActionEventType, AnnotationActionEventType,
@ -1095,6 +1107,7 @@ export {
DocumentActionEventType, DocumentActionEventType,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError, FormatError,
getModificationDate, getModificationDate,
getUuid, getUuid,