diff --git a/src/core/annotation.js b/src/core/annotation.js index 78624d6bf..b2f4cb82e 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -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"); diff --git a/src/core/evaluator.js b/src/core/evaluator.js index c07284aee..be6031937 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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); diff --git a/src/core/operator_list.js b/src/core/operator_list.js index f63ac0718..5f679a3f6 100644 --- a/src/core/operator_list.js +++ b/src/core/operator_list.js @@ -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; } } } diff --git a/src/display/canvas.js b/src/display/canvas.js index e1d7b3b76..dc6eb165a 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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); diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 9a2a95fe0..9d265fbf6 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -936,3 +936,4 @@ !bug1802506.pdf !checkbox_no_appearance.pdf !opt_demo.pdf +!bug1873345.pdf diff --git a/test/pdfs/bug1873345.pdf b/test/pdfs/bug1873345.pdf new file mode 100644 index 000000000..6eb9a2cdf Binary files /dev/null and b/test/pdfs/bug1873345.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 856bec321..7d6010038 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -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",