Repository: pdfminer/pdfminer.six (Python)

Issue:
ASCII85 broken decode?
After update from 20240706 version our team has encountered issues with some PDF files. It looks like only those with ASCII85 encoding are failing.

Error like:
```
Traceback (most recent call last):
  File "<stdin>", line 12, in ascii85decode
  File "/usr/lib/python3.12/base64.py", line 388, in a85decode
    raise ValueError(
ValueError: Ascii85 encoded byte sequences must end with b'~>'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 14, in ascii85decode
  File "/usr/lib/python3.12/base64.py", line 430, in a85decode
    raise ValueError('Non-Ascii85 digit found: %c' % x)
ValueError: Non-Ascii85 digit found: ~
```

I have took a look at changelog, and it looks like the culprit is this change: https://github.com/pdfminer/pdfminer.six/commit/9375618d150a986032739c29341b2e9293b76a1e
https://github.com/pdfminer/pdfminer.six/pull/1031

I assume that the data passed to this function is stripped from the last ">" character (which was not checked for in the previous implementation). Hence Python's function fails correctly because it does not see full "\~>" in Adobe mode, but does report invalid ASCII code "\~" in normal mode.

The tests could not catch: https://github.com/pdfminer/pdfminer.six/blob/46dcf849d0662efade5ca686fd24e9e5e0f7e9b2/tests/test_pdfminer_crypto.py#L27

This test obviously would pass in both old and new code, but only old code would pass this one (short for ">"):
```python
assert ascii85decode(b"E,9)oF*2M7/c~") == b"pleasure."
```
It looks like original author(s) of the old implementation of decoding might have known that the input is short for the last character and thus never checked for ">", but there are no comments in code on this whatsoever...

Unfortunately, I cannot check my assumption on my own as I don't normally work with PDFs and have no tools at hand to create PDF with ASCII85-encoded streams. I cannot share my samples as those contain sensitive data. I hope someone can provide with appropriate PDF for a test? To either confirm or disprove my assumption.

If this is important: we use pdfminer indirectly through pdfplumber and camelot, both failing the same way.


Discussion:
Oh dear, this would be my fault.  The issue is most likely due to incorrect stream lengths, or problems with inline images (inline images are a huge problem all the time).

It's a bit confusing to me though because the new code does explicitly cover the case where "Adobe mode" throws a ValueError: https://github.com/pdfminer/pdfminer.six/commit/9375618d150a986032739c29341b2e9293b76a1e#diff-e2ff8cc927f71f744c7e7828341e45e294def69c6da92bf12b87e655e1232460R1-R20

So I am not really sure how to trigger it.  I understand you can't supply a sample PDF, I *think* I've seen this problem with at least one file in the pdf.js testsuite though.
Can you by chance try it with [playa](https://github.com/dhdaines/playa)?  The ascii85 code is (nearly) the same there, so this will tell you if the issue is related to stream / inline image parsing.

> It's a bit confusing to me though because the new code does explicitly cover the case where "Adobe mode" throws a ValueError: [9375618#diff-e2ff8cc927f71f744c7e7828341e45e294def69c6da92bf12b87e655e1232460R1-R20](https://github.com/pdfminer/pdfminer.six/commit/9375618d150a986032739c29341b2e9293b76a1e#diff-e2ff8cc927f71f744c7e7828341e45e294def69c6da92bf12b87e655e1232460R1-R20)

Answering my own question here, I see that non-Adobe mode will still get upset if there's a trailing non-a85 digit.

Probably the best solution is to explicitly strip non-a85 digits and remove whitespace.
Should *actually* be fixed by #1112 now.
Will look into this and the MR soonish. 
Just published v20250416 that has this fixed.



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index b5eb79e3..5395c96e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
 - `ValueError` when loading xref with invalid position or generation numbers that cannot be parsed as int ([#1099](https://github.com/pdfminer/pdfminer.six/pull/1099))
 - Safely converting PDF stack objects to float or int in PDFInterpreter ([#1100](https://github.com/pdfminer/pdfminer.six/pull/1100))
 - `TypeError` when parsing font bbox with incorrect values ([#1103](https://github.com/pdfminer/pdfminer.six/pull/1103))
+- `ValueError` on incorrect stream lengths for ASCII85 data ([#1112](https://github.com/pdfminer/pdfminer.six/pull/1112))
 
 ## [20250327]
 
diff --git a/pdfminer/ascii85.py b/pdfminer/ascii85.py
index b3f01571..719cb86e 100644
--- a/pdfminer/ascii85.py
+++ b/pdfminer/ascii85.py
@@ -4,6 +4,9 @@
 from base64 import a85decode
 from binascii import unhexlify
 
+start_re = re.compile(rb"^\s*<?\s*~\s*")
+end_re = re.compile(rb"\s*~\s*>?\s*$")
+
 
 def ascii85decode(data: bytes) -> bytes:
     """In ASCII85 encoding, every four bytes are encoded with five ASCII
@@ -11,14 +14,17 @@ def ascii85decode(data: bytes) -> bytes:
     When the length of the original bytes is not a multiple of 4, a special
     rule is used for round up.
 
-    The Adobe's ASCII85 implementation is slightly different from
-    its original in handling the last characters.
-
+    Adobe's ASCII85 implementation expects the input to be terminated
+    by `b"~>"`, and (though this is absent from the PDF spec) it can
+    also begin with `b"<~"`.  We can't reliably expect this to be the
+    case, and there can be off-by-one errors in stream lengths which
+    mean we only see `~` at the end.  Worse yet, `<` and `>` are
+    ASCII85 digits, so we can't strip them.  We settle on a compromise
+    where we strip leading `<~` or `~` and trailing `~` or `~>`.
     """
-    try:
-        return a85decode(data, adobe=True)
-    except ValueError:
-        return a85decode(data)
+    data = start_re.sub(b"", data)
+    data = end_re.sub(b"", data)
+    return a85decode(data)
 
 
 bws_re = re.compile(rb"\s")


Test patch:
diff --git a/tests/test_pdfminer_crypto.py b/tests/test_pdfminer_crypto.py
index a56ece96..953679ad 100644
--- a/tests/test_pdfminer_crypto.py
+++ b/tests/test_pdfminer_crypto.py
@@ -25,6 +25,24 @@ def test_ascii85decode(self):
         """
         assert ascii85decode(b"9jqo^BlbD-BleB1DJ+*+F(f,q") == b"Man is distinguished"
         assert ascii85decode(b"E,9)oF*2M7/c~>") == b"pleasure."
+        assert ascii85decode(b"zE,9)oF*2M7/c~>") == b"\0\0\0\0pleasure."
+        # And some bogus cases you may encounter
+        assert ascii85decode(b"E,9)oF*2M7/c") == b"pleasure."
+        assert ascii85decode(b"E,9)oF*2M7/c~") == b"pleasure."
+        assert ascii85decode(b"<~E,9)oF*2M7/c~") == b"pleasure."
+        assert ascii85decode(b"<~E,9)oF*2M7/c~\n>") == b"pleasure."
+        # Ensure that we don't miss actual ASCII85 digits
+        assert (
+            ascii85decode(b"<^BVT:K:=9<E)pd;BS_1:/aSV;ag~>")
+            == b"VARIOUS UTTER NONSENSE"
+        )
+        assert (
+            ascii85decode(b"<~<^BVT:K:=9<E)pd;BS_1:/aSV;ag~>")
+            == b"VARIOUS UTTER NONSENSE"
+        )
+        assert (
+            ascii85decode(b"<^BVT:K:=9<E)pd;BS_1:/aSV;ag~") == b"VARIOUS UTTER NONSENSE"
+        )
 
     def test_asciihexdecode(self):
         assert asciihexdecode(b"61 62 2e6364   65") == b"ab.cde"