Copy the backdrop for non-isolated groups with a soft mask

PR #21455 drew non-isolated blend-mode groups against their backdrop via a
direct path on the parent canvas, but a soft mask forces the group onto an
intermediate canvas instead. There the inner blend modes composited against
transparency, so e.g. a /Multiply highlight was painted opaquely over the
text behind it, hiding it.

Copy the backdrop into that intermediate canvas so the blends see the real
background. The copy is limited to groups that would otherwise have taken
the direct path (source-over, alpha 1, not knockout/gray) but were displaced
onto the intermediate canvas by the soft mask.

It fixes #21593.
This commit is contained in:
calixteman 2026-07-19 19:12:56 +02:00
parent ee6d5f2102
commit 50601d0341
No known key found for this signature in database
GPG Key ID: 0C5442631EE0691F
4 changed files with 201 additions and 12 deletions

View File

@ -3333,18 +3333,53 @@ class CanvasGraphics {
groupCtx.translate(-offsetX, -offsetY); groupCtx.translate(-offsetX, -offsetY);
groupCtx.transform(...currentTransform); groupCtx.transform(...currentTransform);
if ( const needsBackdropCopy =
!group.isolated && !group.isolated && !group.smask && group.needsIsolation;
!group.smask && const replaceBackdrop =
inSMaskMode && needsBackdropCopy &&
group.needsIsolation !inSMaskMode &&
) { savedKnockoutLevel === 0 &&
// For non-isolated groups that need isolation and are entered from SMask !group.knockout &&
// mode, copy the current canvas background so that inner blend modes !group.isGray &&
// (e.g. "screen") interact correctly with the background rather than group.hasSoftMask &&
// compositing onto a transparent canvas. currentCtx.globalAlpha === 1 &&
// Groups without needsIsolation have no inner blend modes; their content currentCtx.globalCompositeOperation === "source-over" &&
// is composited correctly via the SMask in endGroup without a copy. this.current.transferMaps === "none";
if (needsBackdropCopy && (inSMaskMode || replaceBackdrop)) {
// A non-isolated group that needs isolation (because of an inner blend
// mode and/or a soft mask) can't use the direct path above, so it
// renders on a transparent intermediate canvas. Copy the current
// backdrop into it so the inner blend modes (e.g. "multiply", "screen")
// interact with the real background instead of transparency; otherwise a
// /Multiply highlight, say, would be painted opaquely over the text
// behind it, hiding it (bug 1873345 -- same as the direct path, but here
// the soft mask forces an intermediate canvas).
// This is needed both when entered from SMask mode and at the top level;
// a non-isolated subgroup nested inside a knockout group instead gets its
// backdrop through the hasInnerBackdrop path in endGroup, so it's
// excluded here via savedKnockoutLevel.
//
// Outside SMask mode the copy is limited to a group that *would* have
// qualified for the direct path (source-over, group alpha 1, not
// knockout/gray) but was forced onto the intermediate canvas by its soft
// mask (`hasSoftMask`). Restricting to that domain keeps the change
// surgical:
// - Only the soft-mask case is the bug 1873345 gap; a non-isolated
// blend group pushed onto the intermediate canvas merely by a
// non-default group alpha (no soft mask) is a separate pre-existing
// case, left untouched to avoid changing its rendering (issue 13520).
// - source-over/alpha 1 let endGroup replace the backdrop region with
// the copied-and-updated result; compositing it normally would blend
// partially transparent backdrop pixels twice. Under an outer blend
// mode the outer blend would combine the backdrop with itself (e.g.
// a /Multiply group with an inner soft mask darkens, issue 12798); a
// non-1 group alpha would likewise mix the copied backdrop back in.
// Those groups keep the transparent canvas and blend against the
// real backdrop instead.
// - isGray groups are grayscaled wholesale in endGroup, and an
// inherited transfer filter (`transferMaps`) would be applied on
// write-back, both of which would corrupt the copied backdrop, so
// they're excluded too.
groupCtx.save(); groupCtx.save();
groupCtx.setTransform(1, 0, 0, 1, 0, 0); groupCtx.setTransform(1, 0, 0, 1, 0, 0);
groupCtx.drawImage(currentCtx.canvas, -offsetX, -offsetY); groupCtx.drawImage(currentCtx.canvas, -offsetX, -offsetY);
@ -3413,6 +3448,7 @@ class CanvasGraphics {
offsetX, offsetX,
offsetY, offsetY,
hasInnerBackdrop, hasInnerBackdrop,
replaceBackdrop,
knockoutMaskEntry, knockoutMaskEntry,
// Per-group scratch pools, lazily filled and freed in endGroup. // Per-group scratch pools, lazily filled and freed in endGroup.
knockoutTempEntry: null, knockoutTempEntry: null,
@ -3566,6 +3602,14 @@ class CanvasGraphics {
}); });
} }
} else { } else {
if (groupMeta.replaceBackdrop) {
// "copy" clears the destination outside the source within the
// current clip, so limit it to the intermediate canvas bounds.
const clip = new Path2D();
clip.rect(0, 0, groupCtx.canvas.width, groupCtx.canvas.height);
this.ctx.clip(clip);
this.ctx.globalCompositeOperation = "copy";
}
this.ctx.drawImage(groupCtx.canvas, 0, 0); this.ctx.drawImage(groupCtx.canvas, 0, 0);
} }
this.ctx.restore(); this.ctx.restore();

View File

@ -946,3 +946,4 @@
!issue21579.pdf !issue21579.pdf
!saslprep-r6.pdf !saslprep-r6.pdf
!jpx_smaskindata.pdf !jpx_smaskindata.pdf
!nonisolated_blend_smask.pdf

View File

@ -0,0 +1,137 @@
%PDF-1.7
%âãÏÓ
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 320 240]
/Group << /Type /Group /S /Transparency /CS /DeviceRGB >>
/Resources <<
/Font << /F1 5 0 R >>
/ExtGState << /Half 7 0 R >>
/XObject << /FxText 6 0 R /FxAlpha 12 0 R >>
>>
/Contents 4 0 R >>
endobj
4 0 obj
<< /Length 419 >>
stream
q
/Half gs
0.10 0.35 1 rg
20 160 120 35 re f
180 160 120 35 re f
Q
BT /F1 17 Tf 0 0 0 rg 48 213 Td (The two blue boxes should match) Tj ET
BT /F1 12 Tf 0 0 0 rg 61 140 Td (control) Tj ET
BT /F1 12 Tf 0 0 0 rg 203 140 Td (inside group) Tj ET
BT /F1 12 Tf 0 0 0 rg 58 108 Td (Original issue: text should stay visible) Tj ET
1 1 1 rg
10 15 300 80 re f
BT /F1 30 Tf 0 0 0 rg 105 39 Td (Hidden) Tj ET
/FxText Do
/FxAlpha Do
endstream
endobj
5 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
6 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 320 100]
/Group << /Type /Group /S /Transparency /CS /DeviceRGB /I false >>
/Resources <<
/ExtGState << /Blend 8 0 R /Masked 9 0 R >>
/XObject << /Paint 10 0 R >>
>> /Length 35 >>
stream
/Blend gs
q /Masked gs /Paint Do Q
endstream
endobj
7 0 obj
<< /Type /ExtGState /ca 0.5 /CA 0.5 >>
endobj
8 0 obj
<< /Type /ExtGState /BM /Multiply >>
endobj
9 0 obj
<< /Type /ExtGState /ca 1 /CA 1
/SMask << /Type /Mask /S /Alpha /G 11 0 R >> >>
endobj
10 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 320 100]
/Group << /Type /Group /S /Transparency /CS /DeviceRGB /I true >>
/Resources << >> /Length 36 >>
stream
0.55 0.35 0.95 rg
30 25 260 55 re f
endstream
endobj
11 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [0 0 320 100]
/Group << /Type /Group /S /Transparency /CS /DeviceGray /I true >>
/Resources << >> /Length 22 >>
stream
1 g
30 25 260 55 re f
endstream
endobj
12 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [170 150 310 200]
/Group << /Type /Group /S /Transparency /CS /DeviceRGB /I false >>
/Resources <<
/ExtGState << /Blend 8 0 R /Masked 13 0 R >>
/XObject << /Paint 14 0 R >>
>> /Length 35 >>
stream
/Blend gs
q /Masked gs /Paint Do Q
endstream
endobj
13 0 obj
<< /Type /ExtGState /ca 1 /CA 1
/SMask << /Type /Mask /S /Alpha /G 15 0 R >> >>
endobj
14 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [170 150 310 200]
/Group << /Type /Group /S /Transparency /CS /DeviceRGB /I true >>
/Resources << >> /Length 29 >>
stream
1 0 0 rg
170 150 140 50 re f
endstream
endobj
15 0 obj
<< /Type /XObject /Subtype /Form /FormType 1 /BBox [170 150 310 200]
/Group << /Type /Group /S /Transparency /CS /DeviceGray /I true >>
/Resources << >> /Length 1 >>
stream
endstream
endobj
xref
0 16
0000000000 65535 f
0000000015 00000 n
0000000064 00000 n
0000000121 00000 n
0000000434 00000 n
0000000904 00000 n
0000000974 00000 n
0000001306 00000 n
0000001360 00000 n
0000001412 00000 n
0000001514 00000 n
0000001755 00000 n
0000001983 00000 n
0000002321 00000 n
0000002424 00000 n
0000002662 00000 n
trailer
<< /Size 16 /Root 1 0 R >>
startxref
2872
%%EOF

View File

@ -14486,5 +14486,12 @@
"md5": "f7a8628b14ca176700247f106c16812a", "md5": "f7a8628b14ca176700247f106c16812a",
"rounds": 1, "rounds": 1,
"type": "eq" "type": "eq"
},
{
"id": "nonisolated_blend_smask",
"file": "pdfs/nonisolated_blend_smask.pdf",
"md5": "4b87611416be34894eeb19c158fafbad",
"rounds": 1,
"type": "eq"
} }
] ]