From 1844583faea63005daf83052de84fe14b336065c Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Sat, 27 Apr 2024 17:13:34 +0200 Subject: [PATCH] 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. --- src/core/evaluator.js | 11 +++- src/core/font_renderer.js | 77 +++++++++++++++++---------- src/display/api.js | 6 +-- src/display/font_loader.js | 104 ++++++++++++++++++++++++++----------- src/shared/util.js | 13 +++++ 5 files changed, 150 insertions(+), 61 deletions(-) diff --git a/src/core/evaluator.js b/src/core/evaluator.js index d758f363e17d3c..17dd3bbc15b40c 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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 = { type, name: fontName.name, @@ -4403,7 +4412,7 @@ class PartialEvaluator { loadedName: baseDict.loadedName, composite, fixedPitch: false, - fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX, + fontMatrix, firstChar, lastChar, toUnicode, diff --git a/src/core/font_renderer.js b/src/core/font_renderer.js index 7b5253639e155b..3c7ec113dc15d5 100644 --- a/src/core/font_renderer.js +++ b/src/core/font_renderer.js @@ -16,6 +16,7 @@ import { bytesToString, FONT_IDENTITY_MATRIX, + FontRenderOps, FormatError, unreachable, warn, @@ -180,13 +181,13 @@ function lookupCmap(ranges, unicode) { function compileGlyf(code, cmds, font) { function moveTo(x, y) { - cmds.push({ cmd: "moveTo", args: [x, y] }); + cmds.add(FontRenderOps.MOVE_TO, [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) { - cmds.push({ cmd: "quadraticCurveTo", args: [xa, ya, x, y] }); + cmds.add(FontRenderOps.QUADRATIC_CURVE_TO, [xa, ya, x, y]); } let i = 0; @@ -247,20 +248,22 @@ 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.push( - { cmd: "save" }, - { - cmd: "transform", - args: [scaleX, scale01, scale10, scaleY, x, y], - } - ); + cmds.add(FontRenderOps.SAVE); + cmds.add(FontRenderOps.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.push({ cmd: "restore" }); + cmds.add(FontRenderOps.RESTORE); } } while (flags & 0x20); } else { @@ -365,13 +368,13 @@ function compileGlyf(code, cmds, font) { function compileCharString(charStringCode, cmds, font, glyphId) { function moveTo(x, y) { - cmds.push({ cmd: "moveTo", args: [x, y] }); + cmds.add(FontRenderOps.MOVE_TO, [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) { - 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 = []; @@ -544,7 +547,8 @@ function compileCharString(charStringCode, cmds, font, glyphId) { const bchar = stack.pop(); y = 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( font.cmap, String.fromCharCode(font.glyphNameMap[StandardEncoding[achar]]) @@ -555,7 +559,7 @@ function compileCharString(charStringCode, cmds, font, glyphId) { font, cmap.glyphId ); - cmds.push({ cmd: "restore" }); + cmds.add(FontRenderOps.RESTORE); cmap = lookupCmap( font.cmap, @@ -741,6 +745,27 @@ function compileCharString(charStringCode, cmds, font, glyphId) { 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 { constructor(fontMatrix) { if (this.constructor === CompiledFont) { @@ -757,8 +782,10 @@ class CompiledFont { let fn = this.compiledGlyphs[glyphId]; if (!fn) { try { - fn = this.compileGlyph(this.glyphs[glyphId], glyphId); - this.compiledGlyphs[glyphId] = fn; + fn = this.compiledGlyphs[glyphId] = this.compileGlyph( + this.glyphs[glyphId], + glyphId + ); } catch (ex) { // Avoid attempting to re-compile a corrupt glyph. this.compiledGlyphs[glyphId] = NOOP; @@ -793,16 +820,14 @@ class CompiledFont { } } - const cmds = [ - { cmd: "save" }, - { cmd: "transform", args: fontMatrix.slice() }, - { cmd: "scale", args: ["size", "-size"] }, - ]; + const cmds = new Commands(); + cmds.add(FontRenderOps.SAVE); + cmds.add(FontRenderOps.TRANSFORM, fontMatrix.slice()); + cmds.add(FontRenderOps.SCALE); this.compileGlyphImpl(code, cmds, glyphId); + cmds.add(FontRenderOps.RESTORE); - cmds.push({ cmd: "restore" }); - - return cmds; + return cmds.cmds; } compileGlyphImpl() { diff --git a/src/display/api.js b/src/display/api.js index 70dcf67fb213b9..3644e26e09c653 100644 --- a/src/display/api.js +++ b/src/display/api.js @@ -169,8 +169,8 @@ const DefaultStandardFontDataFactory = * pixels, i.e. width * height. Images above this value will not be rendered. * Use -1 for no limit, which is also the default value. * @property {boolean} [isEvalSupported] - Determines if we can evaluate strings - * as JavaScript. Primarily used to improve performance of font rendering, and - * when parsing PDF functions. The default value is `true`. + * as JavaScript. Primarily used to improve performance of PDF functions. + * The default value is `true`. * @property {boolean} [isOffscreenCanvasSupported] - Determines if we can use * `OffscreenCanvas` in the worker. Primarily used to improve performance of * image conversion/rendering. @@ -384,7 +384,6 @@ function getDocument(src) { }; const transportParams = { ignoreErrors, - isEvalSupported, disableFontFace, fontExtraProperties, enableXfa, @@ -2744,7 +2743,6 @@ class WorkerTransport { ? (font, url) => globalThis.FontInspector.fontAdded(font, url) : null; const font = new FontFaceObject(exportedData, { - isEvalSupported: params.isEvalSupported, disableFontFace: params.disableFontFace, ignoreErrors: params.ignoreErrors, inspectFont, diff --git a/src/display/font_loader.js b/src/display/font_loader.js index 7ca4ebbf5da6db..43b6f0fcf7dcc5 100644 --- a/src/display/font_loader.js +++ b/src/display/font_loader.js @@ -16,7 +16,7 @@ import { assert, bytesToString, - FeatureTest, + FontRenderOps, isNodeJS, shadow, string32, @@ -362,19 +362,13 @@ class FontLoader { class FontFaceObject { constructor( translatedData, - { - isEvalSupported = true, - disableFontFace = false, - ignoreErrors = false, - inspectFont = null, - } + { disableFontFace = false, ignoreErrors = false, inspectFont = null } ) { this.compiledGlyphs = Object.create(null); // importing translated data for (const i in translatedData) { this[i] = translatedData[i]; } - this.isEvalSupported = isEvalSupported !== false; this.disableFontFace = disableFontFace === true; this.ignoreErrors = ignoreErrors === true; this._inspectFont = inspectFont; @@ -440,35 +434,85 @@ class FontFaceObject { throw 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. }); } - // If we can, compile cmds into JS for MAXIMUM SPEED... - if (this.isEvalSupported && FeatureTest.isEvalSupported) { - const jsBuf = []; - for (const current of cmds) { - const args = current.args !== undefined ? current.args.join(",") : ""; - jsBuf.push("c.", current.cmd, "(", args, ");\n"); + 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; } - // 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 (c, size) { - for (const current of cmds) { - if (current.cmd === "scale") { - current.args = [size, -size]; - } - // eslint-disable-next-line prefer-spread - c[current.cmd].apply(c, current.args); + } + + 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); } }); } diff --git a/src/shared/util.js b/src/shared/util.js index 722a7ed63c283f..68d12cd4cd21c1 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -1073,6 +1073,18 @@ function getUuid() { 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 { AbortException, AnnotationActionEventType, @@ -1095,6 +1107,7 @@ export { DocumentActionEventType, FeatureTest, FONT_IDENTITY_MATRIX, + FontRenderOps, FormatError, getModificationDate, getUuid,