Merge pull request #21061 from calixteman/pattern_perf

Avoid as much as possible to have intermediate canvases
This commit is contained in:
calixteman 2026-04-07 21:07:45 +02:00 committed by GitHub
commit 59c21e3110
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 132 additions and 41 deletions

View File

@ -29,6 +29,7 @@ import {
Util, Util,
warn, warn,
} from "../shared/util.js"; } from "../shared/util.js";
import { CheckedOperatorList, OperatorList } from "./operator_list.js";
import { CMapFactory, IdentityCMap } from "./cmap.js"; import { CMapFactory, IdentityCMap } from "./cmap.js";
import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js"; import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js";
import { import {
@ -85,7 +86,6 @@ import { getGlyphsUnicode } from "./glyphlist.js";
import { getMetrics } from "./metrics.js"; import { getMetrics } from "./metrics.js";
import { getUnicodeForGlyph } from "./unicode.js"; import { getUnicodeForGlyph } from "./unicode.js";
import { MurmurHash3_64 } from "../shared/murmurhash3.js"; import { MurmurHash3_64 } from "../shared/murmurhash3.js";
import { OperatorList } from "./operator_list.js";
import { PDFImage } from "./image.js"; import { PDFImage } from "./image.js";
import { Stream } from "./stream.js"; import { Stream } from "./stream.js";
@ -501,7 +501,17 @@ class PartialEvaluator {
if (optionalContent !== undefined) { if (optionalContent !== undefined) {
operatorList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]); operatorList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
} }
const group = dict.get("Group"); const group = dict.get("Group");
let newOpList;
// If it's a group, a new canvas will be created that is the size of the
// bounding box and translated to the correct position so we don't need to
// apply the bounding box to it.
const f32matrix = matrix && new Float32Array(matrix);
const args = [f32matrix, (!group && f32bbox) || null];
const localResources = dict.get("Resources");
if (group) { if (group) {
groupOptions = { groupOptions = {
matrix, matrix,
@ -509,6 +519,7 @@ class PartialEvaluator {
smask, smask,
isolated: false, isolated: false,
knockout: false, knockout: false,
needsIsolation: false,
}; };
const groupSubtype = group.get("S"); const groupSubtype = group.get("S");
@ -532,30 +543,30 @@ class PartialEvaluator {
smask.backdrop = colorSpace.getRgbHex(smask.backdrop, 0); smask.backdrop = colorSpace.getRgbHex(smask.backdrop, 0);
} }
operatorList.addOp(OPS.beginGroup, [groupOptions]); newOpList = new CheckedOperatorList();
} else {
newOpList = operatorList;
operatorList.addOp(OPS.paintFormXObjectBegin, args);
} }
// If it's a group, a new canvas will be created that is the size of the
// bounding box and translated to the correct position so we don't need to
// apply the bounding box to it.
const f32matrix = matrix && new Float32Array(matrix);
const args = [f32matrix, (!group && f32bbox) || null];
operatorList.addOp(OPS.paintFormXObjectBegin, args);
const localResources = dict.get("Resources");
await this.getOperatorList({ await this.getOperatorList({
stream: xobj, stream: xobj,
task, task,
resources: localResources instanceof Dict ? localResources : resources, resources: localResources instanceof Dict ? localResources : resources,
operatorList, operatorList: newOpList,
initialState, initialState,
prevRefs: seenRefs, prevRefs: seenRefs,
}); });
operatorList.addOp(OPS.paintFormXObjectEnd, []);
if (group) { if (group) {
groupOptions.needsIsolation = newOpList.needsIsolation || !!smask;
operatorList.addOp(OPS.beginGroup, [groupOptions]);
operatorList.addOp(OPS.paintFormXObjectBegin, args);
operatorList.addOpList(newOpList);
operatorList.addOp(OPS.paintFormXObjectEnd, []);
operatorList.addOp(OPS.endGroup, [groupOptions]); operatorList.addOp(OPS.endGroup, [groupOptions]);
} else {
operatorList.addOp(OPS.paintFormXObjectEnd, []);
} }
if (optionalContent !== undefined) { if (optionalContent !== undefined) {
@ -972,7 +983,7 @@ class PartialEvaluator {
localTilingPatternCache localTilingPatternCache
) { ) {
// Create an IR of the pattern code. // Create an IR of the pattern code.
const tilingOpList = new OperatorList(); const tilingOpList = new CheckedOperatorList();
// Merge the available resources, to prevent issues when the patternDict // Merge the available resources, to prevent issues when the patternDict
// is missing some /Resources entries (fixes issue6541.pdf). // is missing some /Resources entries (fixes issue6541.pdf).
const patternResources = Dict.merge({ const patternResources = Dict.merge({
@ -988,10 +999,12 @@ class PartialEvaluator {
}) })
.then(function () { .then(function () {
const operatorListIR = tilingOpList.getIR(); const operatorListIR = tilingOpList.getIR();
const { needsIsolation } = tilingOpList;
const tilingPatternIR = getTilingPatternIR( const tilingPatternIR = getTilingPatternIR(
operatorListIR, operatorListIR,
patternDict, patternDict,
color color,
needsIsolation
); );
// Add the dependencies to the parent operator list so they are // Add the dependencies to the parent operator list so they are
// resolved before the sub operator list is executed synchronously. // resolved before the sub operator list is executed synchronously.
@ -1001,6 +1014,7 @@ class PartialEvaluator {
if (patternDict.objId) { if (patternDict.objId) {
localTilingPatternCache.set(/* name = */ null, patternDict.objId, { localTilingPatternCache.set(/* name = */ null, patternDict.objId, {
operatorListIR, operatorListIR,
needsIsolation,
dict: patternDict, dict: patternDict,
}); });
} }
@ -1578,7 +1592,8 @@ class PartialEvaluator {
const tilingPatternIR = getTilingPatternIR( const tilingPatternIR = getTilingPatternIR(
localTilingPattern.operatorListIR, localTilingPattern.operatorListIR,
localTilingPattern.dict, localTilingPattern.dict,
color color,
localTilingPattern.needsIsolation
); );
operatorList.addOp(fn, tilingPatternIR); operatorList.addOp(fn, tilingPatternIR);
return undefined; return undefined;

View File

@ -822,4 +822,35 @@ class OperatorList {
} }
} }
export { OperatorList }; /**
* A subclass of OperatorList that checks whether added group or pattern
* operations require being drawn in isolation (i.e. on a separate canvas).
* A group/pattern needs isolation when it uses non-default compositing
* (blend mode) or a soft mask. The result is exposed via `needsIsolation`.
*/
class CheckedOperatorList extends OperatorList {
needsIsolation = false;
addOp(fn, args) {
if (!this.needsIsolation) {
if (fn === OPS.beginGroup) {
// Propagate isolation only if the nested group itself needs it.
this.needsIsolation = args[0].needsIsolation;
} else if (fn === OPS.setGState) {
for (const [key, val] of args[0]) {
if (key === "BM" && val !== "source-over") {
this.needsIsolation = true;
break;
}
if (key === "SMask" && val !== false) {
this.needsIsolation = true;
break;
}
}
}
}
super.addOp(fn, args);
}
}
export { CheckedOperatorList, OperatorList };

View File

@ -1139,7 +1139,7 @@ class DummyShading extends BaseShading {
} }
} }
function getTilingPatternIR(operatorList, dict, color) { function getTilingPatternIR(operatorList, dict, color, needsIsolation = true) {
const matrix = lookupMatrix(dict.getArray("Matrix"), IDENTITY_MATRIX); const matrix = lookupMatrix(dict.getArray("Matrix"), IDENTITY_MATRIX);
const bbox = lookupNormalRect(dict.getArray("BBox"), null); const bbox = lookupNormalRect(dict.getArray("BBox"), null);
// Ensure that the pattern has a non-zero width and height, to prevent errors // Ensure that the pattern has a non-zero width and height, to prevent errors
@ -1174,6 +1174,7 @@ function getTilingPatternIR(operatorList, dict, color) {
ystep, ystep,
paintType, paintType,
tilingType, tilingType,
needsIsolation,
]; ];
} }

View File

@ -2608,7 +2608,8 @@ class CanvasGraphics {
this.save(opIdx); this.save(opIdx);
// If there's an active soft mask we don't want it enabled for the group, so // If there's an active soft mask we don't want it enabled for the group, so
// clear it out. The mask and suspended canvas will be restored in endGroup. // clear it out. The mask and suspended canvas will be restored in endGroup.
if (this.inSMaskMode) { const { inSMaskMode } = this;
if (inSMaskMode) {
this.endSMaskMode(); this.endSMaskMode();
this.current.activeSMask = null; this.current.activeSMask = null;
} }
@ -2637,6 +2638,28 @@ class CanvasGraphics {
warn("Knockout groups not supported."); warn("Knockout groups not supported.");
} }
if (
!group.needsIsolation &&
currentCtx.globalAlpha === 1 &&
currentCtx.globalCompositeOperation === "source-over" &&
!inSMaskMode
) {
if (group.bbox) {
let clip = new Path2D();
const [x0, y0, x1, y1] = group.bbox;
clip.rect(x0, y0, x1 - x0, y1 - y0);
if (group.matrix) {
const path = new Path2D();
path.addPath(clip, new DOMMatrix(group.matrix));
clip = path;
}
currentCtx.clip(clip);
}
this.groupStack.push(null); // null = no intermediate canvas
this.groupLevel++;
return;
}
const currentTransform = getCurrentTransform(currentCtx); const currentTransform = getCurrentTransform(currentCtx);
if (group.matrix) { if (group.matrix) {
currentCtx.transform(...group.matrix); currentCtx.transform(...group.matrix);
@ -2756,6 +2779,12 @@ class CanvasGraphics {
this.groupLevel--; this.groupLevel--;
const groupCtx = this.ctx; const groupCtx = this.ctx;
const ctx = this.groupStack.pop(); const ctx = this.groupStack.pop();
if (ctx === null) {
// Simple group: content was drawn directly on the parent canvas.
this.restore(opIdx);
return;
}
this.ctx = ctx; this.ctx = ctx;
// Turn off image smoothing to avoid sub pixel interpolation which can // Turn off image smoothing to avoid sub pixel interpolation which can
// look kind of blurry for some pdfs. // look kind of blurry for some pdfs.

View File

@ -614,6 +614,7 @@ class TilingPattern {
this.ystep = IR[6]; this.ystep = IR[6];
this.paintType = IR[7]; this.paintType = IR[7];
this.tilingType = IR[8]; this.tilingType = IR[8];
this.needsIsolation = IR[9] ?? true;
this.ctx = ctx; this.ctx = ctx;
this.canvasGraphicsFactory = canvasGraphicsFactory; this.canvasGraphicsFactory = canvasGraphicsFactory;
this.baseTransform = baseTransform; this.baseTransform = baseTransform;
@ -702,23 +703,6 @@ class TilingPattern {
// Draws a single tile directly onto owner, clipped to path. // Draws a single tile directly onto owner, clipped to path.
drawPattern(owner, path, useEOFill = false, [n, m], opIdx) { drawPattern(owner, path, useEOFill = false, [n, m], opIdx) {
const [x0, y0, x1, y1] = this.bbox; 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(); owner.save();
if (useEOFill) { if (useEOFill) {
@ -730,9 +714,39 @@ class TilingPattern {
// by setTransform. // by setTransform.
owner.ctx.setTransform(...this.patternBaseMatrix); owner.ctx.setTransform(...this.patternBaseMatrix);
owner.ctx.translate(n * this.xstep, m * this.ystep); owner.ctx.translate(n * this.xstep, m * this.ystep);
owner.ctx.drawImage(tmpCanvas.canvas, x0, y0, bboxWidth, bboxHeight); if (
this.needsIsolation ||
owner.ctx.globalAlpha !== 1 ||
owner.ctx.globalCompositeOperation !== "source-over" ||
owner.inSMaskMode
) {
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.ctx.drawImage(tmpCanvas.canvas, x0, y0, bboxWidth, bboxHeight);
owner.canvasFactory.destroy(tmpCanvas);
} else {
// No blend modes or transparency: render the tile directly onto owner.
this.setFillAndStrokeStyleToContext(owner, this.paintType, this.color);
this.clipBbox(owner, x0, y0, x1, y1);
owner.baseTransformStack.push(owner.baseTransform);
owner.baseTransform = getCurrentTransform(owner.ctx);
owner.executeOperatorList(this.operatorList);
owner.baseTransform = owner.baseTransformStack.pop();
}
owner.canvasFactory.destroy(tmpCanvas);
owner.restore(); owner.restore();
} }
@ -892,14 +906,15 @@ class TilingPattern {
clipBbox(graphics, x0, y0, x1, y1) { clipBbox(graphics, x0, y0, x1, y1) {
const bboxWidth = x1 - x0; const bboxWidth = x1 - x0;
const bboxHeight = y1 - y0; const bboxHeight = y1 - y0;
graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight); const clip = new Path2D();
clip.rect(x0, y0, bboxWidth, bboxHeight);
Util.axialAlignedBoundingBox( Util.axialAlignedBoundingBox(
[x0, y0, x1, y1], [x0, y0, x1, y1],
getCurrentTransform(graphics.ctx), getCurrentTransform(graphics.ctx),
graphics.current.minMax graphics.current.minMax
); );
graphics.clip(); graphics.ctx.clip(clip);
graphics.endPath(); graphics.current.updateClipFromPath();
} }
setFillAndStrokeStyleToContext(graphics, paintType, color) { setFillAndStrokeStyleToContext(graphics, paintType, color) {