Fix some issues with lineWidth < 1 after transform (bug 1753075, bug 1743245, bug 1710019)
- it aims to fix: - https://bugzilla.mozilla.org/show_bug.cgi?id=1753075; - https://bugzilla.mozilla.org/show_bug.cgi?id=1743245; - https://bugzilla.mozilla.org/show_bug.cgi?id=1710019; - issue #13211; - issue #14521. - previously we were trying to adjust lineWidth to have something correct after the current transform is applied but this approach was not correct because finally the pixel is rescaled with the same factors in both directions. And sometimes those factors must be different (see bug 1753075). - So the idea of this patch is to apply a scale matrix to the current transform just before setting lineWidth and stroking. This scale matrix is computed in order to ensure that after transform, a pixel will have its two thickness greater than 1.
This commit is contained in:
parent
530af48b8e
commit
46369e4aa5
@ -1117,6 +1117,7 @@ class CanvasGraphics {
|
|||||||
// the transformation must already be set in canvasCtx._transformMatrix.
|
// the transformation must already be set in canvasCtx._transformMatrix.
|
||||||
addContextCurrentTransform(canvasCtx);
|
addContextCurrentTransform(canvasCtx);
|
||||||
}
|
}
|
||||||
|
this._cachedScaleForStroking = null;
|
||||||
this._cachedGetSinglePixelWidth = null;
|
this._cachedGetSinglePixelWidth = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1166,10 +1167,6 @@ class CanvasGraphics {
|
|||||||
this.viewportScale = viewport.scale;
|
this.viewportScale = viewport.scale;
|
||||||
|
|
||||||
this.baseTransform = this.ctx.mozCurrentTransform.slice();
|
this.baseTransform = this.ctx.mozCurrentTransform.slice();
|
||||||
this._combinedScaleFactor = Math.hypot(
|
|
||||||
this.baseTransform[0],
|
|
||||||
this.baseTransform[2]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (this.imageLayer) {
|
if (this.imageLayer) {
|
||||||
this.imageLayer.beginLayout();
|
this.imageLayer.beginLayout();
|
||||||
@ -1426,6 +1423,9 @@ class CanvasGraphics {
|
|||||||
|
|
||||||
// Graphics state
|
// Graphics state
|
||||||
setLineWidth(width) {
|
setLineWidth(width) {
|
||||||
|
if (width !== this.current.lineWidth) {
|
||||||
|
this._cachedScaleForStroking = null;
|
||||||
|
}
|
||||||
this.current.lineWidth = width;
|
this.current.lineWidth = width;
|
||||||
this.ctx.lineWidth = width;
|
this.ctx.lineWidth = width;
|
||||||
}
|
}
|
||||||
@ -1634,6 +1634,7 @@ class CanvasGraphics {
|
|||||||
// Ensure that the clipping path is reset (fixes issue6413.pdf).
|
// Ensure that the clipping path is reset (fixes issue6413.pdf).
|
||||||
this.pendingClip = null;
|
this.pendingClip = null;
|
||||||
|
|
||||||
|
this._cachedScaleForStroking = null;
|
||||||
this._cachedGetSinglePixelWidth = null;
|
this._cachedGetSinglePixelWidth = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1641,6 +1642,7 @@ class CanvasGraphics {
|
|||||||
transform(a, b, c, d, e, f) {
|
transform(a, b, c, d, e, f) {
|
||||||
this.ctx.transform(a, b, c, d, e, f);
|
this.ctx.transform(a, b, c, d, e, f);
|
||||||
|
|
||||||
|
this._cachedScaleForStroking = null;
|
||||||
this._cachedGetSinglePixelWidth = null;
|
this._cachedGetSinglePixelWidth = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1777,7 +1779,6 @@ class CanvasGraphics {
|
|||||||
ctx.globalAlpha = this.current.strokeAlpha;
|
ctx.globalAlpha = this.current.strokeAlpha;
|
||||||
if (this.contentVisible) {
|
if (this.contentVisible) {
|
||||||
if (typeof strokeColor === "object" && strokeColor?.getPattern) {
|
if (typeof strokeColor === "object" && strokeColor?.getPattern) {
|
||||||
const lineWidth = this.getSinglePixelWidth();
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.strokeStyle = strokeColor.getPattern(
|
ctx.strokeStyle = strokeColor.getPattern(
|
||||||
ctx,
|
ctx,
|
||||||
@ -1785,25 +1786,10 @@ class CanvasGraphics {
|
|||||||
ctx.mozCurrentTransformInverse,
|
ctx.mozCurrentTransformInverse,
|
||||||
PathType.STROKE
|
PathType.STROKE
|
||||||
);
|
);
|
||||||
// Prevent drawing too thin lines by enforcing a minimum line width.
|
this.rescaleAndStroke(/* saveRestore */ false);
|
||||||
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
} else {
|
} else {
|
||||||
const lineWidth = this.getSinglePixelWidth();
|
this.rescaleAndStroke(/* saveRestore */ true);
|
||||||
if (lineWidth < 0 && -lineWidth >= this.current.lineWidth) {
|
|
||||||
// The current transform will transform a square pixel into a
|
|
||||||
// parallelogram where both heights are lower than 1 and not equal.
|
|
||||||
ctx.save();
|
|
||||||
ctx.resetTransform();
|
|
||||||
ctx.lineWidth = Math.floor(this._combinedScaleFactor);
|
|
||||||
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) {
|
||||||
@ -2028,7 +2014,7 @@ class CanvasGraphics {
|
|||||||
this.moveText(0, this.current.leading);
|
this.moveText(0, this.current.leading);
|
||||||
}
|
}
|
||||||
|
|
||||||
paintChar(character, x, y, patternTransform, resetLineWidthToOne) {
|
paintChar(character, x, y, patternTransform) {
|
||||||
const ctx = this.ctx;
|
const ctx = this.ctx;
|
||||||
const current = this.current;
|
const current = this.current;
|
||||||
const font = current.font;
|
const font = current.font;
|
||||||
@ -2064,10 +2050,6 @@ class CanvasGraphics {
|
|||||||
fillStrokeMode === TextRenderingMode.STROKE ||
|
fillStrokeMode === TextRenderingMode.STROKE ||
|
||||||
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
||||||
) {
|
) {
|
||||||
if (resetLineWidthToOne) {
|
|
||||||
ctx.resetTransform();
|
|
||||||
ctx.lineWidth = Math.floor(this._combinedScaleFactor);
|
|
||||||
}
|
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@ -2082,18 +2064,9 @@ class CanvasGraphics {
|
|||||||
fillStrokeMode === TextRenderingMode.STROKE ||
|
fillStrokeMode === TextRenderingMode.STROKE ||
|
||||||
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
||||||
) {
|
) {
|
||||||
if (resetLineWidthToOne) {
|
|
||||||
ctx.save();
|
|
||||||
ctx.moveTo(x, y);
|
|
||||||
ctx.resetTransform();
|
|
||||||
ctx.lineWidth = Math.floor(this._combinedScaleFactor);
|
|
||||||
ctx.strokeText(character, 0, 0);
|
|
||||||
ctx.restore();
|
|
||||||
} else {
|
|
||||||
ctx.strokeText(character, x, y);
|
ctx.strokeText(character, x, y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isAddToPathSet) {
|
if (isAddToPathSet) {
|
||||||
const paths = this.pendingTextPaths || (this.pendingTextPaths = []);
|
const paths = this.pendingTextPaths || (this.pendingTextPaths = []);
|
||||||
@ -2182,7 +2155,6 @@ class CanvasGraphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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 =
|
||||||
@ -2191,9 +2163,7 @@ class CanvasGraphics {
|
|||||||
fillStrokeMode === TextRenderingMode.STROKE ||
|
fillStrokeMode === TextRenderingMode.STROKE ||
|
||||||
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
||||||
) {
|
) {
|
||||||
this._cachedGetSinglePixelWidth = null;
|
|
||||||
lineWidth = this.getSinglePixelWidth();
|
lineWidth = this.getSinglePixelWidth();
|
||||||
resetLineWidthToOne = lineWidth < 0;
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
lineWidth /= scale;
|
lineWidth /= scale;
|
||||||
@ -2261,13 +2231,7 @@ class CanvasGraphics {
|
|||||||
// common case
|
// common case
|
||||||
ctx.fillText(character, scaledX, scaledY);
|
ctx.fillText(character, scaledX, scaledY);
|
||||||
} else {
|
} else {
|
||||||
this.paintChar(
|
this.paintChar(character, scaledX, scaledY, patternTransform);
|
||||||
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;
|
||||||
@ -2277,8 +2241,7 @@ class CanvasGraphics {
|
|||||||
accent.fontChar,
|
accent.fontChar,
|
||||||
scaledAccentX,
|
scaledAccentX,
|
||||||
scaledAccentY,
|
scaledAccentY,
|
||||||
patternTransform,
|
patternTransform
|
||||||
resetLineWidthToOne
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2303,6 +2266,7 @@ class CanvasGraphics {
|
|||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
this.compose();
|
this.compose();
|
||||||
|
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2326,6 +2290,7 @@ class CanvasGraphics {
|
|||||||
if (isTextInvisible || fontSize === 0) {
|
if (isTextInvisible || fontSize === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this._cachedScaleForStroking = null;
|
||||||
this._cachedGetSinglePixelWidth = null;
|
this._cachedGetSinglePixelWidth = null;
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@ -3122,46 +3087,112 @@ class CanvasGraphics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSinglePixelWidth() {
|
getSinglePixelWidth() {
|
||||||
if (this._cachedGetSinglePixelWidth === null) {
|
if (!this._cachedGetSinglePixelWidth) {
|
||||||
// If transform is [a b] then a pixel (square) is transformed
|
|
||||||
// [c d]
|
|
||||||
// into a parallelogram: its area is the abs value of the 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 m = this.ctx.mozCurrentTransform;
|
||||||
|
if (m[1] === 0 && m[2] === 0) {
|
||||||
const absDet = Math.abs(m[0] * m[3] - m[2] * m[1]);
|
// Fast path
|
||||||
const sqNorm1 = m[0] ** 2 + m[2] ** 2;
|
this._cachedGetSinglePixelWidth =
|
||||||
const sqNorm2 = m[1] ** 2 + m[3] ** 2;
|
1 / Math.min(Math.abs(m[0]), Math.abs(m[3]));
|
||||||
const pixelHeight = Math.sqrt(Math.max(sqNorm1, sqNorm2)) / absDet;
|
|
||||||
if (sqNorm1 !== sqNorm2 && this._combinedScaleFactor * pixelHeight > 1) {
|
|
||||||
// The parallelogram isn't a square and at least one height
|
|
||||||
// is 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 issue #12295).
|
|
||||||
// For example with matrix [0.001 0, 0, 100], a pixel is transformed
|
|
||||||
// in a rectangle 0.001x100. If we just scale by 1000 (to have a 1)
|
|
||||||
// then we'll get a rectangle 1x1e5 which is wrong.
|
|
||||||
// In this case, we must reset the transform, set linewidth to 1
|
|
||||||
// and then stroke.
|
|
||||||
this._cachedGetSinglePixelWidth = -(
|
|
||||||
this._combinedScaleFactor * pixelHeight
|
|
||||||
);
|
|
||||||
} else if (absDet > Number.EPSILON) {
|
|
||||||
this._cachedGetSinglePixelWidth = pixelHeight;
|
|
||||||
} else {
|
} else {
|
||||||
// Matrix is non-invertible.
|
const absDet = Math.abs(m[0] * m[3] - m[2] * m[1]);
|
||||||
this._cachedGetSinglePixelWidth = 1;
|
const normX = Math.hypot(m[0], m[2]);
|
||||||
|
const normY = Math.hypot(m[1], m[3]);
|
||||||
|
this._cachedGetSinglePixelWidth = Math.max(normX, normY) / absDet;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return this._cachedGetSinglePixelWidth;
|
||||||
|
}
|
||||||
|
|
||||||
return this._cachedGetSinglePixelWidth;
|
getScaleForStroking() {
|
||||||
|
// A pixel has thicknessX = thicknessY = 1;
|
||||||
|
// A transformed pixel is a parallelogram and the thicknesses
|
||||||
|
// corresponds to the heights.
|
||||||
|
// The goal of this function is to rescale before setting the
|
||||||
|
// lineWidth in order to have both thicknesses greater or equal
|
||||||
|
// to 1 after transform.
|
||||||
|
if (!this._cachedScaleForStroking) {
|
||||||
|
const { lineWidth } = this.current;
|
||||||
|
const m = this.ctx.mozCurrentTransform;
|
||||||
|
let scaleX, scaleY;
|
||||||
|
|
||||||
|
if (m[1] === 0 && m[2] === 0) {
|
||||||
|
// Fast path
|
||||||
|
const normX = Math.abs(m[0]);
|
||||||
|
const normY = Math.abs(m[3]);
|
||||||
|
if (lineWidth === 0) {
|
||||||
|
scaleX = 1 / normX;
|
||||||
|
scaleY = 1 / normY;
|
||||||
|
} else {
|
||||||
|
const scaledXLineWidth = normX * lineWidth;
|
||||||
|
const scaledYLineWidth = normY * lineWidth;
|
||||||
|
scaleX = scaledXLineWidth < 1 ? 1 / scaledXLineWidth : 1;
|
||||||
|
scaleY = scaledYLineWidth < 1 ? 1 / scaledYLineWidth : 1;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// A pixel (base (x, y)) is transformed by M into a parallelogram:
|
||||||
|
// - its area is |det(M)|;
|
||||||
|
// - heightY (orthogonal to Mx) has a length: |det(M)| / norm(Mx);
|
||||||
|
// - heightX (orthogonal to My) has a length: |det(M)| / norm(My).
|
||||||
|
// heightX and heightY are the thicknesses of the transformed pixel
|
||||||
|
// and they must be both greater or equal to 1.
|
||||||
|
const absDet = Math.abs(m[0] * m[3] - m[2] * m[1]);
|
||||||
|
const normX = Math.hypot(m[0], m[1]);
|
||||||
|
const normY = Math.hypot(m[2], m[3]);
|
||||||
|
if (lineWidth === 0) {
|
||||||
|
scaleX = normY / absDet;
|
||||||
|
scaleY = normX / absDet;
|
||||||
|
} else {
|
||||||
|
const baseArea = lineWidth * absDet;
|
||||||
|
scaleX = normY > baseArea ? normY / baseArea : 1;
|
||||||
|
scaleY = normX > baseArea ? normX / baseArea : 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._cachedScaleForStroking = [scaleX, scaleY];
|
||||||
|
}
|
||||||
|
return this._cachedScaleForStroking;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rescale before stroking in order to have a final lineWidth
|
||||||
|
// with both thicknesses greater or equal to 1.
|
||||||
|
rescaleAndStroke(saveRestore) {
|
||||||
|
const { ctx } = this;
|
||||||
|
const { lineWidth } = this.current;
|
||||||
|
const [scaleX, scaleY] = this.getScaleForStroking();
|
||||||
|
|
||||||
|
ctx.lineWidth = lineWidth || 1;
|
||||||
|
|
||||||
|
if (scaleX === 1 && scaleY === 1) {
|
||||||
|
ctx.stroke();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let savedMatrix, savedDashes, savedDashOffset;
|
||||||
|
if (saveRestore) {
|
||||||
|
savedMatrix = ctx.mozCurrentTransform.slice();
|
||||||
|
savedDashes = ctx.getLineDash().slice();
|
||||||
|
savedDashOffset = ctx.lineDashOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.scale(scaleX, scaleY);
|
||||||
|
|
||||||
|
// How the dashed line is rendered depends on the current transform...
|
||||||
|
// so we added a rescale to handle too thin lines and consequently
|
||||||
|
// the way the line is dashed will be modified.
|
||||||
|
// If scaleX === scaleY, the dashed lines will be rendered correctly
|
||||||
|
// else we'll have some bugs (but only with too thin lines).
|
||||||
|
// Here we take the max... why not taking the min... or something else.
|
||||||
|
// Anyway, as said it's buggy when scaleX !== scaleY.
|
||||||
|
const scale = Math.max(scaleX, scaleY);
|
||||||
|
ctx.setLineDash(ctx.getLineDash().map(x => x / scale));
|
||||||
|
ctx.lineDashOffset /= scale;
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
if (saveRestore) {
|
||||||
|
ctx.setTransform(...savedMatrix);
|
||||||
|
ctx.setLineDash(savedDashes);
|
||||||
|
ctx.lineDashOffset = savedDashOffset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getCanvasPosition(x, y) {
|
getCanvasPosition(x, y) {
|
||||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -512,3 +512,4 @@
|
|||||||
!issue14307.pdf
|
!issue14307.pdf
|
||||||
!issue14497.pdf
|
!issue14497.pdf
|
||||||
!issue14502.pdf
|
!issue14502.pdf
|
||||||
|
!issue13211.pdf
|
||||||
|
1
test/pdfs/bug1753075.pdf.link
Normal file
1
test/pdfs/bug1753075.pdf.link
Normal file
@ -0,0 +1 @@
|
|||||||
|
https://bugzilla.mozilla.org/attachment.cgi?id=9262522
|
BIN
test/pdfs/issue13211.pdf
Executable file
BIN
test/pdfs/issue13211.pdf
Executable file
Binary file not shown.
@ -6290,5 +6290,19 @@
|
|||||||
"rounds": 1,
|
"rounds": 1,
|
||||||
"link": true,
|
"link": true,
|
||||||
"type": "other"
|
"type": "other"
|
||||||
|
},
|
||||||
|
{ "id": "bug1753075",
|
||||||
|
"file": "pdfs/bug1753075.pdf",
|
||||||
|
"md5": "12716fa2dc3e0b3a61d88fef10abc7cf",
|
||||||
|
"rounds": 1,
|
||||||
|
"link": true,
|
||||||
|
"lastPage": 1,
|
||||||
|
"type": "eq"
|
||||||
|
},
|
||||||
|
{ "id": "issue13211",
|
||||||
|
"file": "pdfs/issue13211.pdf",
|
||||||
|
"md5": "d193853e8a123dc50eeea593a4150b60",
|
||||||
|
"rounds": 1,
|
||||||
|
"type": "eq"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
Loading…
Reference in New Issue
Block a user