@@ -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 ( ) ;
0 commit comments