Merge pull request #14536 from calixteman/thin_line
Fix some issues with lineWidth < 1 after transform (bug 1753075, bug 1743245, bug 1710019)
This commit is contained in:
commit
85ff7b117e
@ -1116,6 +1116,7 @@ class CanvasGraphics {
|
||||
// the transformation must already be set in canvasCtx._transformMatrix.
|
||||
addContextCurrentTransform(canvasCtx);
|
||||
}
|
||||
this._cachedScaleForStroking = null;
|
||||
this._cachedGetSinglePixelWidth = null;
|
||||
}
|
||||
|
||||
@ -1165,10 +1166,6 @@ class CanvasGraphics {
|
||||
this.viewportScale = viewport.scale;
|
||||
|
||||
this.baseTransform = this.ctx.mozCurrentTransform.slice();
|
||||
this._combinedScaleFactor = Math.hypot(
|
||||
this.baseTransform[0],
|
||||
this.baseTransform[2]
|
||||
);
|
||||
|
||||
if (this.imageLayer) {
|
||||
this.imageLayer.beginLayout();
|
||||
@ -1425,6 +1422,9 @@ class CanvasGraphics {
|
||||
|
||||
// Graphics state
|
||||
setLineWidth(width) {
|
||||
if (width !== this.current.lineWidth) {
|
||||
this._cachedScaleForStroking = null;
|
||||
}
|
||||
this.current.lineWidth = width;
|
||||
this.ctx.lineWidth = width;
|
||||
}
|
||||
@ -1633,6 +1633,7 @@ class CanvasGraphics {
|
||||
// Ensure that the clipping path is reset (fixes issue6413.pdf).
|
||||
this.pendingClip = null;
|
||||
|
||||
this._cachedScaleForStroking = null;
|
||||
this._cachedGetSinglePixelWidth = null;
|
||||
}
|
||||
}
|
||||
@ -1640,6 +1641,7 @@ class CanvasGraphics {
|
||||
transform(a, b, c, d, e, f) {
|
||||
this.ctx.transform(a, b, c, d, e, f);
|
||||
|
||||
this._cachedScaleForStroking = null;
|
||||
this._cachedGetSinglePixelWidth = null;
|
||||
}
|
||||
|
||||
@ -1776,7 +1778,6 @@ class CanvasGraphics {
|
||||
ctx.globalAlpha = this.current.strokeAlpha;
|
||||
if (this.contentVisible) {
|
||||
if (typeof strokeColor === "object" && strokeColor?.getPattern) {
|
||||
const lineWidth = this.getSinglePixelWidth();
|
||||
ctx.save();
|
||||
ctx.strokeStyle = strokeColor.getPattern(
|
||||
ctx,
|
||||
@ -1784,25 +1785,10 @@ class CanvasGraphics {
|
||||
ctx.mozCurrentTransformInverse,
|
||||
PathType.STROKE
|
||||
);
|
||||
// Prevent drawing too thin lines by enforcing a minimum line width.
|
||||
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
|
||||
ctx.stroke();
|
||||
this.rescaleAndStroke(/* saveRestore */ false);
|
||||
ctx.restore();
|
||||
} else {
|
||||
const lineWidth = this.getSinglePixelWidth();
|
||||
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();
|
||||
}
|
||||
this.rescaleAndStroke(/* saveRestore */ true);
|
||||
}
|
||||
}
|
||||
if (consumePath) {
|
||||
@ -2027,7 +2013,7 @@ class CanvasGraphics {
|
||||
this.moveText(0, this.current.leading);
|
||||
}
|
||||
|
||||
paintChar(character, x, y, patternTransform, resetLineWidthToOne) {
|
||||
paintChar(character, x, y, patternTransform) {
|
||||
const ctx = this.ctx;
|
||||
const current = this.current;
|
||||
const font = current.font;
|
||||
@ -2063,10 +2049,6 @@ class CanvasGraphics {
|
||||
fillStrokeMode === TextRenderingMode.STROKE ||
|
||||
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
||||
) {
|
||||
if (resetLineWidthToOne) {
|
||||
ctx.resetTransform();
|
||||
ctx.lineWidth = Math.floor(this._combinedScaleFactor);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.restore();
|
||||
@ -2081,16 +2063,7 @@ class CanvasGraphics {
|
||||
fillStrokeMode === TextRenderingMode.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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2181,7 +2154,6 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
let lineWidth = current.lineWidth;
|
||||
let resetLineWidthToOne = false;
|
||||
const scale = current.textMatrixScale;
|
||||
if (scale === 0 || lineWidth === 0) {
|
||||
const fillStrokeMode =
|
||||
@ -2190,9 +2162,7 @@ class CanvasGraphics {
|
||||
fillStrokeMode === TextRenderingMode.STROKE ||
|
||||
fillStrokeMode === TextRenderingMode.FILL_STROKE
|
||||
) {
|
||||
this._cachedGetSinglePixelWidth = null;
|
||||
lineWidth = this.getSinglePixelWidth();
|
||||
resetLineWidthToOne = lineWidth < 0;
|
||||
}
|
||||
} else {
|
||||
lineWidth /= scale;
|
||||
@ -2260,13 +2230,7 @@ class CanvasGraphics {
|
||||
// common case
|
||||
ctx.fillText(character, scaledX, scaledY);
|
||||
} else {
|
||||
this.paintChar(
|
||||
character,
|
||||
scaledX,
|
||||
scaledY,
|
||||
patternTransform,
|
||||
resetLineWidthToOne
|
||||
);
|
||||
this.paintChar(character, scaledX, scaledY, patternTransform);
|
||||
if (accent) {
|
||||
const scaledAccentX =
|
||||
scaledX + (fontSize * accent.offset.x) / fontSizeScale;
|
||||
@ -2276,8 +2240,7 @@ class CanvasGraphics {
|
||||
accent.fontChar,
|
||||
scaledAccentX,
|
||||
scaledAccentY,
|
||||
patternTransform,
|
||||
resetLineWidthToOne
|
||||
patternTransform
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -2302,6 +2265,7 @@ class CanvasGraphics {
|
||||
}
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -2325,6 +2289,7 @@ class CanvasGraphics {
|
||||
if (isTextInvisible || fontSize === 0) {
|
||||
return;
|
||||
}
|
||||
this._cachedScaleForStroking = null;
|
||||
this._cachedGetSinglePixelWidth = null;
|
||||
|
||||
ctx.save();
|
||||
@ -3121,48 +3086,114 @@ class CanvasGraphics {
|
||||
}
|
||||
|
||||
getSinglePixelWidth() {
|
||||
if (this._cachedGetSinglePixelWidth === null) {
|
||||
// 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)|)
|
||||
if (!this._cachedGetSinglePixelWidth) {
|
||||
const m = this.ctx.mozCurrentTransform;
|
||||
|
||||
const absDet = Math.abs(m[0] * m[3] - m[2] * m[1]);
|
||||
const sqNorm1 = m[0] ** 2 + m[2] ** 2;
|
||||
const sqNorm2 = m[1] ** 2 + m[3] ** 2;
|
||||
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;
|
||||
if (m[1] === 0 && m[2] === 0) {
|
||||
// Fast path
|
||||
this._cachedGetSinglePixelWidth =
|
||||
1 / Math.min(Math.abs(m[0]), Math.abs(m[3]));
|
||||
} else {
|
||||
// Matrix is non-invertible.
|
||||
this._cachedGetSinglePixelWidth = 1;
|
||||
const absDet = Math.abs(m[0] * m[3] - m[2] * m[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;
|
||||
}
|
||||
|
||||
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) {
|
||||
const transform = this.ctx.mozCurrentTransform;
|
||||
return [
|
||||
|
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -513,3 +513,4 @@
|
||||
!issue14307.pdf
|
||||
!issue14497.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.
@ -6309,5 +6309,19 @@
|
||||
"rounds": 1,
|
||||
"link": true,
|
||||
"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