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 /Normalrepro-v1-no-bm.pdf,repro-v2-no-bm.pdf— the same files with the single
/BMentry 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
/BMhandled rather than fatal — at minimum ignored when it is/Normal.- Failing that, an error naming the construct and the page, instead of an
IllegalArgumentExceptionabout an obfuscated internal class, and certainly
not “Incorrect password.” on a file with no encryption.
blend-mode-repro.zip (4.8 KB)