diff --git a/src/display/canvas.js b/src/display/canvas.js index c5a1d21d2546b..f1cab4b5216b8 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -13,11 +13,6 @@ * limitations under the License. */ -import { - createMatrix, - getShadingPattern, - TilingPattern, -} from "./pattern_helper.js"; import { FONT_IDENTITY_MATRIX, IDENTITY_MATRIX, @@ -32,6 +27,7 @@ import { Util, warn, } from "../shared/util.js"; +import { getShadingPattern, TilingPattern } from "./pattern_helper.js"; // contexts store most of the state we need natively. // However, PDF needs a bit more state, which we store here. @@ -197,17 +193,6 @@ function addContextCurrentTransform(ctx) { }; } -function getAdjustmentTransformation(transform, width, height) { - // The pattern will be created at the size of the current page or form object, - // but the mask is usually scaled differently and offset, so we must account - // for these to shift and rescale the pattern to the correctly location. - let patternTransform = createMatrix(transform); - patternTransform = patternTransform.scale(1 / width, -1 / height); - patternTransform = patternTransform.translate(0, -height); - patternTransform = patternTransform.inverse(); - return patternTransform; -} - class CachedCanvases { constructor(canvasFactory) { this.canvasFactory = canvasFactory; @@ -1046,6 +1031,154 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { } } + _scaleImage(img, inverseTransform) { + // Vertical or horizontal scaling shall not be more than 2 to not lose the + // pixels during drawImage operation, painting on the temporary canvas(es) + // that are twice smaller in size. + const width = img.width; + const height = img.height; + let widthScale = Math.max( + Math.hypot(inverseTransform[0], inverseTransform[1]), + 1 + ); + let heightScale = Math.max( + Math.hypot(inverseTransform[2], inverseTransform[3]), + 1 + ); + + let paintWidth = width, + paintHeight = height; + let tmpCanvasId = "prescale1"; + let tmpCanvas, tmpCtx; + while ( + (widthScale > 2 && paintWidth > 1) || + (heightScale > 2 && paintHeight > 1) + ) { + let newWidth = paintWidth, + newHeight = paintHeight; + if (widthScale > 2 && paintWidth > 1) { + newWidth = Math.ceil(paintWidth / 2); + widthScale /= paintWidth / newWidth; + } + if (heightScale > 2 && paintHeight > 1) { + newHeight = Math.ceil(paintHeight / 2); + heightScale /= paintHeight / newHeight; + } + tmpCanvas = this.cachedCanvases.getCanvas( + tmpCanvasId, + newWidth, + newHeight + ); + tmpCtx = tmpCanvas.context; + tmpCtx.clearRect(0, 0, newWidth, newHeight); + tmpCtx.drawImage( + img, + 0, + 0, + paintWidth, + paintHeight, + 0, + 0, + newWidth, + newHeight + ); + img = tmpCanvas.canvas; + paintWidth = newWidth; + paintHeight = newHeight; + tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1"; + } + return { + img, + paintWidth, + paintHeight, + }; + } + + _createMaskCanvas(img) { + const ctx = this.ctx; + const width = img.width, + height = img.height; + const fillColor = this.current.fillColor; + const isPatternFill = this.current.patternFill; + const maskCanvas = this.cachedCanvases.getCanvas( + "maskCanvas", + width, + height + ); + const maskCtx = maskCanvas.context; + putBinaryImageMask(maskCtx, img); + + // Create the mask canvas at the size it will be drawn at and also set + // its transform to match the current transform so if there are any + // patterns applied they will be applied relative to the correct + // transform. + const objToCanvas = ctx.mozCurrentTransform; + let maskToCanvas = Util.transform(objToCanvas, [ + 1 / width, + 0, + 0, + -1 / height, + 0, + 0, + ]); + maskToCanvas = Util.transform(maskToCanvas, [1, 0, 0, 1, 0, -height]); + const cord1 = Util.applyTransform([0, 0], maskToCanvas); + const cord2 = Util.applyTransform([width, height], maskToCanvas); + const rect = Util.normalizeRect([cord1[0], cord1[1], cord2[0], cord2[1]]); + const drawnWidth = Math.ceil(rect[2] - rect[0]); + const drawnHeight = Math.ceil(rect[3] - rect[1]); + const fillCanvas = this.cachedCanvases.getCanvas( + "fillCanvas", + drawnWidth, + drawnHeight, + true + ); + const fillCtx = fillCanvas.context; + // The offset will be the top-left cordinate mask. + const offsetX = Math.min(cord1[0], cord2[0]); + const offsetY = Math.min(cord1[1], cord2[1]); + fillCtx.translate(-offsetX, -offsetY); + fillCtx.transform.apply(fillCtx, maskToCanvas); + // Pre-scale if needed to improve image smoothing. + const scaled = this._scaleImage( + maskCanvas.canvas, + fillCtx.mozCurrentTransformInverse + ); + fillCtx.drawImage( + scaled.img, + 0, + 0, + scaled.img.width, + scaled.img.height, + 0, + 0, + width, + height + ); + fillCtx.globalCompositeOperation = "source-in"; + + const inverse = Util.transform(fillCtx.mozCurrentTransformInverse, [ + 1, + 0, + 0, + 1, + -offsetX, + -offsetY, + ]); + fillCtx.fillStyle = isPatternFill + ? fillColor.getPattern(ctx, this, inverse, false) + : fillColor; + + fillCtx.fillRect(0, 0, width, height); + + // Round the offsets to avoid drawing fractional pixels. + return { + canvas: fillCanvas.canvas, + offsetX: Math.round(offsetX), + offsetY: Math.round(offsetY), + }; + } + // Graphics state setLineWidth(width) { this.current.lineWidth = width; @@ -1375,7 +1508,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { if (typeof strokeColor === "object" && strokeColor?.getPattern) { const lineWidth = this.getSinglePixelWidth(); ctx.save(); - ctx.strokeStyle = strokeColor.getPattern(ctx, this); + ctx.strokeStyle = strokeColor.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse + ); // Prevent drawing too thin lines by enforcing a minimum line width. ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth); ctx.stroke(); @@ -1418,7 +1555,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { if (isPatternFill) { ctx.save(); - ctx.fillStyle = fillColor.getPattern(ctx, this); + ctx.fillStyle = fillColor.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse + ); needRestore = true; } @@ -1748,7 +1889,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { // TODO: Patterns are not applied correctly to text if a non-embedded // font is used. E.g. issue 8111 and ShowText-ShadingPattern.pdf. ctx.save(); - const pattern = current.fillColor.getPattern(ctx, this); + const pattern = current.fillColor.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse + ); patternTransform = ctx.mozCurrentTransform; ctx.restore(); ctx.fillStyle = pattern; @@ -2022,7 +2167,12 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { this.save(); const pattern = getShadingPattern(patternIR); - ctx.fillStyle = pattern.getPattern(ctx, this, true); + ctx.fillStyle = pattern.getPattern( + ctx, + this, + ctx.mozCurrentTransformInverse, + true + ); const inv = ctx.mozCurrentTransformInverse; if (inv) { @@ -2279,8 +2429,6 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { const ctx = this.ctx; const width = img.width, height = img.height; - const fillColor = this.current.fillColor; - const isPatternFill = this.current.patternFill; const glyph = this.processingType3; @@ -2296,35 +2444,15 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { glyph.compiled(ctx); return; } + const mask = this._createMaskCanvas(img); + const maskCanvas = mask.canvas; - const maskCanvas = this.cachedCanvases.getCanvas( - "maskCanvas", - width, - height - ); - const maskCtx = maskCanvas.context; - maskCtx.save(); - - putBinaryImageMask(maskCtx, img); - - maskCtx.globalCompositeOperation = "source-in"; - - let patternTransform = null; - if (isPatternFill) { - patternTransform = getAdjustmentTransformation( - ctx.mozCurrentTransform, - width, - height - ); - } - maskCtx.fillStyle = isPatternFill - ? fillColor.getPattern(maskCtx, this, false, patternTransform) - : fillColor; - maskCtx.fillRect(0, 0, width, height); - - maskCtx.restore(); - - this.paintInlineImageXObject(maskCanvas.canvas); + ctx.save(); + // The mask is drawn with the transform applied. Reset the current + // transform to draw to the identity. + ctx.setTransform(1, 0, 0, 1, 0, 0); + ctx.drawImage(maskCanvas, mask.offsetX, mask.offsetY); + ctx.restore(); } paintImageMaskXObjectRepeat( @@ -2338,54 +2466,27 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { if (!this.contentVisible) { return; } - const width = imgData.width; - const height = imgData.height; - const fillColor = this.current.fillColor; - const isPatternFill = this.current.patternFill; - - const maskCanvas = this.cachedCanvases.getCanvas( - "maskCanvas", - width, - height - ); - const maskCtx = maskCanvas.context; - maskCtx.save(); - - putBinaryImageMask(maskCtx, imgData); - - maskCtx.globalCompositeOperation = "source-in"; - const ctx = this.ctx; - let patternTransform = null; - if (isPatternFill) { - patternTransform = getAdjustmentTransformation( - ctx.mozCurrentTransform, - width, - height - ); - } - - maskCtx.fillStyle = isPatternFill - ? fillColor.getPattern(maskCtx, this, false, patternTransform) - : fillColor; - maskCtx.fillRect(0, 0, width, height); - - maskCtx.restore(); + ctx.save(); + const currentTransform = ctx.mozCurrentTransform; + ctx.transform(scaleX, skewX, skewY, scaleY, 0, 0); + const mask = this._createMaskCanvas(imgData); + ctx.setTransform(1, 0, 0, 1, 0, 0); for (let i = 0, ii = positions.length; i < ii; i += 2) { - ctx.save(); - ctx.transform( + const trans = Util.transform(currentTransform, [ scaleX, skewX, skewY, scaleY, positions[i], - positions[i + 1] - ); - ctx.scale(1, -1); - ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1); - ctx.restore(); + positions[i + 1], + ]); + + const [x, y] = Util.applyTransform([0, 0], trans); + ctx.drawImage(mask.canvas, x, y); } + ctx.restore(); } paintImageMaskXObjectGroup(images) { @@ -2413,17 +2514,13 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { maskCtx.globalCompositeOperation = "source-in"; - let patternTransform = null; - if (isPatternFill) { - patternTransform = getAdjustmentTransformation( - ctx.mozCurrentTransform, - width, - height - ); - } - maskCtx.fillStyle = isPatternFill - ? fillColor.getPattern(maskCtx, this, false, patternTransform) + ? fillColor.getPattern( + maskCtx, + this, + ctx.mozCurrentTransformInverse, + false + ) : fillColor; maskCtx.fillRect(0, 0, width, height); @@ -2491,17 +2588,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { // scale the image to the unit square ctx.scale(1 / width, -1 / height); - const currentTransform = ctx.mozCurrentTransformInverse; - let widthScale = Math.max( - Math.hypot(currentTransform[0], currentTransform[1]), - 1 - ); - let heightScale = Math.max( - Math.hypot(currentTransform[2], currentTransform[3]), - 1 - ); - - let imgToPaint, tmpCanvas, tmpCtx; + let imgToPaint; // typeof check is needed due to node.js support, see issue #8489 if ( (typeof HTMLElement === "function" && imgData instanceof HTMLElement) || @@ -2509,61 +2596,26 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { ) { imgToPaint = imgData; } else { - tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", width, height); - tmpCtx = tmpCanvas.context; + const tmpCanvas = this.cachedCanvases.getCanvas( + "inlineImage", + width, + height + ); + const tmpCtx = tmpCanvas.context; putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); imgToPaint = tmpCanvas.canvas; } - let paintWidth = width, - paintHeight = height; - let tmpCanvasId = "prescale1"; - // Vertical or horizontal scaling shall not be more than 2 to not lose the - // pixels during drawImage operation, painting on the temporary canvas(es) - // that are twice smaller in size. - while ( - (widthScale > 2 && paintWidth > 1) || - (heightScale > 2 && paintHeight > 1) - ) { - let newWidth = paintWidth, - newHeight = paintHeight; - if (widthScale > 2 && paintWidth > 1) { - newWidth = Math.ceil(paintWidth / 2); - widthScale /= paintWidth / newWidth; - } - if (heightScale > 2 && paintHeight > 1) { - newHeight = Math.ceil(paintHeight / 2); - heightScale /= paintHeight / newHeight; - } - tmpCanvas = this.cachedCanvases.getCanvas( - tmpCanvasId, - newWidth, - newHeight - ); - tmpCtx = tmpCanvas.context; - tmpCtx.clearRect(0, 0, newWidth, newHeight); - tmpCtx.drawImage( - imgToPaint, - 0, - 0, - paintWidth, - paintHeight, - 0, - 0, - newWidth, - newHeight - ); - imgToPaint = tmpCanvas.canvas; - paintWidth = newWidth; - paintHeight = newHeight; - tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1"; - } - ctx.drawImage( + const scaled = this._scaleImage( imgToPaint, + ctx.mozCurrentTransformInverse + ); + ctx.drawImage( + scaled.img, 0, 0, - paintWidth, - paintHeight, + scaled.paintWidth, + scaled.paintHeight, 0, -height, width, @@ -2576,8 +2628,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { imgData, left: position[0], top: position[1], - width: width / currentTransform[0], - height: height / currentTransform[3], + width: width / ctx.mozCurrentTransformInverse[0], + height: height / ctx.mozCurrentTransformInverse[3], }); } this.restore(); diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index 84abe7057c1f1..6311e0e6f27c6 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -72,7 +72,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern { this._matrix = IR[8]; } - getPattern(ctx, owner, shadingFill = false, patternTransform = null) { + getPattern(ctx, owner, inverse, shadingFill = false) { const tmpCanvas = owner.cachedCanvases.getCanvas( "pattern", owner.ctx.canvas.width, @@ -121,11 +121,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern { tmpCtx.fill(); const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat"); - if (patternTransform) { - pattern.setTransform(patternTransform); - } else { - pattern.setTransform(createMatrix(ctx.mozCurrentTransformInverse)); - } + pattern.setTransform(createMatrix(inverse)); return pattern; } } @@ -380,7 +376,7 @@ class MeshShadingPattern extends BaseShadingPattern { }; } - getPattern(ctx, owner, shadingFill = false, patternTransform = null) { + getPattern(ctx, owner, inverse, shadingFill = false) { applyBoundingBox(ctx, this._bbox); let scale; if (shadingFill) { @@ -535,9 +531,25 @@ class TilingPattern { this.setFillAndStrokeStyleToContext(graphics, paintType, color); + let adjustedX0 = x0; + let adjustedY0 = y0; + let adjustedX1 = x1; + let adjustedY1 = y1; + // Some bounding boxes have negative x0/y0 cordinates which will cause the + // some of the drawing to be off of the canvas. To avoid this shift the + // bounding box over. + if (x0 < 0) { + adjustedX0 = 0; + adjustedX1 += Math.abs(x0); + } + if (y0 < 0) { + adjustedY0 = 0; + adjustedY1 += Math.abs(y0); + } + tmpCtx.translate(-(dimx.scale * adjustedX0), -(dimy.scale * adjustedY0)); graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0); - this.clipBbox(graphics, bbox, x0, y0, x1, y1); + this.clipBbox(graphics, adjustedX0, adjustedY0, adjustedX1, adjustedY1); graphics.baseTransform = graphics.ctx.mozCurrentTransform.slice(); @@ -549,6 +561,8 @@ class TilingPattern { canvas: tmpCanvas.canvas, scaleX: dimx.scale, scaleY: dimy.scale, + offsetX: adjustedX0, + offsetY: adjustedY0, }; } @@ -569,14 +583,12 @@ class TilingPattern { return { scale, size }; } - clipBbox(graphics, bbox, x0, y0, x1, y1) { - if (Array.isArray(bbox) && bbox.length === 4) { - const bboxWidth = x1 - x0; - const bboxHeight = y1 - y0; - graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); - graphics.clip(); - graphics.endPath(); - } + clipBbox(graphics, x0, y0, x1, y1) { + const bboxWidth = x1 - x0; + const bboxHeight = y1 - y0; + graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); + graphics.clip(); + graphics.endPath(); } setFillAndStrokeStyleToContext(graphics, paintType, color) { @@ -603,10 +615,9 @@ class TilingPattern { } } - getPattern(ctx, owner, shadingFill = false, patternTransform = null) { - ctx = this.ctx; + getPattern(ctx, owner, inverse, shadingFill = false) { // PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix. - let matrix = ctx.mozCurrentTransformInverse; + let matrix = inverse; if (!shadingFill) { matrix = Util.transform(matrix, owner.baseTransform); if (this.matrix) { @@ -619,6 +630,10 @@ class TilingPattern { let domMatrix = createMatrix(matrix); // Rescale and so that the ctx.createPattern call generates a pattern with // the desired size. + domMatrix = domMatrix.translate( + temporaryPatternCanvas.offsetX, + temporaryPatternCanvas.offsetY + ); domMatrix = domMatrix.scale( 1 / temporaryPatternCanvas.scaleX, 1 / temporaryPatternCanvas.scaleY diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 88ea9f3d6b9e9..c3185e1712c2c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -206,6 +206,7 @@ !issue11403_reduced.pdf !issue2074.pdf !scan-bad.pdf +!issue13561_reduced.pdf !bug847420.pdf !bug860632.pdf !bug894572.pdf diff --git a/test/pdfs/issue13561_reduced.pdf b/test/pdfs/issue13561_reduced.pdf new file mode 100644 index 0000000000000..e888b68d88af2 Binary files /dev/null and b/test/pdfs/issue13561_reduced.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index f238600fc9b0e..be54ed1d2531f 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -878,6 +878,12 @@ "lastPage": 1, "type": "eq" }, + { "id": "issue13561_reduced", + "file": "pdfs/issue13561_reduced.pdf", + "md5": "e68c315d6349530180dd90f93027147e", + "rounds": 1, + "type": "eq" + }, { "id": "issue5202", "file": "pdfs/issue5202.pdf", "md5": "bb9cc69211112e66aab40828086a4e5a",