Repository: py-pdf/pypdf

Issue:
Null encoding causes parse failure.


## Environment
```
$ python -m platform
macOS-15.3.1-arm64-arm-64bit

$ python -c "import pypdf;print(pypdf._debug_versions)"
pypdf==5.5.0, crypt_provider=('cryptography', '44.0.0'), PIL=none
```

Also recreated on Ubunto 22.04 & Jupyter notebook.

## Code + PDF

```python
pdf = PdfReader(bytes_stream)
pages = pdf.get_num_pages()

pages_per_worker = math.ceil(pages / workers)
start_page = pages_per_worker * worker
end_page = min(pages_per_worker * (worker + 1), pages)

return [
    pdf.get_page(i)
    .extract_text(extraction_mode="plain")
    .encode(encoding="utf-8", errors="replace")
    .decode("utf-8")
    for i in range(start_page, end_page)
]
```

Sorry, the PDF contains proprietary information and can't be shared. The file was analyzed using pdf-online.com:
```
Compliance | pdfa-3u
-- | --
Result | Document validated successfully.
Details | Validating file "problematic.pdf" for conformance level pdfa-3uThe document does conform to the PDF/A-3u standard.Done.
```


## Traceback

```
  File "/Users/shachar/venv/lib/python3.12/site-packages/pypdf/_page.py", line 2378, in extract_text
    return self._extract_text(
           ^^^^^^^^^^^^^^^^^^^
  File "/Users/shachar/venv/lib/python3.12/site-packages/pypdf/_page.py", line 1859, in _extract_text
    cmaps[f] = build_char_map(f, space_width, obj)
               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/shachar/venv/lib/python3.12/site-packages/pypdf/_cmap.py", line 34, in build_char_map
    font_subtype, font_halfspace, font_encoding, font_map = build_char_map_from_dict(
                                                            ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/Users/shachar/venv/lib/python3.12/site-packages/pypdf/_cmap.py", line 57, in build_char_map_from_dict
    encoding, map_dict = get_encoding(ft)
                         ^^^^^^^^^^^^^^^^
  File "/Users/shachar/venv/lib/python3.12/site-packages/pypdf/_cmap.py", line 129, in get_encoding
    encoding = _parse_encoding(ft)
               ^^^^^^^^^^^^^^^^^^^
  File "/Users/shachar/venv/lib/python3.12/site-packages/pypdf/_cmap.py", line 183, in _parse_encoding
    if "/Differences" in enc:
       ^^^^^^^^^^^^^^^^^^^^^
TypeError: argument of type 'NullObject' is not iterable
```



Discussion:
I have prepared a corresponding PR. Please note that future reports without an example file might be closed without a fix. Random online tools are not necessarily an indicator about the correctness of any file - apart from well-known tools/libraries.
Thanks @stefan6419846 ! Is there some tool/library that I can use to provide you with enough context without providing the files?
There is no generic answer to this except: In the best case, try to provide the original, unmodified files which cause these issues. I have used different tooling in the past (messing with the internal structure directly; running `anonymize.py` and `replace-image` from the PyMuPDF examples; run `PdfWriter.remove_text()` on the file), but some of them require further knowledge and/or might remove the issue by fixing it or removing the corresponding features.



Patch:
diff --git a/pypdf/_cmap.py b/pypdf/_cmap.py
index 266f350ab..71ff9ff35 100644
--- a/pypdf/_cmap.py
+++ b/pypdf/_cmap.py
@@ -9,6 +9,7 @@
     ArrayObject,
     DecodedStreamObject,
     DictionaryObject,
+    NullObject,
     StreamObject,
     is_null_or_none,
 )
@@ -133,7 +134,7 @@ def get_encoding(
     # Apply rule from PDF ref 1.7 §5.9.1, 1st bullet:
     #   if cmap not empty encoding should be discarded
     #   (here transformed into identity for those characters)
-    # If encoding is a string it is expected to be an identity translation.
+    # If encoding is a string, it is expected to be an identity translation.
     if isinstance(encoding, dict):
         for x in int_entry:
             if x <= 255:
@@ -154,7 +155,9 @@ def _parse_encoding(
         else:
             encoding = "charmap"
         return encoding
-    enc: Union(str, DictionaryObject) = ft["/Encoding"].get_object()  # type: ignore
+    enc: Union[str, DictionaryObject, NullObject] = cast(
+        Union[str, DictionaryObject, NullObject], ft["/Encoding"].get_object()
+    )
     if isinstance(enc, str):
         try:
             # already done : enc = NameObject.unnumber(enc.encode()).decode()
@@ -181,13 +184,13 @@ def _parse_encoding(
             encoding = charset_encoding["/StandardEncoding"].copy()
     else:
         encoding = charset_encoding["/StandardEncoding"].copy()
-    if "/Differences" in enc:
+    if isinstance(enc, DictionaryObject) and "/Differences" in enc:
         x: int = 0
         o: Union[int, str]
-        for o in cast(DictionaryObject, cast(DictionaryObject, enc)["/Differences"]):
+        for o in cast(DictionaryObject, enc["/Differences"]):
             if isinstance(o, int):
                 x = o
-            else:  # isinstance(o,str):
+            else:  # isinstance(o, str):
                 try:
                     if x < len(encoding):
                         encoding[x] = adobe_glyphs[o]  # type: ignore


Test patch:
diff --git a/tests/test_cmap.py b/tests/test_cmap.py
index 2d9e4bbea..bcf032e8a 100644
--- a/tests/test_cmap.py
+++ b/tests/test_cmap.py
@@ -5,8 +5,9 @@
 import pytest
 
 from pypdf import PdfReader, PdfWriter
-from pypdf._cmap import build_char_map
-from pypdf.generic import ArrayObject, IndirectObject, NameObject, NullObject
+from pypdf._cmap import build_char_map, get_encoding
+from pypdf._codecs import charset_encoding
+from pypdf.generic import ArrayObject, DictionaryObject, IndirectObject, NameObject, NullObject
 
 from . import get_data_from_url
 
@@ -317,3 +318,12 @@ def test_function_in_font_widths(caplog):
     page = reader.pages[455]
     assert "La vulnérabilité correspond aux conséquences potentielles" in page.extract_text()
     assert "Expected numeric value for width, got {'/Bounds': [0.25, 0.25]," in caplog.text
+
+
+def test_get_encoding__encoding_value_is_none():
+    ft = DictionaryObject()
+    ft[NameObject("/Encoding")] = NullObject()
+    assert get_encoding(ft) == (
+        dict(zip(range(256), charset_encoding["/StandardEncoding"])),
+        {}
+    )