Enforce line width to be at least 1px after applied transform

* add a comment to explain how minimal linewidth is computed.
 * when context.linewidth < 1 after transform, firefox and chrome
   don't render in the same way (issue #12810).
 * set lineWidth to 1 after transform and before stroking
   - aims fix issue #12295
   - a pixel can be transformed into a rectangle with both heights < 1.
     A single rescale leads to a rectangle with dim equals to 1 and
     the other to something greater than 1.
 * change the way to render rectangle with null dimensions:
   - right now we rely on the lineWidth set before "re" but
     it can be set after "re" and before "S" and in this case the rendering
     will be wrong.
   - render such rectangles as a single line.
This commit is contained in:
Calixte Denizet 2021-01-04 14:25:30 +01:00
parent 666ef6dac0
commit b3dccd66ab
5 changed files with 113 additions and 40 deletions

View File

@ -31,16 +31,12 @@ import { getShadingPatternFromIR, TilingPattern } from "./pattern_helper.js";
// <canvas> contexts store most of the state we need natively. // <canvas> contexts store most of the state we need natively.
// However, PDF needs a bit more state, which we store here. // However, PDF needs a bit more state, which we store here.
// Minimal font size that would be used during canvas fillText operations. // Minimal font size that would be used during canvas fillText operations.
const MIN_FONT_SIZE = 16; const MIN_FONT_SIZE = 16;
// Maximum font size that would be used during canvas fillText operations. // Maximum font size that would be used during canvas fillText operations.
const MAX_FONT_SIZE = 100; const MAX_FONT_SIZE = 100;
const MAX_GROUP_SIZE = 4096; 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 COMPILE_TYPE3_GLYPHS = true;
const MAX_SIZE_TO_COMPILE = 1000; const MAX_SIZE_TO_COMPILE = 1000;
@ -1273,21 +1269,20 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
case OPS.rectangle: case OPS.rectangle:
x = args[j++]; x = args[j++];
y = args[j++]; y = args[j++];
let width = args[j++]; const width = args[j++];
let height = args[j++]; const height = args[j++];
if (width === 0 && ctx.lineWidth < this.getSinglePixelWidth()) {
width = this.getSinglePixelWidth();
}
if (height === 0 && ctx.lineWidth < this.getSinglePixelWidth()) {
height = this.getSinglePixelWidth();
}
const xw = x + width; const xw = x + width;
const yh = y + height; const yh = y + height;
ctx.moveTo(x, y); ctx.moveTo(x, y);
ctx.lineTo(xw, y); if (width === 0 || height === 0) {
ctx.lineTo(xw, yh); ctx.lineTo(xw, yh);
ctx.lineTo(x, yh); } else {
ctx.lineTo(x, y); ctx.lineTo(xw, y);
ctx.lineTo(xw, yh);
ctx.lineTo(x, yh);
}
ctx.closePath(); ctx.closePath();
break; break;
case OPS.moveTo: case OPS.moveTo:
@ -1361,19 +1356,31 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
const transform = ctx.mozCurrentTransform; const transform = ctx.mozCurrentTransform;
const scale = Util.singularValueDecompose2dScale(transform)[0]; const scale = Util.singularValueDecompose2dScale(transform)[0];
ctx.strokeStyle = strokeColor.getPattern(ctx, this); ctx.strokeStyle = strokeColor.getPattern(ctx, this);
ctx.lineWidth = Math.max( const lineWidth = this.getSinglePixelWidth();
this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, if (lineWidth === -1) {
this.current.lineWidth * scale ctx.resetTransform();
); ctx.lineWidth = 1;
} else {
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth * scale);
}
ctx.stroke(); ctx.stroke();
ctx.restore(); ctx.restore();
} else { } else {
// Prevent drawing too thin lines by enforcing a minimum line width. const lineWidth = this.getSinglePixelWidth();
ctx.lineWidth = Math.max( if (lineWidth === -1) {
this.getSinglePixelWidth() * MIN_WIDTH_FACTOR, // The current transform will transform a square pixel into
this.current.lineWidth // a parallelogramm where both heights are lower than 1 and
); // not equal.
ctx.stroke(); 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) { if (consumePath) {
@ -1577,7 +1584,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
this.moveText(0, this.current.leading); this.moveText(0, this.current.leading);
}, },
paintChar(character, x, y, patternTransform) { paintChar(character, x, y, patternTransform, resetLineWidthToOne) {
const ctx = this.ctx; const ctx = this.ctx;
const current = this.current; const current = this.current;
const font = current.font; const font = current.font;
@ -1613,6 +1620,10 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
fillStrokeMode === TextRenderingMode.STROKE || fillStrokeMode === TextRenderingMode.STROKE ||
fillStrokeMode === TextRenderingMode.FILL_STROKE fillStrokeMode === TextRenderingMode.FILL_STROKE
) { ) {
if (resetLineWidthToOne) {
ctx.resetTransform();
ctx.lineWidth = 1;
}
ctx.stroke(); ctx.stroke();
} }
ctx.restore(); ctx.restore();
@ -1627,7 +1638,16 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
fillStrokeMode === TextRenderingMode.STROKE || fillStrokeMode === TextRenderingMode.STROKE ||
fillStrokeMode === TextRenderingMode.FILL_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 lineWidth = current.lineWidth;
let resetLineWidthToOne = false;
const scale = current.textMatrixScale; const scale = current.textMatrixScale;
if (scale === 0 || lineWidth === 0) { if (scale === 0 || lineWidth === 0) {
const fillStrokeMode = const fillStrokeMode =
@ -1723,7 +1744,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
fillStrokeMode === TextRenderingMode.FILL_STROKE fillStrokeMode === TextRenderingMode.FILL_STROKE
) { ) {
this._cachedGetSinglePixelWidth = null; this._cachedGetSinglePixelWidth = null;
lineWidth = this.getSinglePixelWidth() * MIN_WIDTH_FACTOR; lineWidth = this.getSinglePixelWidth();
resetLineWidthToOne = lineWidth === -1;
} }
} else { } else {
lineWidth /= scale; lineWidth /= scale;
@ -1791,7 +1813,13 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
// common case // common case
ctx.fillText(character, scaledX, scaledY); ctx.fillText(character, scaledX, scaledY);
} else { } else {
this.paintChar(character, scaledX, scaledY, patternTransform); this.paintChar(
character,
scaledX,
scaledY,
patternTransform,
resetLineWidthToOne
);
if (accent) { if (accent) {
const scaledAccentX = const scaledAccentX =
scaledX + (fontSize * accent.offset.x) / fontSizeScale; scaledX + (fontSize * accent.offset.x) / fontSizeScale;
@ -1801,7 +1829,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
accent.fontChar, accent.fontChar,
scaledAccentX, scaledAccentX,
scaledAccentY, scaledAccentY,
patternTransform patternTransform,
resetLineWidthToOne
); );
} }
} }
@ -2622,17 +2651,40 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
} }
ctx.beginPath(); ctx.beginPath();
}, },
getSinglePixelWidth(scale) { getSinglePixelWidth() {
if (this._cachedGetSinglePixelWidth === null) { if (this._cachedGetSinglePixelWidth === null) {
const inverse = this.ctx.mozCurrentTransformInverse; // If transform is [a b] then a pixel (square) is transformed
// max of the current horizontal and vertical scale // [c d]
this._cachedGetSinglePixelWidth = Math.sqrt( // into a parallelogramm: its area is the abs value of determinant.
Math.max( // This parallelogram has 2 heights:
inverse[0] * inverse[0] + inverse[1] * inverse[1], // - Area / |col_1|;
inverse[2] * inverse[2] + inverse[3] * inverse[3] // - 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; return this._cachedGetSinglePixelWidth;
}, },
getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) { getCanvasPosition: function CanvasGraphics_getCanvasPosition(x, y) {

View File

@ -114,6 +114,7 @@
!bug1050040.pdf !bug1050040.pdf
!bug1200096.pdf !bug1200096.pdf
!bug1068432.pdf !bug1068432.pdf
!issue12295.pdf
!bug1146106.pdf !bug1146106.pdf
!bug1245391_reduced.pdf !bug1245391_reduced.pdf
!bug1252420.pdf !bug1252420.pdf
@ -277,6 +278,7 @@
!gradientfill.pdf !gradientfill.pdf
!bug903856.pdf !bug903856.pdf
!bug850854.pdf !bug850854.pdf
!issue12810.pdf
!bug866395.pdf !bug866395.pdf
!issue12010_reduced.pdf !issue12010_reduced.pdf
!issue11718_reduced.pdf !issue11718_reduced.pdf
@ -313,6 +315,7 @@
!issue3371.pdf !issue3371.pdf
!issue2956.pdf !issue2956.pdf
!issue2537r.pdf !issue2537r.pdf
!issue12810.pdf
!issue269_1.pdf !issue269_1.pdf
!bug946506.pdf !bug946506.pdf
!issue3885.pdf !issue3885.pdf

BIN
test/pdfs/issue12295.pdf Normal file

Binary file not shown.

BIN
test/pdfs/issue12810.pdf Normal file

Binary file not shown.

View File

@ -1190,6 +1190,12 @@
"link": false, "link": false,
"type": "eq" "type": "eq"
}, },
{ "id": "issue12295",
"file": "pdfs/issue12295.pdf",
"md5": "c534f74866ba8ada56010d19b57231ec",
"rounds": 1,
"type": "eq"
},
{ "id": "bug1245391-text", { "id": "bug1245391-text",
"file": "pdfs/bug1245391_reduced.pdf", "file": "pdfs/bug1245391_reduced.pdf",
"md5": "6c946045ee0f2f663f269717c0f1614a", "md5": "6c946045ee0f2f663f269717c0f1614a",
@ -2329,6 +2335,12 @@
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
}, },
{ "id": "issue12810",
"file": "pdfs/issue12810.pdf",
"md5": "585e19781308603dd706f941b1ace774",
"rounds": 1,
"type": "eq"
},
{ "id": "pr4606", { "id": "pr4606",
"file": "pdfs/pr4606.pdf", "file": "pdfs/pr4606.pdf",
"md5": "6574fde2314648600056bd0e229df98c", "md5": "6574fde2314648600056bd0e229df98c",
@ -4324,6 +4336,12 @@
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
}, },
{ "id": "issue12810",
"file": "pdfs/issue12810.pdf",
"md5": "585e19781308603dd706f941b1ace774",
"rounds": 1,
"type": "eq"
},
{ "id": "issue2956", { "id": "issue2956",
"file": "pdfs/issue2956.pdf", "file": "pdfs/issue2956.pdf",
"md5": "d8f68cbbb4bf54cde9f7f878acb6d7cd", "md5": "d8f68cbbb4bf54cde9f7f878acb6d7cd",