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:
parent
666ef6dac0
commit
b3dccd66ab
@ -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) {
|
||||||
|
3
test/pdfs/.gitignore
vendored
3
test/pdfs/.gitignore
vendored
@ -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
BIN
test/pdfs/issue12295.pdf
Normal file
Binary file not shown.
BIN
test/pdfs/issue12810.pdf
Normal file
BIN
test/pdfs/issue12810.pdf
Normal file
Binary file not shown.
@ -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",
|
||||||
|
Loading…
Reference in New Issue
Block a user