diff --git a/src/display/canvas.js b/src/display/canvas.js index aa076e63fefd7f..a0ff48ff77dbf3 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -377,6 +377,64 @@ class CachedCanvases { } } +function drawImageAtIntegerCoords( + ctx, + srcImg, + srcX, + srcY, + srcW, + srcH, + destX, + destY, + destW, + destH +) { + const [a, b, c, d, tx, ty] = ctx.mozCurrentTransform; + let scaleX, scaleY; + // The cases of a diagonal or anti-diagonal matrices are treated separately + // in order to avoid some rounding errors which can lead to mis-position + // some images (it's visible in commenting the two ifs and with the pdf in + // https://github.com/mozilla/pdf.js/issues/3351#issue-15307536). + if (b === 0 && c === 0) { + scaleX = Math.abs(a); + scaleY = Math.abs(d); + ctx.setTransform(Math.sign(a), 0, 0, Math.sign(d), 0, 0); + } else if (a === 0 && d === 0) { + scaleX = Math.abs(c); + scaleY = Math.abs(b); + ctx.setTransform(0, Math.sign(b), Math.sign(c), 0, 0, 0); + } else { + const absDet = Math.abs(a * d - b * c); + scaleX = absDet / Math.hypot(a, c); + scaleY = absDet / Math.hypot(b, d); + ctx.setTransform(a / scaleX, b / scaleY, c / scaleX, d / scaleY, 0, 0); + } + + // top-left corner is at (X, Y) and + // bottom-right one is at (X + width, Y + height). + + // If leftX is 4.321 then it's rounded to 4. + // If width is 10.432 then it's rounded to 11! because + // rightX = leftX + width = 14.753 which is rounded to 15 + // so after rounding the total width is 11 (15 - 4). + // It's why we can't just floor/ceil uniformly, it just depends + // on the values we've. + + const tlX = destX * scaleX + tx; + const rTlX = Math.round(tlX); + const tlY = destY * scaleY + ty; + const rTlY = Math.round(tlY); + const brX = (destX + destW) * scaleX + tx; + const rWidth = Math.round(brX) - rTlX; + const brY = (destY + destH) * scaleY + ty; + const rHeight = Math.round(brY) - rTlY; + + ctx.drawImage(srcImg, srcX, srcY, srcW, srcH, rTlX, rTlY, rWidth, rHeight); + ctx.setTransform(a, b, c, d, tx, ty); + + return [rWidth, rHeight]; +} + function compileType3Glyph(imgData) { const POINT_TO_PROCESS_LIMIT = 1000; const POINT_TYPES = new Uint8Array([ @@ -1461,8 +1519,8 @@ class CanvasGraphics { 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 drawnWidth = Math.round(rect[2] - rect[0]); + const drawnHeight = Math.round(rect[3] - rect[1]); const fillCanvas = this.cachedCanvases.getCanvas( "fillCanvas", drawnWidth, @@ -1496,7 +1554,9 @@ class CanvasGraphics { fillCtx.mozCurrentTransform, img.interpolate ); - fillCtx.drawImage( + + drawImageAtIntegerCoords( + fillCtx, scaled, 0, 0, @@ -3005,7 +3065,18 @@ class CanvasGraphics { ctx.save(); ctx.transform.apply(ctx, image.transform); ctx.scale(1, -1); - ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1); + drawImageAtIntegerCoords( + ctx, + maskCanvas.canvas, + 0, + 0, + width, + height, + 0, + -1, + 1, + 1 + ); ctx.restore(); } this.compose(); @@ -3085,7 +3156,9 @@ class CanvasGraphics { ctx.mozCurrentTransform, imgData.interpolate ); - ctx.drawImage( + + const [rWidth, rHeight] = drawImageAtIntegerCoords( + ctx, scaled.img, 0, 0, @@ -3103,8 +3176,8 @@ class CanvasGraphics { imgData, left: position[0], top: position[1], - width: width / ctx.mozCurrentTransformInverse[0], - height: height / ctx.mozCurrentTransformInverse[3], + width: rWidth, + height: rHeight, }); } this.compose(); @@ -3133,7 +3206,8 @@ class CanvasGraphics { ctx.save(); ctx.transform.apply(ctx, entry.transform); ctx.scale(1, -1); - ctx.drawImage( + drawImageAtIntegerCoords( + ctx, tmpCanvas.canvas, entry.x, entry.y, diff --git a/test/pdfs/issue3351.1.pdf.link b/test/pdfs/issue3351.1.pdf.link new file mode 100644 index 00000000000000..9c5232e26809e8 --- /dev/null +++ b/test/pdfs/issue3351.1.pdf.link @@ -0,0 +1,2 @@ +https://github.com/mozilla/pdf.js/files/8582209/Doc2.pdf + diff --git a/test/pdfs/issue3351.2.pdf.link b/test/pdfs/issue3351.2.pdf.link new file mode 100644 index 00000000000000..701a90a58bd1a2 --- /dev/null +++ b/test/pdfs/issue3351.2.pdf.link @@ -0,0 +1,2 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=8741330 + diff --git a/test/pdfs/issue3351.3.pdf.link b/test/pdfs/issue3351.3.pdf.link new file mode 100644 index 00000000000000..02665a4f215eb5 --- /dev/null +++ b/test/pdfs/issue3351.3.pdf.link @@ -0,0 +1,2 @@ +https://bugzilla.mozilla.org/attachment.cgi?id=8741334 + diff --git a/test/test_manifest.json b/test/test_manifest.json index 6082ee0f1b825e..5407767474e431 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -6403,5 +6403,29 @@ "link": true, "lastPage": 1, "type": "eq" + }, + { "id": "issue3351.1", + "file": "pdfs/issue3351.1.pdf", + "md5": "4216245a5f18bb3eac80575ccf0b272d", + "rounds": 1, + "link": true, + "lastPage": 1, + "type": "eq" + }, + { "id": "issue3351.2", + "file": "pdfs/issue3351.2.pdf", + "md5": "fa3fd1659c409c7824ef8838c3071efc", + "rounds": 1, + "link": true, + "lastPage": 1, + "type": "eq" + }, + { "id": "issue3351.3", + "file": "pdfs/issue3351.3.pdf", + "md5": "60e2f2c480b6bc0e7f726743c6896520", + "rounds": 1, + "link": true, + "lastPage": 1, + "type": "eq" } ]