diff --git a/src/display/canvas.js b/src/display/canvas.js index 6a03a7d47..bc13f24aa 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -26,7 +26,12 @@ import { Util, warn, } from "../shared/util.js"; -import { getRGB, PixelsPerInch } from "./display_utils.js"; +import { + getCurrentTransform, + getCurrentTransformInverse, + getRGB, + PixelsPerInch, +} from "./display_utils.js"; import { getShadingPattern, PathType, @@ -54,13 +59,6 @@ const MAX_SIZE_TO_COMPILE = 1000; const FULL_CHUNK_HEIGHT = 16; -// Because of https://bugs.chromium.org/p/chromium/issues/detail?id=1170396 -// some curves aren't rendered correctly. -// Multiplying lineWidth by this factor should help to have "correct" -// rendering with no artifacts. -// Once the bug is fixed upstream, we can remove this constant and its use. -const LINEWIDTH_SCALE_FACTOR = 1.000001; - /** * 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 @@ -191,177 +189,21 @@ function mirrorContextOperations(ctx, destCtx) { }; } -function addContextCurrentTransform(ctx) { - if (ctx._transformStack) { - // Reset the transform stack. - ctx._transformStack = []; - } - // If the context doesn't expose a `mozCurrentTransform`, add a JS based one. - if (ctx.mozCurrentTransform) { - return; - } - ctx._originalSave = ctx.save; - ctx._originalRestore = ctx.restore; - ctx._originalRotate = ctx.rotate; - ctx._originalScale = ctx.scale; - ctx._originalTranslate = ctx.translate; - ctx._originalTransform = ctx.transform; - ctx._originalSetTransform = ctx.setTransform; - ctx._originalResetTransform = ctx.resetTransform; - - ctx._transformMatrix = ctx._transformMatrix || [1, 0, 0, 1, 0, 0]; - ctx._transformStack = []; - - try { - // The call to getOwnPropertyDescriptor throws an exception in Node.js: - // "TypeError: Method lineWidth called on incompatible receiver - // #". - const desc = Object.getOwnPropertyDescriptor( - Object.getPrototypeOf(ctx), - "lineWidth" - ); - - ctx._setLineWidth = desc.set; - ctx._getLineWidth = desc.get; - - Object.defineProperty(ctx, "lineWidth", { - set: function setLineWidth(width) { - this._setLineWidth(width * LINEWIDTH_SCALE_FACTOR); - }, - get: function getLineWidth() { - return this._getLineWidth(); - }, - }); - } catch (_) {} - - Object.defineProperty(ctx, "mozCurrentTransform", { - get: function getCurrentTransform() { - return this._transformMatrix; - }, - }); - - Object.defineProperty(ctx, "mozCurrentTransformInverse", { - get: function getCurrentTransformInverse() { - // Calculation done using WolframAlpha: - // http://www.wolframalpha.com/input/? - // i=Inverse+{{a%2C+c%2C+e}%2C+{b%2C+d%2C+f}%2C+{0%2C+0%2C+1}} - - const [a, b, c, d, e, f] = this._transformMatrix; - const ad_bc = a * d - b * c; - const bc_ad = b * c - a * d; - - return [ - d / ad_bc, - b / bc_ad, - c / bc_ad, - a / ad_bc, - (d * e - c * f) / bc_ad, - (b * e - a * f) / ad_bc, - ]; - }, - }); - - ctx.save = function ctxSave() { - const old = this._transformMatrix; - this._transformStack.push(old); - this._transformMatrix = old.slice(0, 6); - - this._originalSave(); - }; - - ctx.restore = function ctxRestore() { - if (this._transformStack.length === 0) { - warn("Tried to restore a ctx when the stack was already empty."); - } - const prev = this._transformStack.pop(); - if (prev) { - this._transformMatrix = prev; - this._originalRestore(); - } - }; - - ctx.translate = function ctxTranslate(x, y) { - const m = this._transformMatrix; - m[4] = m[0] * x + m[2] * y + m[4]; - m[5] = m[1] * x + m[3] * y + m[5]; - - this._originalTranslate(x, y); - }; - - ctx.scale = function ctxScale(x, y) { - const m = this._transformMatrix; - m[0] *= x; - m[1] *= x; - m[2] *= y; - m[3] *= y; - - this._originalScale(x, y); - }; - - ctx.transform = function ctxTransform(a, b, c, d, e, f) { - const m = this._transformMatrix; - this._transformMatrix = [ - m[0] * a + m[2] * b, - m[1] * a + m[3] * b, - m[0] * c + m[2] * d, - m[1] * c + m[3] * d, - m[0] * e + m[2] * f + m[4], - m[1] * e + m[3] * f + m[5], - ]; - - ctx._originalTransform(a, b, c, d, e, f); - }; - - ctx.setTransform = function ctxSetTransform(a, b, c, d, e, f) { - this._transformMatrix = [a, b, c, d, e, f]; - - ctx._originalSetTransform(a, b, c, d, e, f); - }; - - ctx.resetTransform = function ctxResetTransform() { - this._transformMatrix = [1, 0, 0, 1, 0, 0]; - - ctx._originalResetTransform(); - }; - - ctx.rotate = function ctxRotate(angle) { - const cosValue = Math.cos(angle); - const sinValue = Math.sin(angle); - - const m = this._transformMatrix; - this._transformMatrix = [ - m[0] * cosValue + m[2] * sinValue, - m[1] * cosValue + m[3] * sinValue, - m[0] * -sinValue + m[2] * cosValue, - m[1] * -sinValue + m[3] * cosValue, - m[4], - m[5], - ]; - - this._originalRotate(angle); - }; -} - class CachedCanvases { constructor(canvasFactory) { this.canvasFactory = canvasFactory; this.cache = Object.create(null); } - getCanvas(id, width, height, trackTransform) { + getCanvas(id, width, height) { let canvasEntry; if (this.cache[id] !== undefined) { canvasEntry = this.cache[id]; this.canvasFactory.reset(canvasEntry, width, height); - // reset canvas transform for emulated mozCurrentTransform, if needed - canvasEntry.context.setTransform(1, 0, 0, 1, 0, 0); } else { canvasEntry = this.canvasFactory.create(width, height); this.cache[id] = canvasEntry; } - if (trackTransform) { - addContextCurrentTransform(canvasEntry.context); - } return canvasEntry; } @@ -390,7 +232,7 @@ function drawImageAtIntegerCoords( destW, destH ) { - const [a, b, c, d, tx, ty] = ctx.mozCurrentTransform; + const [a, b, c, d, tx, ty] = getCurrentTransform(ctx); if (b === 0 && c === 0) { // top-left corner is at (X, Y) and // bottom-right one is at (X + width, Y + height). @@ -1250,11 +1092,7 @@ class CanvasGraphics { this.outputScaleY = 1; this.backgroundColor = pageColors?.background || null; this.foregroundColor = pageColors?.foreground || null; - if (canvasCtx) { - // NOTE: if mozCurrentTransform is polyfilled, then the current state of - // the transformation must already be set in canvasCtx._transformMatrix. - addContextCurrentTransform(canvasCtx); - } + this._cachedScaleForStroking = null; this._cachedGetSinglePixelWidth = null; this._cachedBitmapsMap = new Map(); @@ -1350,8 +1188,7 @@ class CanvasGraphics { const transparentCanvas = this.cachedCanvases.getCanvas( "transparent", width, - height, - /* trackTransform */ true + height ); this.compositeCtx = this.ctx; this.transparentCanvas = transparentCanvas.canvas; @@ -1359,7 +1196,7 @@ class CanvasGraphics { this.ctx.save(); // The transform can be applied before rendering, transferring it to // the new canvas. - this.ctx.transform(...this.compositeCtx.mozCurrentTransform); + this.ctx.transform(...getCurrentTransform(this.compositeCtx)); } this.ctx.save(); @@ -1372,7 +1209,7 @@ class CanvasGraphics { this.ctx.transform(...viewport.transform); this.viewportScale = viewport.scale; - this.baseTransform = this.ctx.mozCurrentTransform.slice(); + this.baseTransform = getCurrentTransform(this.ctx); if (this.imageLayer) { this.imageLayer.beginLayout(); @@ -1529,8 +1366,7 @@ class CanvasGraphics { tmpCanvas = this.cachedCanvases.getCanvas( tmpCanvasId, newWidth, - newHeight, - /* trackTransform */ false + newHeight ); tmpCtx = tmpCanvas.context; tmpCtx.clearRect(0, 0, newWidth, newHeight); @@ -1562,7 +1398,7 @@ class CanvasGraphics { const { width, height } = img; const fillColor = this.current.fillColor; const isPatternFill = this.current.patternFill; - const currentTransform = ctx.mozCurrentTransform; + const currentTransform = getCurrentTransform(ctx); let cache, cacheKey, scaled, maskCanvas; if ((img.bitmap || img.data) && img.count > 1) { @@ -1603,12 +1439,7 @@ class CanvasGraphics { } if (!scaled) { - maskCanvas = this.cachedCanvases.getCanvas( - "maskCanvas", - width, - height, - /* trackTransform */ false - ); + maskCanvas = this.cachedCanvases.getCanvas("maskCanvas", width, height); putBinaryImageMask(maskCanvas.context, img); } @@ -1634,8 +1465,7 @@ class CanvasGraphics { const fillCanvas = this.cachedCanvases.getCanvas( "fillCanvas", drawnWidth, - drawnHeight, - /* trackTransform */ true + drawnHeight ); const fillCtx = fillCanvas.context; @@ -1652,7 +1482,7 @@ class CanvasGraphics { // Pre-scale if needed to improve image smoothing. scaled = this._scaleImage( maskCanvas.canvas, - fillCtx.mozCurrentTransformInverse + getCurrentTransformInverse(fillCtx) ); scaled = scaled.img; if (cache && isPatternFill) { @@ -1661,7 +1491,7 @@ class CanvasGraphics { } fillCtx.imageSmoothingEnabled = getImageSmoothingEnabled( - fillCtx.mozCurrentTransform, + getCurrentTransform(fillCtx), img.interpolate ); @@ -1679,7 +1509,7 @@ class CanvasGraphics { ); fillCtx.globalCompositeOperation = "source-in"; - const inverse = Util.transform(fillCtx.mozCurrentTransformInverse, [ + const inverse = Util.transform(getCurrentTransformInverse(fillCtx), [ 1, 0, 0, @@ -1830,13 +1660,12 @@ class CanvasGraphics { const scratchCanvas = this.cachedCanvases.getCanvas( cacheId, drawnWidth, - drawnHeight, - /* trackTransform */ true + drawnHeight ); this.suspendedCtx = this.ctx; this.ctx = scratchCanvas.context; const ctx = this.ctx; - ctx.setTransform(...this.suspendedCtx.mozCurrentTransform); + ctx.setTransform(...getCurrentTransform(this.suspendedCtx)); copyCtxState(this.suspendedCtx, ctx); mirrorContextOperations(ctx, this.suspendedCtx); @@ -1940,7 +1769,7 @@ class CanvasGraphics { let x = current.x, y = current.y; let startX, startY; - const currentTransform = ctx.mozCurrentTransform; + 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: @@ -2096,7 +1925,7 @@ class CanvasGraphics { ctx.strokeStyle = strokeColor.getPattern( ctx, this, - ctx.mozCurrentTransformInverse, + getCurrentTransformInverse(ctx), PathType.STROKE ); this.rescaleAndStroke(/* saveRestore */ false); @@ -2129,7 +1958,7 @@ class CanvasGraphics { ctx.fillStyle = fillColor.getPattern( ctx, this, - ctx.mozCurrentTransformInverse, + getCurrentTransformInverse(ctx), PathType.FILL ); needRestore = true; @@ -2383,7 +2212,7 @@ class CanvasGraphics { if (isAddToPathSet) { const paths = this.pendingTextPaths || (this.pendingTextPaths = []); paths.push({ - transform: ctx.mozCurrentTransform, + transform: getCurrentTransform(ctx), x, y, fontSize, @@ -2398,8 +2227,7 @@ class CanvasGraphics { const { context: ctx } = this.cachedCanvases.getCanvas( "isFontSubpixelAAEnabled", 10, - 10, - /* trackTransform */ false + 10 ); ctx.scale(1.5, 1); ctx.fillText("I", 0, 10); @@ -2459,10 +2287,10 @@ class CanvasGraphics { const pattern = current.fillColor.getPattern( ctx, this, - ctx.mozCurrentTransformInverse, + getCurrentTransformInverse(ctx), PathType.FILL ); - patternTransform = ctx.mozCurrentTransform; + patternTransform = getCurrentTransform(ctx); ctx.restore(); ctx.fillStyle = pattern; } @@ -2663,8 +2491,7 @@ class CanvasGraphics { let pattern; if (IR[0] === "TilingPattern") { const color = IR[1]; - const baseTransform = - this.baseTransform || this.ctx.mozCurrentTransform.slice(); + const baseTransform = this.baseTransform || getCurrentTransform(this.ctx); const canvasGraphicsFactory = { createCanvasGraphics: ctx => { return new CanvasGraphics( @@ -2735,11 +2562,11 @@ class CanvasGraphics { ctx.fillStyle = pattern.getPattern( ctx, this, - ctx.mozCurrentTransformInverse, + getCurrentTransformInverse(ctx), PathType.SHADING ); - const inv = ctx.mozCurrentTransformInverse; + const inv = getCurrentTransformInverse(ctx); if (inv) { const canvas = ctx.canvas; const width = canvas.width; @@ -2790,13 +2617,13 @@ class CanvasGraphics { this.transform(...matrix); } - this.baseTransform = this.ctx.mozCurrentTransform; + this.baseTransform = getCurrentTransform(this.ctx); if (bbox) { const width = bbox[2] - bbox[0]; const height = bbox[3] - bbox[1]; this.ctx.rect(bbox[0], bbox[1], width, height); - this.current.updateRectMinMax(this.ctx.mozCurrentTransform, bbox); + this.current.updateRectMinMax(getCurrentTransform(this.ctx), bbox); this.clip(); this.endPath(); } @@ -2847,7 +2674,7 @@ class CanvasGraphics { warn("Knockout groups not supported."); } - const currentTransform = currentCtx.mozCurrentTransform; + const currentTransform = getCurrentTransform(currentCtx); if (group.matrix) { currentCtx.transform(...group.matrix); } @@ -2859,7 +2686,7 @@ class CanvasGraphics { // will actually be. let bounds = Util.getAxialAlignedBoundingBox( group.bbox, - currentCtx.mozCurrentTransform + getCurrentTransform(currentCtx) ); // Clip the bounding box to the current canvas. const canvasBounds = [ @@ -2896,8 +2723,7 @@ class CanvasGraphics { const scratchCanvas = this.cachedCanvases.getCanvas( cacheId, drawnWidth, - drawnHeight, - /* trackTransform */ true + drawnHeight ); const groupCtx = scratchCanvas.context; @@ -2959,7 +2785,7 @@ class CanvasGraphics { this.restore(); } else { this.ctx.restore(); - const currentMtx = this.ctx.mozCurrentTransform; + const currentMtx = getCurrentTransform(this.ctx); this.restore(); this.ctx.save(); this.ctx.setTransform(...currentMtx); @@ -3003,7 +2829,7 @@ class CanvasGraphics { rect[3] = height; const [scaleX, scaleY] = Util.singularValueDecompose2dScale( - this.ctx.mozCurrentTransform + getCurrentTransform(this.ctx) ); const { viewportScale } = this; const canvasWidth = Math.ceil( @@ -3021,7 +2847,6 @@ class CanvasGraphics { this.annotationCanvasMap.set(id, canvas); this.annotationCanvas.savedCtx = this.ctx; this.ctx = context; - addContextCurrentTransform(this.ctx); this.ctx.setTransform(scaleX, 0, 0, -scaleY, 0, height * scaleY); resetCtxToDefault(this.ctx, this.foregroundColor); @@ -3100,7 +2925,7 @@ class CanvasGraphics { const ctx = this.ctx; ctx.save(); - const currentTransform = ctx.mozCurrentTransform; + const currentTransform = getCurrentTransform(ctx); ctx.transform(scaleX, skewX, skewY, scaleY, 0, 0); const mask = this._createMaskCanvas(img); @@ -3137,8 +2962,7 @@ class CanvasGraphics { const maskCanvas = this.cachedCanvases.getCanvas( "maskCanvas", width, - height, - /* trackTransform */ false + height ); const maskCtx = maskCanvas.context; maskCtx.save(); @@ -3152,7 +2976,7 @@ class CanvasGraphics { ? fillColor.getPattern( maskCtx, this, - ctx.mozCurrentTransformInverse, + getCurrentTransformInverse(ctx), PathType.FILL ) : fillColor; @@ -3241,17 +3065,19 @@ class CanvasGraphics { const tmpCanvas = this.cachedCanvases.getCanvas( "inlineImage", width, - height, - /* trackTransform */ false + height ); const tmpCtx = tmpCanvas.context; putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); imgToPaint = tmpCanvas.canvas; } - const scaled = this._scaleImage(imgToPaint, ctx.mozCurrentTransformInverse); + const scaled = this._scaleImage( + imgToPaint, + getCurrentTransformInverse(ctx) + ); ctx.imageSmoothingEnabled = getImageSmoothingEnabled( - ctx.mozCurrentTransform, + getCurrentTransform(ctx), imgData.interpolate ); @@ -3290,12 +3116,7 @@ class CanvasGraphics { const w = imgData.width; const h = imgData.height; - const tmpCanvas = this.cachedCanvases.getCanvas( - "inlineImage", - w, - h, - /* trackTransform */ false - ); + const tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", w, h); const tmpCtx = tmpCanvas.context; putBinaryImageData(tmpCtx, imgData, this.current.transferMaps); @@ -3409,7 +3230,7 @@ class CanvasGraphics { getSinglePixelWidth() { if (!this._cachedGetSinglePixelWidth) { - const m = this.ctx.mozCurrentTransform; + const m = getCurrentTransform(this.ctx); if (m[1] === 0 && m[2] === 0) { // Fast path this._cachedGetSinglePixelWidth = @@ -3433,7 +3254,7 @@ class CanvasGraphics { // to 1 after transform. if (!this._cachedScaleForStroking) { const { lineWidth } = this.current; - const m = this.ctx.mozCurrentTransform; + const m = getCurrentTransform(this.ctx); let scaleX, scaleY; if (m[1] === 0 && m[2] === 0) { @@ -3489,7 +3310,7 @@ class CanvasGraphics { let savedMatrix, savedDashes, savedDashOffset; if (saveRestore) { - savedMatrix = ctx.mozCurrentTransform.slice(); + savedMatrix = getCurrentTransform(ctx); savedDashes = ctx.getLineDash().slice(); savedDashOffset = ctx.lineDashOffset; } @@ -3517,7 +3338,7 @@ class CanvasGraphics { } getCanvasPosition(x, y) { - const transform = this.ctx.mozCurrentTransform; + const transform = getCurrentTransform(this.ctx); return [ transform[0] * x + transform[2] * y + transform[4], transform[1] * x + transform[3] * y + transform[5], diff --git a/src/display/display_utils.js b/src/display/display_utils.js index 19b8b4f5c..dff0a83c6 100644 --- a/src/display/display_utils.js +++ b/src/display/display_utils.js @@ -641,6 +641,16 @@ function binarySearchFirstItem(items, condition, start = 0) { return minIndex; /* === maxIndex */ } +function getCurrentTransform(ctx) { + const { a, b, c, d, e, f } = ctx.getTransform(); + return [a, b, c, d, e, f]; +} + +function getCurrentTransformInverse(ctx) { + const { a, b, c, d, e, f } = ctx.getTransform().invertSelf(); + return [a, b, c, d, e, f]; +} + export { binarySearchFirstItem, deprecated, @@ -649,6 +659,8 @@ export { DOMStandardFontDataFactory, DOMSVGFactory, getColorValues, + getCurrentTransform, + getCurrentTransformInverse, getFilenameFromUrl, getPdfFilenameFromUrl, getRGB, diff --git a/src/display/pattern_helper.js b/src/display/pattern_helper.js index f5dd57b64..2f1f452e9 100644 --- a/src/display/pattern_helper.js +++ b/src/display/pattern_helper.js @@ -21,6 +21,7 @@ import { Util, warn, } from "../shared/util.js"; +import { getCurrentTransform } from "./display_utils.js"; import { isNodeJS } from "../shared/is_node.js"; const PathType = { @@ -96,7 +97,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern { if (pathType === PathType.STROKE || pathType === PathType.FILL) { const ownerBBox = owner.current.getClippedPathBoundingBox( pathType, - ctx.mozCurrentTransform + getCurrentTransform(ctx) ) || [0, 0, 0, 0]; // Create a canvas that is only as big as the current path. This doesn't // allow us to cache the pattern, but it generally creates much smaller @@ -409,7 +410,7 @@ class MeshShadingPattern extends BaseShadingPattern { applyBoundingBox(ctx, this._bbox); let scale; if (pathType === PathType.SHADING) { - scale = Util.singularValueDecompose2dScale(ctx.mozCurrentTransform); + scale = Util.singularValueDecompose2dScale(getCurrentTransform(ctx)); } else { // Obtain scale from matrix and current transformation matrix. scale = Util.singularValueDecompose2dScale(owner.baseTransform); @@ -584,7 +585,7 @@ class TilingPattern { this.clipBbox(graphics, adjustedX0, adjustedY0, adjustedX1, adjustedY1); - graphics.baseTransform = graphics.ctx.mozCurrentTransform.slice(); + graphics.baseTransform = getCurrentTransform(graphics.ctx); graphics.executeOperatorList(operatorList); @@ -620,7 +621,7 @@ class TilingPattern { const bboxWidth = x1 - x0; const bboxHeight = y1 - y0; graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); - graphics.current.updateRectMinMax(graphics.ctx.mozCurrentTransform, [ + graphics.current.updateRectMinMax(getCurrentTransform(graphics.ctx), [ x0, y0, x1,