Skip to content

Commit

Permalink
Simplify the way to pass the glyph drawing instructions from the work…
Browse files Browse the repository at this point in the history
…er to the main thread

and remove the use of eval in the font loader.
  • Loading branch information
calixteman committed Apr 27, 2024
1 parent 90d4b9c commit 551e639
Show file tree
Hide file tree
Showing 5 changed files with 150 additions and 61 deletions.
11 changes: 10 additions & 1 deletion src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -4403,7 +4412,7 @@ class PartialEvaluator {
loadedName: baseDict.loadedName,
composite,
fixedPitch: false,
fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
fontMatrix,
firstChar,
lastChar,
toUnicode,
Expand Down
77 changes: 51 additions & 26 deletions src/core/font_renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {
bytesToString,
FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError,
unreachable,
warn,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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]])
Expand All @@ -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,
Expand Down Expand Up @@ -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) {
Expand All @@ -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;
Expand Down Expand Up @@ -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() {
Expand Down
6 changes: 2 additions & 4 deletions src/display/api.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -384,7 +384,6 @@ function getDocument(src) {
};
const transportParams = {
ignoreErrors,
isEvalSupported,
disableFontFace,
fontExtraProperties,
enableXfa,
Expand Down Expand Up @@ -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,
Expand Down
104 changes: 74 additions & 30 deletions src/display/font_loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
import {
assert,
bytesToString,
FeatureTest,
FontRenderOps,
isNodeJS,
shadow,
string32,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
});
}
Expand Down
13 changes: 13 additions & 0 deletions src/shared/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -1095,6 +1107,7 @@ export {
DocumentActionEventType,
FeatureTest,
FONT_IDENTITY_MATRIX,
FontRenderOps,
FormatError,
getModificationDate,
getUuid,
Expand Down

1 comment on commit 551e639

@mildsunrise
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(in case someone is confused, this is the fix for CVE-2024-4367)

Please sign in to comment.