diff --git a/src/display/canvas.js b/src/display/canvas.js index 0083289ab..1b8c69bd4 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -31,16 +31,12 @@ import { getShadingPatternFromIR, 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. - // Minimal font size that would be used during canvas fillText operations. const MIN_FONT_SIZE = 16; // Maximum font size that would be used during canvas fillText operations. const MAX_FONT_SIZE = 100; const MAX_GROUP_SIZE = 4096; -// Heuristic value used when enforcing minimum line widths. -const MIN_WIDTH_FACTOR = 0.65; - const COMPILE_TYPE3_GLYPHS = true; const MAX_SIZE_TO_COMPILE = 1000; @@ -1273,21 +1269,20 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { case OPS.rectangle: x = args[j++]; y = args[j++]; - let width = args[j++]; - let height = args[j++]; - if (width === 0 && ctx.lineWidth < this.getSinglePixelWidth()) { - width = this.getSinglePixelWidth(); - } - if (height === 0 && ctx.lineWidth < this.getSinglePixelWidth()) { - height = this.getSinglePixelWidth(); - } + const width = args[j++]; + const height = args[j++]; + const xw = x + width; const yh = y + height; ctx.moveTo(x, y); - ctx.lineTo(xw, y); - ctx.lineTo(xw, yh); - ctx.lineTo(x, yh); - ctx.lineTo(x, y); + if (width === 0 || height === 0) { + ctx.lineTo(xw, yh); + } else { + ctx.lineTo(xw, y); + ctx.lineTo(xw, yh); + ctx.lineTo(x, yh); + } + ctx.closePath(); break; case OPS.moveTo: @@ -1361,19 +1356,31 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { const transform = ctx.mozCurrentTransform; const scale = Util.singularValueDecompose2dScale(transform)[0]; ctx.strokeStyle = strokeColor.getPattern(ctx, this); - ctx.lineWidth = Math.max( - this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, - this.current.lineWidth * scale - ); + const lineWidth = this.getSinglePixelWidth(); + if (lineWidth === -1) { + ctx.resetTransform(); + ctx.lineWidth = 1; + } else { + ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth * scale); + } ctx.stroke(); ctx.restore(); } else { - // Prevent drawing too thin lines by enforcing a minimum line width. - ctx.lineWidth = Math.max( - this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, - this.current.lineWidth - ); - ctx.stroke(); + const lineWidth = this.getSinglePixelWidth(); + if (lineWidth === -1) { + // The current transform will transform a square pixel into + // a parallelogramm where both heights are lower than 1 and + // not equal. + ctx.save(); + ctx.resetTransform(); + ctx.lineWidth = 1; + ctx.stroke(); + ctx.restore(); + } else { + // Prevent drawing too thin lines by enforcing a minimum line width. + ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth); + ctx.stroke(); + } } } if (consumePath) { @@ -1577,7 +1584,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { this.moveText(0, this.current.leading); }, - paintChar(character, x, y, patternTransform) { + paintChar(character, x, y, patternTransform, resetLineWidthToOne) { const ctx = this.ctx; const current = this.current; const font = current.font; @@ -1613,6 +1620,10 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { fillStrokeMode === TextRenderingMode.STROKE || fillStrokeMode === TextRenderingMode.FILL_STROKE ) { + if (resetLineWidthToOne) { + ctx.resetTransform(); + ctx.lineWidth = 1; + } ctx.stroke(); } ctx.restore(); @@ -1627,7 +1638,16 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { fillStrokeMode === TextRenderingMode.STROKE || fillStrokeMode === TextRenderingMode.FILL_STROKE ) { - ctx.strokeText(character, x, y); + if (resetLineWidthToOne) { + ctx.save(); + ctx.moveTo(x, y); + ctx.resetTransform(); + ctx.lineWidth = 1; + ctx.strokeText(character, 0, 0); + ctx.restore(); + } else { + ctx.strokeText(character, x, y); + } } } @@ -1714,6 +1734,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { } let lineWidth = current.lineWidth; + let resetLineWidthToOne = false; const scale = current.textMatrixScale; if (scale === 0 || lineWidth === 0) { const fillStrokeMode = @@ -1723,7 +1744,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { fillStrokeMode === TextRenderingMode.FILL_STROKE ) { this._cachedGetSinglePixelWidth = null; - lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR; + lineWidth = this.getSinglePixelWidth(); + resetLineWidthToOne = lineWidth === -1; } } else { lineWidth /= scale; @@ -1791,7 +1813,13 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { // common case ctx.fillText(character, scaledX, scaledY); } else { - this.paintChar(character, scaledX, scaledY, patternTransform); + this.paintChar( + character, + scaledX, + scaledY, + patternTransform, + resetLineWidthToOne + ); if (accent) { const scaledAccentX = scaledX + (fontSize * accent.offset.x) / fontSizeScale; @@ -1801,7 +1829,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { accent.fontChar, scaledAccentX, scaledAccentY, - patternTransform + patternTransform, + resetLineWidthToOne ); } } @@ -2622,17 +2651,40 @@ const CanvasGraphics = (function CanvasGraphicsClosure() { } ctx.beginPath(); }, - getSinglePixelWidth(scale) { + getSinglePixelWidth() { if (this._cachedGetSinglePixelWidth === null) { - const inverse = this.ctx.mozCurrentTransformInverse; - // max of the current horizontal and vertical scale - this._cachedGetSinglePixelWidth = Math.sqrt( - Math.max( - inverse[0] * inverse[0] + inverse[1] * inverse[1], - inverse[2] * inverse[2] + inverse[3] * inverse[3] - ) - ); + // If transform is [a b] then a pixel (square) is transformed + // [c d] + // into a parallelogramm: its area is the abs value of determinant. + // This parallelogram has 2 heights: + // - Area / |col_1|; + // - Area / |col_2|. + // so in order to get a height of at least 1, pixel height + // must be computed as followed: + // h = max(sqrt(a² + c²) / |det(M)|, sqrt(b² + d²) / |det(M)|). + // This is equivalent to: + // h = max(|line_1_inv(M)|, |line_2_inv(M)|) + const m = this.ctx.mozCurrentTransform; + const sqDet = (m[0] * m[3] - m[2] * m[1]) ** 2; + const sqNorm1 = m[0] ** 2 + m[2] ** 2; + const sqNorm2 = m[1] ** 2 + m[3] ** 2; + if (sqNorm1 !== sqNorm2 && sqNorm1 > sqDet && sqNorm2 > sqDet) { + // The parallelogramm isn't a losange and both heights + // are lower than 1 so the resulting line width must be 1 + // but it cannot be achieved with one scale: when scaling a pixel + // we'll get a rectangle (see isssue #12295). + this._cachedGetSinglePixelWidth = -1; + } else if (sqDet > Number.EPSILON ** 2) { + // The multiplication by the constant 1.000001 is here to have + // a number slightly greater than what we "exactly" want. + this._cachedGetSinglePixelWidth = + Math.sqrt(Math.max(sqNorm1, sqNorm2) / sqDet) * 1.0000001; + } else { + // Matrix is non-invertible.x + this._cachedGetSinglePixelWidth = 1; + } } + return this._cachedGetSinglePixelWidth; }, getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) { diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index f1cda2ee9..5e5f8bf79 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -114,6 +114,7 @@ !bug1050040.pdf !bug1200096.pdf !bug1068432.pdf +!issue12295.pdf !bug1146106.pdf !bug1245391_reduced.pdf !bug1252420.pdf @@ -277,6 +278,7 @@ !gradientfill.pdf !bug903856.pdf !bug850854.pdf +!issue12810.pdf !bug866395.pdf !issue12010_reduced.pdf !issue11718_reduced.pdf @@ -313,6 +315,7 @@ !issue3371.pdf !issue2956.pdf !issue2537r.pdf +!issue12810.pdf !issue269_1.pdf !bug946506.pdf !issue3885.pdf diff --git a/test/pdfs/issue12295.pdf b/test/pdfs/issue12295.pdf new file mode 100644 index 000000000..e7ddb9949 Binary files /dev/null and b/test/pdfs/issue12295.pdf differ diff --git a/test/pdfs/issue12810.pdf b/test/pdfs/issue12810.pdf new file mode 100644 index 000000000..170c65ef5 Binary files /dev/null and b/test/pdfs/issue12810.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index abe1a6d30..6913219f4 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1190,6 +1190,12 @@ "link": false, "type": "eq" }, + { "id": "issue12295", + "file": "pdfs/issue12295.pdf", + "md5": "c534f74866ba8ada56010d19b57231ec", + "rounds": 1, + "type": "eq" + }, { "id": "bug1245391-text", "file": "pdfs/bug1245391_reduced.pdf", "md5": "6c946045ee0f2f663f269717c0f1614a", @@ -2329,6 +2335,12 @@ "rounds": 1, "type": "eq" }, + { "id": "issue12810", + "file": "pdfs/issue12810.pdf", + "md5": "585e19781308603dd706f941b1ace774", + "rounds": 1, + "type": "eq" + }, { "id": "pr4606", "file": "pdfs/pr4606.pdf", "md5": "6574fde2314648600056bd0e229df98c", @@ -4324,6 +4336,12 @@ "rounds": 1, "type": "eq" }, + { "id": "issue12810", + "file": "pdfs/issue12810.pdf", + "md5": "585e19781308603dd706f941b1ace774", + "rounds": 1, + "type": "eq" + }, { "id": "issue2956", "file": "pdfs/issue2956.pdf", "md5": "d8f68cbbb4bf54cde9f7f878acb6d7cd",