PDF comparison fails on any file that uses a blend mode (`/BM` in an ExtGState)

PDF comparison fails on any file that uses a blend mode (/BM in an ExtGState)

Product: GroupDocs.Comparison for Java
Version: 26.8.2 (also reproduces on 26.8.1)
Platform: Java 21 (Zulu), macOS 15 and Linux (Docker, eclipse-temurin:21)
License: reproduces under both a valid metered license and evaluation mode

Summary

Comparer.compare throws before producing any output when either input PDF has a
content stream that invokes (gs) an ExtGState containing a /BM blend mode
entry — including /BM /Normal, which is the PDF default and a no-op:

com.groupdocs.comparison.common.ComparisonException:
  java.lang.IllegalArgumentException:
  com.groupdocs.comparison.internal.c.a.pd.D is not an enum class

This is a parsing failure rather than a diffing one: comparing a file against
itself
fails identically.

This is not an exotic construct. Chrome’s Skia PDF backend writes /BM /Normal
into every page it produces, so every PDF exported from Google Docs fails to
compare
. We hit this on real customer documents in production and could only
find the cause by bisecting the page content stream operator by operator, because
the exception names an obfuscated internal class and nothing else.

Steps to reproduce

Attached are two single-page PDFs of 896 bytes each. Each contains one line of
Helvetica text and one ExtGState. They differ by one word (“fox” vs “cat”).

  • repro-v1.pdf, repro-v2.pdf — with /BM /Normal
  • repro-v1-no-bm.pdf, repro-v2-no-bm.pdf — the same files with the single
    /BM entry removed and nothing else changed
import com.groupdocs.comparison.Comparer;
import com.groupdocs.comparison.options.CompareOptions;

public class Repro {
  public static void main(String[] args) {
    compare("repro-v1.pdf", "repro-v2.pdf", "out-with-bm.pdf");
    compare("repro-v1-no-bm.pdf", "repro-v2-no-bm.pdf", "out-without-bm.pdf");
  }

  static void compare(String source, String target, String out) {
    try (Comparer comparer = new Comparer(source)) {
      comparer.add(target);
      comparer.compare(out, new CompareOptions());
      System.out.println(source + " -> OK");
    } catch (Throwable t) {
      System.out.println(source + " -> FAILED: " + t);
    }
  }
}

Expected: both comparisons succeed.

Actual:

repro-v1.pdf       -> FAILED: com.groupdocs.comparison.common.ComparisonException:
                      java.lang.IllegalArgumentException:
                      com.groupdocs.comparison.internal.c.a.pd.D is not an enum class
repro-v1-no-bm.pdf -> OK

The complete ExtGState in the failing files is:

/G3 << /BM /Normal  /ca 1 >>

MakeRepro.java is also attached: it rebuilds the failing pair from scratch using
only PDFBox 3.x, so you can confirm that the blend mode is the only unusual thing
in them.

What we isolated

Single-page PDFs varying one thing at a time, each compared against itself:

ExtGState invoked via gs Result
(no gs at all) OK
<< >> OK
<< /ca 1 >> OK
<< /ca 1.0 >> OK
<< /SA true >> OK
<< /LC 0 /LJ 0 /ML 10 >> OK
<< /BM /Normal >> FAIL
<< /BM /Multiply >> FAIL
<< /BM [/Normal] >> FAIL

So the trigger is the presence of /BM regardless of its value, and regardless of
whether it is written as a name or in the array form the spec also permits.
Declaring the ExtGState in the page resources is harmless — it only fails once
the content stream actually invokes it.

A second, misleading symptom

On our 26-page documents the same blend mode entry surfaced instead as:

com.groupdocs.comparison.common.ComparisonException: Incorrect password.

on files that are not encrypted at all — PDFBox reports isEncrypted() == false
and every permission granted. Removing the /BM entries makes that message go
away too. We spent a fair amount of time investigating encryption because of it.

Stripping /ExtGState from the page resources while leaving the gs operator in
place produces yet a third message, an NPE in
...internal.l6n.lh.lf(String) — so there appear to be at least three code paths
reporting the same underlying condition, none of them accurately.

Workaround we have shipped

Pre-process every PDF with PDFBox, deleting /BM from each ExtGState reachable
from any page (recursing into form XObject resources), then compare the rewritten
files. On our real 26-page documents this removes two entries per file and turns a
hard failure into a correct side-by-side redline.

What we would like

  1. /BM handled rather than fatal — at minimum ignored when it is /Normal.
  2. Failing that, an error naming the construct and the page, instead of an
    IllegalArgumentException about an obfuscated internal class, and certainly
    not “Incorrect password.” on a file with no encryption.

blend-mode-repro.zip (4.8 KB)

Hi,

Thank you for the exceptionally clear report — the minimal reproducer, the /BM isolation and the self-comparison case saved us a lot of time, and each of your observations turned out to be correct. We reproduced the failure on our side and have identified and fixed the root cause.

What is actually wrong

The problem is not in blend mode handling, and not in Aspose.PDF. It is a defect in our own packaging.

Our release build runs an obfuscation step over the final jar. That step was removing the synthetic values() method from internal enum classes. The JDK’s Enum.valueOf() resolves values() reflectively (through Class.enumConstantDirectory), so once that method is gone, any attempt to resolve such an enum by name fails with

java.lang.IllegalArgumentException: is not an enum type

com.aspose.pdf.BlendMode is parsed by name whenever a content stream invokes an ExtGState that carries /BM. That is why the value itself is irrelevant — even /BM /Normal triggers it — and why comparing a document against itself fails just as reliably.

So this is a broader issue than blend modes: it affected every internal enum that gets resolved from a string. /BM is simply the path most real-world PDFs hit first.

Why you saw “Incorrect password.”

That is a second, independent defect, and we are sorry it sent you investigating encryption. The code that opens a PDF had a catch-all handler that reported every failure during loading as a password error, regardless of the actual cause. On your 26-page documents the enum failure happened during loading and was masked by that handler; on the smaller reproducer it surfaced later, which is why you saw the real message there. Your files are not encrypted and nothing was wrong with them.

Both defects are now fixed:

  • enum metadata is preserved through obfuscation, so Enum.valueOf() works for internal enums again;
  • the PDF loading path no longer relabels unrelated failures as password errors, and it now preserves the original exception as the cause.

We verified the first fix end to end by rebuilding with and without the change on identical input: with the fix, both of your reproducer pairs and both self-comparison cases pass, with no change to the public API.

The fixes are scheduled for GroupDocs.Comparison for Java 26.9 (release will be in the next 1-3 days). We will post here as soon as it is published — we would appreciate it if you could confirm against your real documents at that point.

Your /BM-stripping workaround is a valid stopgap until then, though as you noted it is not something you should have to do, and it will no longer be necessary.

Thanks again for the quality of the report.