From be1f5671bbd9dc34a4688e286f34324240a62e58 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Wed, 19 Mar 2025 20:39:02 +0100 Subject: [PATCH] [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. --- src/core/evaluator.js | 242 +++++++++++++++++--------- src/core/operator_list.js | 6 + src/display/canvas.js | 315 ++++++++++++---------------------- src/display/pattern_helper.js | 12 ++ src/shared/util.js | 61 ++----- test/pdfs/bug1946953.pdf.link | 1 + test/test_manifest.json | 10 ++ test/unit/annotation_spec.js | 34 ++-- test/unit/api_spec.js | 27 ++- test/unit/evaluator_spec.js | 33 ++-- 10 files changed, 367 insertions(+), 374 deletions(-) create mode 100644 test/pdfs/bug1946953.pdf.link diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 70ade2c5b..4eea49380 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -16,6 +16,7 @@ import { AbortException, assert, + DrawOPS, FONT_IDENTITY_MATRIX, FormatError, IDENTITY_MATRIX, @@ -925,7 +926,7 @@ class PartialEvaluator { smaskOptions, operatorList, task, - stateManager.state.clone(), + stateManager.state.clone({ newPath: true }), localColorSpaceCache ); } @@ -1383,80 +1384,112 @@ class PartialEvaluator { return promise; } - buildPath(operatorList, fn, args, parsingText = false) { - const lastIndex = operatorList.length - 1; - if (!args) { - args = []; - } - if ( - lastIndex < 0 || - operatorList.fnArray[lastIndex] !== OPS.constructPath - ) { - // Handle corrupt PDF documents that contains path operators inside of - // text objects, which may shift subsequent text, by enclosing the path - // operator in save/restore operators (fixes issue10542_reduced.pdf). - // - // Note that this will effectively disable the optimization in the - // `else` branch below, but given that this type of corruption is - // *extremely* rare that shouldn't really matter much in practice. - if (parsingText) { - warn(`Encountered path operator "${fn}" inside of a text object.`); - operatorList.addOp(OPS.save, null); + buildPath(fn, args, state) { + const { pathMinMax: minMax, pathBuffer } = state; + switch (fn | 0) { + case OPS.rectangle: { + const x = (state.currentPointX = args[0]); + const y = (state.currentPointY = args[1]); + const width = args[2]; + const height = args[3]; + const xw = x + width; + const yh = y + height; + if (width === 0 || height === 0) { + pathBuffer.push( + DrawOPS.moveTo, + x, + y, + DrawOPS.lineTo, + xw, + yh, + 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; } - - 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; + 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; } - operatorList.addOp(OPS.constructPath, [[fn], args, minMax]); - - if (parsingText) { - operatorList.addOp(OPS.restore, null); + 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; } - } else { - const opArgs = operatorList.argsArray[lastIndex]; - opArgs[0].push(fn); - opArgs[1].push(...args); - const minMax = opArgs[2]; - - // Compute min/max in the worker instead of the main thread. - // If the current matrix (when drawing) is a scaling one - // then min/max can be easily computed in using those values. - // 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.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 xref = this.xref; - let parsingText = false; const localImageCache = new LocalImageCache(); const localColorSpaceCache = new LocalColorSpaceCache(); const localGStateCache = new LocalGStateCache(); @@ -1847,7 +1879,7 @@ class PartialEvaluator { null, operatorList, task, - stateManager.state.clone(), + stateManager.state.clone({ newPath: true }), localColorSpaceCache ) .then(function () { @@ -1909,12 +1941,6 @@ class PartialEvaluator { }) ); return; - case OPS.beginText: - parsingText = true; - break; - case OPS.endText: - parsingText = false; - break; case OPS.endInlineImage: const cacheKey = args[0].cacheKey; if (cacheKey) { @@ -2237,8 +2263,40 @@ class PartialEvaluator { case OPS.curveTo3: case OPS.closePath: case OPS.rectangle: - self.buildPath(operatorList, fn, args, parsingText); + self.buildPath(fn, args, stateManager.state); 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.markPointProps: case OPS.beginCompat: @@ -4935,6 +4993,16 @@ class EvalState { this._fillColorSpace = this._strokeColorSpace = ColorSpaceUtils.gray; this.patternFillColorSpace = null; this.patternStrokeColorSpace = null; + + // Path stuff. + this.currentPointX = this.currentPointY = 0; + this.pathMinMax = new Float32Array([ + Infinity, + Infinity, + -Infinity, + -Infinity, + ]); + this.pathBuffer = []; } get fillColorSpace() { @@ -4953,8 +5021,18 @@ class EvalState { this._strokeColorSpace = this.patternStrokeColorSpace = colorSpace; } - clone() { - return Object.create(this); + clone({ newPath = false } = {}) { + const clone = Object.create(this); + if (newPath) { + clone.pathBuffer = []; + clone.pathMinMax = new Float32Array([ + Infinity, + Infinity, + -Infinity, + -Infinity, + ]); + } + return clone; } } diff --git a/src/core/operator_list.js b/src/core/operator_list.js index 1212e6e93..00a732997 100644 --- a/src/core/operator_list.js +++ b/src/core/operator_list.js @@ -703,6 +703,12 @@ class OperatorList { transfers.push(arg.data.buffer); } break; + case OPS.constructPath: + const [, [data], minMax] = argsArray[i]; + if (data) { + transfers.push(data.buffer, minMax.buffer); + } + break; } } return transfers; diff --git a/src/display/canvas.js b/src/display/canvas.js index a76f68686..b8e56fec9 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -14,6 +14,7 @@ */ import { + DrawOPS, FeatureTest, FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, @@ -58,6 +59,10 @@ const MAX_SIZE_TO_COMPILE = 1000; 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 * will also call the same method on the destCtx. The methods that are @@ -502,19 +507,6 @@ class CanvasExtraState { 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) { const p1 = Util.applyTransform(rect, 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]); } - 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) { const box = [this.minX, this.minY, this.maxX, this.maxY]; if (pathType === PathType.STROKE) { @@ -1612,156 +1588,54 @@ class CanvasGraphics { } // Path - constructPath(ops, args, minMax) { - const ctx = this.ctx; - const current = this.current; - let x = current.x, - y = current.y; - let startX, startY; - const currentTransform = getCurrentTransform(ctx); - - // 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) { - 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; + constructPath(op, data, minMax) { + let [path] = data; + if (!minMax) { + // The path is empty, so no need to update the current minMax. + path ||= data[0] = new Path2D(); + this[op](path); + return; + } + if (!(path instanceof Path2D)) { + // Using a SVG string is slightly slower than using the following loop. + const path2d = (data[0] = new Path2D()); + for (let i = 0, ii = path.length; i < ii; ) { + switch (path[i++]) { + case DrawOPS.moveTo: + path2d.moveTo(path[i++], path[i++]); + break; + case DrawOPS.lineTo: + path2d.lineTo(path[i++], path[i++]); + break; + case DrawOPS.curveTo: + path2d.bezierCurveTo( + path[i++], + path[i++], + path[i++], + path[i++], + path[i++], + path[i++] + ); + break; + case DrawOPS.closePath: + path2d.closePath(); + break; + default: + warn(`Unrecognized drawing path operator: ${path[i - 1]}`); + break; + } } + path = path2d; } - - if (isScalingMatrix) { - current.updateScalingPathMinMax(currentTransform, minMaxForBezier); - } - - current.setCurrentPoint(x, y); + this.current.updateRectMinMax(getCurrentTransform(this.ctx), minMax); + this[op](path); } closePath() { this.ctx.closePath(); } - stroke(consumePath = true) { + stroke(path, consumePath = true) { const ctx = this.ctx; const strokeColor = this.current.strokeColor; // For stroke we want to temporarily change the global alpha to the @@ -1769,6 +1643,9 @@ class CanvasGraphics { ctx.globalAlpha = this.current.strokeAlpha; if (this.contentVisible) { if (typeof strokeColor === "object" && strokeColor?.getPattern) { + const baseTransform = strokeColor.isModifyingCurrentTransform() + ? ctx.getTransform() + : null; ctx.save(); ctx.strokeStyle = strokeColor.getPattern( ctx, @@ -1776,31 +1653,41 @@ class CanvasGraphics { getCurrentTransformInverse(ctx), 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(); } else { - this.rescaleAndStroke(/* saveRestore */ true); + this.rescaleAndStroke(path, /* saveRestore */ true); } } if (consumePath) { - this.consumePath(this.current.getClippedPathBoundingBox()); + this.consumePath(path, this.current.getClippedPathBoundingBox()); } // Restore the global alpha to the fill alpha ctx.globalAlpha = this.current.fillAlpha; } - closeStroke() { - this.closePath(); - this.stroke(); + closeStroke(path) { + this.stroke(path); } - fill(consumePath = true) { + fill(path, consumePath = true) { const ctx = this.ctx; const fillColor = this.current.fillColor; const isPatternFill = this.current.patternFill; let needRestore = false; if (isPatternFill) { + const baseTransform = fillColor.isModifyingCurrentTransform() + ? ctx.getTransform() + : null; ctx.save(); ctx.fillStyle = fillColor.getPattern( ctx, @@ -1808,16 +1695,24 @@ class CanvasGraphics { getCurrentTransformInverse(ctx), PathType.FILL ); + if (baseTransform) { + const newPath = new Path2D(); + newPath.addPath( + path, + ctx.getTransform().invertSelf().multiplySelf(baseTransform) + ); + path = newPath; + } needRestore = true; } const intersect = this.current.getClippedPathBoundingBox(); if (this.contentVisible && intersect !== null) { if (this.pendingEOFill) { - ctx.fill("evenodd"); + ctx.fill(path, "evenodd"); this.pendingEOFill = false; } else { - ctx.fill(); + ctx.fill(path); } } @@ -1825,40 +1720,38 @@ class CanvasGraphics { ctx.restore(); } if (consumePath) { - this.consumePath(intersect); + this.consumePath(path, intersect); } } - eoFill() { + eoFill(path) { this.pendingEOFill = true; - this.fill(); + this.fill(path); } - fillStroke() { - this.fill(false); - this.stroke(false); + fillStroke(path) { + this.fill(path, false); + this.stroke(path, false); - this.consumePath(); + this.consumePath(path); } - eoFillStroke() { + eoFillStroke(path) { this.pendingEOFill = true; - this.fillStroke(); + this.fillStroke(path); } - closeFillStroke() { - this.closePath(); - this.fillStroke(); + closeFillStroke(path) { + this.fillStroke(path); } - closeEOFillStroke() { + closeEOFillStroke(path) { this.pendingEOFill = true; - this.closePath(); - this.fillStroke(); + this.fillStroke(path); } - endPath() { - this.consumePath(); + endPath(path) { + this.consumePath(path); } // Clipping @@ -3168,7 +3061,7 @@ class CanvasGraphics { // Helper functions - consumePath(clipBox) { + consumePath(path, clipBox) { const isEmpty = this.current.isEmptyClip(); if (this.pendingClip) { this.current.updateClipFromPath(); @@ -3180,9 +3073,9 @@ class CanvasGraphics { if (this.pendingClip) { if (!isEmpty) { if (this.pendingClip === EO_CLIP) { - ctx.clip("evenodd"); + ctx.clip(path, "evenodd"); } else { - ctx.clip(); + ctx.clip(path); } } this.pendingClip = null; @@ -3267,15 +3160,16 @@ class CanvasGraphics { // Rescale before stroking in order to have a final lineWidth // with both thicknesses greater or equal to 1. - rescaleAndStroke(saveRestore) { - const { ctx } = this; - const { lineWidth } = this.current; + rescaleAndStroke(path, saveRestore) { + const { + ctx, + current: { lineWidth }, + } = this; const [scaleX, scaleY] = this.getScaleForStroking(); - ctx.lineWidth = lineWidth || 1; - - if (scaleX === 1 && scaleY === 1) { - ctx.stroke(); + if (scaleX === scaleY) { + ctx.lineWidth = (lineWidth || 1) * scaleX; + ctx.stroke(path); return; } @@ -3285,6 +3179,10 @@ class CanvasGraphics { } 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... // so we added a rescale to handle too thin lines and consequently @@ -3299,7 +3197,8 @@ class CanvasGraphics { ctx.lineDashOffset /= scale; } - ctx.stroke(); + ctx.lineWidth = lineWidth || 1; + ctx.stroke(newPath); if (saveRestore) { ctx.restore(); diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 077ec990b..20ed4c57a 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -43,6 +43,10 @@ class BaseShadingPattern { } } + isModifyingCurrentTransform() { + return false; + } + getPattern() { unreachable("Abstract method `getPattern` called."); } @@ -388,6 +392,10 @@ class MeshShadingPattern extends BaseShadingPattern { }; } + isModifyingCurrentTransform() { + return true; + } + getPattern(ctx, owner, inverse, pathType) { applyBoundingBox(ctx, this._bbox); let scale; @@ -704,6 +712,10 @@ class TilingPattern { } } + isModifyingCurrentTransform() { + return false; + } + getPattern(ctx, owner, inverse, pathType) { // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix. let matrix = inverse; diff --git a/src/shared/util.js b/src/shared/util.js index e3c3ad2dc..00afff8f5 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -341,6 +341,15 @@ const OPS = { 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 = { NEED_PASSWORD: 1, INCORRECT_PASSWORD: 2, @@ -667,57 +676,6 @@ class Util { 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. static transform(m1, m2) { return [ @@ -1192,6 +1150,7 @@ export { bytesToString, createValidAbsoluteUrl, DocumentActionEventType, + DrawOPS, FeatureTest, FONT_IDENTITY_MATRIX, FormatError, diff --git a/test/pdfs/bug1946953.pdf.link b/test/pdfs/bug1946953.pdf.link new file mode 100644 index 000000000..aa7d0f2e7 --- /dev/null +++ b/test/pdfs/bug1946953.pdf.link @@ -0,0 +1 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=9464841 diff --git a/test/test_manifest.json b/test/test_manifest.json index a07b827bf..47a566983 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -11995,5 +11995,15 @@ "md5": "9993aa298c0214a3d3ff5f90ce0d40bb", "rounds": 1, "type": "eq" + }, + { + "id": "bug1946953", + "file": "pdfs/bug1946953.pdf", + "md5": "a71fb64e348b9c7161945e48e75c6681", + "rounds": 1, + "link": true, + "firstPage": 1, + "lastPage": 1, + "type": "eq" } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 8124fe6cf..a75747b20 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -26,6 +26,7 @@ import { AnnotationFieldFlag, AnnotationFlag, AnnotationType, + DrawOPS, OPS, RenderingIntentFlag, stringToBytes, @@ -4285,14 +4286,13 @@ describe("annotation", function () { null ); - expect(opList.fnArray.length).toEqual(16); + expect(opList.fnArray.length).toEqual(15); expect(opList.fnArray).toEqual([ OPS.beginAnnotation, OPS.save, OPS.transform, - OPS.constructPath, OPS.clip, - OPS.endPath, + OPS.constructPath, OPS.beginText, OPS.setFillRGBColor, OPS.setCharSpacing, @@ -4659,7 +4659,7 @@ describe("annotation", function () { null ); - expect(opList.argsArray.length).toEqual(8); + expect(opList.argsArray.length).toEqual(7); expect(opList.fnArray).toEqual([ OPS.beginAnnotation, OPS.setLineWidth, @@ -4667,7 +4667,6 @@ describe("annotation", function () { OPS.setLineJoin, OPS.setStrokeRGBColor, OPS.constructPath, - OPS.stroke, OPS.endAnnotation, ]); @@ -4680,10 +4679,23 @@ describe("annotation", function () { // Color. expect(opList.argsArray[4]).toEqual(new Uint8ClampedArray([0, 255, 0])); // Path. - expect(opList.argsArray[5][0]).toEqual([OPS.moveTo, OPS.curveTo]); - expect(opList.argsArray[5][1]).toEqual([1, 2, 3, 4, 5, 6, 7, 8]); + expect(opList.argsArray[5][0]).toEqual(OPS.stroke); + expect(opList.argsArray[5][1]).toEqual([ + new Float32Array([ + DrawOPS.moveTo, + 1, + 2, + DrawOPS.curveTo, + 3, + 4, + 5, + 6, + 7, + 8, + ]), + ]); // 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 ); - expect(opList.argsArray.length).toEqual(6); + expect(opList.argsArray.length).toEqual(5); expect(opList.fnArray).toEqual([ OPS.beginAnnotation, OPS.setFillRGBColor, OPS.setGState, OPS.constructPath, - OPS.eoFill, OPS.endAnnotation, ]); }); @@ -4953,13 +4964,12 @@ describe("annotation", function () { null ); - expect(opList.argsArray.length).toEqual(6); + expect(opList.argsArray.length).toEqual(5); expect(opList.fnArray).toEqual([ OPS.beginAnnotation, OPS.setFillRGBColor, OPS.setGState, OPS.constructPath, - OPS.fill, OPS.endAnnotation, ]); }); diff --git a/test/unit/api_spec.js b/test/unit/api_spec.js index 2f0fdc040..f8c100611 100644 --- a/test/unit/api_spec.js +++ b/test/unit/api_spec.js @@ -17,6 +17,7 @@ import { AnnotationEditorType, AnnotationMode, AnnotationType, + DrawOPS, ImageKind, InvalidPDFException, isNodeJS, @@ -794,17 +795,25 @@ describe("api", function () { OPS.setLineWidth, OPS.setStrokeRGBColor, OPS.constructPath, - OPS.closeStroke, ]); expect(opList.argsArray).toEqual([ [0.5], new Uint8ClampedArray([255, 0, 0]), [ - [OPS.moveTo, OPS.lineTo], - [0, 9.75, 0.5, 9.75], - [0, 9.75, 0.5, 9.75], + OPS.closeStroke, + [ + 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); @@ -4236,8 +4245,8 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) const opListAnnotEnable = await pdfPage.getOperatorList({ annotationMode: AnnotationMode.ENABLE, }); - expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(140); - expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(140); + expect(opListAnnotEnable.fnArray.length).toBeGreaterThan(130); + expect(opListAnnotEnable.argsArray.length).toBeGreaterThan(130); expect(opListAnnotEnable.lastChunk).toEqual(true); expect(opListAnnotEnable.separateAnnots).toEqual({ form: false, @@ -4270,8 +4279,8 @@ Caron Broadcasting, Inc., an Ohio corporation (“Lessee”).`) const opListAnnotEnableStorage = await pdfPage.getOperatorList({ annotationMode: AnnotationMode.ENABLE_STORAGE, }); - expect(opListAnnotEnableStorage.fnArray.length).toBeGreaterThan(170); - expect(opListAnnotEnableStorage.argsArray.length).toBeGreaterThan(170); + expect(opListAnnotEnableStorage.fnArray.length).toBeGreaterThan(150); + expect(opListAnnotEnableStorage.argsArray.length).toBeGreaterThan(150); expect(opListAnnotEnableStorage.lastChunk).toEqual(true); expect(opListAnnotEnableStorage.separateAnnots).toEqual({ form: false, diff --git a/test/unit/evaluator_spec.js b/test/unit/evaluator_spec.js index 3edc9a8ad..0aa6bc685 100644 --- a/test/unit/evaluator_spec.js +++ b/test/unit/evaluator_spec.js @@ -74,8 +74,8 @@ describe("evaluator", function () { ); expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(result.fnArray.length).toEqual(1); - expect(result.fnArray[0]).toEqual(OPS.fill); - expect(result.argsArray[0]).toEqual(null); + expect(result.fnArray[0]).toEqual(OPS.constructPath); + expect(result.argsArray[0]).toEqual([OPS.fill, [null], null]); }); it("should handle one operation", async function () { @@ -130,9 +130,14 @@ describe("evaluator", function () { ); expect(!!result.fnArray && !!result.argsArray).toEqual(true); expect(result.fnArray.length).toEqual(3); - expect(result.fnArray[0]).toEqual(OPS.fill); - expect(result.fnArray[1]).toEqual(OPS.fill); - expect(result.fnArray[2]).toEqual(OPS.fill); + expect(result.fnArray).toEqual([ + OPS.constructPath, + 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 () { @@ -145,10 +150,14 @@ describe("evaluator", function () { resources ); expect(!!result.fnArray && !!result.argsArray).toEqual(true); - expect(result.fnArray.length).toEqual(3); - expect(result.fnArray[0]).toEqual(OPS.eoFillStroke); - expect(result.fnArray[1]).toEqual(OPS.fillStroke); - expect(result.fnArray[2]).toEqual(OPS.eoFill); + expect(result.fnArray).toEqual([ + OPS.constructPath, + OPS.constructPath, + 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 () { @@ -160,7 +169,7 @@ describe("evaluator", function () { ); expect(!!result.fnArray && !!result.argsArray).toEqual(true); 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.argsArray.length).toEqual(2); expect(result.argsArray[1].length).toEqual(1); @@ -178,13 +187,13 @@ describe("evaluator", function () { expect(result.fnArray.length).toEqual(3); expect(result.fnArray[0]).toEqual(OPS.setFlatness); 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[0].length).toEqual(1); expect(result.argsArray[0][0]).toEqual(true); expect(result.argsArray[1].length).toEqual(1); expect(result.argsArray[1][0]).toEqual(false); - expect(result.argsArray[2]).toEqual(null); + expect(result.argsArray[2]).toEqual([OPS.endPath, [null], null]); }); });