diff --git a/src/display/canvas.js b/src/display/canvas.js index 14cb734d5..d9ab2025e 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -3333,18 +3333,53 @@ class CanvasGraphics { groupCtx.translate(-offsetX, -offsetY); groupCtx.transform(...currentTransform); - if ( - !group.isolated && - !group.smask && - inSMaskMode && - group.needsIsolation - ) { - // For non-isolated groups that need isolation and are entered from SMask - // mode, copy the current canvas background so that inner blend modes - // (e.g. "screen") interact correctly with the background rather than - // compositing onto a transparent canvas. - // Groups without needsIsolation have no inner blend modes; their content - // is composited correctly via the SMask in endGroup without a copy. + const needsBackdropCopy = + !group.isolated && !group.smask && group.needsIsolation; + const replaceBackdrop = + needsBackdropCopy && + !inSMaskMode && + savedKnockoutLevel === 0 && + !group.knockout && + !group.isGray && + group.hasSoftMask && + currentCtx.globalAlpha === 1 && + currentCtx.globalCompositeOperation === "source-over" && + 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.setTransform(1, 0, 0, 1, 0, 0); groupCtx.drawImage(currentCtx.canvas, -offsetX, -offsetY); @@ -3413,6 +3448,7 @@ class CanvasGraphics { offsetX, offsetY, hasInnerBackdrop, + replaceBackdrop, knockoutMaskEntry, // Per-group scratch pools, lazily filled and freed in endGroup. knockoutTempEntry: null, @@ -3566,6 +3602,14 @@ class CanvasGraphics { }); } } 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.restore(); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index e25516a81..a2873d619 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -946,3 +946,4 @@ !issue21579.pdf !saslprep-r6.pdf !jpx_smaskindata.pdf +!nonisolated_blend_smask.pdf diff --git a/test/pdfs/nonisolated_blend_smask.pdf b/test/pdfs/nonisolated_blend_smask.pdf new file mode 100644 index 000000000..560f9e561 --- /dev/null +++ b/test/pdfs/nonisolated_blend_smask.pdf @@ -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 diff --git a/test/test_manifest.json b/test/test_manifest.json index c60a97fdf..72fa8944e 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -14486,5 +14486,12 @@ "md5": "f7a8628b14ca176700247f106c16812a", "rounds": 1, "type": "eq" + }, + { + "id": "nonisolated_blend_smask", + "file": "pdfs/nonisolated_blend_smask.pdf", + "md5": "4b87611416be34894eeb19c158fafbad", + "rounds": 1, + "type": "eq" } ]