Repository: py-pdf/pypdf

Issue:
Merging fails with missing /Dests
Append a page to a `PdfWriter` might fail.

## Environment

*pypdf* 5.4.0 with Python 3.11

## Code + PDF

This is a minimal, complete example that shows the issue:

```python
from pypdf import PdfWriter

writer = PdfWriter(clone_from='file1.pdf')
writer.append('file2.pdf')
```

I still have to find/generate a corresponding PDF file which does not contain personal information.

## Traceback

This is the complete traceback I see:

```
[...]
  File "/home/user/tmp/run.py", line xxx, in xxx
    writer.append('file2.pdf')
  File "/home/user/tmp//lib/python3.11/site-packages/pypdf/_writer.py", line 2621, in append
    self.merge(
  File "/home/user/tmp//lib/python3.11/site-packages/pypdf/_writer.py", line 2753, in merge
    _process_named_dests(dest)
  File "/home/user/tmp//lib/python3.11/site-packages/pypdf/_writer.py", line 2724, in _process_named_dests
    cast(DictionaryObject, self._root_object["/Names"])["/Dests"],
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^
  File "/home/user/tmp//lib/python3.11/site-packages/pypdf/generic/_data_structures.py", line 478, in __getitem__
    return dict.__getitem__(self, key).get_object()
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: '/Dests'
```



Discussion:

Patch:
diff --git a/pypdf/_writer.py b/pypdf/_writer.py
index 6e1340bb9..3ec7af34b 100644
--- a/pypdf/_writer.py
+++ b/pypdf/_writer.py
@@ -2796,8 +2796,8 @@ def _process_named_dests(dest: Any) -> None:
                 List[Any],
                 cast(
                     DictionaryObject,
-                    cast(DictionaryObject, self._root_object["/Names"])["/Dests"],
-                )["/Names"],
+                    cast(DictionaryObject, self._root_object["/Names"]).get("/Dests", DictionaryObject()),
+                ).get("/Names", DictionaryObject()),
             ):
                 # already exists: should not duplicate it
                 pass


Test patch:
diff --git a/tests/test_writer.py b/tests/test_writer.py
index 11a6e57ea..7bb3c3741 100644
--- a/tests/test_writer.py
+++ b/tests/test_writer.py
@@ -7,6 +7,7 @@
 from pathlib import Path
 from tempfile import NamedTemporaryFile
 from typing import Any
+from unittest import mock
 
 import pytest
 
@@ -23,6 +24,7 @@
 from pypdf.generic import (
     ArrayObject,
     ContentStream,
+    Destination,
     DictionaryObject,
     Fit,
     IndirectObject,
@@ -2693,3 +2695,22 @@ def test_compress_identical_objects__after_remove_images():
     writer = PdfWriter(clone_from=RESOURCE_ROOT / "AutoCad_Diagram.pdf")
     writer.remove_images()
     writer.compress_identical_objects(remove_identicals=True, remove_orphans=True)
+
+
+def test_merge__process_named_dests__no_dests_in_source_file():
+    """Test for #3279"""
+    writer = PdfWriter(clone_from=RESOURCE_ROOT / "crazyones.pdf")
+
+    # Hacky solution to avoid attribute errors.
+    names = DictionaryObject()
+    names.indirect_reference = names
+    writer.root_object[NameObject("/Names")] = names
+
+    reader = PdfReader(RESOURCE_ROOT / "hello-world.pdf")
+    destination = Destination(title="test.pdf", page=reader.pages[0], fit=Fit("/Fit"))
+    with mock.patch.object(reader, "_get_named_destinations", return_value={"test.pdf": destination}):
+        writer.append(reader)
+        # The page now points to the appended one.
+        assert writer.named_destinations == {
+            "test.pdf": Destination(title="test.pdf", page=writer.pages[1].indirect_reference, fit=Fit("/Fit"))
+        }