Skip to content

Commit d314788

Browse files
Merge pull request #21604 from calixteman/issue21593
Copy the backdrop for non-isolated groups with a soft mask
2 parents 1a1090a + 50601d0 commit d314788

4 files changed

Lines changed: 64 additions & 12 deletions

File tree

src/display/canvas.js

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3333,18 +3333,53 @@ class CanvasGraphics {
33333333
groupCtx.translate(-offsetX, -offsetY);
33343334
groupCtx.transform(...currentTransform);
33353335

3336-
if (
3337-
!group.isolated &&
3338-
!group.smask &&
3339-
inSMaskMode &&
3340-
group.needsIsolation
3341-
) {
3342-
// For non-isolated groups that need isolation and are entered from SMask
3343-
// mode, copy the current canvas background so that inner blend modes
3344-
// (e.g. "screen") interact correctly with the background rather than
3345-
// compositing onto a transparent canvas.
3346-
// Groups without needsIsolation have no inner blend modes; their content
3347-
// is composited correctly via the SMask in endGroup without a copy.
3336+
const needsBackdropCopy =
3337+
!group.isolated && !group.smask && group.needsIsolation;
3338+
const replaceBackdrop =
3339+
needsBackdropCopy &&
3340+
!inSMaskMode &&
3341+
savedKnockoutLevel === 0 &&
3342+
!group.knockout &&
3343+
!group.isGray &&
3344+
group.hasSoftMask &&
3345+
currentCtx.globalAlpha === 1 &&
3346+
currentCtx.globalCompositeOperation === "source-over" &&
3347+
this.current.transferMaps === "none";
3348+
if (needsBackdropCopy && (inSMaskMode || replaceBackdrop)) {
3349+
// A non-isolated group that needs isolation (because of an inner blend
3350+
// mode and/or a soft mask) can't use the direct path above, so it
3351+
// renders on a transparent intermediate canvas. Copy the current
3352+
// backdrop into it so the inner blend modes (e.g. "multiply", "screen")
3353+
// interact with the real background instead of transparency; otherwise a
3354+
// /Multiply highlight, say, would be painted opaquely over the text
3355+
// behind it, hiding it (bug 1873345 -- same as the direct path, but here
3356+
// the soft mask forces an intermediate canvas).
3357+
// This is needed both when entered from SMask mode and at the top level;
3358+
// a non-isolated subgroup nested inside a knockout group instead gets its
3359+
// backdrop through the hasInnerBackdrop path in endGroup, so it's
3360+
// excluded here via savedKnockoutLevel.
3361+
//
3362+
// Outside SMask mode the copy is limited to a group that *would* have
3363+
// qualified for the direct path (source-over, group alpha 1, not
3364+
// knockout/gray) but was forced onto the intermediate canvas by its soft
3365+
// mask (`hasSoftMask`). Restricting to that domain keeps the change
3366+
// surgical:
3367+
// - Only the soft-mask case is the bug 1873345 gap; a non-isolated
3368+
// blend group pushed onto the intermediate canvas merely by a
3369+
// non-default group alpha (no soft mask) is a separate pre-existing
3370+
// case, left untouched to avoid changing its rendering (issue 13520).
3371+
// - source-over/alpha 1 let endGroup replace the backdrop region with
3372+
// the copied-and-updated result; compositing it normally would blend
3373+
// partially transparent backdrop pixels twice. Under an outer blend
3374+
// mode the outer blend would combine the backdrop with itself (e.g.
3375+
// a /Multiply group with an inner soft mask darkens, issue 12798); a
3376+
// non-1 group alpha would likewise mix the copied backdrop back in.
3377+
// Those groups keep the transparent canvas and blend against the
3378+
// real backdrop instead.
3379+
// - isGray groups are grayscaled wholesale in endGroup, and an
3380+
// inherited transfer filter (`transferMaps`) would be applied on
3381+
// write-back, both of which would corrupt the copied backdrop, so
3382+
// they're excluded too.
33483383
groupCtx.save();
33493384
groupCtx.setTransform(1, 0, 0, 1, 0, 0);
33503385
groupCtx.drawImage(currentCtx.canvas, -offsetX, -offsetY);
@@ -3413,6 +3448,7 @@ class CanvasGraphics {
34133448
offsetX,
34143449
offsetY,
34153450
hasInnerBackdrop,
3451+
replaceBackdrop,
34163452
knockoutMaskEntry,
34173453
// Per-group scratch pools, lazily filled and freed in endGroup.
34183454
knockoutTempEntry: null,
@@ -3566,6 +3602,14 @@ class CanvasGraphics {
35663602
});
35673603
}
35683604
} else {
3605+
if (groupMeta.replaceBackdrop) {
3606+
// "copy" clears the destination outside the source within the
3607+
// current clip, so limit it to the intermediate canvas bounds.
3608+
const clip = new Path2D();
3609+
clip.rect(0, 0, groupCtx.canvas.width, groupCtx.canvas.height);
3610+
this.ctx.clip(clip);
3611+
this.ctx.globalCompositeOperation = "copy";
3612+
}
35693613
this.ctx.drawImage(groupCtx.canvas, 0, 0);
35703614
}
35713615
this.ctx.restore();

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -947,3 +947,4 @@
947947
!issue21579.pdf
948948
!saslprep-r6.pdf
949949
!jpx_smaskindata.pdf
950+
!nonisolated_blend_smask.pdf
3.18 KB
Binary file not shown.

test/test_manifest.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14501,5 +14501,12 @@
1450114501
"md5": "f7a8628b14ca176700247f106c16812a",
1450214502
"rounds": 1,
1450314503
"type": "eq"
14504+
},
14505+
{
14506+
"id": "nonisolated_blend_smask",
14507+
"file": "pdfs/nonisolated_blend_smask.pdf",
14508+
"md5": "4b87611416be34894eeb19c158fafbad",
14509+
"rounds": 1,
14510+
"type": "eq"
1450414511
}
1450514512
]

0 commit comments

Comments
 (0)