Merge pull request #21455 from calixteman/bug1873345

Draw non-isolated blend-mode groups against their backdrop (bug 1873345)
This commit is contained in:
calixteman 2026-06-16 15:26:00 +02:00 committed by GitHub
commit fdeed2af5e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 40 additions and 21 deletions

View File

@ -5025,16 +5025,7 @@ class HighlightAnnotation extends MarkupAnnotation {
const quadPoints = (this.data.quadPoints = getQuadPoints(dict, null));
if (quadPoints) {
const resources = this.appearance?.dict.get("Resources");
if (!this.appearance || !resources?.has("ExtGState")) {
if (this.appearance) {
// Workaround for cases where there's no /ExtGState-entry directly
// available, e.g. when the appearance stream contains a /XObject of
// the /Form-type, since that causes the highlighting to completely
// obscure the PDF content below it (fixes issue13242.pdf).
warn("HighlightAnnotation - ignoring built-in appearance stream.");
}
if (!this.appearance) {
// Default color is yellow in Acrobat Reader
const fillColor = getPdfColorArray(this.color, [1, 1, 0]);
const fillAlpha = dict.get("CA");

View File

@ -523,6 +523,7 @@ class PartialEvaluator {
isolated: false,
knockout: false,
needsIsolation: false,
hasSoftMask: false,
isGray: false,
};
@ -573,6 +574,7 @@ class PartialEvaluator {
if (group) {
groupOptions.needsIsolation = newOpList.needsIsolation || !!smask;
groupOptions.hasSoftMask = newOpList.hasSoftMask || !!smask;
operatorList.addOp(OPS.beginGroup, [groupOptions]);
operatorList.addOp(OPS.paintFormXObjectBegin, args);
operatorList.addOpList(newOpList);

View File

@ -830,24 +830,29 @@ class OperatorList {
* operations require being drawn in isolation (i.e. on a separate canvas).
* A group/pattern needs isolation when it uses non-default compositing
* (blend mode) or a soft mask. The result is exposed via `needsIsolation`.
*
* `hasSoftMask` separately flags the use of a soft mask: unlike a plain blend
* mode, which a non-isolated group can apply directly against its backdrop, a
* soft mask always requires a real intermediate canvas (see bug 1873345).
*/
class CheckedOperatorList extends OperatorList {
needsIsolation = false;
hasSoftMask = false;
addOp(fn, args) {
if (!this.needsIsolation) {
if (!this.needsIsolation || !this.hasSoftMask) {
if (fn === OPS.beginGroup) {
// Propagate isolation only if the nested group itself needs it.
this.needsIsolation = args[0].needsIsolation;
this.needsIsolation ||= args[0].needsIsolation;
this.hasSoftMask ||= args[0].hasSoftMask;
} else if (fn === OPS.setGState) {
for (const [key, val] of args[0]) {
if (key === "BM" && val !== "source-over") {
this.needsIsolation = true;
break;
}
if (key === "SMask" && val !== false) {
} else if (key === "SMask" && val !== false) {
this.needsIsolation = true;
break;
this.hasSoftMask = true;
}
}
}

View File

@ -3210,12 +3210,13 @@ class CanvasGraphics {
}
const currentCtx = this.ctx;
if (!group.isolated && !group.knockout && this.#knockoutGroupLevel === 0) {
info("TODO: Fully support non-isolated non-knockout groups.");
}
if (
!group.needsIsolation &&
// A non-isolated group blends with its backdrop, so drawing it directly
// on the parent canvas (rather than on a transparent intermediate one)
// is correct even when it contains blend modes (bug 1873345). A soft
// mask still needs its own canvas though, and an isolated group requires
// a transparent backdrop, so both keep the intermediate canvas.
(!group.needsIsolation || (!group.isolated && !group.hasSoftMask)) &&
!group.knockout &&
!group.isGray &&
this.#knockoutGroupLevel === 0 &&
@ -3234,12 +3235,24 @@ class CanvasGraphics {
}
currentCtx.clip(clip);
}
// Unlike the intermediate-canvas path below, the content is drawn
// straight onto the parent canvas with no later compositing step, so the
// inherited blend mode, alpha constants and transfer function must stay
// active here rather than being reset (issue 20722); the conditions
// above already guarantee a Normal blend and an opaque (ca === 1) state.
this.groupStack.push(null); // null = no intermediate canvas
this.#groupStackMeta.push(null);
this.groupLevel++;
return;
}
// Reached only when the direct path above didn't apply, e.g. a soft mask,
// non-default group alpha or blend mode: we still composite on a
// transparent intermediate canvas rather than the real backdrop.
if (!group.isolated && !group.knockout && this.#knockoutGroupLevel === 0) {
info("TODO: Fully support non-isolated non-knockout groups.");
}
const currentTransform = getCurrentTransform(currentCtx);
if (group.matrix) {
currentCtx.transform(...group.matrix);

View File

@ -936,3 +936,4 @@
!bug1802506.pdf
!checkbox_no_appearance.pdf
!opt_demo.pdf
!bug1873345.pdf

BIN
test/pdfs/bug1873345.pdf Normal file

Binary file not shown.

View File

@ -6786,6 +6786,13 @@
"type": "eq",
"about": "Every blend mode that PDF supports."
},
{
"id": "bug1873345",
"file": "pdfs/bug1873345.pdf",
"md5": "03483b94a2c02ac5f98c17ccf14de8ea",
"rounds": 1,
"type": "eq"
},
{
"id": "transparency_group",
"file": "pdfs/transparency_group.pdf",