Use a binary format for the glyph paths

We used a SVG string which can be pass to the Path2D ctor but it's a bit slower than
building the path step by step.
Having numerical data instead of a string will help the font data serialization.
This commit is contained in:
Calixte Denizet 2025-10-16 08:52:28 +02:00
parent 745e42701f
commit bb2a1126e6
5 changed files with 74 additions and 45 deletions

View File

@ -16,6 +16,8 @@
import { import {
assert, assert,
bytesToString, bytesToString,
DrawOPS,
FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FormatError, FormatError,
unreachable, unreachable,
@ -169,16 +171,16 @@ function compileGlyf(code, cmds, font) {
function moveTo(x, y) { function moveTo(x, y) {
if (firstPoint) { if (firstPoint) {
// Close the current subpath in adding a straight line to the first point. // Close the current subpath in adding a straight line to the first point.
cmds.add("L", firstPoint); cmds.add(DrawOPS.lineTo, firstPoint);
} }
firstPoint = [x, y]; firstPoint = [x, y];
cmds.add("M", [x, y]); cmds.add(DrawOPS.moveTo, [x, y]);
} }
function lineTo(x, y) { function lineTo(x, y) {
cmds.add("L", [x, y]); cmds.add(DrawOPS.lineTo, [x, y]);
} }
function quadraticCurveTo(xa, ya, x, y) { function quadraticCurveTo(xa, ya, x, y) {
cmds.add("Q", [xa, ya, x, y]); cmds.add(DrawOPS.quadraticCurveTo, [xa, ya, x, y]);
} }
let i = 0; let i = 0;
@ -355,16 +357,16 @@ function compileCharString(charStringCode, cmds, font, glyphId) {
function moveTo(x, y) { function moveTo(x, y) {
if (firstPoint) { if (firstPoint) {
// Close the current subpath in adding a straight line to the first point. // Close the current subpath in adding a straight line to the first point.
cmds.add("L", firstPoint); cmds.add(DrawOPS.lineTo, firstPoint);
} }
firstPoint = [x, y]; firstPoint = [x, y];
cmds.add("M", [x, y]); cmds.add(DrawOPS.moveTo, [x, y]);
} }
function lineTo(x, y) { function lineTo(x, y) {
cmds.add("L", [x, y]); cmds.add(DrawOPS.lineTo, [x, y]);
} }
function bezierCurveTo(x1, y1, x2, y2, x, y) { function bezierCurveTo(x1, y1, x2, y2, x, y) {
cmds.add("C", [x1, y1, x2, y2, x, y]); cmds.add(DrawOPS.curveTo, [x1, y1, x2, y2, x, y]);
} }
const stack = []; const stack = [];
@ -749,7 +751,7 @@ class Commands {
for (let i = 0, ii = args.length; i < ii; i += 2) { for (let i = 0, ii = args.length; i < ii; i += 2) {
Util.applyTransform(args, currentTransform, i); Util.applyTransform(args, currentTransform, i);
} }
this.cmds.push(`${cmd}${args.join(" ")}`); this.cmds.push(cmd, ...args);
} else { } else {
this.cmds.push(cmd); this.cmds.push(cmd);
} }
@ -771,8 +773,13 @@ class Commands {
this.currentTransform = this.transformStack.pop() || [1, 0, 0, 1, 0, 0]; this.currentTransform = this.transformStack.pop() || [1, 0, 0, 1, 0, 0];
} }
getSVG() { getPath() {
return this.cmds.join(""); if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
return new Float16Array(this.cmds);
}
return new (
FeatureTest.isFloat16ArraySupported ? Float16Array : Float32Array
)(this.cmds);
} }
} }
@ -834,9 +841,9 @@ class CompiledFont {
const cmds = new Commands(); const cmds = new Commands();
cmds.transform(fontMatrix.slice()); cmds.transform(fontMatrix.slice());
this.compileGlyphImpl(code, cmds, glyphId); this.compileGlyphImpl(code, cmds, glyphId);
cmds.add("Z"); cmds.add(DrawOPS.closePath);
return cmds.getSVG(); return cmds.getPath();
} }
compileGlyphImpl() { compileGlyphImpl() {

View File

@ -18,7 +18,6 @@ import {
Dependencies, Dependencies,
} from "./canvas_dependency_tracker.js"; } from "./canvas_dependency_tracker.js";
import { import {
DrawOPS,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
ImageKind, ImageKind,
@ -33,6 +32,7 @@ import {
import { import {
getCurrentTransform, getCurrentTransform,
getCurrentTransformInverse, getCurrentTransformInverse,
makePathFromDrawOPS,
OutputScale, OutputScale,
PixelsPerInch, PixelsPerInch,
} from "./display_utils.js"; } from "./display_utils.js";
@ -1489,35 +1489,7 @@ class CanvasGraphics {
} }
if (!(path instanceof Path2D)) { if (!(path instanceof Path2D)) {
// Using a SVG string is slightly slower than using the following loop. path = data[0] = makePathFromDrawOPS(path);
const path2d = (data[0] = new Path2D());
for (let i = 0, ii = path.length; i < ii; ) {
switch (path[i++]) {
case DrawOPS.moveTo:
path2d.moveTo(path[i++], path[i++]);
break;
case DrawOPS.lineTo:
path2d.lineTo(path[i++], path[i++]);
break;
case DrawOPS.curveTo:
path2d.bezierCurveTo(
path[i++],
path[i++],
path[i++],
path[i++],
path[i++],
path[i++]
);
break;
case DrawOPS.closePath:
path2d.closePath();
break;
default:
warn(`Unrecognized drawing path operator: ${path[i - 1]}`);
break;
}
}
path = path2d;
} }
Util.axialAlignedBoundingBox( Util.axialAlignedBoundingBox(
minMax, minMax,

View File

@ -15,6 +15,7 @@
import { import {
BaseException, BaseException,
DrawOPS,
FeatureTest, FeatureTest,
shadow, shadow,
Util, Util,
@ -995,6 +996,44 @@ function renderRichText({ html, dir, className }, container) {
container.append(fragment); container.append(fragment);
} }
function makePathFromDrawOPS(data) {
// Using a SVG string is slightly slower than using the following loop.
const path = new Path2D();
if (!data) {
return path;
}
for (let i = 0, ii = data.length; i < ii; ) {
switch (data[i++]) {
case DrawOPS.moveTo:
path.moveTo(data[i++], data[i++]);
break;
case DrawOPS.lineTo:
path.lineTo(data[i++], data[i++]);
break;
case DrawOPS.curveTo:
path.bezierCurveTo(
data[i++],
data[i++],
data[i++],
data[i++],
data[i++],
data[i++]
);
break;
case DrawOPS.quadraticCurveTo:
path.quadraticCurveTo(data[i++], data[i++], data[i++], data[i++]);
break;
case DrawOPS.closePath:
path.closePath();
break;
default:
warn(`Unrecognized drawing path operator: ${data[i - 1]}`);
break;
}
}
return path;
}
export { export {
applyOpacity, applyOpacity,
ColorScheme, ColorScheme,
@ -1012,6 +1051,7 @@ export {
isDataScheme, isDataScheme,
isPdfFile, isPdfFile,
isValidFetchUrl, isValidFetchUrl,
makePathFromDrawOPS,
noContextMenu, noContextMenu,
OutputScale, OutputScale,
PageViewport, PageViewport,

View File

@ -23,6 +23,7 @@ import {
unreachable, unreachable,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { makePathFromDrawOPS } from "./display_utils.js";
class FontLoader { class FontLoader {
#systemFonts = new Set(); #systemFonts = new Set();
@ -435,7 +436,7 @@ class FontFaceObject {
} catch (ex) { } catch (ex) {
warn(`getPathGenerator - ignoring character: "${ex}".`); warn(`getPathGenerator - ignoring character: "${ex}".`);
} }
const path = new Path2D(cmds || ""); const path = makePathFromDrawOPS(cmds);
if (!this.fontExtraProperties) { if (!this.fontExtraProperties) {
// Remove the raw path-string, since we don't need it anymore. // Remove the raw path-string, since we don't need it anymore.

View File

@ -354,7 +354,8 @@ const DrawOPS = {
moveTo: 0, moveTo: 0,
lineTo: 1, lineTo: 1,
curveTo: 2, curveTo: 2,
closePath: 3, quadraticCurveTo: 3,
closePath: 4,
}; };
const PasswordResponses = { const PasswordResponses = {
@ -649,6 +650,14 @@ class FeatureTest {
); );
} }
static get isFloat16ArraySupported() {
return shadow(
this,
"isFloat16ArraySupported",
typeof Float16Array !== "undefined"
);
}
static get platform() { static get platform() {
const { platform, userAgent } = navigator; const { platform, userAgent } = navigator;