Merge pull request #13683 from brendandahl/mask-fixes

Fix transformations when painting image masks and tiling patterns.
This commit is contained in:
Jonas Jenwald 2021-07-07 10:24:01 +02:00 committed by GitHub
commit 05ebb6329b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 253 additions and 179 deletions

View File

@ -13,11 +13,6 @@
* limitations under the License.
*/
import {
createMatrix,
getShadingPattern,
TilingPattern,
} from "./pattern_helper.js";
import {
FONT_IDENTITY_MATRIX,
IDENTITY_MATRIX,
@ -32,6 +27,7 @@ import {
Util,
warn,
} from "../shared/util.js";
import { getShadingPattern, 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.
@ -197,17 +193,6 @@ function addContextCurrentTransform(ctx) {
};
}
function getAdjustmentTransformation(transform, width, height) {
// The pattern will be created at the size of the current page or form object,
// but the mask is usually scaled differently and offset, so we must account
// for these to shift and rescale the pattern to the correctly location.
let patternTransform = createMatrix(transform);
patternTransform = patternTransform.scale(1 / width, -1 / height);
patternTransform = patternTransform.translate(0, -height);
patternTransform = patternTransform.inverse();
return patternTransform;
}
class CachedCanvases {
constructor(canvasFactory) {
this.canvasFactory = canvasFactory;
@ -1046,6 +1031,154 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
}
}
_scaleImage(img, inverseTransform) {
// Vertical or horizontal scaling shall not be more than 2 to not lose the
// pixels during drawImage operation, painting on the temporary canvas(es)
// that are twice smaller in size.
const width = img.width;
const height = img.height;
let widthScale = Math.max(
Math.hypot(inverseTransform[0], inverseTransform[1]),
1
);
let heightScale = Math.max(
Math.hypot(inverseTransform[2], inverseTransform[3]),
1
);
let paintWidth = width,
paintHeight = height;
let tmpCanvasId = "prescale1";
let tmpCanvas, tmpCtx;
while (
(widthScale > 2 && paintWidth > 1) ||
(heightScale > 2 && paintHeight > 1)
) {
let newWidth = paintWidth,
newHeight = paintHeight;
if (widthScale > 2 && paintWidth > 1) {
newWidth = Math.ceil(paintWidth / 2);
widthScale /= paintWidth / newWidth;
}
if (heightScale > 2 && paintHeight > 1) {
newHeight = Math.ceil(paintHeight / 2);
heightScale /= paintHeight / newHeight;
}
tmpCanvas = this.cachedCanvases.getCanvas(
tmpCanvasId,
newWidth,
newHeight
);
tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, newWidth, newHeight);
tmpCtx.drawImage(
img,
0,
0,
paintWidth,
paintHeight,
0,
0,
newWidth,
newHeight
);
img = tmpCanvas.canvas;
paintWidth = newWidth;
paintHeight = newHeight;
tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1";
}
return {
img,
paintWidth,
paintHeight,
};
}
_createMaskCanvas(img) {
const ctx = this.ctx;
const width = img.width,
height = img.height;
const fillColor = this.current.fillColor;
const isPatternFill = this.current.patternFill;
const maskCanvas = this.cachedCanvases.getCanvas(
"maskCanvas",
width,
height
);
const maskCtx = maskCanvas.context;
putBinaryImageMask(maskCtx, img);
// Create the mask canvas at the size it will be drawn at and also set
// its transform to match the current transform so if there are any
// patterns applied they will be applied relative to the correct
// transform.
const objToCanvas = ctx.mozCurrentTransform;
let maskToCanvas = Util.transform(objToCanvas, [
1 / width,
0,
0,
-1 / height,
0,
0,
]);
maskToCanvas = Util.transform(maskToCanvas, [1, 0, 0, 1, 0, -height]);
const cord1 = Util.applyTransform([0, 0], maskToCanvas);
const cord2 = Util.applyTransform([width, height], maskToCanvas);
const rect = Util.normalizeRect([cord1[0], cord1[1], cord2[0], cord2[1]]);
const drawnWidth = Math.ceil(rect[2] - rect[0]);
const drawnHeight = Math.ceil(rect[3] - rect[1]);
const fillCanvas = this.cachedCanvases.getCanvas(
"fillCanvas",
drawnWidth,
drawnHeight,
true
);
const fillCtx = fillCanvas.context;
// The offset will be the top-left cordinate mask.
const offsetX = Math.min(cord1[0], cord2[0]);
const offsetY = Math.min(cord1[1], cord2[1]);
fillCtx.translate(-offsetX, -offsetY);
fillCtx.transform.apply(fillCtx, maskToCanvas);
// Pre-scale if needed to improve image smoothing.
const scaled = this._scaleImage(
maskCanvas.canvas,
fillCtx.mozCurrentTransformInverse
);
fillCtx.drawImage(
scaled.img,
0,
0,
scaled.img.width,
scaled.img.height,
0,
0,
width,
height
);
fillCtx.globalCompositeOperation = "source-in";
const inverse = Util.transform(fillCtx.mozCurrentTransformInverse, [
1,
0,
0,
1,
-offsetX,
-offsetY,
]);
fillCtx.fillStyle = isPatternFill
? fillColor.getPattern(ctx, this, inverse, false)
: fillColor;
fillCtx.fillRect(0, 0, width, height);
// Round the offsets to avoid drawing fractional pixels.
return {
canvas: fillCanvas.canvas,
offsetX: Math.round(offsetX),
offsetY: Math.round(offsetY),
};
}
// Graphics state
setLineWidth(width) {
this.current.lineWidth = width;
@ -1375,7 +1508,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
if (typeof strokeColor === "object" && strokeColor?.getPattern) {
const lineWidth = this.getSinglePixelWidth();
ctx.save();
ctx.strokeStyle = strokeColor.getPattern(ctx, this);
ctx.strokeStyle = strokeColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
);
// Prevent drawing too thin lines by enforcing a minimum line width.
ctx.lineWidth = Math.max(lineWidth, this.current.lineWidth);
ctx.stroke();
@ -1418,7 +1555,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
if (isPatternFill) {
ctx.save();
ctx.fillStyle = fillColor.getPattern(ctx, this);
ctx.fillStyle = fillColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
);
needRestore = true;
}
@ -1748,7 +1889,11 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
// TODO: Patterns are not applied correctly to text if a non-embedded
// font is used. E.g. issue 8111 and ShowText-ShadingPattern.pdf.
ctx.save();
const pattern = current.fillColor.getPattern(ctx, this);
const pattern = current.fillColor.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse
);
patternTransform = ctx.mozCurrentTransform;
ctx.restore();
ctx.fillStyle = pattern;
@ -2022,7 +2167,12 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
this.save();
const pattern = getShadingPattern(patternIR);
ctx.fillStyle = pattern.getPattern(ctx, this, true);
ctx.fillStyle = pattern.getPattern(
ctx,
this,
ctx.mozCurrentTransformInverse,
true
);
const inv = ctx.mozCurrentTransformInverse;
if (inv) {
@ -2279,8 +2429,6 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
const ctx = this.ctx;
const width = img.width,
height = img.height;
const fillColor = this.current.fillColor;
const isPatternFill = this.current.patternFill;
const glyph = this.processingType3;
@ -2296,35 +2444,15 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
glyph.compiled(ctx);
return;
}
const mask = this._createMaskCanvas(img);
const maskCanvas = mask.canvas;
const maskCanvas = this.cachedCanvases.getCanvas(
"maskCanvas",
width,
height
);
const maskCtx = maskCanvas.context;
maskCtx.save();
putBinaryImageMask(maskCtx, img);
maskCtx.globalCompositeOperation = "source-in";
let patternTransform = null;
if (isPatternFill) {
patternTransform = getAdjustmentTransformation(
ctx.mozCurrentTransform,
width,
height
);
}
maskCtx.fillStyle = isPatternFill
? fillColor.getPattern(maskCtx, this, false, patternTransform)
: fillColor;
maskCtx.fillRect(0, 0, width, height);
maskCtx.restore();
this.paintInlineImageXObject(maskCanvas.canvas);
ctx.save();
// The mask is drawn with the transform applied. Reset the current
// transform to draw to the identity.
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.drawImage(maskCanvas, mask.offsetX, mask.offsetY);
ctx.restore();
}
paintImageMaskXObjectRepeat(
@ -2338,54 +2466,27 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
if (!this.contentVisible) {
return;
}
const width = imgData.width;
const height = imgData.height;
const fillColor = this.current.fillColor;
const isPatternFill = this.current.patternFill;
const maskCanvas = this.cachedCanvases.getCanvas(
"maskCanvas",
width,
height
);
const maskCtx = maskCanvas.context;
maskCtx.save();
putBinaryImageMask(maskCtx, imgData);
maskCtx.globalCompositeOperation = "source-in";
const ctx = this.ctx;
let patternTransform = null;
if (isPatternFill) {
patternTransform = getAdjustmentTransformation(
ctx.mozCurrentTransform,
width,
height
);
}
maskCtx.fillStyle = isPatternFill
? fillColor.getPattern(maskCtx, this, false, patternTransform)
: fillColor;
maskCtx.fillRect(0, 0, width, height);
maskCtx.restore();
ctx.save();
const currentTransform = ctx.mozCurrentTransform;
ctx.transform(scaleX, skewX, skewY, scaleY, 0, 0);
const mask = this._createMaskCanvas(imgData);
ctx.setTransform(1, 0, 0, 1, 0, 0);
for (let i = 0, ii = positions.length; i < ii; i += 2) {
ctx.save();
ctx.transform(
const trans = Util.transform(currentTransform, [
scaleX,
skewX,
skewY,
scaleY,
positions[i],
positions[i + 1]
);
ctx.scale(1, -1);
ctx.drawImage(maskCanvas.canvas, 0, 0, width, height, 0, -1, 1, 1);
ctx.restore();
positions[i + 1],
]);
const [x, y] = Util.applyTransform([0, 0], trans);
ctx.drawImage(mask.canvas, x, y);
}
ctx.restore();
}
paintImageMaskXObjectGroup(images) {
@ -2413,17 +2514,13 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
maskCtx.globalCompositeOperation = "source-in";
let patternTransform = null;
if (isPatternFill) {
patternTransform = getAdjustmentTransformation(
ctx.mozCurrentTransform,
width,
height
);
}
maskCtx.fillStyle = isPatternFill
? fillColor.getPattern(maskCtx, this, false, patternTransform)
? fillColor.getPattern(
maskCtx,
this,
ctx.mozCurrentTransformInverse,
false
)
: fillColor;
maskCtx.fillRect(0, 0, width, height);
@ -2491,17 +2588,7 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
// scale the image to the unit square
ctx.scale(1 / width, -1 / height);
const currentTransform = ctx.mozCurrentTransformInverse;
let widthScale = Math.max(
Math.hypot(currentTransform[0], currentTransform[1]),
1
);
let heightScale = Math.max(
Math.hypot(currentTransform[2], currentTransform[3]),
1
);
let imgToPaint, tmpCanvas, tmpCtx;
let imgToPaint;
// typeof check is needed due to node.js support, see issue #8489
if (
(typeof HTMLElement === "function" && imgData instanceof HTMLElement) ||
@ -2509,61 +2596,26 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
) {
imgToPaint = imgData;
} else {
tmpCanvas = this.cachedCanvases.getCanvas("inlineImage", width, height);
tmpCtx = tmpCanvas.context;
const tmpCanvas = this.cachedCanvases.getCanvas(
"inlineImage",
width,
height
);
const tmpCtx = tmpCanvas.context;
putBinaryImageData(tmpCtx, imgData, this.current.transferMaps);
imgToPaint = tmpCanvas.canvas;
}
let paintWidth = width,
paintHeight = height;
let tmpCanvasId = "prescale1";
// Vertical or horizontal scaling shall not be more than 2 to not lose the
// pixels during drawImage operation, painting on the temporary canvas(es)
// that are twice smaller in size.
while (
(widthScale > 2 && paintWidth > 1) ||
(heightScale > 2 && paintHeight > 1)
) {
let newWidth = paintWidth,
newHeight = paintHeight;
if (widthScale > 2 && paintWidth > 1) {
newWidth = Math.ceil(paintWidth / 2);
widthScale /= paintWidth / newWidth;
}
if (heightScale > 2 && paintHeight > 1) {
newHeight = Math.ceil(paintHeight / 2);
heightScale /= paintHeight / newHeight;
}
tmpCanvas = this.cachedCanvases.getCanvas(
tmpCanvasId,
newWidth,
newHeight
);
tmpCtx = tmpCanvas.context;
tmpCtx.clearRect(0, 0, newWidth, newHeight);
tmpCtx.drawImage(
imgToPaint,
0,
0,
paintWidth,
paintHeight,
0,
0,
newWidth,
newHeight
);
imgToPaint = tmpCanvas.canvas;
paintWidth = newWidth;
paintHeight = newHeight;
tmpCanvasId = tmpCanvasId === "prescale1" ? "prescale2" : "prescale1";
}
ctx.drawImage(
const scaled = this._scaleImage(
imgToPaint,
ctx.mozCurrentTransformInverse
);
ctx.drawImage(
scaled.img,
0,
0,
paintWidth,
paintHeight,
scaled.paintWidth,
scaled.paintHeight,
0,
-height,
width,
@ -2576,8 +2628,8 @@ const CanvasGraphics = (function CanvasGraphicsClosure() {
imgData,
left: position[0],
top: position[1],
width: width / currentTransform[0],
height: height / currentTransform[3],
width: width / ctx.mozCurrentTransformInverse[0],
height: height / ctx.mozCurrentTransformInverse[3],
});
}
this.restore();

View File

@ -72,7 +72,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
this._matrix = IR[8];
}
getPattern(ctx, owner, shadingFill = false, patternTransform = null) {
getPattern(ctx, owner, inverse, shadingFill = false) {
const tmpCanvas = owner.cachedCanvases.getCanvas(
"pattern",
owner.ctx.canvas.width,
@ -121,11 +121,7 @@ class RadialAxialShadingPattern extends BaseShadingPattern {
tmpCtx.fill();
const pattern = ctx.createPattern(tmpCanvas.canvas, "repeat");
if (patternTransform) {
pattern.setTransform(patternTransform);
} else {
pattern.setTransform(createMatrix(ctx.mozCurrentTransformInverse));
}
pattern.setTransform(createMatrix(inverse));
return pattern;
}
}
@ -380,7 +376,7 @@ class MeshShadingPattern extends BaseShadingPattern {
};
}
getPattern(ctx, owner, shadingFill = false, patternTransform = null) {
getPattern(ctx, owner, inverse, shadingFill = false) {
applyBoundingBox(ctx, this._bbox);
let scale;
if (shadingFill) {
@ -535,9 +531,25 @@ class TilingPattern {
this.setFillAndStrokeStyleToContext(graphics, paintType, color);
let adjustedX0 = x0;
let adjustedY0 = y0;
let adjustedX1 = x1;
let adjustedY1 = y1;
// Some bounding boxes have negative x0/y0 cordinates which will cause the
// some of the drawing to be off of the canvas. To avoid this shift the
// bounding box over.
if (x0 < 0) {
adjustedX0 = 0;
adjustedX1 += Math.abs(x0);
}
if (y0 < 0) {
adjustedY0 = 0;
adjustedY1 += Math.abs(y0);
}
tmpCtx.translate(-(dimx.scale * adjustedX0), -(dimy.scale * adjustedY0));
graphics.transform(dimx.scale, 0, 0, dimy.scale, 0, 0);
this.clipBbox(graphics, bbox, x0, y0, x1, y1);
this.clipBbox(graphics, adjustedX0, adjustedY0, adjustedX1, adjustedY1);
graphics.baseTransform = graphics.ctx.mozCurrentTransform.slice();
@ -549,6 +561,8 @@ class TilingPattern {
canvas: tmpCanvas.canvas,
scaleX: dimx.scale,
scaleY: dimy.scale,
offsetX: adjustedX0,
offsetY: adjustedY0,
};
}
@ -569,14 +583,12 @@ class TilingPattern {
return { scale, size };
}
clipBbox(graphics, bbox, x0, y0, x1, y1) {
if (Array.isArray(bbox) && bbox.length === 4) {
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
graphics.clip();
graphics.endPath();
}
clipBbox(graphics, x0, y0, x1, y1) {
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
graphics.clip();
graphics.endPath();
}
setFillAndStrokeStyleToContext(graphics, paintType, color) {
@ -603,10 +615,9 @@ class TilingPattern {
}
}
getPattern(ctx, owner, shadingFill = false, patternTransform = null) {
ctx = this.ctx;
getPattern(ctx, owner, inverse, shadingFill = false) {
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = ctx.mozCurrentTransformInverse;
let matrix = inverse;
if (!shadingFill) {
matrix = Util.transform(matrix, owner.baseTransform);
if (this.matrix) {
@ -619,6 +630,10 @@ class TilingPattern {
let domMatrix = createMatrix(matrix);
// Rescale and so that the ctx.createPattern call generates a pattern with
// the desired size.
domMatrix = domMatrix.translate(
temporaryPatternCanvas.offsetX,
temporaryPatternCanvas.offsetY
);
domMatrix = domMatrix.scale(
1 / temporaryPatternCanvas.scaleX,
1 / temporaryPatternCanvas.scaleY

View File

@ -206,6 +206,7 @@
!issue11403_reduced.pdf
!issue2074.pdf
!scan-bad.pdf
!issue13561_reduced.pdf
!bug847420.pdf
!bug860632.pdf
!bug894572.pdf

Binary file not shown.

View File

@ -878,6 +878,12 @@
"lastPage": 1,
"type": "eq"
},
{ "id": "issue13561_reduced",
"file": "pdfs/issue13561_reduced.pdf",
"md5": "e68c315d6349530180dd90f93027147e",
"rounds": 1,
"type": "eq"
},
{ "id": "issue5202",
"file": "pdfs/issue5202.pdf",
"md5": "bb9cc69211112e66aab40828086a4e5a",