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:
parent
23c42f891b
commit
2b05924504
@ -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() {
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user