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 69e1c5317..936cd9ebf 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 [ @@ -1223,6 +1181,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 5db36fd5e..50b925fa6 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -12002,5 +12002,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]); }); });