Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 1 commit into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 92 additions & 40 deletions src/display/canvas.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,12 @@ import { getShadingPatternFromIR, TilingPattern } from "./pattern_helper.js";

// <canvas> 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;

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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();
Expand All @@ -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);
}
}
}

Expand Down Expand Up @@ -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 =
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -1801,7 +1829,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
accent.fontChar,
scaledAccentX,
scaledAccentY,
patternTransform
patternTransform,
resetLineWidthToOne
);
}
}
Expand Down Expand Up @@ -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) {
Expand Down
3 changes: 3 additions & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
!bug1050040.pdf
!bug1200096.pdf
!bug1068432.pdf
!issue12295.pdf
!bug1146106.pdf
!bug1245391_reduced.pdf
!bug1252420.pdf
Expand Down Expand Up @@ -277,6 +278,7 @@
!gradientfill.pdf
!bug903856.pdf
!bug850854.pdf
!issue12810.pdf
!bug866395.pdf
!issue12010_reduced.pdf
!issue11718_reduced.pdf
Expand Down Expand Up @@ -313,6 +315,7 @@
!issue3371.pdf
!issue2956.pdf
!issue2537r.pdf
!issue12810.pdf
!issue269_1.pdf
!bug946506.pdf
!issue3885.pdf
Expand Down
Binary file added test/pdfs/issue12295.pdf
Binary file not shown.
Binary file added test/pdfs/issue12810.pdf
Binary file not shown.
18 changes: 18 additions & 0 deletions test/test_manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand Down