Don't use an intermediate canvas when rendering a tiling pattern bigger than the rectangle to fill

This commit is contained in:
Calixte Denizet 2024-09-28 18:58:56 +02:00
parent a9e439bce1
commit 4c019e7712
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
5 changed files with 296 additions and 79 deletions

View File

@ -318,6 +318,8 @@ class CanvasExtraState {
strokeColor = "#000000";
tilingPatternDims = null;
patternFill = false;
patternStroke = false;
@ -343,6 +345,7 @@ class CanvasExtraState {
const clone = Object.create(this);
clone.clipBox = this.clipBox.slice();
clone.minMax = this.minMax.slice();
clone.tilingPatternDims = this.tilingPatternDims?.slice();
return clone;
}
@ -1494,6 +1497,9 @@ class CanvasGraphics {
if (!minMax) {
// The path is empty, so no need to update the current minMax.
path ||= data[0] = new Path2D();
if (op !== OPS.stroke && op !== OPS.closeStroke) {
this.current.tilingPatternDims = null;
}
this[op](opIdx, path);
return;
}
@ -1521,6 +1527,27 @@ class CanvasGraphics {
getCurrentTransform(this.ctx),
this.current.minMax
);
const tilingDims = this.current.tilingPatternDims;
if (
tilingDims &&
op !== OPS.stroke &&
op !== OPS.closeStroke &&
this.current.fillColor instanceof TilingPattern
) {
// Intersect with clip box to get the actual fill area, then convert
// to pattern space.
const clippedBBox = Util.intersect(
this.current.clipBox,
this.current.minMax
);
if (!clippedBBox) {
this.current.tilingPatternDims = null;
} else {
this.current.fillColor.updatePatternDims(clippedBBox, tilingDims);
}
}
this[op](opIdx, path);
this._pathStartIdx = opIdx;
@ -1590,8 +1617,21 @@ class CanvasGraphics {
const fillColor = this.current.fillColor;
const isPatternFill = this.current.patternFill;
let needRestore = false;
const intersect = this.current.getClippedPathBoundingBox();
if (isPatternFill) {
const dims = this.current.tilingPatternDims;
const tileIdx = dims && fillColor.canSkipPatternCanvas(dims);
if (tileIdx) {
// Draw the tile directly, skipping the pattern canvas.
fillColor.drawPattern(this, path, this.pendingEOFill, tileIdx, opIdx);
this.pendingEOFill = false;
if (consumePath) {
this.consumePath(opIdx, path, intersect);
}
this.current.tilingPatternDims = null;
return;
}
const baseTransform = fillColor.isModifyingCurrentTransform()
? ctx.getTransform()
: null;
@ -1615,7 +1655,6 @@ class CanvasGraphics {
needRestore = true;
}
const intersect = this.current.getClippedPathBoundingBox();
if (this.contentVisible && intersect !== null) {
if (this.pendingEOFill) {
ctx.fill(path, "evenodd");
@ -2422,8 +2461,13 @@ class CanvasGraphics {
setFillColorN(opIdx, ...args) {
this.dependencyTracker?.recordSimpleData("fillColor", opIdx);
this.current.fillColor = this.getColorN_Pattern(opIdx, args);
const pattern = (this.current.fillColor = this.getColorN_Pattern(
opIdx,
args
));
this.current.patternFill = true;
this.current.tilingPatternDims =
pattern instanceof TilingPattern ? [0, 0, 0, 0] : null;
}
setStrokeRGBColor(opIdx, color) {
@ -2442,12 +2486,14 @@ class CanvasGraphics {
this.dependencyTracker?.recordSimpleData("fillColor", opIdx);
this.ctx.fillStyle = this.current.fillColor = color;
this.current.patternFill = false;
this.current.tilingPatternDims = null;
}
setFillTransparent(opIdx) {
this.dependencyTracker?.recordSimpleData("fillColor", opIdx);
this.ctx.fillStyle = this.current.fillColor = "transparent";
this.current.patternFill = false;
this.current.tilingPatternDims = null;
}
_getPattern(opIdx, objId, matrix = null) {

View File

@ -616,22 +616,134 @@ class TilingPattern {
this.ctx = ctx;
this.canvasGraphicsFactory = canvasGraphicsFactory;
this.baseTransform = baseTransform;
// baseTransform * patternMatrix.
this.patternBaseMatrix = this.matrix
? Util.transform(baseTransform, this.matrix)
: baseTransform;
}
// Returns [n, m] tile index if the fill area fits within one tile,
// null otherwise.
canSkipPatternCanvas([width, height, offsetX, offsetY]) {
const [x0, y0, x1, y1] = this.bbox;
const absXStep = Math.abs(this.xstep);
const absYStep = Math.abs(this.ystep);
// dims is in pattern space, so compare directly with xstep/ystep.
if (width > absXStep + 1e-6 || height > absYStep + 1e-6) {
return null;
}
// Tile n covers [x0+n·xstep, x1+n·xstep]; find the range intersecting
// [offsetX, offsetX+width].
const nXFirst = Math.floor((offsetX - x1) / absXStep) + 1;
const nXLast = Math.ceil((offsetX + width - x0) / absXStep) - 1;
const nYFirst = Math.floor((offsetY - y1) / absYStep) + 1;
const nYLast = Math.ceil((offsetY + height - y0) / absYStep) - 1;
return nXLast <= nXFirst && nYLast <= nYFirst ? [nXFirst, nYFirst] : null;
}
// Converts clippedBBox from device space to pattern space and stores it
// as [width, height, offsetX, offsetY] in dims.
updatePatternDims(clippedBBox, dims) {
const inv = Util.inverseTransform(this.patternBaseMatrix);
const c1 = [clippedBBox[0], clippedBBox[1]];
const c2 = [clippedBBox[2], clippedBBox[3]];
Util.applyTransform(c1, inv);
Util.applyTransform(c2, inv);
dims[0] = Math.abs(c2[0] - c1[0]);
dims[1] = Math.abs(c2[1] - c1[1]);
dims[2] = Math.min(c1[0], c2[0]);
dims[3] = Math.min(c1[1], c2[1]);
}
// Renders the tile operators onto a fresh canvas and returns it.
_renderTileCanvas(owner, opIdx, dimx, dimy) {
const [x0, y0, x1, y1] = this.bbox;
const tmpCanvas = owner.canvasFactory.create(dimx.size, dimy.size);
const tmpCtx = tmpCanvas.context;
const graphics = this.canvasGraphicsFactory.createCanvasGraphics(
tmpCtx,
opIdx
);
graphics.groupLevel = owner.groupLevel;
this.setFillAndStrokeStyleToContext(graphics, this.paintType, this.color);
tmpCtx.translate(-dimx.scale * x0, -dimy.scale * y0);
// 0: sub-ops are indexed under the top-level opIdx from
// createCanvasGraphics.
graphics.transform(0, dimx.scale, 0, 0, dimy.scale, 0, 0);
// Required to balance the save/restore in CanvasGraphics beginDrawing.
tmpCtx.save();
graphics.dependencyTracker?.save();
this.clipBbox(graphics, x0, y0, x1, y1);
graphics.baseTransform = getCurrentTransform(graphics.ctx);
graphics.executeOperatorList(this.operatorList);
graphics.endDrawing();
graphics.dependencyTracker?.restore();
tmpCtx.restore();
return tmpCanvas;
}
_getCombinedScales() {
const scale = new Float32Array(2);
Util.singularValueDecompose2dScale(this.matrix, scale);
const [matrixScaleX, matrixScaleY] = scale;
Util.singularValueDecompose2dScale(this.baseTransform, scale);
return [matrixScaleX * scale[0], matrixScaleY * scale[1]];
}
// Draws a single tile directly onto owner, clipped to path.
drawPattern(owner, path, useEOFill = false, [n, m], opIdx) {
const [x0, y0, x1, y1] = this.bbox;
const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0;
const [combinedScaleX, combinedScaleY] = this._getCombinedScales();
const dimx = this.getSizeAndScale(
bboxWidth,
this.ctx.canvas.width,
combinedScaleX
);
const dimy = this.getSizeAndScale(
bboxHeight,
this.ctx.canvas.height,
combinedScaleY
);
// Isolate blend modes from the main canvas.
const tmpCanvas = this._renderTileCanvas(owner, opIdx, dimx, dimy);
owner.save();
if (useEOFill) {
owner.ctx.clip(path, "evenodd");
} else {
owner.ctx.clip(path);
}
// Position tile (n, m) in device space; the clip above is unaffected
// by setTransform.
owner.ctx.setTransform(...this.patternBaseMatrix);
owner.ctx.translate(n * this.xstep, m * this.ystep);
owner.ctx.drawImage(tmpCanvas.canvas, x0, y0, bboxWidth, bboxHeight);
owner.canvasFactory.destroy(tmpCanvas);
owner.restore();
}
createPatternCanvas(owner, opIdx) {
const {
bbox,
operatorList,
paintType,
tilingType,
color,
canvasGraphicsFactory,
} = this;
const [x0, y0, x1, y1] = this.bbox;
const width = x1 - x0;
const height = y1 - y0;
let { xstep, ystep } = this;
xstep = Math.abs(xstep);
ystep = Math.abs(ystep);
info("TilingType: " + tilingType);
info("TilingType: " + this.tilingType);
// A tiling pattern as defined by PDF spec 8.7.2 is a cell whose size is
// described by bbox, and may repeat regularly by shifting the cell by
@ -651,45 +763,32 @@ class TilingPattern {
// "Figures on adjacent tiles should not overlap" (PDF spec 8.7.3.1),
// but overlapping cells without common pixels are still valid.
const x0 = bbox[0],
y0 = bbox[1],
x1 = bbox[2],
y1 = bbox[3];
const width = x1 - x0;
const height = y1 - y0;
// Obtain scale from matrix and current transformation matrix.
const scale = new Float32Array(2);
Util.singularValueDecompose2dScale(this.matrix, scale);
const [matrixScaleX, matrixScaleY] = scale;
Util.singularValueDecompose2dScale(this.baseTransform, scale);
const combinedScaleX = matrixScaleX * scale[0];
const combinedScaleY = matrixScaleY * scale[1];
const [combinedScaleX, combinedScaleY] = this._getCombinedScales();
// Use width and height values that are as close as possible to the end
// result when the pattern is used. Too low value makes the pattern look
// blurry. Too large value makes it look too crispy.
let canvasWidth = width,
canvasHeight = height,
redrawHorizontally = false,
redrawVertically = false;
const xScaledStep = Math.ceil(xstep * combinedScaleX);
const yScaledStep = Math.ceil(ystep * combinedScaleY);
const xScaledWidth = Math.ceil(width * combinedScaleX);
const yScaledHeight = Math.ceil(height * combinedScaleY);
if (xScaledStep >= xScaledWidth) {
if (
Math.ceil(xstep * combinedScaleX) >= Math.ceil(width * combinedScaleX)
) {
canvasWidth = xstep;
} else {
redrawHorizontally = true;
}
if (yScaledStep >= yScaledHeight) {
if (
Math.ceil(ystep * combinedScaleY) >= Math.ceil(height * combinedScaleY)
) {
canvasHeight = ystep;
} else {
redrawVertically = true;
}
// Use width and height values that are as close as possible to the end
// result when the pattern is used. Too low value makes the pattern look
// blurry. Too large value makes it look too crispy.
const dimx = this.getSizeAndScale(
canvasWidth,
this.ctx.canvas.width,
@ -701,43 +800,7 @@ class TilingPattern {
combinedScaleY
);
const tmpCanvas = owner.canvasFactory.create(dimx.size, dimy.size);
const tmpCtx = tmpCanvas.context;
const graphics = canvasGraphicsFactory.createCanvasGraphics(tmpCtx, opIdx);
graphics.groupLevel = owner.groupLevel;
this.setFillAndStrokeStyleToContext(graphics, paintType, color);
tmpCtx.translate(-dimx.scale * x0, -dimy.scale * y0);
graphics.transform(
// We pass 0 as the 'opIdx' argument, but the value is irrelevant.
// We know that we are in a 'CanvasNestedDependencyTracker' that captures
// all the sub-operations needed to create this pattern canvas and uses
// the top-level operation index as their index.
0,
dimx.scale,
0,
0,
dimy.scale,
0,
0
);
// To match CanvasGraphics beginDrawing we must save the context here or
// else we end up with unbalanced save/restores.
tmpCtx.save();
graphics.dependencyTracker?.save();
this.clipBbox(graphics, x0, y0, x1, y1);
graphics.baseTransform = getCurrentTransform(graphics.ctx);
graphics.executeOperatorList(operatorList);
graphics.endDrawing();
graphics.dependencyTracker?.restore();
tmpCtx.restore();
const tmpCanvas = this._renderTileCanvas(owner, opIdx, dimx, dimy);
if (redrawHorizontally || redrawVertically) {
// The tile is overlapping itself, so we create a new tile with
@ -862,14 +925,12 @@ class TilingPattern {
}
getPattern(ctx, owner, inverse, pathType, opIdx) {
// PDF spec 8.7.2 NOTE 1: pattern's matrix is relative to initial matrix.
let matrix = inverse;
if (pathType !== PathType.SHADING) {
matrix = Util.transform(matrix, owner.baseTransform);
if (this.matrix) {
matrix = Util.transform(matrix, this.matrix);
}
}
// PDF spec 8.7.2: prepend inverse CTM to patternBaseMatrix to position
// the CSS pattern.
const matrix =
pathType !== PathType.SHADING
? Util.transform(inverse, this.patternBaseMatrix)
: inverse;
const temporaryPatternCanvas = this.createPatternCanvas(owner, opIdx);

View File

@ -894,3 +894,4 @@
!hello_world_rotated.pdf
!bug2025674.pdf
!bug2026037.pdf
!tiling_patterns_variations.pdf

View File

@ -0,0 +1,102 @@
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page
/Parent 2 0 R
/MediaBox [0 0 600 800]
/Resources <<
/Pattern << /P1 5 0 R /P2 6 0 R /P3 7 0 R /P4 8 0 R /P5 9 0 R >>
/ColorSpace << /Cs [/Pattern] /Cs2 [/Pattern /DeviceRGB] >>
>>
/Contents 4 0 R
>>
endobj
4 0 obj
<< /Length 637 >>
stream
1 g
0 0 600 800 re f
q /Cs cs /P1 scn 20 600 150 150 re f Q
q /Cs cs /P2 scn 220 600 150 150 re f Q
q /Cs cs /P3 scn 420 600 150 150 re f Q
q /Cs cs /P1 scn 250 460 30 30 re f Q
q 250 460 30 30 re W n /Cs cs /P1 scn 220 400 150 150 re f Q
q /Cs cs /P4 scn 420 400 150 150 re f Q
q /Cs cs /P4 scn 20 250 30 30 re f Q
q /Cs cs /P1 scn 1 0 0 1 250 200 cm 0.5 0 0 0.5 0 0 cm 0 0 60 60 re f Q
q /Cs2 cs 0.8 0.2 0.2 /P5 scn 420 200 150 150 re f Q
0 G 0.5 w
20 600 150 150 re S
220 600 150 150 re S
420 600 150 150 re S
20 400 150 150 re S
220 400 150 150 re S
420 400 150 150 re S
20 200 150 150 re S
220 200 150 150 re S
420 200 150 150 re S
endstream
endobj
5 0 obj
<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 50 50] /XStep 50 /YStep 50 /Resources << >> /Length 30 >>
stream
0.2 0.4 0.8 rg
5 5 40 40 re f
endstream
endobj
6 0 obj
<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 20 20] /XStep 50 /YStep 50 /Resources << >> /Length 30 >>
stream
0.9 0.5 0.1 rg
0 0 20 20 re f
endstream
endobj
7 0 obj
<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 70 70] /XStep 50 /YStep 50 /Resources << >> /Length 30 >>
stream
0.1 0.7 0.2 rg
0 0 70 70 re f
endstream
endobj
8 0 obj
<< /Type /Pattern /PatternType 1 /PaintType 1 /TilingType 1 /BBox [0 0 50 50] /XStep 50 /YStep 50 /Matrix [0.866025 0.5 -0.5 0.866025 0 0] /Resources << >> /Length 30 >>
stream
0.7 0.1 0.7 rg
5 5 40 40 re f
endstream
endobj
9 0 obj
<< /Type /Pattern /PatternType 1 /PaintType 2 /TilingType 1 /BBox [0 0 30 30] /XStep 30 /YStep 30 /Resources << >> /Length 31 >>
stream
0 0 10 30 re f
20 0 10 30 re f
endstream
endobj
xref
0 10
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000369 00000 n
0000001058 00000 n
0000001250 00000 n
0000001442 00000 n
0000001634 00000 n
0000001867 00000 n
trailer
<< /Size 10 /Root 1 0 R >>
startxref
2060
%%EOF

View File

@ -14028,5 +14028,12 @@
"rounds": 1,
"link": true,
"type": "eq"
},
{
"id": "tiling_patterns_variations",
"file": "pdfs/tiling_patterns_variations.pdf",
"md5": "2870c3136be00ddd975149b2c7d1e6df",
"rounds": 1,
"type": "eq"
}
]