mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-06-22 16:05:56 +02:00
Merge pull request #21101 from calixteman/improve_smask
Improve SMask compositing by pre-baking backdrop and filter
This commit is contained in:
commit
59908ccad3
@ -651,6 +651,10 @@ class CanvasGraphics {
|
|||||||
this.smaskStack = [];
|
this.smaskStack = [];
|
||||||
this.tempSMask = null;
|
this.tempSMask = null;
|
||||||
this.smaskGroupCanvases = [];
|
this.smaskGroupCanvases = [];
|
||||||
|
this.smaskPreparedEntry = null;
|
||||||
|
this.smaskPreparedFor = null;
|
||||||
|
this.smaskPreparedOffsetX = 0;
|
||||||
|
this.smaskPreparedOffsetY = 0;
|
||||||
this.suspendedCtx = null;
|
this.suspendedCtx = null;
|
||||||
this.contentVisible = true;
|
this.contentVisible = true;
|
||||||
this.markedContentStack = markedContentStack || [];
|
this.markedContentStack = markedContentStack || [];
|
||||||
@ -845,6 +849,7 @@ class CanvasGraphics {
|
|||||||
this.canvasFactory.destroy(canvas);
|
this.canvasFactory.destroy(canvas);
|
||||||
}
|
}
|
||||||
this.smaskGroupCanvases.length = 0;
|
this.smaskGroupCanvases.length = 0;
|
||||||
|
this._clearPreparedSMask();
|
||||||
this.tempSMask = null;
|
this.tempSMask = null;
|
||||||
this.smaskStack.length = 0;
|
this.smaskStack.length = 0;
|
||||||
|
|
||||||
@ -1257,31 +1262,138 @@ class CanvasGraphics {
|
|||||||
return !!this.suspendedCtx;
|
return !!this.suspendedCtx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_clearPreparedSMask() {
|
||||||
|
if (this.smaskPreparedEntry) {
|
||||||
|
this.canvasFactory.destroy(this.smaskPreparedEntry);
|
||||||
|
this.smaskPreparedEntry = null;
|
||||||
|
}
|
||||||
|
this.smaskPreparedFor = null;
|
||||||
|
this.smaskPreparedOffsetX = 0;
|
||||||
|
this.smaskPreparedOffsetY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ensurePreparedSMask(smask, width, height) {
|
||||||
|
if (smask === this.smaskPreparedFor) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._clearPreparedSMask();
|
||||||
|
this._prepareSMaskCanvas(smask, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
checkSMaskState(opIdx) {
|
checkSMaskState(opIdx) {
|
||||||
const inSMaskMode = this.inSMaskMode;
|
const inSMaskMode = this.inSMaskMode;
|
||||||
if (this.current.activeSMask && !inSMaskMode) {
|
if (this.current.activeSMask && !inSMaskMode) {
|
||||||
this.beginSMaskMode(opIdx);
|
this.beginSMaskMode(opIdx);
|
||||||
} else if (!this.current.activeSMask && inSMaskMode) {
|
} else if (!this.current.activeSMask && inSMaskMode) {
|
||||||
this.endSMaskMode();
|
this.endSMaskMode();
|
||||||
|
} else if (this.current.activeSMask && inSMaskMode) {
|
||||||
|
// The active SMask may have changed while SMask mode stayed active
|
||||||
|
// (e.g. a direct SMask A->B replacement, or a restore() that surfaces
|
||||||
|
// a different saved mask). _ensurePreparedSMask is a no-op when the
|
||||||
|
// same mask object is re-encountered.
|
||||||
|
this._ensurePreparedSMask(
|
||||||
|
this.current.activeSMask,
|
||||||
|
this.ctx.canvas.width,
|
||||||
|
this.ctx.canvas.height
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Else, the state is okay and nothing needs to be done.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Soft mask mode takes the current main drawing canvas and replaces it with
|
* Backdrop cases use a layer-sized canvas so that the backdrop color
|
||||||
* a temporary canvas. Any drawing operations that happen on the temporary
|
* correctly extends to pixels outside the mask canvas bounds.
|
||||||
* canvas need to be composed with the main canvas that was suspended (see
|
* Filter-only cases use a mask-sized canvas to avoid a large allocation when
|
||||||
* `compose()`). The temporary canvas also duplicates many of its operations
|
* the mask is small relative to the page; `composeSMask` then uses
|
||||||
* on the suspended canvas to keep them in sync, so that when the soft mask
|
* `smaskPreparedOffsetX/Y` to translate dirty-box coordinates into the
|
||||||
* mode ends any clipping paths or transformations will still be active and in
|
* smaller canvas's coordinate space. Plain-alpha masks with no backdrop or
|
||||||
* the right order on the canvas' graphics state stack.
|
* transfer map need no canvas at all.
|
||||||
|
*/
|
||||||
|
_prepareSMaskCanvas(smask, width, height) {
|
||||||
|
const { canvas: maskCanvas, subtype, backdrop, transferMap } = smask;
|
||||||
|
const hasFilter =
|
||||||
|
subtype === "Luminosity" || (subtype === "Alpha" && transferMap);
|
||||||
|
if (!backdrop && !hasFilter) {
|
||||||
|
// No canvas to prepare, but record the mask so that checkSMaskState's
|
||||||
|
// identity check does not keep re-entering the rebuild path for the same
|
||||||
|
// plain-alpha mask on every restore()/setGState() call.
|
||||||
|
this.smaskPreparedFor = smask;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let preparedEntry, offsetX, offsetY;
|
||||||
|
|
||||||
|
if (backdrop && hasFilter) {
|
||||||
|
// Both backdrop and filter: must apply backdrop BEFORE filter (spec
|
||||||
|
// order). Use a layer-sized intermediate so that pixels outside the
|
||||||
|
// mask canvas bounds get the backdrop color before filtering.
|
||||||
|
const srcEntry = this.canvasFactory.create(width, height);
|
||||||
|
const sCtx = srcEntry.context;
|
||||||
|
sCtx.drawImage(maskCanvas, smask.offsetX, smask.offsetY);
|
||||||
|
sCtx.globalCompositeOperation = "destination-atop";
|
||||||
|
sCtx.fillStyle = backdrop;
|
||||||
|
sCtx.fillRect(0, 0, width, height);
|
||||||
|
sCtx.globalCompositeOperation = "source-over";
|
||||||
|
|
||||||
|
preparedEntry = this.canvasFactory.create(width, height);
|
||||||
|
const pCtx = preparedEntry.context;
|
||||||
|
pCtx.filter =
|
||||||
|
subtype === "Alpha"
|
||||||
|
? this.filterFactory.addAlphaFilter(transferMap)
|
||||||
|
: this.filterFactory.addLuminosityFilter(transferMap);
|
||||||
|
pCtx.drawImage(srcEntry.canvas, 0, 0);
|
||||||
|
pCtx.filter = "none";
|
||||||
|
this.canvasFactory.destroy(srcEntry);
|
||||||
|
offsetX = offsetY = 0;
|
||||||
|
} else if (hasFilter) {
|
||||||
|
// Filter only, no backdrop: use a mask-sized canvas to avoid allocating
|
||||||
|
// a full width × height page canvas for what may be a small mask. The
|
||||||
|
// mask is drawn at (0, 0) and composeSMask compensates via
|
||||||
|
// smaskPreparedOffsetX/Y.
|
||||||
|
preparedEntry = this.canvasFactory.create(
|
||||||
|
maskCanvas.width,
|
||||||
|
maskCanvas.height
|
||||||
|
);
|
||||||
|
const pCtx = preparedEntry.context;
|
||||||
|
pCtx.filter =
|
||||||
|
subtype === "Alpha"
|
||||||
|
? this.filterFactory.addAlphaFilter(transferMap)
|
||||||
|
: this.filterFactory.addLuminosityFilter(transferMap);
|
||||||
|
pCtx.drawImage(maskCanvas, 0, 0);
|
||||||
|
pCtx.filter = "none";
|
||||||
|
({ offsetX, offsetY } = smask);
|
||||||
|
} else {
|
||||||
|
// Backdrop only (no filter): layer-sized canvas. destination-atop on
|
||||||
|
// the full width × height fills every transparent pixel — including those
|
||||||
|
// outside the mask canvas bounds — with the backdrop color.
|
||||||
|
preparedEntry = this.canvasFactory.create(width, height);
|
||||||
|
const pCtx = preparedEntry.context;
|
||||||
|
pCtx.drawImage(maskCanvas, smask.offsetX, smask.offsetY);
|
||||||
|
pCtx.globalCompositeOperation = "destination-atop";
|
||||||
|
pCtx.fillStyle = backdrop;
|
||||||
|
pCtx.fillRect(0, 0, width, height);
|
||||||
|
pCtx.globalCompositeOperation = "source-over";
|
||||||
|
offsetX = offsetY = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.smaskPreparedEntry = preparedEntry;
|
||||||
|
this.smaskPreparedFor = smask;
|
||||||
|
this.smaskPreparedOffsetX = offsetX;
|
||||||
|
this.smaskPreparedOffsetY = offsetY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Replaces the current drawing canvas with a temporary scratch canvas and
|
||||||
|
* suspends the main context. Drawing operations on the scratch canvas are
|
||||||
|
* composited back via `compose()`. The scratch canvas mirrors many operations
|
||||||
|
* onto the suspended canvas to keep their graphics-state stacks in sync, so
|
||||||
|
* that clipping paths and transformations remain correct when soft mask mode
|
||||||
|
* ends.
|
||||||
*/
|
*/
|
||||||
beginSMaskMode(opIdx) {
|
beginSMaskMode(opIdx) {
|
||||||
if (this.inSMaskMode) {
|
if (this.inSMaskMode) {
|
||||||
throw new Error("beginSMaskMode called while already in smask mode");
|
throw new Error("beginSMaskMode called while already in smask mode");
|
||||||
}
|
}
|
||||||
const drawnWidth = this.ctx.canvas.width;
|
const { width: drawnWidth, height: drawnHeight } = this.ctx.canvas;
|
||||||
const drawnHeight = this.ctx.canvas.height;
|
|
||||||
const scratchCanvas = this.canvasFactory.create(drawnWidth, drawnHeight);
|
const scratchCanvas = this.canvasFactory.create(drawnWidth, drawnHeight);
|
||||||
this.smaskScratchCanvas = scratchCanvas;
|
this.smaskScratchCanvas = scratchCanvas;
|
||||||
this.suspendedCtx = this.ctx;
|
this.suspendedCtx = this.ctx;
|
||||||
@ -1290,6 +1402,12 @@ class CanvasGraphics {
|
|||||||
copyCtxState(this.suspendedCtx, ctx);
|
copyCtxState(this.suspendedCtx, ctx);
|
||||||
mirrorContextOperations(ctx, this.suspendedCtx);
|
mirrorContextOperations(ctx, this.suspendedCtx);
|
||||||
|
|
||||||
|
this._ensurePreparedSMask(
|
||||||
|
this.current.activeSMask,
|
||||||
|
drawnWidth,
|
||||||
|
drawnHeight
|
||||||
|
);
|
||||||
|
|
||||||
this.setGState(opIdx, [["BM", "source-over"]]);
|
this.setGState(opIdx, [["BM", "source-over"]]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1306,6 +1424,7 @@ class CanvasGraphics {
|
|||||||
this.suspendedCtx = null;
|
this.suspendedCtx = null;
|
||||||
this.canvasFactory.destroy(this.smaskScratchCanvas);
|
this.canvasFactory.destroy(this.smaskScratchCanvas);
|
||||||
this.smaskScratchCanvas = null;
|
this.smaskScratchCanvas = null;
|
||||||
|
this._clearPreparedSMask();
|
||||||
}
|
}
|
||||||
|
|
||||||
compose(dirtyBox) {
|
compose(dirtyBox) {
|
||||||
@ -1326,10 +1445,16 @@ class CanvasGraphics {
|
|||||||
|
|
||||||
this.composeSMask(suspendedCtx, smask, this.ctx, dirtyBox);
|
this.composeSMask(suspendedCtx, smask, this.ctx, dirtyBox);
|
||||||
// Whatever was drawn has been moved to the suspended canvas, now clear it
|
// Whatever was drawn has been moved to the suspended canvas, now clear it
|
||||||
// out of the current canvas.
|
// out of the current canvas. Only the dirty box region needs clearing —
|
||||||
|
// everything outside it is already transparent.
|
||||||
this.ctx.save();
|
this.ctx.save();
|
||||||
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
this.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
|
this.ctx.clearRect(
|
||||||
|
dirtyBox[0],
|
||||||
|
dirtyBox[1],
|
||||||
|
dirtyBox[2] - dirtyBox[0],
|
||||||
|
dirtyBox[3] - dirtyBox[1]
|
||||||
|
);
|
||||||
this.ctx.restore();
|
this.ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1341,24 +1466,67 @@ class CanvasGraphics {
|
|||||||
if (layerWidth === 0 || layerHeight === 0) {
|
if (layerWidth === 0 || layerHeight === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.genericComposeSMask(
|
|
||||||
smask.context,
|
const preparedEntry = this.smaskPreparedEntry;
|
||||||
layerCtx,
|
if (preparedEntry) {
|
||||||
layerWidth,
|
// Fast path: backdrop and/or filter pre-applied. For layer-sized entries
|
||||||
layerHeight,
|
// (backdrop cases) smaskPreparedOffsetX/Y are 0 so source and destination
|
||||||
smask.subtype,
|
// coordinates are identical. For mask-sized entries (filter-only) we
|
||||||
smask.backdrop,
|
// subtract the mask's layer offset to convert the dirty-box position into
|
||||||
smask.transferMap,
|
// the smaller canvas's coordinate space.
|
||||||
layerOffsetX,
|
// Out-of-bounds source pixels are treated as transparent by the specs,
|
||||||
layerOffsetY,
|
// which is correct for a no-backdrop mask.
|
||||||
smask.offsetX,
|
const srcX = layerOffsetX - this.smaskPreparedOffsetX;
|
||||||
smask.offsetY
|
const srcY = layerOffsetY - this.smaskPreparedOffsetY;
|
||||||
);
|
layerCtx.save();
|
||||||
|
layerCtx.globalAlpha = 1;
|
||||||
|
layerCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
const clip = new Path2D();
|
||||||
|
clip.rect(layerOffsetX, layerOffsetY, layerWidth, layerHeight);
|
||||||
|
layerCtx.clip(clip);
|
||||||
|
layerCtx.globalCompositeOperation = "destination-in";
|
||||||
|
layerCtx.drawImage(
|
||||||
|
preparedEntry.canvas,
|
||||||
|
srcX,
|
||||||
|
srcY,
|
||||||
|
layerWidth,
|
||||||
|
layerHeight,
|
||||||
|
layerOffsetX,
|
||||||
|
layerOffsetY,
|
||||||
|
layerWidth,
|
||||||
|
layerHeight
|
||||||
|
);
|
||||||
|
layerCtx.restore();
|
||||||
|
} else {
|
||||||
|
this.genericComposeSMask(
|
||||||
|
smask.context,
|
||||||
|
layerCtx,
|
||||||
|
layerWidth,
|
||||||
|
layerHeight,
|
||||||
|
layerOffsetX,
|
||||||
|
layerOffsetY,
|
||||||
|
smask.offsetX,
|
||||||
|
smask.offsetY
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.save();
|
ctx.save();
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
ctx.globalCompositeOperation = smask.blendMode || "source-over";
|
ctx.globalCompositeOperation = smask.blendMode || "source-over";
|
||||||
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
ctx.drawImage(layerCtx.canvas, 0, 0);
|
// Only blit the dirty box region — the rest of the scratch canvas is
|
||||||
|
// still transparent from the clearRect in compose().
|
||||||
|
ctx.drawImage(
|
||||||
|
layerCtx.canvas,
|
||||||
|
layerOffsetX,
|
||||||
|
layerOffsetY,
|
||||||
|
layerWidth,
|
||||||
|
layerHeight,
|
||||||
|
layerOffsetX,
|
||||||
|
layerOffsetY,
|
||||||
|
layerWidth,
|
||||||
|
layerHeight
|
||||||
|
);
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1367,60 +1535,21 @@ class CanvasGraphics {
|
|||||||
layerCtx,
|
layerCtx,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
subtype,
|
|
||||||
backdrop,
|
|
||||||
transferMap,
|
|
||||||
layerOffsetX,
|
layerOffsetX,
|
||||||
layerOffsetY,
|
layerOffsetY,
|
||||||
maskOffsetX,
|
maskOffsetX,
|
||||||
maskOffsetY
|
maskOffsetY
|
||||||
) {
|
) {
|
||||||
let maskCanvas = maskCtx.canvas;
|
// This path is only reached when there is no backdrop and no filter
|
||||||
let maskX = layerOffsetX - maskOffsetX;
|
// (those cases are handled by the _prepareSMaskCanvas fast path).
|
||||||
let maskY = layerOffsetY - maskOffsetY;
|
// A simple destination-in blit of the mask onto the layer suffices.
|
||||||
|
const maskCanvas = maskCtx.canvas;
|
||||||
let maskExtensionEntry = null;
|
const maskX = layerOffsetX - maskOffsetX;
|
||||||
if (backdrop) {
|
const maskY = layerOffsetY - maskOffsetY;
|
||||||
if (
|
|
||||||
maskX < 0 ||
|
|
||||||
maskY < 0 ||
|
|
||||||
maskX + width > maskCanvas.width ||
|
|
||||||
maskY + height > maskCanvas.height
|
|
||||||
) {
|
|
||||||
maskExtensionEntry = this.canvasFactory.create(width, height);
|
|
||||||
const ctx = maskExtensionEntry.context;
|
|
||||||
ctx.drawImage(maskCanvas, -maskX, -maskY);
|
|
||||||
ctx.globalCompositeOperation = "destination-atop";
|
|
||||||
ctx.fillStyle = backdrop;
|
|
||||||
ctx.fillRect(0, 0, width, height);
|
|
||||||
ctx.globalCompositeOperation = "source-over";
|
|
||||||
|
|
||||||
maskCanvas = maskExtensionEntry.canvas;
|
|
||||||
maskX = maskY = 0;
|
|
||||||
} else {
|
|
||||||
maskCtx.save();
|
|
||||||
maskCtx.globalAlpha = 1;
|
|
||||||
maskCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
||||||
const clip = new Path2D();
|
|
||||||
clip.rect(maskX, maskY, width, height);
|
|
||||||
maskCtx.clip(clip);
|
|
||||||
maskCtx.globalCompositeOperation = "destination-atop";
|
|
||||||
maskCtx.fillStyle = backdrop;
|
|
||||||
maskCtx.fillRect(maskX, maskY, width, height);
|
|
||||||
maskCtx.restore();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layerCtx.save();
|
layerCtx.save();
|
||||||
layerCtx.globalAlpha = 1;
|
layerCtx.globalAlpha = 1;
|
||||||
layerCtx.setTransform(1, 0, 0, 1, 0, 0);
|
layerCtx.setTransform(1, 0, 0, 1, 0, 0);
|
||||||
|
|
||||||
if (subtype === "Alpha" && transferMap) {
|
|
||||||
layerCtx.filter = this.filterFactory.addAlphaFilter(transferMap);
|
|
||||||
} else if (subtype === "Luminosity") {
|
|
||||||
layerCtx.filter = this.filterFactory.addLuminosityFilter(transferMap);
|
|
||||||
}
|
|
||||||
|
|
||||||
const clip = new Path2D();
|
const clip = new Path2D();
|
||||||
clip.rect(layerOffsetX, layerOffsetY, width, height);
|
clip.rect(layerOffsetX, layerOffsetY, width, height);
|
||||||
layerCtx.clip(clip);
|
layerCtx.clip(clip);
|
||||||
@ -1437,9 +1566,6 @@ class CanvasGraphics {
|
|||||||
height
|
height
|
||||||
);
|
);
|
||||||
layerCtx.restore();
|
layerCtx.restore();
|
||||||
if (maskExtensionEntry) {
|
|
||||||
this.canvasFactory.destroy(maskExtensionEntry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
save(opIdx) {
|
save(opIdx) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user