[api-minor] Use a Path2D when doing a path operation in the canvas (bug 1946953)

With this patch, all the paths components are collected in the worker until a path
operation is met (i.e., stroke, fill, ...).
Then in the canvas a Path2D is created and will replace the path data transfered from the worker,
this way when rescaling, the Path2D can be reused.
In term of performances, using Path2D is very slightly improving speed when scaling the canvas.
This commit is contained in:
Calixte Denizet 2025-03-19 20:39:02 +01:00
parent a229914b46
commit be1f5671bb
10 changed files with 367 additions and 374 deletions

View File

@ -16,6 +16,7 @@
import { import {
AbortException, AbortException,
assert, assert,
DrawOPS,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FormatError, FormatError,
IDENTITY_MATRIX, IDENTITY_MATRIX,
@ -925,7 +926,7 @@ class PartialEvaluator {
smaskOptions, smaskOptions,
operatorList, operatorList,
task, task,
stateManager.state.clone(), stateManager.state.clone({ newPath: true }),
localColorSpaceCache localColorSpaceCache
); );
} }
@ -1383,80 +1384,112 @@ class PartialEvaluator {
return promise; return promise;
} }
buildPath(operatorList, fn, args, parsingText = false) { buildPath(fn, args, state) {
const lastIndex = operatorList.length - 1; const { pathMinMax: minMax, pathBuffer } = state;
if (!args) { switch (fn | 0) {
args = []; case OPS.rectangle: {
} const x = (state.currentPointX = args[0]);
if ( const y = (state.currentPointY = args[1]);
lastIndex < 0 || const width = args[2];
operatorList.fnArray[lastIndex] !== OPS.constructPath const height = args[3];
) { const xw = x + width;
// Handle corrupt PDF documents that contains path operators inside of const yh = y + height;
// text objects, which may shift subsequent text, by enclosing the path if (width === 0 || height === 0) {
// operator in save/restore operators (fixes issue10542_reduced.pdf). pathBuffer.push(
// DrawOPS.moveTo,
// Note that this will effectively disable the optimization in the x,
// `else` branch below, but given that this type of corruption is y,
// *extremely* rare that shouldn't really matter much in practice. DrawOPS.lineTo,
if (parsingText) { xw,
warn(`Encountered path operator "${fn}" inside of a text object.`); yh,
operatorList.addOp(OPS.save, null); DrawOPS.closePath
);
} else {
pathBuffer.push(
DrawOPS.moveTo,
x,
y,
DrawOPS.lineTo,
xw,
y,
DrawOPS.lineTo,
xw,
yh,
DrawOPS.lineTo,
x,
yh,
DrawOPS.closePath
);
}
minMax[0] = Math.min(minMax[0], x, xw);
minMax[1] = Math.min(minMax[1], y, yh);
minMax[2] = Math.max(minMax[2], x, xw);
minMax[3] = Math.max(minMax[3], y, yh);
break;
} }
case OPS.moveTo: {
let minMax; const x = (state.currentPointX = args[0]);
switch (fn) { const y = (state.currentPointY = args[1]);
case OPS.rectangle: pathBuffer.push(DrawOPS.moveTo, x, y);
const x = args[0] + args[2]; minMax[0] = Math.min(minMax[0], x);
const y = args[1] + args[3]; minMax[1] = Math.min(minMax[1], y);
minMax = [ minMax[2] = Math.max(minMax[2], x);
Math.min(args[0], x), minMax[3] = Math.max(minMax[3], y);
Math.min(args[1], y), break;
Math.max(args[0], x),
Math.max(args[1], y),
];
break;
case OPS.moveTo:
case OPS.lineTo:
minMax = [args[0], args[1], args[0], args[1]];
break;
default:
minMax = [Infinity, Infinity, -Infinity, -Infinity];
break;
} }
operatorList.addOp(OPS.constructPath, [[fn], args, minMax]); case OPS.lineTo: {
const x = (state.currentPointX = args[0]);
if (parsingText) { const y = (state.currentPointY = args[1]);
operatorList.addOp(OPS.restore, null); pathBuffer.push(DrawOPS.lineTo, x, y);
minMax[0] = Math.min(minMax[0], x);
minMax[1] = Math.min(minMax[1], y);
minMax[2] = Math.max(minMax[2], x);
minMax[3] = Math.max(minMax[3], y);
break;
} }
} else { case OPS.curveTo: {
const opArgs = operatorList.argsArray[lastIndex]; const startX = state.currentPointX;
opArgs[0].push(fn); const startY = state.currentPointY;
opArgs[1].push(...args); const [x1, y1, x2, y2, x, y] = args;
const minMax = opArgs[2]; state.currentPointX = x;
state.currentPointY = y;
// Compute min/max in the worker instead of the main thread. pathBuffer.push(DrawOPS.curveTo, x1, y1, x2, y2, x, y);
// If the current matrix (when drawing) is a scaling one Util.bezierBoundingBox(startX, startY, x1, y1, x2, y2, x, y, minMax);
// then min/max can be easily computed in using those values. break;
// Only rectangle, lineTo and moveTo are handled here since
// Bezier stuff requires to have the starting point.
switch (fn) {
case OPS.rectangle:
const x = args[0] + args[2];
const y = args[1] + args[3];
minMax[0] = Math.min(minMax[0], args[0], x);
minMax[1] = Math.min(minMax[1], args[1], y);
minMax[2] = Math.max(minMax[2], args[0], x);
minMax[3] = Math.max(minMax[3], args[1], y);
break;
case OPS.moveTo:
case OPS.lineTo:
minMax[0] = Math.min(minMax[0], args[0]);
minMax[1] = Math.min(minMax[1], args[1]);
minMax[2] = Math.max(minMax[2], args[0]);
minMax[3] = Math.max(minMax[3], args[1]);
break;
} }
case OPS.curveTo2: {
const startX = state.currentPointX;
const startY = state.currentPointY;
const [x1, y1, x, y] = args;
state.currentPointX = x;
state.currentPointY = y;
pathBuffer.push(DrawOPS.curveTo, startX, startY, x1, y1, x, y);
Util.bezierBoundingBox(
startX,
startY,
startX,
startY,
x1,
y1,
x,
y,
minMax
);
break;
}
case OPS.curveTo3: {
const startX = state.currentPointX;
const startY = state.currentPointY;
const [x1, y1, x, y] = args;
state.currentPointX = x;
state.currentPointY = y;
pathBuffer.push(DrawOPS.curveTo, x1, y1, x, y, x, y);
Util.bezierBoundingBox(startX, startY, x1, y1, x, y, x, y, minMax);
break;
}
case OPS.closePath:
pathBuffer.push(DrawOPS.closePath);
break;
} }
} }
@ -1731,7 +1764,6 @@ class PartialEvaluator {
const self = this; const self = this;
const xref = this.xref; const xref = this.xref;
let parsingText = false;
const localImageCache = new LocalImageCache(); const localImageCache = new LocalImageCache();
const localColorSpaceCache = new LocalColorSpaceCache(); const localColorSpaceCache = new LocalColorSpaceCache();
const localGStateCache = new LocalGStateCache(); const localGStateCache = new LocalGStateCache();
@ -1847,7 +1879,7 @@ class PartialEvaluator {
null, null,
operatorList, operatorList,
task, task,
stateManager.state.clone(), stateManager.state.clone({ newPath: true }),
localColorSpaceCache localColorSpaceCache
) )
.then(function () { .then(function () {
@ -1909,12 +1941,6 @@ class PartialEvaluator {
}) })
); );
return; return;
case OPS.beginText:
parsingText = true;
break;
case OPS.endText:
parsingText = false;
break;
case OPS.endInlineImage: case OPS.endInlineImage:
const cacheKey = args[0].cacheKey; const cacheKey = args[0].cacheKey;
if (cacheKey) { if (cacheKey) {
@ -2237,8 +2263,40 @@ class PartialEvaluator {
case OPS.curveTo3: case OPS.curveTo3:
case OPS.closePath: case OPS.closePath:
case OPS.rectangle: case OPS.rectangle:
self.buildPath(operatorList, fn, args, parsingText); self.buildPath(fn, args, stateManager.state);
continue; continue;
case OPS.stroke:
case OPS.closeStroke:
case OPS.fill:
case OPS.eoFill:
case OPS.fillStroke:
case OPS.eoFillStroke:
case OPS.closeFillStroke:
case OPS.closeEOFillStroke:
case OPS.endPath: {
const {
state: { pathBuffer, pathMinMax },
} = stateManager;
if (
fn === OPS.closeStroke ||
fn === OPS.closeFillStroke ||
fn === OPS.closeEOFillStroke
) {
pathBuffer.push(DrawOPS.closePath);
}
if (pathBuffer.length === 0) {
operatorList.addOp(OPS.constructPath, [fn, [null], null]);
} else {
operatorList.addOp(OPS.constructPath, [
fn,
[new Float32Array(pathBuffer)],
pathMinMax.slice(),
]);
pathBuffer.length = 0;
pathMinMax.set([Infinity, Infinity, -Infinity, -Infinity], 0);
}
continue;
}
case OPS.markPoint: case OPS.markPoint:
case OPS.markPointProps: case OPS.markPointProps:
case OPS.beginCompat: case OPS.beginCompat:
@ -4935,6 +4993,16 @@ class EvalState {
this._fillColorSpace = this._strokeColorSpace = ColorSpaceUtils.gray; this._fillColorSpace = this._strokeColorSpace = ColorSpaceUtils.gray;
this.patternFillColorSpace = null; this.patternFillColorSpace = null;
this.patternStrokeColorSpace = null; this.patternStrokeColorSpace = null;
// Path stuff.
this.currentPointX = this.currentPointY = 0;
this.pathMinMax = new Float32Array([
Infinity,
Infinity,
-Infinity,
-Infinity,
]);
this.pathBuffer = [];
} }
get fillColorSpace() { get fillColorSpace() {
@ -4953,8 +5021,18 @@ class EvalState {
this._strokeColorSpace = this.patternStrokeColorSpace = colorSpace; this._strokeColorSpace = this.patternStrokeColorSpace = colorSpace;
} }
clone() { clone({ newPath = false } = {}) {
return Object.create(this); const clone = Object.create(this);
if (newPath) {
clone.pathBuffer = [];
clone.pathMinMax = new Float32Array([
Infinity,
Infinity,
-Infinity,
-Infinity,
]);
}
return clone;
} }
} }

View File

@ -703,6 +703,12 @@ class OperatorList {
transfers.push(arg.data.buffer); transfers.push(arg.data.buffer);
} }
break; break;
case OPS.constructPath:
const [, [data], minMax] = argsArray[i];
if (data) {
transfers.push(data.buffer, minMax.buffer);
}
break;
} }
} }
return transfers; return transfers;

View File

@ -14,6 +14,7 @@
*/ */
import { import {
DrawOPS,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
IDENTITY_MATRIX, IDENTITY_MATRIX,
@ -58,6 +59,10 @@ const MAX_SIZE_TO_COMPILE = 1000;
const FULL_CHUNK_HEIGHT = 16; const FULL_CHUNK_HEIGHT = 16;
// Only used in rescaleAndStroke. The goal is to avoid
// creating a new DOMMatrix object each time we need it.
const SCALE_MATRIX = new DOMMatrix();
/** /**
* Overrides certain methods on a 2d ctx so that when they are called they * Overrides certain methods on a 2d ctx so that when they are called they
* will also call the same method on the destCtx. The methods that are * will also call the same method on the destCtx. The methods that are
@ -502,19 +507,6 @@ class CanvasExtraState {
return clone; return clone;
} }
setCurrentPoint(x, y) {
this.x = x;
this.y = y;
}
updatePathMinMax(transform, x, y) {
[x, y] = Util.applyTransform([x, y], transform);
this.minX = Math.min(this.minX, x);
this.minY = Math.min(this.minY, y);
this.maxX = Math.max(this.maxX, x);
this.maxY = Math.max(this.maxY, y);
}
updateRectMinMax(transform, rect) { updateRectMinMax(transform, rect) {
const p1 = Util.applyTransform(rect, transform); const p1 = Util.applyTransform(rect, transform);
const p2 = Util.applyTransform(rect.slice(2), transform); const p2 = Util.applyTransform(rect.slice(2), transform);
@ -527,22 +519,6 @@ class CanvasExtraState {
this.maxY = Math.max(this.maxY, p1[1], p2[1], p3[1], p4[1]); this.maxY = Math.max(this.maxY, p1[1], p2[1], p3[1], p4[1]);
} }
updateScalingPathMinMax(transform, minMax) {
Util.scaleMinMax(transform, minMax);
this.minX = Math.min(this.minX, minMax[0]);
this.minY = Math.min(this.minY, minMax[1]);
this.maxX = Math.max(this.maxX, minMax[2]);
this.maxY = Math.max(this.maxY, minMax[3]);
}
updateCurvePathMinMax(transform, x0, y0, x1, y1, x2, y2, x3, y3, minMax) {
const box = Util.bezierBoundingBox(x0, y0, x1, y1, x2, y2, x3, y3, minMax);
if (minMax) {
return;
}
this.updateRectMinMax(transform, box);
}
getPathBoundingBox(pathType = PathType.FILL, transform = null) { getPathBoundingBox(pathType = PathType.FILL, transform = null) {
const box = [this.minX, this.minY, this.maxX, this.maxY]; const box = [this.minX, this.minY, this.maxX, this.maxY];
if (pathType === PathType.STROKE) { if (pathType === PathType.STROKE) {
@ -1612,156 +1588,54 @@ class CanvasGraphics {
} }
// Path // Path
constructPath(ops, args, minMax) { constructPath(op, data, minMax) {
const ctx = this.ctx; let [path] = data;
const current = this.current; if (!minMax) {
let x = current.x, // The path is empty, so no need to update the current minMax.
y = current.y; path ||= data[0] = new Path2D();
let startX, startY; this[op](path);
const currentTransform = getCurrentTransform(ctx); return;
}
// Most of the time the current transform is a scaling matrix if (!(path instanceof Path2D)) {
// so we don't need to transform points before computing min/max: // Using a SVG string is slightly slower than using the following loop.
// we can compute min/max first and then smartly "apply" the const path2d = (data[0] = new Path2D());
// transform (see Util.scaleMinMax). for (let i = 0, ii = path.length; i < ii; ) {
// For rectangle, moveTo and lineTo, min/max are computed in the switch (path[i++]) {
// worker (see evaluator.js). case DrawOPS.moveTo:
const isScalingMatrix = path2d.moveTo(path[i++], path[i++]);
(currentTransform[0] === 0 && currentTransform[3] === 0) || break;
(currentTransform[1] === 0 && currentTransform[2] === 0); case DrawOPS.lineTo:
const minMaxForBezier = isScalingMatrix ? minMax.slice(0) : null; path2d.lineTo(path[i++], path[i++]);
break;
for (let i = 0, j = 0, ii = ops.length; i < ii; i++) { case DrawOPS.curveTo:
switch (ops[i] | 0) { path2d.bezierCurveTo(
case OPS.rectangle: path[i++],
x = args[j++]; path[i++],
y = args[j++]; path[i++],
const width = args[j++]; path[i++],
const height = args[j++]; path[i++],
path[i++]
const xw = x + width; );
const yh = y + height; break;
ctx.moveTo(x, y); case DrawOPS.closePath:
if (width === 0 || height === 0) { path2d.closePath();
ctx.lineTo(xw, yh); break;
} else { default:
ctx.lineTo(xw, y); warn(`Unrecognized drawing path operator: ${path[i - 1]}`);
ctx.lineTo(xw, yh); break;
ctx.lineTo(x, yh); }
}
if (!isScalingMatrix) {
current.updateRectMinMax(currentTransform, [x, y, xw, yh]);
}
ctx.closePath();
break;
case OPS.moveTo:
x = args[j++];
y = args[j++];
ctx.moveTo(x, y);
if (!isScalingMatrix) {
current.updatePathMinMax(currentTransform, x, y);
}
break;
case OPS.lineTo:
x = args[j++];
y = args[j++];
ctx.lineTo(x, y);
if (!isScalingMatrix) {
current.updatePathMinMax(currentTransform, x, y);
}
break;
case OPS.curveTo:
startX = x;
startY = y;
x = args[j + 4];
y = args[j + 5];
ctx.bezierCurveTo(
args[j],
args[j + 1],
args[j + 2],
args[j + 3],
x,
y
);
current.updateCurvePathMinMax(
currentTransform,
startX,
startY,
args[j],
args[j + 1],
args[j + 2],
args[j + 3],
x,
y,
minMaxForBezier
);
j += 6;
break;
case OPS.curveTo2:
startX = x;
startY = y;
ctx.bezierCurveTo(
x,
y,
args[j],
args[j + 1],
args[j + 2],
args[j + 3]
);
current.updateCurvePathMinMax(
currentTransform,
startX,
startY,
x,
y,
args[j],
args[j + 1],
args[j + 2],
args[j + 3],
minMaxForBezier
);
x = args[j + 2];
y = args[j + 3];
j += 4;
break;
case OPS.curveTo3:
startX = x;
startY = y;
x = args[j + 2];
y = args[j + 3];
ctx.bezierCurveTo(args[j], args[j + 1], x, y, x, y);
current.updateCurvePathMinMax(
currentTransform,
startX,
startY,
args[j],
args[j + 1],
x,
y,
x,
y,
minMaxForBezier
);
j += 4;
break;
case OPS.closePath:
ctx.closePath();
break;
} }
path = path2d;
} }
this.current.updateRectMinMax(getCurrentTransform(this.ctx), minMax);
if (isScalingMatrix) { this[op](path);
current.updateScalingPathMinMax(currentTransform, minMaxForBezier);
}
current.setCurrentPoint(x, y);
} }
closePath() { closePath() {
this.ctx.closePath(); this.ctx.closePath();
} }
stroke(consumePath = true) { stroke(path, consumePath = true) {
const ctx = this.ctx; const ctx = this.ctx;
const strokeColor = this.current.strokeColor; const strokeColor = this.current.strokeColor;
// For stroke we want to temporarily change the global alpha to the // For stroke we want to temporarily change the global alpha to the
@ -1769,6 +1643,9 @@ class CanvasGraphics {
ctx.globalAlpha = this.current.strokeAlpha; ctx.globalAlpha = this.current.strokeAlpha;
if (this.contentVisible) { if (this.contentVisible) {
if (typeof strokeColor === "object" && strokeColor?.getPattern) { if (typeof strokeColor === "object" && strokeColor?.getPattern) {
const baseTransform = strokeColor.isModifyingCurrentTransform()
? ctx.getTransform()
: null;
ctx.save(); ctx.save();
ctx.strokeStyle = strokeColor.getPattern( ctx.strokeStyle = strokeColor.getPattern(
ctx, ctx,
@ -1776,31 +1653,41 @@ class CanvasGraphics {
getCurrentTransformInverse(ctx), getCurrentTransformInverse(ctx),
PathType.STROKE PathType.STROKE
); );
this.rescaleAndStroke(/* saveRestore */ false); if (baseTransform) {
const newPath = new Path2D();
newPath.addPath(
path,
ctx.getTransform().invertSelf().multiplySelf(baseTransform)
);
path = newPath;
}
this.rescaleAndStroke(path, /* saveRestore */ false);
ctx.restore(); ctx.restore();
} else { } else {
this.rescaleAndStroke(/* saveRestore */ true); this.rescaleAndStroke(path, /* saveRestore */ true);
} }
} }
if (consumePath) { if (consumePath) {
this.consumePath(this.current.getClippedPathBoundingBox()); this.consumePath(path, this.current.getClippedPathBoundingBox());
} }
// Restore the global alpha to the fill alpha // Restore the global alpha to the fill alpha
ctx.globalAlpha = this.current.fillAlpha; ctx.globalAlpha = this.current.fillAlpha;
} }
closeStroke() { closeStroke(path) {
this.closePath(); this.stroke(path);
this.stroke();
} }
fill(consumePath = true) { fill(path, consumePath = true) {
const ctx = this.ctx; const ctx = this.ctx;
const fillColor = this.current.fillColor; const fillColor = this.current.fillColor;
const isPatternFill = this.current.patternFill; const isPatternFill = this.current.patternFill;
let needRestore = false; let needRestore = false;
if (isPatternFill) { if (isPatternFill) {
const baseTransform = fillColor.isModifyingCurrentTransform()
? ctx.getTransform()
: null;
ctx.save(); ctx.save();
ctx.fillStyle = fillColor.getPattern( ctx.fillStyle = fillColor.getPattern(
ctx, ctx,
@ -1808,16 +1695,24 @@ class CanvasGraphics {
getCurrentTransformInverse(ctx), getCurrentTransformInverse(ctx),
PathType.FILL PathType.FILL
); );
if (baseTransform) {
const newPath = new Path2D();
newPath.addPath(
path,
ctx.getTransform().invertSelf().multiplySelf(baseTransform)
);
path = newPath;
}
needRestore = true; needRestore = true;
} }
const intersect = this.current.getClippedPathBoundingBox(); const intersect = this.current.getClippedPathBoundingBox();
if (this.contentVisible && intersect !== null) { if (this.contentVisible && intersect !== null) {
if (this.pendingEOFill) { if (this.pendingEOFill) {
ctx.fill("evenodd"); ctx.fill(path, "evenodd");
this.pendingEOFill = false; this.pendingEOFill = false;
} else { } else {
ctx.fill(); ctx.fill(path);
} }
} }
@ -1825,40 +1720,38 @@ class CanvasGraphics {
ctx.restore(); ctx.restore();
} }
if (consumePath) { if (consumePath) {
this.consumePath(intersect); this.consumePath(path, intersect);
} }
} }
eoFill() { eoFill(path) {
this.pendingEOFill = true; this.pendingEOFill = true;
this.fill(); this.fill(path);
} }
fillStroke() { fillStroke(path) {
this.fill(false); this.fill(path, false);
this.stroke(false); this.stroke(path, false);
this.consumePath(); this.consumePath(path);
} }
eoFillStroke() { eoFillStroke(path) {
this.pendingEOFill = true; this.pendingEOFill = true;
this.fillStroke(); this.fillStroke(path);
} }
closeFillStroke() { closeFillStroke(path) {
this.closePath(); this.fillStroke(path);
this.fillStroke();
} }
closeEOFillStroke() { closeEOFillStroke(path) {
this.pendingEOFill = true; this.pendingEOFill = true;
this.closePath(); this.fillStroke(path);
this.fillStroke();
} }
endPath() { endPath(path) {
this.consumePath(); this.consumePath(path);
} }
// Clipping // Clipping
@ -3168,7 +3061,7 @@ class CanvasGraphics {
// Helper functions // Helper functions
consumePath(clipBox) { consumePath(path, clipBox) {
const isEmpty = this.current.isEmptyClip(); const isEmpty = this.current.isEmptyClip();
if (this.pendingClip) { if (this.pendingClip) {
this.current.updateClipFromPath(); this.current.updateClipFromPath();
@ -3180,9 +3073,9 @@ class CanvasGraphics {
if (this.pendingClip) { if (this.pendingClip) {
if (!isEmpty) { if (!isEmpty) {
if (this.pendingClip === EO_CLIP) { if (this.pendingClip === EO_CLIP) {
ctx.clip("evenodd"); ctx.clip(path, "evenodd");
} else { } else {
ctx.clip(); ctx.clip(path);
} }
} }
this.pendingClip = null; this.pendingClip = null;
@ -3267,15 +3160,16 @@ class CanvasGraphics {
// Rescale before stroking in order to have a final lineWidth // Rescale before stroking in order to have a final lineWidth
// with both thicknesses greater or equal to 1. // with both thicknesses greater or equal to 1.
rescaleAndStroke(saveRestore) { rescaleAndStroke(path, saveRestore) {
const { ctx } = this; const {
const { lineWidth } = this.current; ctx,
current: { lineWidth },
} = this;
const [scaleX, scaleY] = this.getScaleForStroking(); const [scaleX, scaleY] = this.getScaleForStroking();
ctx.lineWidth = lineWidth || 1; if (scaleX === scaleY) {
ctx.lineWidth = (lineWidth || 1) * scaleX;
if (scaleX === 1 && scaleY === 1) { ctx.stroke(path);
ctx.stroke();
return; return;
} }
@ -3285,6 +3179,10 @@ class CanvasGraphics {
} }
ctx.scale(scaleX, scaleY); ctx.scale(scaleX, scaleY);
SCALE_MATRIX.a = 1 / scaleX;
SCALE_MATRIX.d = 1 / scaleY;
const newPath = new Path2D();
newPath.addPath(path, SCALE_MATRIX);
// How the dashed line is rendered depends on the current transform... // How the dashed line is rendered depends on the current transform...
// so we added a rescale to handle too thin lines and consequently // so we added a rescale to handle too thin lines and consequently
@ -3299,7 +3197,8 @@ class CanvasGraphics {
ctx.lineDashOffset /= scale; ctx.lineDashOffset /= scale;
} }
ctx.stroke(); ctx.lineWidth = lineWidth || 1;
ctx.stroke(newPath);
if (saveRestore) { if (saveRestore) {
ctx.restore(); ctx.restore();

View File

@ -43,6 +43,10 @@ class BaseShadingPattern {
} }
} }
isModifyingCurrentTransform() {
return false;
}
getPattern() { getPattern() {
unreachable("Abstract method `getPattern` called."); unreachable("Abstract method `getPattern` called.");
} }
@ -388,6 +392,10 @@ class MeshShadingPattern extends BaseShadingPattern {
}; };
} }
isModifyingCurrentTransform() {
return true;
}
getPattern(ctx, owner, inverse, pathType) { getPattern(ctx, owner, inverse, pathType) {
applyBoundingBox(ctx, this._bbox); applyBoundingBox(ctx, this._bbox);
let scale; let scale;
@ -704,6 +712,10 @@ class TilingPattern {
} }
} }
isModifyingCurrentTransform() {
return false;
}
getPattern(ctx, owner, inverse, pathType) { getPattern(ctx, owner, inverse, pathType) {
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix. // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = inverse; let matrix = inverse;

View File

@ -341,6 +341,15 @@ const OPS = {
setFillTransparent: 93, setFillTransparent: 93,
}; };
// In order to have a switch statement that is fast (i.e. which use a jump
// table), we need to have the OPS in a contiguous range.
const DrawOPS = {
moveTo: 0,
lineTo: 1,
curveTo: 2,
closePath: 3,
};
const PasswordResponses = { const PasswordResponses = {
NEED_PASSWORD: 1, NEED_PASSWORD: 1,
INCORRECT_PASSWORD: 2, INCORRECT_PASSWORD: 2,
@ -667,57 +676,6 @@ class Util {
return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`; return `#${hexNumbers[r]}${hexNumbers[g]}${hexNumbers[b]}`;
} }
// Apply a scaling matrix to some min/max values.
// If a scaling factor is negative then min and max must be
// swapped.
static scaleMinMax(transform, minMax) {
let temp;
if (transform[0]) {
if (transform[0] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[0];
minMax[2] *= transform[0];
if (transform[3] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[3];
minMax[3] *= transform[3];
} else {
temp = minMax[0];
minMax[0] = minMax[1];
minMax[1] = temp;
temp = minMax[2];
minMax[2] = minMax[3];
minMax[3] = temp;
if (transform[1] < 0) {
temp = minMax[1];
minMax[1] = minMax[3];
minMax[3] = temp;
}
minMax[1] *= transform[1];
minMax[3] *= transform[1];
if (transform[2] < 0) {
temp = minMax[0];
minMax[0] = minMax[2];
minMax[2] = temp;
}
minMax[0] *= transform[2];
minMax[2] *= transform[2];
}
minMax[0] += transform[4];
minMax[1] += transform[5];
minMax[2] += transform[4];
minMax[3] += transform[5];
}
// Concatenates two transformation matrices together and returns the result. // Concatenates two transformation matrices together and returns the result.
static transform(m1, m2) { static transform(m1, m2) {
return [ return [
@ -1192,6 +1150,7 @@ export {
bytesToString, bytesToString,
createValidAbsoluteUrl, createValidAbsoluteUrl,
DocumentActionEventType, DocumentActionEventType,
DrawOPS,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FormatError, FormatError,

View File

@ -0,0 +1 @@
https://bugzilla.mozilla.org/attachment.cgi?id=9464841

View File

@ -11995,5 +11995,15 @@
"md5": "9993aa298c0214a3d3ff5f90ce0d40bb", "md5": "9993aa298c0214a3d3ff5f90ce0d40bb",
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
},
{
"id": "bug1946953",
"file": "pdfs/bug1946953.pdf",
"md5": "a71fb64e348b9c7161945e48e75c6681",
"rounds": 1,
"link": true,
"firstPage": 1,
"lastPage": 1,
"type": "eq"
} }
] ]

View File

@ -26,6 +26,7 @@ import {
AnnotationFieldFlag, AnnotationFieldFlag,
AnnotationFlag, AnnotationFlag,
AnnotationType, AnnotationType,
DrawOPS,
OPS, OPS,
RenderingIntentFlag, RenderingIntentFlag,
stringToBytes, stringToBytes,
@ -4285,14 +4286,13 @@ describe("annotation", function () {
null null
); );
expect(opList.fnArray.length).toEqual(16); expect(opList.fnArray.length).toEqual(15);
expect(opList.fnArray).toEqual([ expect(opList.fnArray).toEqual([
OPS.beginAnnotation, OPS.beginAnnotation,
OPS.save, OPS.save,
OPS.transform, OPS.transform,
OPS.constructPath,
OPS.clip, OPS.clip,
OPS.endPath, OPS.constructPath,
OPS.beginText, OPS.beginText,
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.setCharSpacing, OPS.setCharSpacing,
@ -4659,7 +4659,7 @@ describe("annotation", function () {
null null
); );
expect(opList.argsArray.length).toEqual(8); expect(opList.argsArray.length).toEqual(7);
expect(opList.fnArray).toEqual([ expect(opList.fnArray).toEqual([
OPS.beginAnnotation, OPS.beginAnnotation,
OPS.setLineWidth, OPS.setLineWidth,
@ -4667,7 +4667,6 @@ describe("annotation", function () {
OPS.setLineJoin, OPS.setLineJoin,
OPS.setStrokeRGBColor, OPS.setStrokeRGBColor,
OPS.constructPath, OPS.constructPath,
OPS.stroke,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
@ -4680,10 +4679,23 @@ describe("annotation", function () {
// Color. // Color.
expect(opList.argsArray[4]).toEqual(new Uint8ClampedArray([0, 255, 0])); expect(opList.argsArray[4]).toEqual(new Uint8ClampedArray([0, 255, 0]));
// Path. // Path.
expect(opList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]); expect(opList.argsArray[5][0]).toEqual(OPS.stroke);
expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); expect(opList.argsArray[5][1]).toEqual([
new Float32Array([
DrawOPS.moveTo,
1,
2,
DrawOPS.curveTo,
3,
4,
5,
6,
7,
8,
]),
]);
// Min-max. // Min-max.
expect(opList.argsArray[5][2]).toEqual([1, 2, 1, 2]); expect(opList.argsArray[5][2]).toEqual(new Float32Array([1, 2, 7, 8]));
}); });
}); });
@ -4831,13 +4843,12 @@ describe("annotation", function () {
null null
); );
expect(opList.argsArray.length).toEqual(6); expect(opList.argsArray.length).toEqual(5);
expect(opList.fnArray).toEqual([ expect(opList.fnArray).toEqual([
OPS.beginAnnotation, OPS.beginAnnotation,
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.setGState, OPS.setGState,
OPS.constructPath, OPS.constructPath,
OPS.eoFill,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
}); });
@ -4953,13 +4964,12 @@ describe("annotation", function () {
null null
); );
expect(opList.argsArray.length).toEqual(6); expect(opList.argsArray.length).toEqual(5);
expect(opList.fnArray).toEqual([ expect(opList.fnArray).toEqual([
OPS.beginAnnotation, OPS.beginAnnotation,
OPS.setFillRGBColor, OPS.setFillRGBColor,
OPS.setGState, OPS.setGState,
OPS.constructPath, OPS.constructPath,
OPS.fill,
OPS.endAnnotation, OPS.endAnnotation,
]); ]);
}); });

View File

@ -17,6 +17,7 @@ import {
AnnotationEditorType, AnnotationEditorType,
AnnotationMode, AnnotationMode,
AnnotationType, AnnotationType,
DrawOPS,
ImageKind, ImageKind,
InvalidPDFException, InvalidPDFException,
isNodeJS, isNodeJS,
@ -794,17 +795,25 @@ describe("api", function () {
OPS.setLineWidth, OPS.setLineWidth,
OPS.setStrokeRGBColor, OPS.setStrokeRGBColor,
OPS.constructPath, OPS.constructPath,
OPS.closeStroke,
]); ]);
expect(opList.argsArray).toEqual([ expect(opList.argsArray).toEqual([
[0.5], [0.5],
new Uint8ClampedArray([255, 0, 0]), new Uint8ClampedArray([255, 0, 0]),
[ [
[OPS.moveTo, OPS.lineTo], OPS.closeStroke,
[0, 9.75, 0.5, 9.75], [
[0, 9.75, 0.5, 9.75], new Float32Array([
DrawOPS.moveTo,
0,
9.75,
DrawOPS.lineTo,
0.5,
9.75,
DrawOPS.closePath,
]),
],
new Float32Array([0, 9.75, 0.5, 9.75]),
], ],
null,
]); ]);
expect(opList.lastChunk).toEqual(true); expect(opList.lastChunk).toEqual(true);
@ -4236,8 +4245,8 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
const opListAnnotEnable = await pdfPage.getOperatorList({ const opListAnnotEnable = await pdfPage.getOperatorList({
annotationMode: AnnotationMode.ENABLE, annotationMode: AnnotationMode.ENABLE,
}); });
expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(140); expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(130);
expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(140); expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(130);
expect(opListAnnotEnable.lastChunk).toEqual(true); expect(opListAnnotEnable.lastChunk).toEqual(true);
expect(opListAnnotEnable.separateAnnots).toEqual({ expect(opListAnnotEnable.separateAnnots).toEqual({
form: false, form: false,
@ -4270,8 +4279,8 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`)
const opListAnnotEnableStorage = await pdfPage.getOperatorList({ const opListAnnotEnableStorage = await pdfPage.getOperatorList({
annotationMode: AnnotationMode.ENABLE_STORAGE, annotationMode: AnnotationMode.ENABLE_STORAGE,
}); });
expect(opListAnnotEnableStorage.fnArray.length).toBeGreaterThan(170); expect(opListAnnotEnableStorage.fnArray.length).toBeGreaterThan(150);
expect(opListAnnotEnableStorage.argsArray.length).toBeGreaterThan(170); expect(opListAnnotEnableStorage.argsArray.length).toBeGreaterThan(150);
expect(opListAnnotEnableStorage.lastChunk).toEqual(true); expect(opListAnnotEnableStorage.lastChunk).toEqual(true);
expect(opListAnnotEnableStorage.separateAnnots).toEqual({ expect(opListAnnotEnableStorage.separateAnnots).toEqual({
form: false, form: false,

View File

@ -74,8 +74,8 @@ describe("evaluator", function () {
); );
expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray.length).toEqual(1); expect(result.fnArray.length).toEqual(1);
expect(result.fnArray[0]).toEqual(OPS.fill); expect(result.fnArray[0]).toEqual(OPS.constructPath);
expect(result.argsArray[0]).toEqual(null); expect(result.argsArray[0]).toEqual([OPS.fill, [null], null]);
}); });
it("should handle one operation", async function () { it("should handle one operation", async function () {
@ -130,9 +130,14 @@ describe("evaluator", function () {
); );
expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray.length).toEqual(3); expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[0]).toEqual(OPS.fill); expect(result.fnArray).toEqual([
expect(result.fnArray[1]).toEqual(OPS.fill); OPS.constructPath,
expect(result.fnArray[2]).toEqual(OPS.fill); OPS.constructPath,
OPS.constructPath,
]);
expect(result.argsArray[0][0]).toEqual(OPS.fill);
expect(result.argsArray[1][0]).toEqual(OPS.fill);
expect(result.argsArray[2][0]).toEqual(OPS.fill);
}); });
it("should handle three glued operations #2", async function () { it("should handle three glued operations #2", async function () {
@ -145,10 +150,14 @@ describe("evaluator", function () {
resources resources
); );
expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray.length).toEqual(3); expect(result.fnArray).toEqual([
expect(result.fnArray[0]).toEqual(OPS.eoFillStroke); OPS.constructPath,
expect(result.fnArray[1]).toEqual(OPS.fillStroke); OPS.constructPath,
expect(result.fnArray[2]).toEqual(OPS.eoFill); OPS.constructPath,
]);
expect(result.argsArray[0][0]).toEqual(OPS.eoFillStroke);
expect(result.argsArray[1][0]).toEqual(OPS.fillStroke);
expect(result.argsArray[2][0]).toEqual(OPS.eoFill);
}); });
it("should handle glued operations and operands", async function () { it("should handle glued operations and operands", async function () {
@ -160,7 +169,7 @@ describe("evaluator", function () {
); );
expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(!!result.fnArray && !!result.argsArray).toEqual(true);
expect(result.fnArray.length).toEqual(2); expect(result.fnArray.length).toEqual(2);
expect(result.fnArray[0]).toEqual(OPS.fill); expect(result.fnArray[0]).toEqual(OPS.constructPath);
expect(result.fnArray[1]).toEqual(OPS.setTextRise); expect(result.fnArray[1]).toEqual(OPS.setTextRise);
expect(result.argsArray.length).toEqual(2); expect(result.argsArray.length).toEqual(2);
expect(result.argsArray[1].length).toEqual(1); expect(result.argsArray[1].length).toEqual(1);
@ -178,13 +187,13 @@ describe("evaluator", function () {
expect(result.fnArray.length).toEqual(3); expect(result.fnArray.length).toEqual(3);
expect(result.fnArray[0]).toEqual(OPS.setFlatness); expect(result.fnArray[0]).toEqual(OPS.setFlatness);
expect(result.fnArray[1]).toEqual(OPS.setRenderingIntent); expect(result.fnArray[1]).toEqual(OPS.setRenderingIntent);
expect(result.fnArray[2]).toEqual(OPS.endPath); expect(result.fnArray[2]).toEqual(OPS.constructPath);
expect(result.argsArray.length).toEqual(3); expect(result.argsArray.length).toEqual(3);
expect(result.argsArray[0].length).toEqual(1); expect(result.argsArray[0].length).toEqual(1);
expect(result.argsArray[0][0]).toEqual(true); expect(result.argsArray[0][0]).toEqual(true);
expect(result.argsArray[1].length).toEqual(1); expect(result.argsArray[1].length).toEqual(1);
expect(result.argsArray[1][0]).toEqual(false); expect(result.argsArray[1][0]).toEqual(false);
expect(result.argsArray[2]).toEqual(null); expect(result.argsArray[2]).toEqual([OPS.endPath, [null], null]);
}); });
}); });