mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-04-09 06:44:07 +02:00
Avoid as much as possible to have intermediate canvases
We try to detect in the worker if some patterns or groups need to be drawn or not in isolation. When they don't, we just draw them on the main canvas instead of drawing on a new canvas. A pattern or a group is considered as being in isolation if it has some compositing rules or some transparency. It improves the rendering performance of the pdf in bug 1731514.
This commit is contained in:
parent
70baf9cd5f
commit
00ea8db6bf
@ -29,6 +29,7 @@ import {
|
||||
Util,
|
||||
warn,
|
||||
} from "../shared/util.js";
|
||||
import { CheckedOperatorList, OperatorList } from "./operator_list.js";
|
||||
import { CMapFactory, IdentityCMap } from "./cmap.js";
|
||||
import { Cmd, Dict, EOF, isName, Name, Ref, RefSet } from "./primitives.js";
|
||||
import {
|
||||
@ -85,7 +86,6 @@ import { getGlyphsUnicode } from "./glyphlist.js";
|
||||
import { getMetrics } from "./metrics.js";
|
||||
import { getUnicodeForGlyph } from "./unicode.js";
|
||||
import { MurmurHash3_64 } from "../shared/murmurhash3.js";
|
||||
import { OperatorList } from "./operator_list.js";
|
||||
import { PDFImage } from "./image.js";
|
||||
import { Stream } from "./stream.js";
|
||||
|
||||
@ -501,7 +501,17 @@ class PartialEvaluator {
|
||||
if (optionalContent !== undefined) {
|
||||
operatorList.addOp(OPS.beginMarkedContentProps, ["OC", optionalContent]);
|
||||
}
|
||||
|
||||
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) {
|
||||
groupOptions = {
|
||||
matrix,
|
||||
@ -509,6 +519,7 @@ class PartialEvaluator {
|
||||
smask,
|
||||
isolated: false,
|
||||
knockout: false,
|
||||
needsIsolation: false,
|
||||
};
|
||||
|
||||
const groupSubtype = group.get("S");
|
||||
@ -532,30 +543,30 @@ class PartialEvaluator {
|
||||
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({
|
||||
stream: xobj,
|
||||
task,
|
||||
resources: localResources instanceof Dict ? localResources : resources,
|
||||
operatorList,
|
||||
operatorList: newOpList,
|
||||
initialState,
|
||||
prevRefs: seenRefs,
|
||||
});
|
||||
operatorList.addOp(OPS.paintFormXObjectEnd, []);
|
||||
|
||||
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]);
|
||||
} else {
|
||||
operatorList.addOp(OPS.paintFormXObjectEnd, []);
|
||||
}
|
||||
|
||||
if (optionalContent !== undefined) {
|
||||
@ -972,7 +983,7 @@ class PartialEvaluator {
|
||||
localTilingPatternCache
|
||||
) {
|
||||
// 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
|
||||
// is missing some /Resources entries (fixes issue6541.pdf).
|
||||
const patternResources = Dict.merge({
|
||||
@ -988,10 +999,12 @@ class PartialEvaluator {
|
||||
})
|
||||
.then(function () {
|
||||
const operatorListIR = tilingOpList.getIR();
|
||||
const { needsIsolation } = tilingOpList;
|
||||
const tilingPatternIR = getTilingPatternIR(
|
||||
operatorListIR,
|
||||
patternDict,
|
||||
color
|
||||
color,
|
||||
needsIsolation
|
||||
);
|
||||
// Add the dependencies to the parent operator list so they are
|
||||
// resolved before the sub operator list is executed synchronously.
|
||||
@ -1001,6 +1014,7 @@ class PartialEvaluator {
|
||||
if (patternDict.objId) {
|
||||
localTilingPatternCache.set(/* name = */ null, patternDict.objId, {
|
||||
operatorListIR,
|
||||
needsIsolation,
|
||||
dict: patternDict,
|
||||
});
|
||||
}
|
||||
@ -1578,7 +1592,8 @@ class PartialEvaluator {
|
||||
const tilingPatternIR = getTilingPatternIR(
|
||||
localTilingPattern.operatorListIR,
|
||||
localTilingPattern.dict,
|
||||
color
|
||||
color,
|
||||
localTilingPattern.needsIsolation
|
||||
);
|
||||
operatorList.addOp(fn, tilingPatternIR);
|
||||
return undefined;
|
||||
|
||||
@ -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 };
|
||||
|
||||
@ -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 bbox = lookupNormalRect(dict.getArray("BBox"), null);
|
||||
// Ensure that the pattern has a non-zero width and height, to prevent errors
|
||||
@ -1174,6 +1174,7 @@ function getTilingPatternIR(operatorList, dict, color) {
|
||||
ystep,
|
||||
paintType,
|
||||
tilingType,
|
||||
needsIsolation,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@ -2608,7 +2608,8 @@ class CanvasGraphics {
|
||||
this.save(opIdx);
|
||||
// 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.
|
||||
if (this.inSMaskMode) {
|
||||
const { inSMaskMode } = this;
|
||||
if (inSMaskMode) {
|
||||
this.endSMaskMode();
|
||||
this.current.activeSMask = null;
|
||||
}
|
||||
@ -2637,6 +2638,28 @@ class CanvasGraphics {
|
||||
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);
|
||||
if (group.matrix) {
|
||||
currentCtx.transform(...group.matrix);
|
||||
@ -2756,6 +2779,12 @@ class CanvasGraphics {
|
||||
this.groupLevel--;
|
||||
const groupCtx = this.ctx;
|
||||
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;
|
||||
// Turn off image smoothing to avoid sub pixel interpolation which can
|
||||
// look kind of blurry for some pdfs.
|
||||
|
||||
@ -614,6 +614,7 @@ class TilingPattern {
|
||||
this.ystep = IR[6];
|
||||
this.paintType = IR[7];
|
||||
this.tilingType = IR[8];
|
||||
this.needsIsolation = IR[9] ?? true;
|
||||
this.ctx = ctx;
|
||||
this.canvasGraphicsFactory = canvasGraphicsFactory;
|
||||
this.baseTransform = baseTransform;
|
||||
@ -702,23 +703,6 @@ class TilingPattern {
|
||||
// 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) {
|
||||
@ -730,9 +714,39 @@ class TilingPattern {
|
||||
// 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);
|
||||
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();
|
||||
}
|
||||
|
||||
@ -892,14 +906,15 @@ class TilingPattern {
|
||||
clipBbox(graphics, x0, y0, x1, y1) {
|
||||
const bboxWidth = x1 - x0;
|
||||
const bboxHeight = y1 - y0;
|
||||
graphics.ctx.rect(x0, y0, bboxWidth, bboxHeight);
|
||||
const clip = new Path2D();
|
||||
clip.rect(x0, y0, bboxWidth, bboxHeight);
|
||||
Util.axialAlignedBoundingBox(
|
||||
[x0, y0, x1, y1],
|
||||
getCurrentTransform(graphics.ctx),
|
||||
graphics.current.minMax
|
||||
);
|
||||
graphics.clip();
|
||||
graphics.endPath();
|
||||
graphics.ctx.clip(clip);
|
||||
graphics.current.updateClipFromPath();
|
||||
}
|
||||
|
||||
setFillAndStrokeStyleToContext(graphics, paintType, color) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user