mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-13 00:34:04 +02:00
Don't use an intermediate canvas when rendering a tiling pattern bigger than the rectangle to fill
This commit is contained in:
parent
a9e439bce1
commit
4c019e7712
@ -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) {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -894,3 +894,4 @@
|
||||
!hello_world_rotated.pdf
|
||||
!bug2025674.pdf
|
||||
!bug2026037.pdf
|
||||
!tiling_patterns_variations.pdf
|
||||
|
||||
102
test/pdfs/tiling_patterns_variations.pdf
Executable file
102
test/pdfs/tiling_patterns_variations.pdf
Executable 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
|
||||
@ -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"
|
||||
}
|
||||
]
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user