Source code for pdfnaut.common.copying

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from pdfnaut.cos.helpers import ensure
from pdfnaut.cos.parser import FreeObject

from ..cos.objects.base import PdfHexString, PdfName, PdfNull, PdfObject, PdfReference
from ..cos.objects.containers import PdfArray, PdfDictionary
from ..cos.objects.stream import PdfStream

if TYPE_CHECKING:
    from pdfnaut.cos.parser import PdfParser


class _PlaceholderType:
    def __repr__(self) -> str:
        return "PLACEHOLDER"


LOGGER = logging.getLogger(__name__)
PLACEHOLDER = _PlaceholderType()


def _is_page_or_page_tree(obj: PdfObject) -> bool:
    """Reports whether an object ``obj`` is a page object or a page tree node."""

    if not isinstance(obj, PdfDictionary) or "Type" not in obj:
        return False

    if not isinstance(tp := obj["Type"], PdfName) or tp.value not in [b"Page", b"Pages"]:
        return False

    return True


[docs] def copy_object(obj: PdfObject) -> PdfObject: """Performs a deep copy of a PDF object ``obj``. Returns the copied object. Deep copying works by creating a new object for the container then adding a copy of each element it contains into the new object. Numbers, literal strings, booleans, and the null object are not copied and are returned as is. Unlike :meth:`.clone_in_document`, when a reference is found, it is simply copied into the object without modifying the referred object. """ if isinstance(obj, PdfDictionary): kv = PdfDictionary() for key, value in obj.data.items(): kv.data[key] = copy_object(value) return kv elif isinstance(obj, PdfStream): return PdfStream( ensure(copy_object(obj.details), PdfDictionary), obj.raw, ensure(copy_object(obj._crypt_params), PdfDictionary), ) elif isinstance(obj, PdfArray): arr = PdfArray() for value in obj.data: arr.data.append(copy_object(value)) return arr elif isinstance(obj, PdfName): return PdfName(obj.value) elif isinstance(obj, PdfHexString): return PdfHexString(obj.raw) elif isinstance(obj, PdfReference): return PdfReference(obj.object_number, obj.generation) return obj
[docs] def clone_into_document( dest: PdfParser, root: PdfObject, *, ignore_keys: list[str] | None = None ) -> PdfObject: """Clones an object ``root`` and its contents into document ``dest``. Returns the cloned object. If the root object is a dictionary and the ``ignore_keys`` argument is provided, those keys will be ignored when cloning the root object. Cloning of an object is performed by deep-copying each element contained in it. When a reference is found, it is determined whether it is suitable for cloning into the document. A reference is determined suitable for cloning if it does not refer back to the ``root`` object. If it is unsuitable, a placeholder is added if the reference is ``root`` itself. If the reference may point back to the object (such as the reference being for a page tree), it is nulled. If the reference is suitable, its contents are added into the document and the new reference replaces the old reference in the object. """ if ignore_keys is None: ignore_keys = [] cloned_map = {} references = set() def inner(obj: PdfObject) -> PdfObject | _PlaceholderType: if obj in cloned_map: # object is already cloned return cloned_map[obj] if isinstance(obj, PdfReference): referred = obj.get() if referred is root: # object refers to our origin object. in which case, simply set # a placeholder for later processing return PLACEHOLDER if _is_page_or_page_tree(referred): # avoid going to pages or anything that might lead us back to the page tree LOGGER.warning("object %s cannot be reliably copied and has been set to null.", obj) return PdfNull() cloned_direct = inner(referred) assert not isinstance(cloned_direct, _PlaceholderType) cloned_map[obj] = dest.objects.add(cloned_direct) references.add(cloned_map[obj]) return cloned_map[obj] elif isinstance(obj, PdfDictionary): kv = PdfDictionary() cloned_map[obj] = kv for key, value in obj.data.items(): if obj is root and key in ignore_keys: continue cloned_direct = inner(value) assert not isinstance(cloned_direct, _PlaceholderType) kv.data[key] = cloned_direct return kv elif isinstance(obj, PdfStream): stm = PdfStream( ensure(inner(obj.details), PdfDictionary), obj.raw, ensure(inner(obj._crypt_params), PdfDictionary), ) cloned_map[obj] = stm return stm elif isinstance(obj, PdfArray): arr = PdfArray() cloned_map[obj] = arr for value in obj.data: cloned_direct = inner(value) assert not isinstance(cloned_direct, _PlaceholderType) arr.data.append(cloned_direct) return arr elif isinstance(obj, PdfName): return PdfName(obj.value) elif isinstance(obj, PdfHexString): return PdfHexString(obj.raw) return obj cloned = inner(root) assert not isinstance(cloned, _PlaceholderType) def replace_placeholders(obj: PdfObject | _PlaceholderType) -> PdfObject: if isinstance(obj, _PlaceholderType): return dest.objects.add(cloned) elif isinstance(obj, PdfArray): return PdfArray(replace_placeholders(it) for it in obj.data) elif isinstance(obj, PdfDictionary): return PdfDictionary({key: replace_placeholders(val) for key, val in obj.data.items()}) elif isinstance(obj, PdfStream): obj.details = ensure(replace_placeholders(obj.details), PdfDictionary) obj._crypt_params = ensure(replace_placeholders(obj._crypt_params), PdfDictionary) return obj return obj final = replace_placeholders(cloned) for ref in references: direct = dest.objects[ref.object_number] assert not isinstance(direct, FreeObject) dest.objects[ref.object_number] = replace_placeholders(direct) return final