Compare commits

...

6 Commits

Author SHA1 Message Date
calixteman
d009e4b3a7
Merge pull request #19689 from calixteman/use_path2d
[api-minor] Use a Path2D when doing a path operation in the canvas (bug 1946953)
2025-03-22 21:46:27 +01:00
Tim van der Meij
72212a892b
Merge pull request #19705 from timvandermeij/integration-tests-scripting-isolate
Isolate the scripting integration tests
2025-03-22 20:38:50 +01:00
Calixte Denizet
be1f5671bb [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.
2025-03-22 20:35:24 +01:00
Tim van der Meij
1c73e526e0
Isolate the scripting integration tests
To avoid being able to introduce dependencies between tests, and to
bring existing dependencies to the surface, this commit makes sure that
we close the document between tests so that we can't accidentally rely
on state set by a previous test. This prevents multiple tests from
failing if one of them fails and makes debugging easier by being able to
run each test on their own independent of other tests.

This commit, combined with the previous ones, is enough to make the
scripting integration test suite pass consistently if random mode in
Jasmine is enabled, proving that the tests are fully isolated now.
2025-03-22 18:31:23 +01:00
Tim van der Meij
0f3692a621
Remove the dependency between the order-dependent scripting integration tests
The integration tests are order-dependent because they rely on input
field state set by a previous test. This commit fixes the issue by
updating the values to match the initial state of the document, which
makes sure that we don't build upon values from previous tests while
still testing the intended logic in the individual tests like before.
2025-03-22 18:31:23 +01:00
Tim van der Meij
334d681359
Refactor the order-dependent scripting integration tests
By checking for the expected value directly we can shorten the code, and
it simplifies removing the dependencies between the tests in the next
commit (by having fewer places to change). Note that this follows the
same pattern as PRs #19192, #19001 and #18399 and also helps to remove
any further possibilities for intermittent failures.
2025-03-22 18:31:10 +01:00
11 changed files with 486 additions and 530 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
} );
let minMax;
switch (fn) {
case OPS.rectangle:
const x = args[0] + args[2];
const y = args[1] + args[3];
minMax = [
Math.min(args[0], x),
Math.min(args[1], y),
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]);
if (parsingText) {
operatorList.addOp(OPS.restore, null);
}
} else { } else {
const opArgs = operatorList.argsArray[lastIndex]; pathBuffer.push(
opArgs[0].push(fn); DrawOPS.moveTo,
opArgs[1].push(...args); x,
const minMax = opArgs[2]; y,
DrawOPS.lineTo,
// Compute min/max in the worker instead of the main thread. xw,
// If the current matrix (when drawing) is a scaling one y,
// then min/max can be easily computed in using those values. DrawOPS.lineTo,
// Only rectangle, lineTo and moveTo are handled here since xw,
// Bezier stuff requires to have the starting point. yh,
switch (fn) { DrawOPS.lineTo,
case OPS.rectangle: x,
const x = args[0] + args[2]; yh,
const y = args[1] + args[3]; DrawOPS.closePath
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[0] = Math.min(minMax[0], x, xw);
minMax[3] = Math.max(minMax[3], args[1], y); minMax[1] = Math.min(minMax[1], y, yh);
break; minMax[2] = Math.max(minMax[2], x, xw);
case OPS.moveTo: minMax[3] = Math.max(minMax[3], y, yh);
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; break;
} }
case OPS.moveTo: {
const x = (state.currentPointX = args[0]);
const y = (state.currentPointY = args[1]);
pathBuffer.push(DrawOPS.moveTo, 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;
}
case OPS.lineTo: {
const x = (state.currentPointX = args[0]);
const y = (state.currentPointY = args[1]);
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;
}
case OPS.curveTo: {
const startX = state.currentPointX;
const startY = state.currentPointY;
const [x1, y1, x2, y2, x, y] = args;
state.currentPointX = x;
state.currentPointY = y;
pathBuffer.push(DrawOPS.curveTo, x1, y1, x2, y2, x, y);
Util.bezierBoundingBox(startX, startY, x1, y1, x2, y2, x, y, minMax);
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
// so we don't need to transform points before computing min/max:
// we can compute min/max first and then smartly "apply" the
// transform (see Util.scaleMinMax).
// For rectangle, moveTo and lineTo, min/max are computed in the
// worker (see evaluator.js).
const isScalingMatrix =
(currentTransform[0] === 0 && currentTransform[3] === 0) ||
(currentTransform[1] === 0 && currentTransform[2] === 0);
const minMaxForBezier = isScalingMatrix ? minMax.slice(0) : null;
for (let i = 0, j = 0, ii = ops.length; i < ii; i++) {
switch (ops[i] | 0) {
case OPS.rectangle:
x = args[j++];
y = args[j++];
const width = args[j++];
const height = args[j++];
const xw = x + width;
const yh = y + height;
ctx.moveTo(x, y);
if (width === 0 || height === 0) {
ctx.lineTo(xw, yh);
} else {
ctx.lineTo(xw, y);
ctx.lineTo(xw, yh);
ctx.lineTo(x, yh);
} }
if (!isScalingMatrix) { if (!(path instanceof Path2D)) {
current.updateRectMinMax(currentTransform, [x, y, xw, yh]); // Using a SVG string is slightly slower than using the following loop.
} const path2d = (data[0] = new Path2D());
ctx.closePath(); for (let i = 0, ii = path.length; i < ii; ) {
switch (path[i++]) {
case DrawOPS.moveTo:
path2d.moveTo(path[i++], path[i++]);
break; break;
case OPS.moveTo: case DrawOPS.lineTo:
x = args[j++]; path2d.lineTo(path[i++], path[i++]);
y = args[j++];
ctx.moveTo(x, y);
if (!isScalingMatrix) {
current.updatePathMinMax(currentTransform, x, y);
}
break; break;
case OPS.lineTo: case DrawOPS.curveTo:
x = args[j++]; path2d.bezierCurveTo(
y = args[j++]; path[i++],
ctx.lineTo(x, y); path[i++],
if (!isScalingMatrix) { path[i++],
current.updatePathMinMax(currentTransform, x, y); path[i++],
} path[i++],
break; path[i++]
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; break;
case OPS.curveTo2: case DrawOPS.closePath:
startX = x; path2d.closePath();
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; break;
case OPS.curveTo3: default:
startX = x; warn(`Unrecognized drawing path operator: ${path[i - 1]}`);
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; break;
} }
} }
path = path2d;
if (isScalingMatrix) {
current.updateScalingPathMinMax(currentTransform, minMaxForBezier);
} }
this.current.updateRectMinMax(getCurrentTransform(this.ctx), minMax);
current.setCurrentPoint(x, y); this[op](path);
} }
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 [
@ -1223,6 +1181,7 @@ export {
bytesToString, bytesToString,
createValidAbsoluteUrl, createValidAbsoluteUrl,
DocumentActionEventType, DocumentActionEventType,
DrawOPS,
FeatureTest, FeatureTest,
FONT_IDENTITY_MATRIX, FONT_IDENTITY_MATRIX,
FormatError, FormatError,

View File

@ -74,11 +74,11 @@ describe("Interaction", () => {
describe("in 160F-2019.pdf", () => { describe("in 160F-2019.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("160F-2019.pdf", getSelector("416R")); pages = await loadAndWait("160F-2019.pdf", getSelector("416R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -150,15 +150,12 @@ describe("Interaction", () => {
await page.type(getSelector("416R"), "3.14159"); await page.type(getSelector("416R"), "3.14159");
await page.click(getSelector("419R")); await page.click(getSelector("419R"));
await page.waitForFunction(
const valueFnStr = `${getQuerySelector("416R")}.value !== "3.14159"`; `${getQuerySelector("416R")}.value === "3,14"`
await page.waitForFunction(valueFnStr); );
await page.waitForFunction(
const text = await page.$eval(getSelector("416R"), el => el.value); `${getQuerySelector("427R")}.value === "3,14"`
expect(text).withContext(`In ${browserName}`).toEqual("3,14"); );
const sum = await page.$eval(getSelector("427R"), el => el.value);
expect(sum).withContext(`In ${browserName}`).toEqual("3,14");
}) })
); );
}); });
@ -170,36 +167,23 @@ describe("Interaction", () => {
await page.type(getSelector("448R"), "61803"); await page.type(getSelector("448R"), "61803");
await page.click(getSelector("419R")); await page.click(getSelector("419R"));
await page.waitForFunction(
const valueOneFnStr = `${getQuerySelector("448R")}.value !== "61803"`; `${getQuerySelector("448R")}.value === "61.803,00"`
await page.waitForFunction(valueOneFnStr); );
let text = await page.$eval(getSelector("448R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("61.803,00");
await page.click(getSelector("448R")); await page.click(getSelector("448R"));
await page.waitForFunction(
const valueTwoFnStr = `${getQuerySelector( `${getQuerySelector("448R")}.value === "61803"`
"448R" );
)}.value !== "61.803,00"`;
await page.waitForFunction(valueTwoFnStr);
text = await page.$eval(getSelector("448R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("61803");
// Clear the textfield // Clear the textfield
await clearInput(page, getSelector("448R")); await clearInput(page, getSelector("448R"));
await page.type(getSelector("448R"), "1.61803"); await page.type(getSelector("448R"), "1.61803");
await page.click(getSelector("419R")); await page.click(getSelector("419R"));
await page.waitForFunction(
const valueThreeFnStr = `${getQuerySelector( `${getQuerySelector("448R")}.value === "1,62"`
"448R" );
)}.value !== "1.61803"`;
await page.waitForFunction(valueThreeFnStr);
text = await page.$eval(getSelector("448R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("1,62");
}) })
); );
}); });
@ -209,24 +193,14 @@ describe("Interaction", () => {
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
await waitForScripting(page); await waitForScripting(page);
const prevSum = await page.$eval(getSelector("427R"), el => el.value);
await page.type(getSelector("422R"), "2.7182818"); await page.type(getSelector("422R"), "2.7182818");
await page.keyboard.press("Tab"); await page.keyboard.press("Tab");
await page.waitForFunction( await page.waitForFunction(
`${getQuerySelector("422R")}.value !== "2.7182818"` `${getQuerySelector("422R")}.value === "2,72"`
); );
const text = await page.$eval(getSelector("422R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("2,72");
await page.waitForFunction( await page.waitForFunction(
`${getQuerySelector("427R")}.value !== "${prevSum}"` `${getQuerySelector("427R")}.value === "2,72"`
); );
const sum = await page.$eval(getSelector("427R"), el => el.value);
expect(sum).withContext(`In ${browserName}`).toEqual("5,86");
}) })
); );
}); });
@ -236,21 +210,14 @@ describe("Interaction", () => {
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
await waitForScripting(page); await waitForScripting(page);
let sum = await page.$eval(getSelector("471R"), el => el.value);
expect(sum).withContext(`In ${browserName}`).toEqual("4,24");
await page.type(getSelector("436R"), "0.69314"); await page.type(getSelector("436R"), "0.69314");
await page.keyboard.press("Escape"); await page.keyboard.press("Escape");
const text = await page.$eval(getSelector("436R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("0.69314");
await page.waitForFunction( await page.waitForFunction(
`${getQuerySelector("471R")}.value !== "${sum}"` `${getQuerySelector("436R")}.value === "0.69314"`
);
await page.waitForFunction(
`${getQuerySelector("471R")}.value === "0,69"`
); );
sum = await page.$eval(getSelector("471R"), el => el.value);
expect(sum).withContext(`In ${browserName}`).toEqual("3,55");
}) })
); );
}); });
@ -260,19 +227,14 @@ describe("Interaction", () => {
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
await waitForScripting(page); await waitForScripting(page);
const prevSum = await page.$eval(getSelector("427R"), el => el.value);
await page.type(getSelector("419R"), "0.577215"); await page.type(getSelector("419R"), "0.577215");
await page.keyboard.press("Enter"); await page.keyboard.press("Enter");
const text = await page.$eval(getSelector("419R"), el => el.value);
expect(text).toEqual("0.577215");
await page.waitForFunction( await page.waitForFunction(
`${getQuerySelector("427R")}.value !== "${prevSum}"` `${getQuerySelector("419R")}.value === "0.577215"`
);
await page.waitForFunction(
`${getQuerySelector("427R")}.value === "0,58"`
); );
const sum = await page.$eval(getSelector("427R"), el => el.value);
expect(sum).toEqual("6,44");
}) })
); );
}); });
@ -329,11 +291,11 @@ describe("Interaction", () => {
describe("in js-buttons.pdf", () => { describe("in js-buttons.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("js-buttons.pdf", getSelector("80R")); pages = await loadAndWait("js-buttons.pdf", getSelector("80R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -456,7 +418,7 @@ describe("Interaction", () => {
describe("in doc_actions.pdf for printing", () => { describe("in doc_actions.pdf for printing", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("doc_actions.pdf", getSelector("47R"), null, { pages = await loadAndWait("doc_actions.pdf", getSelector("47R"), null, {
earlySetup: () => { earlySetup: () => {
// No need to trigger the print dialog. // No need to trigger the print dialog.
@ -497,11 +459,11 @@ describe("Interaction", () => {
describe("in doc_actions.pdf for saving", () => { describe("in doc_actions.pdf for saving", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("doc_actions.pdf", getSelector("47R")); pages = await loadAndWait("doc_actions.pdf", getSelector("47R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -544,11 +506,11 @@ describe("Interaction", () => {
describe("in doc_actions.pdf for page actions", () => { describe("in doc_actions.pdf for page actions", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("doc_actions.pdf", getSelector("47R")); pages = await loadAndWait("doc_actions.pdf", getSelector("47R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -601,11 +563,11 @@ describe("Interaction", () => {
describe("in js-authors.pdf", () => { describe("in js-authors.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("js-authors.pdf", getSelector("25R")); pages = await loadAndWait("js-authors.pdf", getSelector("25R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -632,11 +594,11 @@ describe("Interaction", () => {
describe("in listbox_actions.pdf", () => { describe("in listbox_actions.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("listbox_actions.pdf", getSelector("33R")); pages = await loadAndWait("listbox_actions.pdf", getSelector("33R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -774,11 +736,11 @@ describe("Interaction", () => {
describe("in js-colors.pdf", () => { describe("in js-colors.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("js-colors.pdf", getSelector("34R")); pages = await loadAndWait("js-colors.pdf", getSelector("34R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -847,11 +809,11 @@ describe("Interaction", () => {
describe("in issue13132.pdf", () => { describe("in issue13132.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue13132.pdf", getSelector("171R")); pages = await loadAndWait("issue13132.pdf", getSelector("171R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -906,11 +868,11 @@ describe("Interaction", () => {
describe("Check field properties", () => { describe("Check field properties", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("evaljs.pdf", getSelector("55R")); pages = await loadAndWait("evaljs.pdf", getSelector("55R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -981,11 +943,11 @@ describe("Interaction", () => {
describe("in issue13269.pdf", () => { describe("in issue13269.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue13269.pdf", getSelector("27R")); pages = await loadAndWait("issue13269.pdf", getSelector("27R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1016,13 +978,14 @@ describe("Interaction", () => {
describe("in secHandler.pdf", () => { describe("in secHandler.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("secHandler.pdf", getSelector("25R")); pages = await loadAndWait("secHandler.pdf", getSelector("25R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
it("must print securityHandler value in a text field", async () => { it("must print securityHandler value in a text field", async () => {
await Promise.all( await Promise.all(
pages.map(async ([browserName, page]) => { pages.map(async ([browserName, page]) => {
@ -1044,7 +1007,7 @@ describe("Interaction", () => {
describe("in issue14307.pdf (1)", () => { describe("in issue14307.pdf (1)", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue14307.pdf", getSelector("30R")); pages = await loadAndWait("issue14307.pdf", getSelector("30R"));
pages.map(async ([, page]) => { pages.map(async ([, page]) => {
page.on("dialog", async dialog => { page.on("dialog", async dialog => {
@ -1053,7 +1016,7 @@ describe("Interaction", () => {
}); });
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1087,7 +1050,7 @@ describe("Interaction", () => {
describe("in issue14307.pdf (2)", () => { describe("in issue14307.pdf (2)", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue14307.pdf", getSelector("30R")); pages = await loadAndWait("issue14307.pdf", getSelector("30R"));
pages.map(async ([, page]) => { pages.map(async ([, page]) => {
page.on("dialog", async dialog => { page.on("dialog", async dialog => {
@ -1096,7 +1059,7 @@ describe("Interaction", () => {
}); });
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1132,7 +1095,7 @@ describe("Interaction", () => {
describe("in issue14307.pdf (3)", () => { describe("in issue14307.pdf (3)", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue14307.pdf", getSelector("30R")); pages = await loadAndWait("issue14307.pdf", getSelector("30R"));
pages.map(async ([, page]) => { pages.map(async ([, page]) => {
page.on("dialog", async dialog => { page.on("dialog", async dialog => {
@ -1141,7 +1104,7 @@ describe("Interaction", () => {
}); });
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1177,7 +1140,7 @@ describe("Interaction", () => {
describe("in issue14862.pdf", () => { describe("in issue14862.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue14862.pdf", getSelector("27R")); pages = await loadAndWait("issue14862.pdf", getSelector("27R"));
pages.map(async ([, page]) => { pages.map(async ([, page]) => {
page.on("dialog", async dialog => { page.on("dialog", async dialog => {
@ -1186,7 +1149,7 @@ describe("Interaction", () => {
}); });
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1272,7 +1235,7 @@ describe("Interaction", () => {
describe("in issue14705.pdf", () => { describe("in issue14705.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue14705.pdf", getSelector("29R")); pages = await loadAndWait("issue14705.pdf", getSelector("29R"));
pages.map(async ([, page]) => { pages.map(async ([, page]) => {
page.on("dialog", async dialog => { page.on("dialog", async dialog => {
@ -1281,7 +1244,7 @@ describe("Interaction", () => {
}); });
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1316,11 +1279,11 @@ describe("Interaction", () => {
describe("in bug1766987.pdf", () => { describe("in bug1766987.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1766987.pdf", getSelector("75R")); pages = await loadAndWait("bug1766987.pdf", getSelector("75R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1348,11 +1311,11 @@ describe("Interaction", () => {
describe("in issue15053.pdf", () => { describe("in issue15053.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue15053.pdf", getSelector("44R")); pages = await loadAndWait("issue15053.pdf", getSelector("44R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1401,11 +1364,11 @@ describe("Interaction", () => {
describe("in bug1675139.pdf", () => { describe("in bug1675139.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1675139.pdf", getSelector("48R")); pages = await loadAndWait("bug1675139.pdf", getSelector("48R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1447,11 +1410,11 @@ describe("Interaction", () => {
describe("in issue15092.pdf", () => { describe("in issue15092.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue15092.pdf", getSelector("39R")); pages = await loadAndWait("issue15092.pdf", getSelector("39R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1482,11 +1445,11 @@ describe("Interaction", () => {
describe("in bug1782564.pdf", () => { describe("in bug1782564.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1782564.pdf", getSelector("7R")); pages = await loadAndWait("bug1782564.pdf", getSelector("7R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1527,11 +1490,11 @@ describe("Interaction", () => {
describe("in bug1802888.pdf", () => { describe("in bug1802888.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1802888.pdf", getSelector("30R")); pages = await loadAndWait("bug1802888.pdf", getSelector("30R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1557,11 +1520,11 @@ describe("Interaction", () => {
describe("in issue15753.pdf", () => { describe("in issue15753.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue15753.pdf", getSelector("27R")); pages = await loadAndWait("issue15753.pdf", getSelector("27R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1602,11 +1565,11 @@ describe("Interaction", () => {
describe("in issue15815.pdf", () => { describe("in issue15815.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue15815.pdf", getSelector("24R")); pages = await loadAndWait("issue15815.pdf", getSelector("24R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1649,11 +1612,11 @@ describe("Interaction", () => {
describe("in issue15818.pdf", () => { describe("in issue15818.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue15818.pdf", getSelector("27R")); pages = await loadAndWait("issue15818.pdf", getSelector("27R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1696,7 +1659,7 @@ describe("Interaction", () => {
describe("in autoprint.pdf", () => { describe("in autoprint.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
// Autoprinting is triggered by the `Open` event, which is one of the // Autoprinting is triggered by the `Open` event, which is one of the
// first events to be dispatched to the sandbox, even before scripting // first events to be dispatched to the sandbox, even before scripting
// is reported to be ready. It's therefore important that `loadAndWait` // is reported to be ready. It's therefore important that `loadAndWait`
@ -1725,7 +1688,7 @@ describe("Interaction", () => {
}); });
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1747,11 +1710,11 @@ describe("Interaction", () => {
describe("in bug1811694.pdf", () => { describe("in bug1811694.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1811694.pdf", getSelector("25R")); pages = await loadAndWait("bug1811694.pdf", getSelector("25R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1779,11 +1742,11 @@ describe("Interaction", () => {
describe("in bug1811510.pdf", () => { describe("in bug1811510.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1811510.pdf", getSelector("22R")); pages = await loadAndWait("bug1811510.pdf", getSelector("22R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1819,11 +1782,11 @@ describe("Interaction", () => {
describe("in issue16067.pdf", () => { describe("in issue16067.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue16067.pdf", getSelector("6R")); pages = await loadAndWait("issue16067.pdf", getSelector("6R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1854,11 +1817,11 @@ describe("Interaction", () => {
describe("in bug1825002.pdf", () => { describe("in bug1825002.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1825002.pdf", getSelector("23R")); pages = await loadAndWait("bug1825002.pdf", getSelector("23R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1908,11 +1871,11 @@ describe("Interaction", () => {
describe("in bug1844576.pdf", () => { describe("in bug1844576.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1844576.pdf", getSelector("9R")); pages = await loadAndWait("bug1844576.pdf", getSelector("9R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -1963,14 +1926,14 @@ describe("Interaction", () => {
describe("in annotation_hidden_noview.pdf", () => { describe("in annotation_hidden_noview.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait( pages = await loadAndWait(
"annotation_hidden_noview.pdf", "annotation_hidden_noview.pdf",
getSelector("11R") getSelector("11R")
); );
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2022,11 +1985,11 @@ describe("Interaction", () => {
describe("in issue16863.pdf", () => { describe("in issue16863.pdf", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue16863.pdf", getSelector("334R")); pages = await loadAndWait("issue16863.pdf", getSelector("334R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2086,7 +2049,7 @@ describe("Interaction", () => {
let pages; let pages;
let otherPages; let otherPages;
beforeAll(async () => { beforeEach(async () => {
otherPages = await Promise.all( otherPages = await Promise.all(
global.integrationSessions.map(async session => global.integrationSessions.map(async session =>
session.browser.newPage() session.browser.newPage()
@ -2095,7 +2058,7 @@ describe("Interaction", () => {
pages = await loadAndWait("evaljs.pdf", getSelector("55R")); pages = await loadAndWait("evaljs.pdf", getSelector("55R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
await Promise.all(otherPages.map(page => page.close())); await Promise.all(otherPages.map(page => page.close()));
}); });
@ -2129,11 +2092,11 @@ describe("Interaction", () => {
describe("Textfield with a Blur callback", () => { describe("Textfield with a Blur callback", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1863910.pdf", getSelector("25R")); pages = await loadAndWait("bug1863910.pdf", getSelector("25R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2161,11 +2124,11 @@ describe("Interaction", () => {
describe("Radio button without T value", () => { describe("Radio button without T value", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1860602.pdf", getSelector("22R")); pages = await loadAndWait("bug1860602.pdf", getSelector("22R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2218,11 +2181,11 @@ describe("Interaction", () => {
describe("Textfield with a number and some decimals", () => { describe("Textfield with a number and some decimals", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue17540.pdf", getSelector("15R")); pages = await loadAndWait("issue17540.pdf", getSelector("15R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2251,11 +2214,11 @@ describe("Interaction", () => {
describe("Textfield with a zip code starting with 0", () => { describe("Textfield with a zip code starting with 0", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1889122.pdf", getSelector("24R")); pages = await loadAndWait("bug1889122.pdf", getSelector("24R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2279,11 +2242,11 @@ describe("Interaction", () => {
describe("Value of event.change when a choice list is modified", () => { describe("Value of event.change when a choice list is modified", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue17998.pdf", getSelector("7R")); pages = await loadAndWait("issue17998.pdf", getSelector("7R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2313,11 +2276,11 @@ describe("Interaction", () => {
describe("PageOpen and PageClose actions in fields", () => { describe("PageOpen and PageClose actions in fields", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue18305.pdf", getSelector("7R")); pages = await loadAndWait("issue18305.pdf", getSelector("7R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2358,11 +2321,11 @@ describe("Interaction", () => {
describe("Compute product of different fields", () => { describe("Compute product of different fields", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue18536.pdf", getSelector("34R")); pages = await loadAndWait("issue18536.pdf", getSelector("34R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2395,11 +2358,11 @@ describe("Interaction", () => {
describe("Calculate field value even if one callback throws", () => { describe("Calculate field value even if one callback throws", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue18561.pdf", getSelector("24R")); pages = await loadAndWait("issue18561.pdf", getSelector("24R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2423,11 +2386,11 @@ describe("Interaction", () => {
describe("Correctly format numbers", () => { describe("Correctly format numbers", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1918115.pdf", getSelector("33R")); pages = await loadAndWait("bug1918115.pdf", getSelector("33R"));
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2451,11 +2414,11 @@ describe("Interaction", () => {
describe("Change radio property", () => { describe("Change radio property", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1922766.pdf", "[data-annotation-id='44R']"); pages = await loadAndWait("bug1922766.pdf", "[data-annotation-id='44R']");
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2494,11 +2457,11 @@ describe("Interaction", () => {
describe("Date creation must be timezone consistent", () => { describe("Date creation must be timezone consistent", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("bug1934157.pdf", "[data-annotation-id='24R']"); pages = await loadAndWait("bug1934157.pdf", "[data-annotation-id='24R']");
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });
@ -2532,11 +2495,11 @@ describe("Interaction", () => {
describe("Skip throwing actions (issue 19505)", () => { describe("Skip throwing actions (issue 19505)", () => {
let pages; let pages;
beforeAll(async () => { beforeEach(async () => {
pages = await loadAndWait("issue19505.pdf", "[data-annotation-id='24R']"); pages = await loadAndWait("issue19505.pdf", "[data-annotation-id='24R']");
}); });
afterAll(async () => { afterEach(async () => {
await closePages(pages); await closePages(pages);
}); });

View File

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

View File

@ -12002,5 +12002,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]);
}); });
}); });