about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/PyPDF2/_writer.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/PyPDF2/_writer.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-4a52a71956a8d46fcb7294ac71734504bb09bcc2.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/PyPDF2/_writer.py')
-rw-r--r--.venv/lib/python3.12/site-packages/PyPDF2/_writer.py2822
1 files changed, 2822 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/PyPDF2/_writer.py b/.venv/lib/python3.12/site-packages/PyPDF2/_writer.py
new file mode 100644
index 00000000..b2e92cdb
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/PyPDF2/_writer.py
@@ -0,0 +1,2822 @@
+# Copyright (c) 2006, Mathieu Fenniak
+# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>
+#
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+# * The name of the author may not be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+import codecs
+import collections
+import decimal
+import logging
+import random
+import re
+import struct
+import time
+import uuid
+import warnings
+from hashlib import md5
+from io import BytesIO, FileIO, IOBase
+from pathlib import Path
+from types import TracebackType
+from typing import (
+    IO,
+    Any,
+    Callable,
+    Deque,
+    Dict,
+    Iterable,
+    List,
+    Optional,
+    Pattern,
+    Tuple,
+    Type,
+    Union,
+    cast,
+)
+
+from ._encryption import Encryption
+from ._page import PageObject, _VirtualList
+from ._reader import PdfReader
+from ._security import _alg33, _alg34, _alg35
+from ._utils import (
+    StrByteType,
+    StreamType,
+    _get_max_pdf_version_header,
+    b_,
+    deprecate_with_replacement,
+    deprecation_bookmark,
+    deprecation_with_replacement,
+    logger_warning,
+)
+from .constants import AnnotationDictionaryAttributes
+from .constants import CatalogAttributes as CA
+from .constants import CatalogDictionary
+from .constants import Core as CO
+from .constants import EncryptionDictAttributes as ED
+from .constants import (
+    FieldDictionaryAttributes,
+    FieldFlag,
+    FileSpecificationDictionaryEntries,
+    GoToActionArguments,
+    InteractiveFormDictEntries,
+)
+from .constants import PageAttributes as PG
+from .constants import PagesAttributes as PA
+from .constants import StreamAttributes as SA
+from .constants import TrailerKeys as TK
+from .constants import TypFitArguments, UserAccessPermissions
+from .generic import (
+    PAGE_FIT,
+    AnnotationBuilder,
+    ArrayObject,
+    BooleanObject,
+    ByteStringObject,
+    ContentStream,
+    DecodedStreamObject,
+    Destination,
+    DictionaryObject,
+    Fit,
+    FloatObject,
+    IndirectObject,
+    NameObject,
+    NullObject,
+    NumberObject,
+    PdfObject,
+    RectangleObject,
+    StreamObject,
+    TextStringObject,
+    TreeObject,
+    create_string_object,
+    hex_to_rgb,
+)
+from .pagerange import PageRange, PageRangeSpec
+from .types import (
+    BorderArrayType,
+    FitType,
+    LayoutType,
+    OutlineItemType,
+    OutlineType,
+    PagemodeType,
+    ZoomArgType,
+)
+
+logger = logging.getLogger(__name__)
+
+
+OPTIONAL_READ_WRITE_FIELD = FieldFlag(0)
+ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions((2**31 - 1) - 3)
+
+
+class PdfWriter:
+    """
+    This class supports writing PDF files out, given pages produced by another
+    class (typically :class:`PdfReader<PyPDF2.PdfReader>`).
+    """
+
+    def __init__(self, fileobj: StrByteType = "") -> None:
+        self._header = b"%PDF-1.3"
+        self._objects: List[PdfObject] = []  # array of indirect objects
+        self._idnum_hash: Dict[bytes, IndirectObject] = {}
+        self._id_translated: Dict[int, Dict[int, int]] = {}
+
+        # The root of our page tree node.
+        pages = DictionaryObject()
+        pages.update(
+            {
+                NameObject(PA.TYPE): NameObject("/Pages"),
+                NameObject(PA.COUNT): NumberObject(0),
+                NameObject(PA.KIDS): ArrayObject(),
+            }
+        )
+        self._pages = self._add_object(pages)
+
+        # info object
+        info = DictionaryObject()
+        info.update(
+            {
+                NameObject("/Producer"): create_string_object(
+                    codecs.BOM_UTF16_BE + "PyPDF2".encode("utf-16be")
+                )
+            }
+        )
+        self._info = self._add_object(info)
+
+        # root object
+        self._root_object = DictionaryObject()
+        self._root_object.update(
+            {
+                NameObject(PA.TYPE): NameObject(CO.CATALOG),
+                NameObject(CO.PAGES): self._pages,
+            }
+        )
+        self._root = self._add_object(self._root_object)
+        self.fileobj = fileobj
+        self.with_as_usage = False
+
+    def __enter__(self) -> "PdfWriter":
+        """Store that writer is initialized by 'with'."""
+        self.with_as_usage = True
+        return self
+
+    def __exit__(
+        self,
+        exc_type: Optional[Type[BaseException]],
+        exc: Optional[BaseException],
+        traceback: Optional[TracebackType],
+    ) -> None:
+        """Write data to the fileobj."""
+        if self.fileobj:
+            self.write(self.fileobj)
+
+    @property
+    def pdf_header(self) -> bytes:
+        """
+        Header of the PDF document that is written.
+
+        This should be something like b'%PDF-1.5'. It is recommended to set the
+        lowest version that supports all features which are used within the
+        PDF file.
+        """
+        return self._header
+
+    @pdf_header.setter
+    def pdf_header(self, new_header: bytes) -> None:
+        self._header = new_header
+
+    def _add_object(self, obj: PdfObject) -> IndirectObject:
+        if hasattr(obj, "indirect_reference") and obj.indirect_reference.pdf == self:  # type: ignore
+            return obj.indirect_reference  # type: ignore
+        self._objects.append(obj)
+        obj.indirect_reference = IndirectObject(len(self._objects), 0, self)
+        return obj.indirect_reference
+
+    def get_object(
+        self,
+        indirect_reference: Union[None, int, IndirectObject] = None,
+        ido: Optional[IndirectObject] = None,
+    ) -> PdfObject:
+        if ido is not None:  # deprecated
+            if indirect_reference is not None:
+                raise ValueError(
+                    "Please only set 'indirect_reference'. The 'ido' argument is deprecated."
+                )
+            else:
+                indirect_reference = ido
+                warnings.warn(
+                    "The parameter 'ido' is depreciated and will be removed in PyPDF2 4.0.0.",
+                    DeprecationWarning,
+                )
+        assert (
+            indirect_reference is not None
+        )  # the None value is only there to keep the deprecated name
+        if isinstance(indirect_reference, int):
+            return self._objects[indirect_reference - 1]
+        if indirect_reference.pdf != self:
+            raise ValueError("pdf must be self")
+        return self._objects[indirect_reference.idnum - 1]  # type: ignore
+
+    def getObject(
+        self, ido: Union[int, IndirectObject]
+    ) -> PdfObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`get_object` instead.
+        """
+        deprecation_with_replacement("getObject", "get_object", "3.0.0")
+        return self.get_object(ido)
+
+    def _add_page(
+        self,
+        page: PageObject,
+        action: Callable[[Any, IndirectObject], None],
+        excluded_keys: Iterable[str] = (),
+    ) -> PageObject:
+        assert cast(str, page[PA.TYPE]) == CO.PAGE
+        page_org = page
+        excluded_keys = list(excluded_keys)
+        excluded_keys += [PA.PARENT, "/StructParents"]
+        # acrobat does not accept to have two indirect ref pointing on the same page;
+        # therefore in order to add easily multiple copies of the same page, we need to create a new
+        # dictionary for the page, however the objects below (including content) is not duplicated
+        try:  # delete an already existing page
+            del self._id_translated[id(page_org.indirect_reference.pdf)][  # type: ignore
+                page_org.indirect_reference.idnum  # type: ignore
+            ]
+        except Exception:
+            pass
+        page = cast("PageObject", page_org.clone(self, False, excluded_keys))
+        # page_ind = self._add_object(page)
+        if page_org.pdf is not None:
+            other = page_org.pdf.pdf_header
+            if isinstance(other, str):
+                other = other.encode()  # type: ignore
+            self.pdf_header = _get_max_pdf_version_header(self.pdf_header, other)  # type: ignore
+        page[NameObject(PA.PARENT)] = self._pages
+        pages = cast(DictionaryObject, self.get_object(self._pages))
+        assert page.indirect_reference is not None
+        action(pages[PA.KIDS], page.indirect_reference)
+        page_count = cast(int, pages[PA.COUNT])
+        pages[NameObject(PA.COUNT)] = NumberObject(page_count + 1)
+        return page
+
+    def set_need_appearances_writer(self) -> None:
+        # See 12.7.2 and 7.7.2 for more information:
+        # http://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
+        try:
+            catalog = self._root_object
+            # get the AcroForm tree
+            if CatalogDictionary.ACRO_FORM not in catalog:
+                self._root_object.update(
+                    {
+                        NameObject(CatalogDictionary.ACRO_FORM): IndirectObject(
+                            len(self._objects), 0, self
+                        )
+                    }
+                )
+
+            need_appearances = NameObject(InteractiveFormDictEntries.NeedAppearances)
+            self._root_object[CatalogDictionary.ACRO_FORM][need_appearances] = BooleanObject(True)  # type: ignore
+        except Exception as exc:
+            logger.error("set_need_appearances_writer() catch : ", repr(exc))
+
+    def add_page(
+        self,
+        page: PageObject,
+        excluded_keys: Iterable[str] = (),
+    ) -> PageObject:
+        """
+        Add a page to this PDF file.
+        Recommended for advanced usage including the adequate excluded_keys
+
+        The page is usually acquired from a :class:`PdfReader<PyPDF2.PdfReader>`
+        instance.
+
+        :param PageObject page: The page to add to the document. Should be
+            an instance of :class:`PageObject<PyPDF2._page.PageObject>`
+        """
+        return self._add_page(page, list.append, excluded_keys)
+
+    def addPage(
+        self,
+        page: PageObject,
+        excluded_keys: Iterable[str] = (),
+    ) -> PageObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_page` instead.
+        """
+        deprecation_with_replacement("addPage", "add_page", "3.0.0")
+        return self.add_page(page, excluded_keys)
+
+    def insert_page(
+        self,
+        page: PageObject,
+        index: int = 0,
+        excluded_keys: Iterable[str] = (),
+    ) -> PageObject:
+        """
+        Insert a page in this PDF file. The page is usually acquired from a
+        :class:`PdfReader<PyPDF2.PdfReader>` instance.
+
+        :param PageObject page: The page to add to the document.
+        :param int index: Position at which the page will be inserted.
+        """
+        return self._add_page(page, lambda l, p: l.insert(index, p))
+
+    def insertPage(
+        self,
+        page: PageObject,
+        index: int = 0,
+        excluded_keys: Iterable[str] = (),
+    ) -> PageObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`insert_page` instead.
+        """
+        deprecation_with_replacement("insertPage", "insert_page", "3.0.0")
+        return self.insert_page(page, index, excluded_keys)
+
+    def get_page(
+        self, page_number: Optional[int] = None, pageNumber: Optional[int] = None
+    ) -> PageObject:
+        """
+        Retrieve a page by number from this PDF file.
+
+        :param int page_number: The page number to retrieve
+            (pages begin at zero)
+        :return: the page at the index given by *page_number*
+        """
+        if pageNumber is not None:  # pragma: no cover
+            if page_number is not None:
+                raise ValueError("Please only use the page_number parameter")
+            deprecate_with_replacement(
+                "get_page(pageNumber)", "get_page(page_number)", "4.0.0"
+            )
+            page_number = pageNumber
+        if page_number is None and pageNumber is None:  # pragma: no cover
+            raise ValueError("Please specify the page_number")
+        pages = cast(Dict[str, Any], self.get_object(self._pages))
+        # TODO: crude hack
+        return cast(PageObject, pages[PA.KIDS][page_number].get_object())
+
+    def getPage(self, pageNumber: int) -> PageObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :code:`writer.pages[page_number]` instead.
+        """
+        deprecation_with_replacement("getPage", "writer.pages[page_number]", "3.0.0")
+        return self.get_page(pageNumber)
+
+    def _get_num_pages(self) -> int:
+        pages = cast(Dict[str, Any], self.get_object(self._pages))
+        return int(pages[NameObject("/Count")])
+
+    def getNumPages(self) -> int:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :code:`len(writer.pages)` instead.
+        """
+        deprecation_with_replacement("getNumPages", "len(writer.pages)", "3.0.0")
+        return self._get_num_pages()
+
+    @property
+    def pages(self) -> List[PageObject]:
+        """Property that emulates a list of :class:`PageObject<PyPDF2._page.PageObject>`."""
+        return _VirtualList(self._get_num_pages, self.get_page)  # type: ignore
+
+    def add_blank_page(
+        self, width: Optional[float] = None, height: Optional[float] = None
+    ) -> PageObject:
+        """
+        Append a blank page to this PDF file and returns it. If no page size
+        is specified, use the size of the last page.
+
+        :param float width: The width of the new page expressed in default user
+            space units.
+        :param float height: The height of the new page expressed in default
+            user space units.
+        :return: the newly appended page
+        :raises PageSizeNotDefinedError: if width and height are not defined
+            and previous page does not exist.
+        """
+        page = PageObject.create_blank_page(self, width, height)
+        self.add_page(page)
+        return page
+
+    def addBlankPage(
+        self, width: Optional[float] = None, height: Optional[float] = None
+    ) -> PageObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_blank_page` instead.
+        """
+        deprecation_with_replacement("addBlankPage", "add_blank_page", "3.0.0")
+        return self.add_blank_page(width, height)
+
+    def insert_blank_page(
+        self,
+        width: Optional[decimal.Decimal] = None,
+        height: Optional[decimal.Decimal] = None,
+        index: int = 0,
+    ) -> PageObject:
+        """
+        Insert a blank page to this PDF file and returns it. If no page size
+        is specified, use the size of the last page.
+
+        :param float width: The width of the new page expressed in default user
+            space units.
+        :param float height: The height of the new page expressed in default
+            user space units.
+        :param int index: Position to add the page.
+        :return: the newly appended page
+        :raises PageSizeNotDefinedError: if width and height are not defined
+            and previous page does not exist.
+        """
+        if width is None or height is None and (self._get_num_pages() - 1) >= index:
+            oldpage = self.pages[index]
+            width = oldpage.mediabox.width
+            height = oldpage.mediabox.height
+        page = PageObject.create_blank_page(self, width, height)
+        self.insert_page(page, index)
+        return page
+
+    def insertBlankPage(
+        self,
+        width: Optional[decimal.Decimal] = None,
+        height: Optional[decimal.Decimal] = None,
+        index: int = 0,
+    ) -> PageObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`insertBlankPage` instead.
+        """
+        deprecation_with_replacement("insertBlankPage", "insert_blank_page", "3.0.0")
+        return self.insert_blank_page(width, height, index)
+
+    @property
+    def open_destination(
+        self,
+    ) -> Union[None, Destination, TextStringObject, ByteStringObject]:
+        """
+        Property to access the opening destination ("/OpenAction" entry in the
+        PDF catalog).
+        it returns `None` if the entry does not exist is not set.
+
+        :param destination:.
+        the property can be set to a Destination, a Page or an string(NamedDest) or
+            None (to remove "/OpenAction")
+
+        (value stored in "/OpenAction" entry in the Pdf Catalog)
+        """
+        if "/OpenAction" not in self._root_object:
+            return None
+        oa = self._root_object["/OpenAction"]
+        if isinstance(oa, (str, bytes)):
+            return create_string_object(str(oa))
+        elif isinstance(oa, ArrayObject):
+            try:
+                page, typ = oa[0:2]  # type: ignore
+                array = oa[2:]
+                fit = Fit(typ, tuple(array))
+                return Destination("OpenAction", page, fit)
+            except Exception as exc:
+                raise Exception(f"Invalid Destination {oa}: {exc}")
+        else:
+            return None
+
+    @open_destination.setter
+    def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None:
+        if dest is None:
+            try:
+                del self._root_object["/OpenAction"]
+            except KeyError:
+                pass
+        elif isinstance(dest, str):
+            self._root_object[NameObject("/OpenAction")] = TextStringObject(dest)
+        elif isinstance(dest, Destination):
+            self._root_object[NameObject("/OpenAction")] = dest.dest_array
+        elif isinstance(dest, PageObject):
+            self._root_object[NameObject("/OpenAction")] = Destination(
+                "Opening",
+                dest.indirect_reference
+                if dest.indirect_reference is not None
+                else NullObject(),
+                PAGE_FIT,
+            ).dest_array
+
+    def add_js(self, javascript: str) -> None:
+        """
+        Add Javascript which will launch upon opening this PDF.
+
+        :param str javascript: Your Javascript.
+
+        >>> output.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")
+        # Example: This will launch the print window when the PDF is opened.
+        """
+        # Names / JavaScript prefered to be able to add multiple scripts
+        if "/Names" not in self._root_object:
+            self._root_object[NameObject(CA.NAMES)] = DictionaryObject()
+        names = cast(DictionaryObject, self._root_object[CA.NAMES])
+        if "/JavaScript" not in names:
+            names[NameObject("/JavaScript")] = DictionaryObject(
+                {NameObject("/Names"): ArrayObject()}
+            )
+            # cast(DictionaryObject, names[NameObject("/JavaScript")])[NameObject("/Names")] = ArrayObject()
+        js_list = cast(
+            ArrayObject, cast(DictionaryObject, names["/JavaScript"])["/Names"]
+        )
+
+        js = DictionaryObject()
+        js.update(
+            {
+                NameObject(PA.TYPE): NameObject("/Action"),
+                NameObject("/S"): NameObject("/JavaScript"),
+                NameObject("/JS"): TextStringObject(f"{javascript}"),
+            }
+        )
+        # We need a name for parameterized javascript in the pdf file, but it can be anything.
+        js_list.append(create_string_object(str(uuid.uuid4())))
+        js_list.append(self._add_object(js))
+
+    def addJS(self, javascript: str) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_js` instead.
+        """
+        deprecation_with_replacement("addJS", "add_js", "3.0.0")
+        return self.add_js(javascript)
+
+    def add_attachment(self, filename: str, data: Union[str, bytes]) -> None:
+        """
+        Embed a file inside the PDF.
+
+        :param str filename: The filename to display.
+        :param str data: The data in the file.
+
+        Reference:
+        https://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/PDF32000_2008.pdf
+        Section 7.11.3
+        """
+        # We need three entries:
+        # * The file's data
+        # * The /Filespec entry
+        # * The file's name, which goes in the Catalog
+
+        # The entry for the file
+        # Sample:
+        # 8 0 obj
+        # <<
+        #  /Length 12
+        #  /Type /EmbeddedFile
+        # >>
+        # stream
+        # Hello world!
+        # endstream
+        # endobj
+
+        file_entry = DecodedStreamObject()
+        file_entry.set_data(data)
+        file_entry.update({NameObject(PA.TYPE): NameObject("/EmbeddedFile")})
+
+        # The Filespec entry
+        # Sample:
+        # 7 0 obj
+        # <<
+        #  /Type /Filespec
+        #  /F (hello.txt)
+        #  /EF << /F 8 0 R >>
+        # >>
+
+        ef_entry = DictionaryObject()
+        ef_entry.update({NameObject("/F"): file_entry})
+
+        filespec = DictionaryObject()
+        filespec.update(
+            {
+                NameObject(PA.TYPE): NameObject("/Filespec"),
+                NameObject(FileSpecificationDictionaryEntries.F): create_string_object(
+                    filename
+                ),  # Perhaps also try TextStringObject
+                NameObject(FileSpecificationDictionaryEntries.EF): ef_entry,
+            }
+        )
+
+        # Then create the entry for the root, as it needs a reference to the Filespec
+        # Sample:
+        # 1 0 obj
+        # <<
+        #  /Type /Catalog
+        #  /Outlines 2 0 R
+        #  /Pages 3 0 R
+        #  /Names << /EmbeddedFiles << /Names [(hello.txt) 7 0 R] >> >>
+        # >>
+        # endobj
+
+        embedded_files_names_dictionary = DictionaryObject()
+        embedded_files_names_dictionary.update(
+            {
+                NameObject(CA.NAMES): ArrayObject(
+                    [create_string_object(filename), filespec]
+                )
+            }
+        )
+
+        embedded_files_dictionary = DictionaryObject()
+        embedded_files_dictionary.update(
+            {NameObject("/EmbeddedFiles"): embedded_files_names_dictionary}
+        )
+        # Update the root
+        self._root_object.update({NameObject(CA.NAMES): embedded_files_dictionary})
+
+    def addAttachment(
+        self, fname: str, fdata: Union[str, bytes]
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_attachment` instead.
+        """
+        deprecation_with_replacement("addAttachment", "add_attachment", "3.0.0")
+        return self.add_attachment(fname, fdata)
+
+    def append_pages_from_reader(
+        self,
+        reader: PdfReader,
+        after_page_append: Optional[Callable[[PageObject], None]] = None,
+    ) -> None:
+        """
+        Copy pages from reader to writer. Includes an optional callback parameter
+        which is invoked after pages are appended to the writer.
+
+        :param PdfReader reader: a PdfReader object from which to copy page
+            annotations to this writer object.  The writer's annots
+            will then be updated
+        :param Callable[[PageObject], None] after_page_append:
+            Callback function that is invoked after each page is appended to
+            the writer. Signature includes a reference to the appended page
+            (delegates to append_pages_from_reader). The single parameter of the
+            callback is a reference to the page just appended to the document.
+        """
+        # Get page count from writer and reader
+        reader_num_pages = len(reader.pages)
+        # Copy pages from reader to writer
+        for reader_page_number in range(reader_num_pages):
+            reader_page = reader.pages[reader_page_number]
+            writer_page = self.add_page(reader_page)
+            # Trigger callback, pass writer page as parameter
+            if callable(after_page_append):
+                after_page_append(writer_page)
+
+    def appendPagesFromReader(
+        self,
+        reader: PdfReader,
+        after_page_append: Optional[Callable[[PageObject], None]] = None,
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`append_pages_from_reader` instead.
+        """
+        deprecation_with_replacement(
+            "appendPagesFromReader", "append_pages_from_reader", "3.0.0"
+        )
+        self.append_pages_from_reader(reader, after_page_append)
+
+    def update_page_form_field_values(
+        self,
+        page: PageObject,
+        fields: Dict[str, Any],
+        flags: FieldFlag = OPTIONAL_READ_WRITE_FIELD,
+    ) -> None:
+        """
+        Update the form field values for a given page from a fields dictionary.
+
+        Copy field texts and values from fields to page.
+        If the field links to a parent object, add the information to the parent.
+
+        :param PageObject page: Page reference from PDF writer where the
+            annotations and field data will be updated.
+        :param dict fields: a Python dictionary of field names (/T) and text
+            values (/V)
+        :param int flags: An integer (0 to 7). The first bit sets ReadOnly, the
+            second bit sets Required, the third bit sets NoExport. See
+            PDF Reference Table 8.70 for details.
+        """
+        self.set_need_appearances_writer()
+        # Iterate through pages, update field values
+        if PG.ANNOTS not in page:
+            logger_warning("No fields to update on this page", __name__)
+            return
+        for j in range(len(page[PG.ANNOTS])):  # type: ignore
+            writer_annot = page[PG.ANNOTS][j].get_object()  # type: ignore
+            # retrieve parent field values, if present
+            writer_parent_annot = {}  # fallback if it's not there
+            if PG.PARENT in writer_annot:
+                writer_parent_annot = writer_annot[PG.PARENT]
+            for field in fields:
+                if writer_annot.get(FieldDictionaryAttributes.T) == field:
+                    if writer_annot.get(FieldDictionaryAttributes.FT) == "/Btn":
+                        writer_annot.update(
+                            {
+                                NameObject(
+                                    AnnotationDictionaryAttributes.AS
+                                ): NameObject(fields[field])
+                            }
+                        )
+                    writer_annot.update(
+                        {
+                            NameObject(FieldDictionaryAttributes.V): TextStringObject(
+                                fields[field]
+                            )
+                        }
+                    )
+                    if flags:
+                        writer_annot.update(
+                            {
+                                NameObject(FieldDictionaryAttributes.Ff): NumberObject(
+                                    flags
+                                )
+                            }
+                        )
+                elif writer_parent_annot.get(FieldDictionaryAttributes.T) == field:
+                    writer_parent_annot.update(
+                        {
+                            NameObject(FieldDictionaryAttributes.V): TextStringObject(
+                                fields[field]
+                            )
+                        }
+                    )
+
+    def updatePageFormFieldValues(
+        self,
+        page: PageObject,
+        fields: Dict[str, Any],
+        flags: FieldFlag = OPTIONAL_READ_WRITE_FIELD,
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`update_page_form_field_values` instead.
+        """
+        deprecation_with_replacement(
+            "updatePageFormFieldValues", "update_page_form_field_values", "3.0.0"
+        )
+        return self.update_page_form_field_values(page, fields, flags)
+
+    def clone_reader_document_root(self, reader: PdfReader) -> None:
+        """
+        Copy the reader document root to the writer.
+
+        :param reader:  PdfReader from the document root should be copied.
+        """
+        self._root_object = cast(DictionaryObject, reader.trailer[TK.ROOT])
+
+    def cloneReaderDocumentRoot(self, reader: PdfReader) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`clone_reader_document_root` instead.
+        """
+        deprecation_with_replacement(
+            "cloneReaderDocumentRoot", "clone_reader_document_root", "3.0.0"
+        )
+        self.clone_reader_document_root(reader)
+
+    def clone_document_from_reader(
+        self,
+        reader: PdfReader,
+        after_page_append: Optional[Callable[[PageObject], None]] = None,
+    ) -> None:
+        """
+        Create a copy (clone) of a document from a PDF file reader
+
+        :param reader: PDF file reader instance from which the clone
+            should be created.
+        :param Callable[[PageObject], None] after_page_append:
+            Callback function that is invoked after each page is appended to
+            the writer. Signature includes a reference to the appended page
+            (delegates to append_pages_from_reader). The single parameter of the
+            callback is a reference to the page just appended to the document.
+        """
+        # TODO : ppZZ may be limited because we do not copy all info...
+        self.clone_reader_document_root(reader)
+        self.append_pages_from_reader(reader, after_page_append)
+
+    def cloneDocumentFromReader(
+        self,
+        reader: PdfReader,
+        after_page_append: Optional[Callable[[PageObject], None]] = None,
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`clone_document_from_reader` instead.
+        """
+        deprecation_with_replacement(
+            "cloneDocumentFromReader", "clone_document_from_reader", "3.0.0"
+        )
+        self.clone_document_from_reader(reader, after_page_append)
+
+    def encrypt(
+        self,
+        user_password: Optional[str] = None,
+        owner_password: Optional[str] = None,
+        use_128bit: bool = True,
+        permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS,
+        user_pwd: Optional[str] = None,  # deprecated
+        owner_pwd: Optional[str] = None,  # deprecated
+    ) -> None:
+        """
+        Encrypt this PDF file with the PDF Standard encryption handler.
+
+        :param str user_password: The "user password", which allows for opening
+            and reading the PDF file with the restrictions provided.
+        :param str owner_password: The "owner password", which allows for
+            opening the PDF files without any restrictions.  By default,
+            the owner password is the same as the user password.
+        :param bool use_128bit: flag as to whether to use 128bit
+            encryption.  When false, 40bit encryption will be used.  By default,
+            this flag is on.
+        :param unsigned int permissions_flag: permissions as described in
+            TABLE 3.20 of the PDF 1.7 specification. A bit value of 1 means the
+            permission is grantend. Hence an integer value of -1 will set all
+            flags.
+            Bit position 3 is for printing, 4 is for modifying content, 5 and 6
+            control annotations, 9 for form fields, 10 for extraction of
+            text and graphics.
+        """
+        if user_pwd is not None:
+            if user_password is not None:
+                raise ValueError(
+                    "Please only set 'user_password'. "
+                    "The 'user_pwd' argument is deprecated."
+                )
+            else:
+                warnings.warn(
+                    "Please use 'user_password' instead of 'user_pwd'. "
+                    "The 'user_pwd' argument is deprecated and "
+                    "will be removed in PyPDF2 4.0.0."
+                )
+                user_password = user_pwd
+        if user_password is None:  # deprecated
+            # user_password is only Optional for due to the deprecated user_pwd
+            raise ValueError("user_password may not be None")
+
+        if owner_pwd is not None:  # deprecated
+            if owner_password is not None:
+                raise ValueError(
+                    "The argument owner_pwd of encrypt is deprecated. Use owner_password only."
+                )
+            else:
+                old_term = "owner_pwd"
+                new_term = "owner_password"
+                warnings.warn(
+                    message=(
+                        f"{old_term} is deprecated as an argument and will be "
+                        f"removed in PyPDF2 4.0.0. Use {new_term} instead"
+                    ),
+                    category=DeprecationWarning,
+                )
+                owner_password = owner_pwd
+
+        if owner_password is None:
+            owner_password = user_password
+        if use_128bit:
+            V = 2
+            rev = 3
+            keylen = int(128 / 8)
+        else:
+            V = 1
+            rev = 2
+            keylen = int(40 / 8)
+        P = permissions_flag
+        O = ByteStringObject(_alg33(owner_password, user_password, rev, keylen))  # type: ignore[arg-type]
+        ID_1 = ByteStringObject(md5((repr(time.time())).encode("utf8")).digest())
+        ID_2 = ByteStringObject(md5((repr(random.random())).encode("utf8")).digest())
+        self._ID = ArrayObject((ID_1, ID_2))
+        if rev == 2:
+            U, key = _alg34(user_password, O, P, ID_1)
+        else:
+            assert rev == 3
+            U, key = _alg35(user_password, rev, keylen, O, P, ID_1, False)  # type: ignore[arg-type]
+        encrypt = DictionaryObject()
+        encrypt[NameObject(SA.FILTER)] = NameObject("/Standard")
+        encrypt[NameObject("/V")] = NumberObject(V)
+        if V == 2:
+            encrypt[NameObject(SA.LENGTH)] = NumberObject(keylen * 8)
+        encrypt[NameObject(ED.R)] = NumberObject(rev)
+        encrypt[NameObject(ED.O)] = ByteStringObject(O)
+        encrypt[NameObject(ED.U)] = ByteStringObject(U)
+        encrypt[NameObject(ED.P)] = NumberObject(P)
+        self._encrypt = self._add_object(encrypt)
+        self._encrypt_key = key
+
+    def write_stream(self, stream: StreamType) -> None:
+        if hasattr(stream, "mode") and "b" not in stream.mode:
+            logger_warning(
+                f"File <{stream.name}> to write to is not in binary mode. "  # type: ignore
+                "It may not be written to correctly.",
+                __name__,
+            )
+
+        if not self._root:
+            self._root = self._add_object(self._root_object)
+
+        # PDF objects sometimes have circular references to their /Page objects
+        # inside their object tree (for example, annotations).  Those will be
+        # indirect references to objects that we've recreated in this PDF.  To
+        # address this problem, PageObject's store their original object
+        # reference number, and we add it to the external reference map before
+        # we sweep for indirect references.  This forces self-page-referencing
+        # trees to reference the correct new object location, rather than
+        # copying in a new copy of the page object.
+        self._sweep_indirect_references(self._root)
+
+        object_positions = self._write_header(stream)
+        xref_location = self._write_xref_table(stream, object_positions)
+        self._write_trailer(stream)
+        stream.write(b_(f"\nstartxref\n{xref_location}\n%%EOF\n"))  # eof
+
+    def write(self, stream: Union[Path, StrByteType]) -> Tuple[bool, IO]:
+        """
+        Write the collection of pages added to this object out as a PDF file.
+
+        :param stream: An object to write the file to.  The object can support
+            the write method and the tell method, similar to a file object, or
+            be a file path, just like the fileobj, just named it stream to keep
+            existing workflow.
+        """
+        my_file = False
+
+        if stream == "":
+            raise ValueError(f"Output(stream={stream}) is empty.")
+
+        if isinstance(stream, (str, Path)):
+            stream = FileIO(stream, "wb")
+            self.with_as_usage = True  #
+            my_file = True
+
+        self.write_stream(stream)
+
+        if self.with_as_usage:
+            stream.close()
+
+        return my_file, stream
+
+    def _write_header(self, stream: StreamType) -> List[int]:
+        object_positions = []
+        stream.write(self.pdf_header + b"\n")
+        stream.write(b"%\xE2\xE3\xCF\xD3\n")
+        for i, obj in enumerate(self._objects):
+            obj = self._objects[i]
+            # If the obj is None we can't write anything
+            if obj is not None:
+                idnum = i + 1
+                object_positions.append(stream.tell())
+                stream.write(b_(str(idnum)) + b" 0 obj\n")
+                key = None
+                if hasattr(self, "_encrypt") and idnum != self._encrypt.idnum:
+                    pack1 = struct.pack("<i", i + 1)[:3]
+                    pack2 = struct.pack("<i", 0)[:2]
+                    key = self._encrypt_key + pack1 + pack2
+                    assert len(key) == (len(self._encrypt_key) + 5)
+                    md5_hash = md5(key).digest()
+                    key = md5_hash[: min(16, len(self._encrypt_key) + 5)]
+                obj.write_to_stream(stream, key)
+                stream.write(b"\nendobj\n")
+        return object_positions
+
+    def _write_xref_table(self, stream: StreamType, object_positions: List[int]) -> int:
+        xref_location = stream.tell()
+        stream.write(b"xref\n")
+        stream.write(b_(f"0 {len(self._objects) + 1}\n"))
+        stream.write(b_(f"{0:0>10} {65535:0>5} f \n"))
+        for offset in object_positions:
+            stream.write(b_(f"{offset:0>10} {0:0>5} n \n"))
+        return xref_location
+
+    def _write_trailer(self, stream: StreamType) -> None:
+        stream.write(b"trailer\n")
+        trailer = DictionaryObject()
+        trailer.update(
+            {
+                NameObject(TK.SIZE): NumberObject(len(self._objects) + 1),
+                NameObject(TK.ROOT): self._root,
+                NameObject(TK.INFO): self._info,
+            }
+        )
+        if hasattr(self, "_ID"):
+            trailer[NameObject(TK.ID)] = self._ID
+        if hasattr(self, "_encrypt"):
+            trailer[NameObject(TK.ENCRYPT)] = self._encrypt
+        trailer.write_to_stream(stream, None)
+
+    def add_metadata(self, infos: Dict[str, Any]) -> None:
+        """
+        Add custom metadata to the output.
+
+        :param dict infos: a Python dictionary where each key is a field
+            and each value is your new metadata.
+        """
+        args = {}
+        for key, value in list(infos.items()):
+            args[NameObject(key)] = create_string_object(value)
+        self.get_object(self._info).update(args)  # type: ignore
+
+    def addMetadata(self, infos: Dict[str, Any]) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_metadata` instead.
+        """
+        deprecation_with_replacement("addMetadata", "add_metadata", "3.0.0")
+        self.add_metadata(infos)
+
+    def _sweep_indirect_references(
+        self,
+        root: Union[
+            ArrayObject,
+            BooleanObject,
+            DictionaryObject,
+            FloatObject,
+            IndirectObject,
+            NameObject,
+            PdfObject,
+            NumberObject,
+            TextStringObject,
+            NullObject,
+        ],
+    ) -> None:
+        stack: Deque[
+            Tuple[
+                Any,
+                Optional[Any],
+                Any,
+                List[PdfObject],
+            ]
+        ] = collections.deque()
+        discovered = []
+        parent = None
+        grant_parents: List[PdfObject] = []
+        key_or_id = None
+
+        # Start from root
+        stack.append((root, parent, key_or_id, grant_parents))
+
+        while len(stack):
+            data, parent, key_or_id, grant_parents = stack.pop()
+
+            # Build stack for a processing depth-first
+            if isinstance(data, (ArrayObject, DictionaryObject)):
+                for key, value in data.items():
+                    stack.append(
+                        (
+                            value,
+                            data,
+                            key,
+                            grant_parents + [parent] if parent is not None else [],
+                        )
+                    )
+            elif isinstance(data, IndirectObject):
+                if data.pdf != self:
+                    data = self._resolve_indirect_object(data)
+
+                    if str(data) not in discovered:
+                        discovered.append(str(data))
+                        stack.append((data.get_object(), None, None, []))
+
+            # Check if data has a parent and if it is a dict or an array update the value
+            if isinstance(parent, (DictionaryObject, ArrayObject)):
+                if isinstance(data, StreamObject):
+                    # a dictionary value is a stream.  streams must be indirect
+                    # objects, so we need to change this value.
+                    data = self._resolve_indirect_object(self._add_object(data))
+
+                update_hashes = []
+
+                # Data changed and thus the hash value changed
+                if parent[key_or_id] != data:
+                    update_hashes = [parent.hash_value()] + [
+                        grant_parent.hash_value() for grant_parent in grant_parents
+                    ]
+                    parent[key_or_id] = data
+
+                # Update old hash value to new hash value
+                for old_hash in update_hashes:
+                    indirect_reference = self._idnum_hash.pop(old_hash, None)
+
+                    if indirect_reference is not None:
+                        indirect_reference_obj = indirect_reference.get_object()
+
+                        if indirect_reference_obj is not None:
+                            self._idnum_hash[
+                                indirect_reference_obj.hash_value()
+                            ] = indirect_reference
+
+    def _resolve_indirect_object(self, data: IndirectObject) -> IndirectObject:
+        """
+        Resolves indirect object to this pdf indirect objects.
+
+        If it is a new object then it is added to self._objects
+        and new idnum is given and generation is always 0.
+        """
+        if hasattr(data.pdf, "stream") and data.pdf.stream.closed:
+            raise ValueError(f"I/O operation on closed file: {data.pdf.stream.name}")
+
+        if data.pdf == self:
+            return data
+
+        # Get real object indirect object
+        real_obj = data.pdf.get_object(data)
+
+        if real_obj is None:
+            logger_warning(
+                f"Unable to resolve [{data.__class__.__name__}: {data}], "
+                "returning NullObject instead",
+                __name__,
+            )
+            real_obj = NullObject()
+
+        hash_value = real_obj.hash_value()
+
+        # Check if object is handled
+        if hash_value in self._idnum_hash:
+            return self._idnum_hash[hash_value]
+
+        if data.pdf == self:
+            self._idnum_hash[hash_value] = IndirectObject(data.idnum, 0, self)
+        # This is new object in this pdf
+        else:
+            self._idnum_hash[hash_value] = self._add_object(real_obj)
+
+        return self._idnum_hash[hash_value]
+
+    def get_reference(self, obj: PdfObject) -> IndirectObject:
+        idnum = self._objects.index(obj) + 1
+        ref = IndirectObject(idnum, 0, self)
+        assert ref.get_object() == obj
+        return ref
+
+    def getReference(self, obj: PdfObject) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`get_reference` instead.
+        """
+        deprecation_with_replacement("getReference", "get_reference", "3.0.0")
+        return self.get_reference(obj)
+
+    def get_outline_root(self) -> TreeObject:
+        if CO.OUTLINES in self._root_object:
+            # TABLE 3.25 Entries in the catalog dictionary
+            outline = cast(TreeObject, self._root_object[CO.OUTLINES])
+            idnum = self._objects.index(outline) + 1
+            outline_ref = IndirectObject(idnum, 0, self)
+            assert outline_ref.get_object() == outline
+        else:
+            outline = TreeObject()
+            outline.update({})
+            outline_ref = self._add_object(outline)
+            self._root_object[NameObject(CO.OUTLINES)] = outline_ref
+
+        return outline
+
+    def get_threads_root(self) -> ArrayObject:
+        """
+        the list of threads see §8.3.2 from PDF 1.7 spec
+
+                :return: an Array (possibly empty) of Dictionaries with "/F" and "/I" properties
+        """
+        if CO.THREADS in self._root_object:
+            # TABLE 3.25 Entries in the catalog dictionary
+            threads = cast(ArrayObject, self._root_object[CO.THREADS])
+        else:
+            threads = ArrayObject()
+            self._root_object[NameObject(CO.THREADS)] = threads
+        return threads
+
+    @property
+    def threads(self) -> ArrayObject:
+        """
+        Read-only property for the list of threads see §8.3.2 from PDF 1.7 spec
+
+        :return: an Array (possibly empty) of Dictionaries with "/F" and "/I" properties
+        """
+        return self.get_threads_root()
+
+    def getOutlineRoot(self) -> TreeObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`get_outline_root` instead.
+        """
+        deprecation_with_replacement("getOutlineRoot", "get_outline_root", "3.0.0")
+        return self.get_outline_root()
+
+    def get_named_dest_root(self) -> ArrayObject:
+        if CA.NAMES in self._root_object and isinstance(
+            self._root_object[CA.NAMES], DictionaryObject
+        ):
+            names = cast(DictionaryObject, self._root_object[CA.NAMES])
+            names_ref = names.indirect_reference
+            if CA.DESTS in names and isinstance(names[CA.DESTS], DictionaryObject):
+                # 3.6.3 Name Dictionary (PDF spec 1.7)
+                dests = cast(DictionaryObject, names[CA.DESTS])
+                dests_ref = dests.indirect_reference
+                if CA.NAMES in dests:
+                    # TABLE 3.33 Entries in a name tree node dictionary
+                    nd = cast(ArrayObject, dests[CA.NAMES])
+                else:
+                    nd = ArrayObject()
+                    dests[NameObject(CA.NAMES)] = nd
+            else:
+                dests = DictionaryObject()
+                dests_ref = self._add_object(dests)
+                names[NameObject(CA.DESTS)] = dests_ref
+                nd = ArrayObject()
+                dests[NameObject(CA.NAMES)] = nd
+
+        else:
+            names = DictionaryObject()
+            names_ref = self._add_object(names)
+            self._root_object[NameObject(CA.NAMES)] = names_ref
+            dests = DictionaryObject()
+            dests_ref = self._add_object(dests)
+            names[NameObject(CA.DESTS)] = dests_ref
+            nd = ArrayObject()
+            dests[NameObject(CA.NAMES)] = nd
+
+        return nd
+
+    def getNamedDestRoot(self) -> ArrayObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`get_named_dest_root` instead.
+        """
+        deprecation_with_replacement("getNamedDestRoot", "get_named_dest_root", "3.0.0")
+        return self.get_named_dest_root()
+
+    def add_outline_item_destination(
+        self,
+        page_destination: Union[None, PageObject, TreeObject] = None,
+        parent: Union[None, TreeObject, IndirectObject] = None,
+        before: Union[None, TreeObject, IndirectObject] = None,
+        dest: Union[None, PageObject, TreeObject] = None,  # deprecated
+    ) -> IndirectObject:
+        if page_destination is not None and dest is not None:  # deprecated
+            raise ValueError(
+                "The argument dest of add_outline_item_destination is deprecated. Use page_destination only."
+            )
+        if dest is not None:  # deprecated
+            old_term = "dest"
+            new_term = "page_destination"
+            warnings.warn(
+                message=(
+                    f"{old_term} is deprecated as an argument and will be "
+                    f"removed in PyPDF2 4.0.0. Use {new_term} instead"
+                ),
+                category=DeprecationWarning,
+            )
+            page_destination = dest
+        if page_destination is None:  # deprecated
+            # argument is only Optional due to deprecated argument.
+            raise ValueError("page_destination may not be None")
+
+        if parent is None:
+            parent = self.get_outline_root()
+
+        parent = cast(TreeObject, parent.get_object())
+        page_destination_ref = self._add_object(page_destination)
+        if before is not None:
+            before = before.indirect_reference
+        parent.insert_child(page_destination_ref, before, self)
+
+        return page_destination_ref
+
+    def add_bookmark_destination(
+        self,
+        dest: Union[PageObject, TreeObject],
+        parent: Union[None, TreeObject, IndirectObject] = None,
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 2.9.0
+
+            Use :meth:`add_outline_item_destination` instead.
+        """
+        deprecation_with_replacement(
+            "add_bookmark_destination", "add_outline_item_destination", "3.0.0"
+        )
+        return self.add_outline_item_destination(dest, parent)
+
+    def addBookmarkDestination(
+        self, dest: PageObject, parent: Optional[TreeObject] = None
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_outline_item_destination` instead.
+        """
+        deprecation_with_replacement(
+            "addBookmarkDestination", "add_outline_item_destination", "3.0.0"
+        )
+        return self.add_outline_item_destination(dest, parent)
+
+    @deprecation_bookmark(bookmark="outline_item")
+    def add_outline_item_dict(
+        self,
+        outline_item: OutlineItemType,
+        parent: Union[None, TreeObject, IndirectObject] = None,
+        before: Union[None, TreeObject, IndirectObject] = None,
+    ) -> IndirectObject:
+        outline_item_object = TreeObject()
+        for k, v in list(outline_item.items()):
+            outline_item_object[NameObject(str(k))] = v
+        outline_item_object.update(outline_item)
+
+        if "/A" in outline_item:
+            action = DictionaryObject()
+            a_dict = cast(DictionaryObject, outline_item["/A"])
+            for k, v in list(a_dict.items()):
+                action[NameObject(str(k))] = v
+            action_ref = self._add_object(action)
+            outline_item_object[NameObject("/A")] = action_ref
+
+        return self.add_outline_item_destination(outline_item_object, parent, before)
+
+    @deprecation_bookmark(bookmark="outline_item")
+    def add_bookmark_dict(
+        self, outline_item: OutlineItemType, parent: Optional[TreeObject] = None
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 2.9.0
+
+            Use :meth:`add_outline_item_dict` instead.
+        """
+        deprecation_with_replacement(
+            "add_bookmark_dict", "add_outline_item_dict", "3.0.0"
+        )
+        return self.add_outline_item_dict(outline_item, parent)
+
+    @deprecation_bookmark(bookmark="outline_item")
+    def addBookmarkDict(
+        self, outline_item: OutlineItemType, parent: Optional[TreeObject] = None
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_outline_item_dict` instead.
+        """
+        deprecation_with_replacement(
+            "addBookmarkDict", "add_outline_item_dict", "3.0.0"
+        )
+        return self.add_outline_item_dict(outline_item, parent)
+
+    def add_outline_item(
+        self,
+        title: str,
+        page_number: Union[None, PageObject, IndirectObject, int],
+        parent: Union[None, TreeObject, IndirectObject] = None,
+        before: Union[None, TreeObject, IndirectObject] = None,
+        color: Optional[Union[Tuple[float, float, float], str]] = None,
+        bold: bool = False,
+        italic: bool = False,
+        fit: Fit = PAGE_FIT,
+        pagenum: Optional[int] = None,  # deprecated
+    ) -> IndirectObject:
+        """
+        Add an outline item (commonly referred to as a "Bookmark") to this PDF file.
+
+        :param str title: Title to use for this outline item.
+        :param int page_number: Page number this outline item will point to.
+        :param parent: A reference to a parent outline item to create nested
+            outline items.
+        :param parent: A reference to a parent outline item to create nested
+            outline items.
+        :param tuple color: Color of the outline item's font as a red, green, blue tuple
+            from 0.0 to 1.0 or as a Hex String (#RRGGBB)
+        :param bool bold: Outline item font is bold
+        :param bool italic: Outline item font is italic
+        :param Fit fit: The fit of the destination page.
+        """
+        page_ref: Union[None, NullObject, IndirectObject, NumberObject]
+        if isinstance(italic, Fit):  # it means that we are on the old params
+            if fit is not None and page_number is None:
+                page_number = fit  # type: ignore
+            return self.add_outline_item(
+                title, page_number, parent, None, before, color, bold, italic  # type: ignore
+            )
+        if page_number is not None and pagenum is not None:
+            raise ValueError(
+                "The argument pagenum of add_outline_item is deprecated. Use page_number only."
+            )
+        if page_number is None:
+            action_ref = None
+        else:
+            if isinstance(page_number, IndirectObject):
+                page_ref = page_number
+            elif isinstance(page_number, PageObject):
+                page_ref = page_number.indirect_reference
+            elif isinstance(page_number, int):
+                try:
+                    page_ref = self.pages[page_number].indirect_reference
+                except IndexError:
+                    page_ref = NumberObject(page_number)
+            if page_ref is None:
+                logger_warning(
+                    f"can not find reference of page {page_number}",
+                    __name__,
+                )
+                page_ref = NullObject()
+            dest = Destination(
+                NameObject("/" + title + " outline item"),
+                page_ref,
+                fit,
+            )
+
+            action_ref = self._add_object(
+                DictionaryObject(
+                    {
+                        NameObject(GoToActionArguments.D): dest.dest_array,
+                        NameObject(GoToActionArguments.S): NameObject("/GoTo"),
+                    }
+                )
+            )
+        outline_item = _create_outline_item(action_ref, title, color, italic, bold)
+
+        if parent is None:
+            parent = self.get_outline_root()
+        return self.add_outline_item_destination(outline_item, parent, before)
+
+    def add_bookmark(
+        self,
+        title: str,
+        pagenum: int,  # deprecated, but the whole method is deprecated
+        parent: Union[None, TreeObject, IndirectObject] = None,
+        color: Optional[Tuple[float, float, float]] = None,
+        bold: bool = False,
+        italic: bool = False,
+        fit: FitType = "/Fit",
+        *args: ZoomArgType,
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 2.9.0
+
+            Use :meth:`add_outline_item` instead.
+        """
+        deprecation_with_replacement("add_bookmark", "add_outline_item", "3.0.0")
+        return self.add_outline_item(
+            title,
+            pagenum,
+            parent,
+            color,  # type: ignore
+            bold,  # type: ignore
+            italic,
+            Fit(fit_type=fit, fit_args=args),  # type: ignore
+        )
+
+    def addBookmark(
+        self,
+        title: str,
+        pagenum: int,
+        parent: Union[None, TreeObject, IndirectObject] = None,
+        color: Optional[Tuple[float, float, float]] = None,
+        bold: bool = False,
+        italic: bool = False,
+        fit: FitType = "/Fit",
+        *args: ZoomArgType,
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_outline_item` instead.
+        """
+        deprecation_with_replacement("addBookmark", "add_outline_item", "3.0.0")
+        return self.add_outline_item(
+            title,
+            pagenum,
+            parent,
+            None,
+            color,
+            bold,
+            italic,
+            Fit(fit_type=fit, fit_args=args),
+        )
+
+    def add_outline(self) -> None:
+        raise NotImplementedError(
+            "This method is not yet implemented. Use :meth:`add_outline_item` instead."
+        )
+
+    def add_named_destination_array(
+        self, title: TextStringObject, destination: Union[IndirectObject, ArrayObject]
+    ) -> None:
+        nd = self.get_named_dest_root()
+        i = 0
+        while i < len(nd):
+            if title < nd[i]:
+                nd.insert(i, destination)
+                nd.insert(i, TextStringObject(title))
+                return
+            else:
+                i += 2
+        nd.extend([TextStringObject(title), destination])
+        return
+
+    def add_named_destination_object(
+        self,
+        page_destination: Optional[PdfObject] = None,
+        dest: Optional[PdfObject] = None,
+    ) -> IndirectObject:
+        if page_destination is not None and dest is not None:
+            raise ValueError(
+                "The argument dest of add_named_destination_object is deprecated. Use page_destination only."
+            )
+        if dest is not None:  # deprecated
+            old_term = "dest"
+            new_term = "page_destination"
+            warnings.warn(
+                message=(
+                    f"{old_term} is deprecated as an argument and will be "
+                    f"removed in PyPDF2 4.0.0. Use {new_term} instead"
+                ),
+                category=DeprecationWarning,
+            )
+            page_destination = dest
+        if page_destination is None:  # deprecated
+            raise ValueError("page_destination may not be None")
+
+        page_destination_ref = self._add_object(page_destination.dest_array)  # type: ignore
+        self.add_named_destination_array(
+            cast("TextStringObject", page_destination["/Title"]), page_destination_ref  # type: ignore
+        )
+
+        return page_destination_ref
+
+    def addNamedDestinationObject(
+        self, dest: Destination
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_named_destination_object` instead.
+        """
+        deprecation_with_replacement(
+            "addNamedDestinationObject", "add_named_destination_object", "3.0.0"
+        )
+        return self.add_named_destination_object(dest)
+
+    def add_named_destination(
+        self,
+        title: str,
+        page_number: Optional[int] = None,
+        pagenum: Optional[int] = None,  # deprecated
+    ) -> IndirectObject:
+        if page_number is not None and pagenum is not None:
+            raise ValueError(
+                "The argument pagenum of add_outline_item is deprecated. Use page_number only."
+            )
+        if pagenum is not None:
+            old_term = "pagenum"
+            new_term = "page_number"
+            warnings.warn(
+                message=(
+                    f"{old_term} is deprecated as an argument and will be "
+                    f"removed in PyPDF2 4.0.0. Use {new_term} instead"
+                ),
+                category=DeprecationWarning,
+            )
+            page_number = pagenum
+        if page_number is None:
+            raise ValueError("page_number may not be None")
+        page_ref = self.get_object(self._pages)[PA.KIDS][page_number]  # type: ignore
+        dest = DictionaryObject()
+        dest.update(
+            {
+                NameObject(GoToActionArguments.D): ArrayObject(
+                    [page_ref, NameObject(TypFitArguments.FIT_H), NumberObject(826)]
+                ),
+                NameObject(GoToActionArguments.S): NameObject("/GoTo"),
+            }
+        )
+
+        dest_ref = self._add_object(dest)
+        nd = self.get_named_dest_root()
+        if not isinstance(title, TextStringObject):
+            title = TextStringObject(str(title))
+        nd.extend([title, dest_ref])
+        return dest_ref
+
+    def addNamedDestination(
+        self, title: str, pagenum: int
+    ) -> IndirectObject:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_named_destination` instead.
+        """
+        deprecation_with_replacement(
+            "addNamedDestination", "add_named_destination", "3.0.0"
+        )
+        return self.add_named_destination(title, pagenum)
+
+    def remove_links(self) -> None:
+        """Remove links and annotations from this output."""
+        pg_dict = cast(DictionaryObject, self.get_object(self._pages))
+        pages = cast(ArrayObject, pg_dict[PA.KIDS])
+        for page in pages:
+            page_ref = cast(DictionaryObject, self.get_object(page))
+            if PG.ANNOTS in page_ref:
+                del page_ref[PG.ANNOTS]
+
+    def removeLinks(self) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`remove_links` instead.
+        """
+        deprecation_with_replacement("removeLinks", "remove_links", "3.0.0")
+        return self.remove_links()
+
+    def remove_images(self, ignore_byte_string_object: bool = False) -> None:
+        """
+        Remove images from this output.
+
+        :param bool ignore_byte_string_object: optional parameter
+            to ignore ByteString Objects.
+        """
+        pg_dict = cast(DictionaryObject, self.get_object(self._pages))
+        pages = cast(ArrayObject, pg_dict[PA.KIDS])
+        jump_operators = (
+            b"cm",
+            b"w",
+            b"J",
+            b"j",
+            b"M",
+            b"d",
+            b"ri",
+            b"i",
+            b"gs",
+            b"W",
+            b"b",
+            b"s",
+            b"S",
+            b"f",
+            b"F",
+            b"n",
+            b"m",
+            b"l",
+            b"c",
+            b"v",
+            b"y",
+            b"h",
+            b"B",
+            b"Do",
+            b"sh",
+        )
+        for page in pages:
+            page_ref = cast(DictionaryObject, self.get_object(page))
+            content = page_ref["/Contents"].get_object()
+            if not isinstance(content, ContentStream):
+                content = ContentStream(content, page_ref)
+
+            _operations = []
+            seq_graphics = False
+            for operands, operator in content.operations:
+                if operator in [b"Tj", b"'"]:
+                    text = operands[0]
+                    if ignore_byte_string_object and not isinstance(
+                        text, TextStringObject
+                    ):
+                        operands[0] = TextStringObject()
+                elif operator == b'"':
+                    text = operands[2]
+                    if ignore_byte_string_object and not isinstance(
+                        text, TextStringObject
+                    ):
+                        operands[2] = TextStringObject()
+                elif operator == b"TJ":
+                    for i in range(len(operands[0])):
+                        if ignore_byte_string_object and not isinstance(
+                            operands[0][i], TextStringObject
+                        ):
+                            operands[0][i] = TextStringObject()
+
+                if operator == b"q":
+                    seq_graphics = True
+                if operator == b"Q":
+                    seq_graphics = False
+                if seq_graphics and operator in jump_operators:
+                    continue
+                if operator == b"re":
+                    continue
+                _operations.append((operands, operator))
+
+            content.operations = _operations
+            page_ref.__setitem__(NameObject("/Contents"), content)
+
+    def removeImages(
+        self, ignoreByteStringObject: bool = False
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`remove_images` instead.
+        """
+        deprecation_with_replacement("removeImages", "remove_images", "3.0.0")
+        return self.remove_images(ignoreByteStringObject)
+
+    def remove_text(self, ignore_byte_string_object: bool = False) -> None:
+        """
+        Remove text from this output.
+
+        :param bool ignore_byte_string_object: optional parameter
+            to ignore ByteString Objects.
+        """
+        pg_dict = cast(DictionaryObject, self.get_object(self._pages))
+        pages = cast(List[IndirectObject], pg_dict[PA.KIDS])
+        for page in pages:
+            page_ref = cast(PageObject, self.get_object(page))
+            content = page_ref["/Contents"].get_object()
+            if not isinstance(content, ContentStream):
+                content = ContentStream(content, page_ref)
+            for operands, operator in content.operations:
+                if operator in [b"Tj", b"'"]:
+                    text = operands[0]
+                    if not ignore_byte_string_object:
+                        if isinstance(text, TextStringObject):
+                            operands[0] = TextStringObject()
+                    else:
+                        if isinstance(text, (TextStringObject, ByteStringObject)):
+                            operands[0] = TextStringObject()
+                elif operator == b'"':
+                    text = operands[2]
+                    if not ignore_byte_string_object:
+                        if isinstance(text, TextStringObject):
+                            operands[2] = TextStringObject()
+                    else:
+                        if isinstance(text, (TextStringObject, ByteStringObject)):
+                            operands[2] = TextStringObject()
+                elif operator == b"TJ":
+                    for i in range(len(operands[0])):
+                        if not ignore_byte_string_object:
+                            if isinstance(operands[0][i], TextStringObject):
+                                operands[0][i] = TextStringObject()
+                        else:
+                            if isinstance(
+                                operands[0][i], (TextStringObject, ByteStringObject)
+                            ):
+                                operands[0][i] = TextStringObject()
+
+            page_ref.__setitem__(NameObject("/Contents"), content)
+
+    def removeText(
+        self, ignoreByteStringObject: bool = False
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`remove_text` instead.
+        """
+        deprecation_with_replacement("removeText", "remove_text", "3.0.0")
+        return self.remove_text(ignoreByteStringObject)
+
+    def add_uri(
+        self,
+        page_number: int,
+        uri: str,
+        rect: RectangleObject,
+        border: Optional[ArrayObject] = None,
+        pagenum: Optional[int] = None,
+    ) -> None:
+        """
+        Add an URI from a rectangular area to the specified page.
+        This uses the basic structure of :meth:`add_link`
+
+        :param int page_number: index of the page on which to place the URI action.
+        :param str uri: URI of resource to link to.
+        :param Tuple[int, int, int, int] rect: :class:`RectangleObject<PyPDF2.generic.RectangleObject>` or array of four
+            integers specifying the clickable rectangular area
+            ``[xLL, yLL, xUR, yUR]``, or string in the form ``"[ xLL yLL xUR yUR ]"``.
+        :param ArrayObject border: if provided, an array describing border-drawing
+            properties. See the PDF spec for details. No border will be
+            drawn if this argument is omitted.
+        """
+        if pagenum is not None:
+            warnings.warn(
+                "The 'pagenum' argument of add_uri is deprecated and will be "
+                "removed in PyPDF2 4.0.0. Use 'page_number' instead.",
+                category=DeprecationWarning,
+            )
+            page_number = pagenum
+        page_link = self.get_object(self._pages)[PA.KIDS][page_number]  # type: ignore
+        page_ref = cast(Dict[str, Any], self.get_object(page_link))
+
+        border_arr: BorderArrayType
+        if border is not None:
+            border_arr = [NameObject(n) for n in border[:3]]
+            if len(border) == 4:
+                dash_pattern = ArrayObject([NameObject(n) for n in border[3]])
+                border_arr.append(dash_pattern)
+        else:
+            border_arr = [NumberObject(2)] * 3
+
+        if isinstance(rect, str):
+            rect = NameObject(rect)
+        elif isinstance(rect, RectangleObject):
+            pass
+        else:
+            rect = RectangleObject(rect)
+
+        lnk2 = DictionaryObject()
+        lnk2.update(
+            {
+                NameObject("/S"): NameObject("/URI"),
+                NameObject("/URI"): TextStringObject(uri),
+            }
+        )
+        lnk = DictionaryObject()
+        lnk.update(
+            {
+                NameObject(AnnotationDictionaryAttributes.Type): NameObject(PG.ANNOTS),
+                NameObject(AnnotationDictionaryAttributes.Subtype): NameObject("/Link"),
+                NameObject(AnnotationDictionaryAttributes.P): page_link,
+                NameObject(AnnotationDictionaryAttributes.Rect): rect,
+                NameObject("/H"): NameObject("/I"),
+                NameObject(AnnotationDictionaryAttributes.Border): ArrayObject(
+                    border_arr
+                ),
+                NameObject("/A"): lnk2,
+            }
+        )
+        lnk_ref = self._add_object(lnk)
+
+        if PG.ANNOTS in page_ref:
+            page_ref[PG.ANNOTS].append(lnk_ref)
+        else:
+            page_ref[NameObject(PG.ANNOTS)] = ArrayObject([lnk_ref])
+
+    def addURI(
+        self,
+        pagenum: int,  # deprecated, but method is deprecated already
+        uri: str,
+        rect: RectangleObject,
+        border: Optional[ArrayObject] = None,
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_uri` instead.
+        """
+        deprecation_with_replacement("addURI", "add_uri", "3.0.0")
+        return self.add_uri(pagenum, uri, rect, border)
+
+    def add_link(
+        self,
+        pagenum: int,  # deprecated, but method is deprecated already
+        page_destination: int,
+        rect: RectangleObject,
+        border: Optional[ArrayObject] = None,
+        fit: FitType = "/Fit",
+        *args: ZoomArgType,
+    ) -> None:
+        deprecation_with_replacement(
+            "add_link", "add_annotation(AnnotationBuilder.link(...))"
+        )
+
+        if isinstance(rect, str):
+            rect = rect.strip()[1:-1]
+            rect = RectangleObject(
+                [float(num) for num in rect.split(" ") if len(num) > 0]
+            )
+        elif isinstance(rect, RectangleObject):
+            pass
+        else:
+            rect = RectangleObject(rect)
+
+        annotation = AnnotationBuilder.link(
+            rect=rect,
+            border=border,
+            target_page_index=page_destination,
+            fit=Fit(fit_type=fit, fit_args=args),
+        )
+        return self.add_annotation(page_number=pagenum, annotation=annotation)
+
+    def addLink(
+        self,
+        pagenum: int,  # deprecated, but method is deprecated already
+        page_destination: int,
+        rect: RectangleObject,
+        border: Optional[ArrayObject] = None,
+        fit: FitType = "/Fit",
+        *args: ZoomArgType,
+    ) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :meth:`add_link` instead.
+        """
+        deprecate_with_replacement(
+            "addLink", "add_annotation(AnnotationBuilder.link(...))", "4.0.0"
+        )
+        return self.add_link(pagenum, page_destination, rect, border, fit, *args)
+
+    _valid_layouts = (
+        "/NoLayout",
+        "/SinglePage",
+        "/OneColumn",
+        "/TwoColumnLeft",
+        "/TwoColumnRight",
+        "/TwoPageLeft",
+        "/TwoPageRight",
+    )
+
+    def _get_page_layout(self) -> Optional[LayoutType]:
+        try:
+            return cast(LayoutType, self._root_object["/PageLayout"])
+        except KeyError:
+            return None
+
+    def getPageLayout(self) -> Optional[LayoutType]:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_layout` instead.
+        """
+        deprecation_with_replacement("getPageLayout", "page_layout", "3.0.0")
+        return self._get_page_layout()
+
+    def _set_page_layout(self, layout: Union[NameObject, LayoutType]) -> None:
+        """
+        Set the page layout.
+
+        :param str layout: The page layout to be used.
+
+        .. list-table:: Valid ``layout`` arguments
+           :widths: 50 200
+
+           * - /NoLayout
+             - Layout explicitly not specified
+           * - /SinglePage
+             - Show one page at a time
+           * - /OneColumn
+             - Show one column at a time
+           * - /TwoColumnLeft
+             - Show pages in two columns, odd-numbered pages on the left
+           * - /TwoColumnRight
+             - Show pages in two columns, odd-numbered pages on the right
+           * - /TwoPageLeft
+             - Show two pages at a time, odd-numbered pages on the left
+           * - /TwoPageRight
+             - Show two pages at a time, odd-numbered pages on the right
+        """
+        if not isinstance(layout, NameObject):
+            if layout not in self._valid_layouts:
+                logger_warning(
+                    f"Layout should be one of: {'', ''.join(self._valid_layouts)}",
+                    __name__,
+                )
+            layout = NameObject(layout)
+        self._root_object.update({NameObject("/PageLayout"): layout})
+
+    def set_page_layout(self, layout: LayoutType) -> None:
+        """
+        Set the page layout.
+
+        :param str layout: The page layout to be used
+
+        .. list-table:: Valid ``layout`` arguments
+           :widths: 50 200
+
+           * - /NoLayout
+             - Layout explicitly not specified
+           * - /SinglePage
+             - Show one page at a time
+           * - /OneColumn
+             - Show one column at a time
+           * - /TwoColumnLeft
+             - Show pages in two columns, odd-numbered pages on the left
+           * - /TwoColumnRight
+             - Show pages in two columns, odd-numbered pages on the right
+           * - /TwoPageLeft
+             - Show two pages at a time, odd-numbered pages on the left
+           * - /TwoPageRight
+             - Show two pages at a time, odd-numbered pages on the right
+        """
+        self._set_page_layout(layout)
+
+    def setPageLayout(self, layout: LayoutType) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_layout` instead.
+        """
+        deprecation_with_replacement(
+            "writer.setPageLayout(val)", "writer.page_layout = val", "3.0.0"
+        )
+        return self._set_page_layout(layout)
+
+    @property
+    def page_layout(self) -> Optional[LayoutType]:
+        """
+        Page layout property.
+
+        .. list-table:: Valid ``layout`` values
+           :widths: 50 200
+
+           * - /NoLayout
+             - Layout explicitly not specified
+           * - /SinglePage
+             - Show one page at a time
+           * - /OneColumn
+             - Show one column at a time
+           * - /TwoColumnLeft
+             - Show pages in two columns, odd-numbered pages on the left
+           * - /TwoColumnRight
+             - Show pages in two columns, odd-numbered pages on the right
+           * - /TwoPageLeft
+             - Show two pages at a time, odd-numbered pages on the left
+           * - /TwoPageRight
+             - Show two pages at a time, odd-numbered pages on the right
+        """
+        return self._get_page_layout()
+
+    @page_layout.setter
+    def page_layout(self, layout: LayoutType) -> None:
+        self._set_page_layout(layout)
+
+    @property
+    def pageLayout(self) -> Optional[LayoutType]:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_layout` instead.
+        """
+        deprecation_with_replacement("pageLayout", "page_layout", "3.0.0")
+        return self.page_layout
+
+    @pageLayout.setter
+    def pageLayout(self, layout: LayoutType) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_layout` instead.
+        """
+        deprecation_with_replacement("pageLayout", "page_layout", "3.0.0")
+        self.page_layout = layout
+
+    _valid_modes = (
+        "/UseNone",
+        "/UseOutlines",
+        "/UseThumbs",
+        "/FullScreen",
+        "/UseOC",
+        "/UseAttachments",
+    )
+
+    def _get_page_mode(self) -> Optional[PagemodeType]:
+        try:
+            return cast(PagemodeType, self._root_object["/PageMode"])
+        except KeyError:
+            return None
+
+    def getPageMode(self) -> Optional[PagemodeType]:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_mode` instead.
+        """
+        deprecation_with_replacement("getPageMode", "page_mode", "3.0.0")
+        return self._get_page_mode()
+
+    def set_page_mode(self, mode: PagemodeType) -> None:
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_mode` instead.
+        """
+        if isinstance(mode, NameObject):
+            mode_name: NameObject = mode
+        else:
+            if mode not in self._valid_modes:
+                logger_warning(
+                    f"Mode should be one of: {', '.join(self._valid_modes)}", __name__
+                )
+            mode_name = NameObject(mode)
+        self._root_object.update({NameObject("/PageMode"): mode_name})
+
+    def setPageMode(self, mode: PagemodeType) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_mode` instead.
+        """
+        deprecation_with_replacement(
+            "writer.setPageMode(val)", "writer.page_mode = val", "3.0.0"
+        )
+        self.set_page_mode(mode)
+
+    @property
+    def page_mode(self) -> Optional[PagemodeType]:
+        """
+        Page mode property.
+
+        .. list-table:: Valid ``mode`` values
+           :widths: 50 200
+
+           * - /UseNone
+             - Do not show outline or thumbnails panels
+           * - /UseOutlines
+             - Show outline (aka bookmarks) panel
+           * - /UseThumbs
+             - Show page thumbnails panel
+           * - /FullScreen
+             - Fullscreen view
+           * - /UseOC
+             - Show Optional Content Group (OCG) panel
+           * - /UseAttachments
+             - Show attachments panel
+        """
+        return self._get_page_mode()
+
+    @page_mode.setter
+    def page_mode(self, mode: PagemodeType) -> None:
+        self.set_page_mode(mode)
+
+    @property
+    def pageMode(self) -> Optional[PagemodeType]:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_mode` instead.
+        """
+        deprecation_with_replacement("pageMode", "page_mode", "3.0.0")
+        return self.page_mode
+
+    @pageMode.setter
+    def pageMode(self, mode: PagemodeType) -> None:  # pragma: no cover
+        """
+        .. deprecated:: 1.28.0
+
+            Use :py:attr:`page_mode` instead.
+        """
+        deprecation_with_replacement("pageMode", "page_mode", "3.0.0")
+        self.page_mode = mode
+
+    def add_annotation(self, page_number: int, annotation: Dict[str, Any]) -> None:
+        to_add = cast(DictionaryObject, _pdf_objectify(annotation))
+        to_add[NameObject("/P")] = self.get_object(self._pages)["/Kids"][page_number]  # type: ignore
+        page = self.pages[page_number]
+        if page.annotations is None:
+            page[NameObject("/Annots")] = ArrayObject()
+        assert page.annotations is not None
+
+        # Internal link annotations need the correct object type for the
+        # destination
+        if to_add.get("/Subtype") == "/Link" and NameObject("/Dest") in to_add:
+            tmp = cast(dict, to_add[NameObject("/Dest")])
+            dest = Destination(
+                NameObject("/LinkName"),
+                tmp["target_page_index"],
+                Fit(
+                    fit_type=tmp["fit"], fit_args=dict(tmp)["fit_args"]
+                ),  # I have no clue why this dict-hack is necessary
+            )
+            to_add[NameObject("/Dest")] = dest.dest_array
+
+        ind_obj = self._add_object(to_add)
+
+        page.annotations.append(ind_obj)
+
+    def clean_page(self, page: Union[PageObject, IndirectObject]) -> PageObject:
+        """
+        Perform some clean up in the page.
+        Currently: convert NameObject nameddestination to TextStringObject (required for names/dests list)
+        """
+        page = cast("PageObject", page.get_object())
+        for a in page.get("/Annots", []):
+            a_obj = a.get_object()
+            d = a_obj.get("/Dest", None)
+            act = a_obj.get("/A", None)
+            if isinstance(d, NameObject):
+                a_obj[NameObject("/Dest")] = TextStringObject(d)
+            elif act is not None:
+                act = act.get_object()
+                d = act.get("/D", None)
+                if isinstance(d, NameObject):
+                    act[NameObject("/D")] = TextStringObject(d)
+        return page
+
+    def _create_stream(
+        self, fileobj: Union[Path, StrByteType, PdfReader]
+    ) -> Tuple[IOBase, Optional[Encryption]]:
+        # If the fileobj parameter is a string, assume it is a path
+        # and create a file object at that location. If it is a file,
+        # copy the file's contents into a BytesIO stream object; if
+        # it is a PdfReader, copy that reader's stream into a
+        # BytesIO stream.
+        # If fileobj is none of the above types, it is not modified
+        encryption_obj = None
+        stream: IOBase
+        if isinstance(fileobj, (str, Path)):
+            with FileIO(fileobj, "rb") as f:
+                stream = BytesIO(f.read())
+        elif isinstance(fileobj, PdfReader):
+            if fileobj._encryption:
+                encryption_obj = fileobj._encryption
+            orig_tell = fileobj.stream.tell()
+            fileobj.stream.seek(0)
+            stream = BytesIO(fileobj.stream.read())
+
+            # reset the stream to its original location
+            fileobj.stream.seek(orig_tell)
+        elif hasattr(fileobj, "seek") and hasattr(fileobj, "read"):
+            fileobj.seek(0)
+            filecontent = fileobj.read()
+            stream = BytesIO(filecontent)
+        else:
+            raise NotImplementedError(
+                "PdfMerger.merge requires an object that PdfReader can parse. "
+                "Typically, that is a Path or a string representing a Path, "
+                "a file object, or an object implementing .seek and .read. "
+                "Passing a PdfReader directly works as well."
+            )
+        return stream, encryption_obj
+
+    def append(
+        self,
+        fileobj: Union[StrByteType, PdfReader, Path],
+        outline_item: Union[
+            str, None, PageRange, Tuple[int, int], Tuple[int, int, int], List[int]
+        ] = None,
+        pages: Union[
+            None, PageRange, Tuple[int, int], Tuple[int, int, int], List[int]
+        ] = None,
+        import_outline: bool = True,
+        excluded_fields: Optional[Union[List[str], Tuple[str, ...]]] = None,
+    ) -> None:
+        """
+        Identical to the :meth:`merge()<merge>` method, but assumes you want to
+        concatenate all pages onto the end of the file instead of specifying a
+        position.
+
+        :param fileobj: A File Object or an object that supports the standard
+            read and seek methods similar to a File Object. Could also be a
+            string representing a path to a PDF file.
+
+        :param str outline_item: Optionally, you may specify a string to build an outline
+            (aka 'bookmark') to identify the
+            beginning of the included file.
+
+        :param pages: can be a :class:`PageRange<PyPDF2.pagerange.PageRange>`
+            or a ``(start, stop[, step])`` tuple
+            or a list of pages to be processed
+            to merge only the specified range of pages from the source
+            document into the output document.
+
+        :param bool import_outline: You may prevent the source document's
+            outline (collection of outline items, previously referred to as
+            'bookmarks') from being imported by specifying this as ``False``.
+
+        :param List excluded_fields: provide the list of fields/keys to be ignored
+            if "/Annots" is part of the list, the annotation will be ignored
+            if "/B" is part of the list, the articles will be ignored
+        """
+        if excluded_fields is None:
+            excluded_fields = ()
+        if isinstance(outline_item, (tuple, list, PageRange)):
+            if isinstance(pages, bool):
+                if not isinstance(import_outline, bool):
+                    excluded_fields = import_outline
+                import_outline = pages
+            pages = outline_item
+            self.merge(None, fileobj, None, pages, import_outline, excluded_fields)
+        else:  # if isinstance(outline_item,str):
+            self.merge(
+                None, fileobj, outline_item, pages, import_outline, excluded_fields
+            )
+
+    @deprecation_bookmark(bookmark="outline_item", import_bookmarks="import_outline")
+    def merge(
+        self,
+        position: Optional[int],
+        fileobj: Union[Path, StrByteType, PdfReader],
+        outline_item: Optional[str] = None,
+        pages: Optional[PageRangeSpec] = None,
+        import_outline: bool = True,
+        excluded_fields: Optional[Union[List[str], Tuple[str, ...]]] = (),
+    ) -> None:
+        """
+        Merge the pages from the given file into the output file at the
+        specified page number.
+
+        :param int position: The *page number* to insert this file. File will
+            be inserted after the given number.
+
+        :param fileobj: A File Object or an object that supports the standard
+            read and seek methods similar to a File Object. Could also be a
+            string representing a path to a PDF file.
+
+        :param str outline_item: Optionally, you may specify a string to build an outline
+            (aka 'bookmark') to identify the
+            beginning of the included file.
+
+        :param pages: can be a :class:`PageRange<PyPDF2.pagerange.PageRange>`
+            or a ``(start, stop[, step])`` tuple
+            or a list of pages to be processed
+            to merge only the specified range of pages from the source
+            document into the output document.
+
+        :param bool import_outline: You may prevent the source document's
+            outline (collection of outline items, previously referred to as
+            'bookmarks') from being imported by specifying this as ``False``.
+
+        :param List excluded_fields: provide the list of fields/keys to be ignored
+            if "/Annots" is part of the list, the annotation will be ignored
+            if "/B" is part of the list, the articles will be ignored
+        """
+        if isinstance(fileobj, PdfReader):
+            reader = fileobj
+        else:
+            stream, encryption_obj = self._create_stream(fileobj)
+            # Create a new PdfReader instance using the stream
+            # (either file or BytesIO or StringIO) created above
+            reader = PdfReader(stream, strict=False)  # type: ignore[arg-type]
+
+        if excluded_fields is None:
+            excluded_fields = ()
+        # Find the range of pages to merge.
+        if pages is None:
+            pages = list(range(0, len(reader.pages)))
+        elif isinstance(pages, PageRange):
+            pages = list(range(*pages.indices(len(reader.pages))))
+        elif isinstance(pages, list):
+            pass  # keep unchanged
+        elif isinstance(pages, tuple) and len(pages) <= 3:
+            pages = list(range(*pages))
+        elif not isinstance(pages, tuple):
+            raise TypeError(
+                '"pages" must be a tuple of (start, stop[, step]) or a list'
+            )
+
+        srcpages = {}
+        for i in pages:
+            pg = reader.pages[i]
+            assert pg.indirect_reference is not None
+            if position is None:
+                srcpages[pg.indirect_reference.idnum] = self.add_page(
+                    pg, list(excluded_fields) + ["/B", "/Annots"]  # type: ignore
+                )
+            else:
+                srcpages[pg.indirect_reference.idnum] = self.insert_page(
+                    pg, position, list(excluded_fields) + ["/B", "/Annots"]  # type: ignore
+                )
+                position += 1
+            srcpages[pg.indirect_reference.idnum].original_page = pg
+
+        reader._namedDests = (
+            reader.named_destinations
+        )  # need for the outline processing below
+        for dest in reader._namedDests.values():
+            arr = dest.dest_array
+            # try:
+            if isinstance(dest["/Page"], NullObject):
+                pass  # self.add_named_destination_array(dest["/Title"],arr)
+            elif dest["/Page"].indirect_reference.idnum in srcpages:
+                arr[NumberObject(0)] = srcpages[
+                    dest["/Page"].indirect_reference.idnum
+                ].indirect_reference
+                self.add_named_destination_array(dest["/Title"], arr)
+            # except Exception as e:
+            #    logger_warning(f"can not insert {dest} : {e.msg}",__name__)
+
+        outline_item_typ: TreeObject
+        if outline_item is not None:
+            outline_item_typ = cast(
+                "TreeObject",
+                self.add_outline_item(
+                    TextStringObject(outline_item),
+                    list(srcpages.values())[0].indirect_reference,
+                    fit=PAGE_FIT,
+                ).get_object(),
+            )
+        else:
+            outline_item_typ = self.get_outline_root()
+
+        _ro = cast("DictionaryObject", reader.trailer[TK.ROOT])
+        if import_outline and CO.OUTLINES in _ro:
+            outline = self._get_filtered_outline(
+                _ro.get(CO.OUTLINES, None), srcpages, reader
+            )
+            self._insert_filtered_outline(
+                outline, outline_item_typ, None
+            )  # TODO : use before parameter
+
+        if "/Annots" not in excluded_fields:
+            for pag in srcpages.values():
+                lst = self._insert_filtered_annotations(
+                    pag.original_page.get("/Annots", ()), pag, srcpages, reader
+                )
+                if len(lst) > 0:
+                    pag[NameObject("/Annots")] = lst
+                self.clean_page(pag)
+
+        if "/B" not in excluded_fields:
+            self.add_filtered_articles("", srcpages, reader)
+
+        return
+
+    def _add_articles_thread(
+        self,
+        thread: DictionaryObject,  # thread entry from the reader's array of threads
+        pages: Dict[int, PageObject],
+        reader: PdfReader,
+    ) -> IndirectObject:
+        """
+        clone the thread with only the applicable articles
+
+        """
+        nthread = thread.clone(
+            self, force_duplicate=True, ignore_fields=("/F",)
+        )  # use of clone to keep link between reader and writer
+        self.threads.append(nthread.indirect_reference)
+        first_article = cast("DictionaryObject", thread["/F"])
+        current_article: Optional[DictionaryObject] = first_article
+        new_article: Optional[DictionaryObject] = None
+        while current_article is not None:
+            pag = self._get_cloned_page(
+                cast("PageObject", current_article["/P"]), pages, reader
+            )
+            if pag is not None:
+                if new_article is None:
+                    new_article = cast(
+                        "DictionaryObject",
+                        self._add_object(DictionaryObject()).get_object(),
+                    )
+                    new_first = new_article
+                    nthread[NameObject("/F")] = new_article.indirect_reference
+                else:
+                    new_article2 = cast(
+                        "DictionaryObject",
+                        self._add_object(
+                            DictionaryObject(
+                                {NameObject("/V"): new_article.indirect_reference}
+                            )
+                        ).get_object(),
+                    )
+                    new_article[NameObject("/N")] = new_article2.indirect_reference
+                    new_article = new_article2
+                new_article[NameObject("/P")] = pag
+                new_article[NameObject("/T")] = nthread.indirect_reference
+                new_article[NameObject("/R")] = current_article["/R"]
+                pag_obj = cast("PageObject", pag.get_object())
+                if "/B" not in pag_obj:
+                    pag_obj[NameObject("/B")] = ArrayObject()
+                cast("ArrayObject", pag_obj["/B"]).append(
+                    new_article.indirect_reference
+                )
+            current_article = cast("DictionaryObject", current_article["/N"])
+            if current_article == first_article:
+                new_article[NameObject("/N")] = new_first.indirect_reference  # type: ignore
+                new_first[NameObject("/V")] = new_article.indirect_reference  # type: ignore
+                current_article = None
+        assert nthread.indirect_reference is not None
+        return nthread.indirect_reference
+
+    def add_filtered_articles(
+        self,
+        fltr: Union[Pattern, str],  # thread entry from the reader's array of threads
+        pages: Dict[int, PageObject],
+        reader: PdfReader,
+    ) -> None:
+        """
+        Add articles matching the defined criteria
+        """
+        if isinstance(fltr, str):
+            fltr = re.compile(fltr)
+        elif not isinstance(fltr, Pattern):
+            fltr = re.compile("")
+        for p in pages.values():
+            pp = p.original_page
+            for a in pp.get("/B", ()):
+                thr = a.get_object()["/T"]
+                if thr.indirect_reference.idnum not in self._id_translated[
+                    id(reader)
+                ] and fltr.search(thr["/I"]["/Title"]):
+                    self._add_articles_thread(thr, pages, reader)
+
+    def _get_cloned_page(
+        self,
+        page: Union[None, int, IndirectObject, PageObject, NullObject],
+        pages: Dict[int, PageObject],
+        reader: PdfReader,
+    ) -> Optional[IndirectObject]:
+        if isinstance(page, NullObject):
+            return None
+        if isinstance(page, int):
+            _i = reader.pages[page].indirect_reference
+        # elif isinstance(page, PageObject):
+        #    _i = page.indirect_reference
+        elif isinstance(page, DictionaryObject) and page.get("/Type", "") == "/Page":
+            _i = page.indirect_reference
+        elif isinstance(page, IndirectObject):
+            _i = page
+        try:
+            return pages[_i.idnum].indirect_reference  # type: ignore
+        except Exception:
+            return None
+
+    def _insert_filtered_annotations(
+        self,
+        annots: Union[IndirectObject, List[DictionaryObject]],
+        page: PageObject,
+        pages: Dict[int, PageObject],
+        reader: PdfReader,
+    ) -> List[Destination]:
+        outlist = ArrayObject()
+        if isinstance(annots, IndirectObject):
+            annots = cast("List", annots.get_object())
+        for an in annots:
+            ano = cast("DictionaryObject", an.get_object())
+            if (
+                ano["/Subtype"] != "/Link"
+                or "/A" not in ano
+                or cast("DictionaryObject", ano["/A"])["/S"] != "/GoTo"
+                or "/Dest" in ano
+            ):
+                if "/Dest" not in ano:
+                    outlist.append(ano.clone(self).indirect_reference)
+                else:
+                    d = ano["/Dest"]
+                    if isinstance(d, str):
+                        # it is a named dest
+                        if str(d) in self.get_named_dest_root():
+                            outlist.append(ano.clone(self).indirect_reference)
+                    else:
+                        d = cast("ArrayObject", d)
+                        p = self._get_cloned_page(d[0], pages, reader)
+                        if p is not None:
+                            anc = ano.clone(self, ignore_fields=("/Dest",))
+                            anc[NameObject("/Dest")] = ArrayObject([p] + d[1:])
+                            outlist.append(anc.indirect_reference)
+            else:
+                d = cast("DictionaryObject", ano["/A"])["/D"]
+                if isinstance(d, str):
+                    # it is a named dest
+                    if str(d) in self.get_named_dest_root():
+                        outlist.append(ano.clone(self).indirect_reference)
+                else:
+                    d = cast("ArrayObject", d)
+                    p = self._get_cloned_page(d[0], pages, reader)
+                    if p is not None:
+                        anc = ano.clone(self, ignore_fields=("/D",))
+                        anc = cast("DictionaryObject", anc)
+                        cast("DictionaryObject", anc["/A"])[
+                            NameObject("/D")
+                        ] = ArrayObject([p] + d[1:])
+                        outlist.append(anc.indirect_reference)
+        return outlist
+
+    def _get_filtered_outline(
+        self,
+        node: Any,
+        pages: Dict[int, PageObject],
+        reader: PdfReader,
+    ) -> List[Destination]:
+        """Extract outline item entries that are part of the specified page set."""
+        new_outline = []
+        node = node.get_object()
+        if node.get("/Type", "") == "/Outlines" or "/Title" not in node:
+            node = node.get("/First", None)
+            if node is not None:
+                node = node.get_object()
+                new_outline += self._get_filtered_outline(node, pages, reader)
+        else:
+            v: Union[None, IndirectObject, NullObject]
+            while node is not None:
+                node = node.get_object()
+                o = cast("Destination", reader._build_outline_item(node))
+                v = self._get_cloned_page(cast("PageObject", o["/Page"]), pages, reader)
+                if v is None:
+                    v = NullObject()
+                o[NameObject("/Page")] = v
+                if "/First" in node:
+                    o.childs = self._get_filtered_outline(node["/First"], pages, reader)
+                else:
+                    o.childs = []
+                if not isinstance(o["/Page"], NullObject) or len(o.childs) > 0:
+                    new_outline.append(o)
+                node = node.get("/Next", None)
+        return new_outline
+
+    def _clone_outline(self, dest: Destination) -> TreeObject:
+        n_ol = TreeObject()
+        self._add_object(n_ol)
+        n_ol[NameObject("/Title")] = TextStringObject(dest["/Title"])
+        if not isinstance(dest["/Page"], NullObject):
+            if dest.node is not None and "/A" in dest.node:
+                n_ol[NameObject("/A")] = dest.node["/A"].clone(self)
+            # elif "/D" in dest.node:
+            #    n_ol[NameObject("/Dest")] = dest.node["/D"].clone(self)
+            # elif "/Dest" in dest.node:
+            #    n_ol[NameObject("/Dest")] = dest.node["/Dest"].clone(self)
+            else:
+                n_ol[NameObject("/Dest")] = dest.dest_array
+        # TODO: /SE
+        if dest.node is not None:
+            n_ol[NameObject("/F")] = NumberObject(dest.node.get("/F", 0))
+            n_ol[NameObject("/C")] = ArrayObject(
+                dest.node.get(
+                    "/C", [FloatObject(0.0), FloatObject(0.0), FloatObject(0.0)]
+                )
+            )
+        return n_ol
+
+    def _insert_filtered_outline(
+        self,
+        outlines: List[Destination],
+        parent: Union[TreeObject, IndirectObject],
+        before: Union[None, TreeObject, IndirectObject] = None,
+    ) -> None:
+        for dest in outlines:
+            # TODO  : can be improved to keep A and SE entries (ignored for the moment)
+            # np=self.add_outline_item_destination(dest,parent,before)
+            if dest.get("/Type", "") == "/Outlines" or "/Title" not in dest:
+                np = parent
+            else:
+                np = self._clone_outline(dest)
+                cast(TreeObject, parent.get_object()).insert_child(np, before, self)
+            self._insert_filtered_outline(dest.childs, np, None)
+
+    def close(self) -> None:
+        """To match the functions from Merger"""
+        return
+
+    # @deprecation_bookmark(bookmark="outline_item")
+    def find_outline_item(
+        self,
+        outline_item: Dict[str, Any],
+        root: Optional[OutlineType] = None,
+    ) -> Optional[List[int]]:
+        if root is None:
+            o = self.get_outline_root()
+        else:
+            o = cast("TreeObject", root)
+
+        i = 0
+        while o is not None:
+            if (
+                o.indirect_reference == outline_item
+                or o.get("/Title", None) == outline_item
+            ):
+                return [i]
+            else:
+                if "/First" in o:
+                    res = self.find_outline_item(
+                        outline_item, cast(OutlineType, o["/First"])
+                    )
+                    if res:
+                        return ([i] if "/Title" in o else []) + res
+            if "/Next" in o:
+                i += 1
+                o = cast(TreeObject, o["/Next"])
+            else:
+                return None
+
+    @deprecation_bookmark(bookmark="outline_item")
+    def find_bookmark(
+        self,
+        outline_item: Dict[str, Any],
+        root: Optional[OutlineType] = None,
+    ) -> Optional[List[int]]:  # pragma: no cover
+        """
+        .. deprecated:: 2.9.0
+            Use :meth:`find_outline_item` instead.
+        """
+        return self.find_outline_item(outline_item, root)
+
+    def reset_translation(
+        self, reader: Union[None, PdfReader, IndirectObject] = None
+    ) -> None:
+        """
+        reset the translation table between reader and the writer object.
+        late cloning will create new independent objects
+
+        :param reader: PdfReader or IndirectObject refering a PdfReader object.
+                       if set to None or omitted, all tables will be reset.
+        """
+        if reader is None:
+            self._id_translated = {}
+        elif isinstance(reader, PdfReader):
+            try:
+                del self._id_translated[id(reader)]
+            except Exception:
+                pass
+        elif isinstance(reader, IndirectObject):
+            try:
+                del self._id_translated[id(reader.pdf)]
+            except Exception:
+                pass
+        else:
+            raise Exception("invalid parameter {reader}")
+
+
+def _pdf_objectify(obj: Union[Dict[str, Any], str, int, List[Any]]) -> PdfObject:
+    if isinstance(obj, PdfObject):
+        return obj
+    if isinstance(obj, dict):
+        to_add = DictionaryObject()
+        for key, value in obj.items():
+            name_key = NameObject(key)
+            casted_value = _pdf_objectify(value)
+            to_add[name_key] = casted_value
+        return to_add
+    elif isinstance(obj, list):
+        arr = ArrayObject()
+        for el in obj:
+            arr.append(_pdf_objectify(el))
+        return arr
+    elif isinstance(obj, str):
+        if obj.startswith("/"):
+            return NameObject(obj)
+        else:
+            return TextStringObject(obj)
+    elif isinstance(obj, (int, float)):
+        return FloatObject(obj)
+    else:
+        raise NotImplementedError(
+            f"type(obj)={type(obj)} could not be casted to PdfObject"
+        )
+
+
+def _create_outline_item(
+    action_ref: Union[None, IndirectObject],
+    title: str,
+    color: Union[Tuple[float, float, float], str, None],
+    italic: bool,
+    bold: bool,
+) -> TreeObject:
+    outline_item = TreeObject()
+    if action_ref is not None:
+        outline_item[NameObject("/A")] = action_ref
+    outline_item.update(
+        {
+            NameObject("/Title"): create_string_object(title),
+        }
+    )
+    if color:
+        if isinstance(color, str):
+            color = hex_to_rgb(color)
+        prec = decimal.Decimal("1.00000")
+        outline_item.update(
+            {
+                NameObject("/C"): ArrayObject(
+                    [FloatObject(decimal.Decimal(c).quantize(prec)) for c in color]
+                )
+            }
+        )
+    if italic or bold:
+        format_flag = 0
+        if italic:
+            format_flag += 1
+        if bold:
+            format_flag += 2
+        outline_item.update({NameObject("/F"): NumberObject(format_flag)})
+    return outline_item
+
+
+class PdfFileWriter(PdfWriter):  # pragma: no cover
+    def __init__(self, *args: Any, **kwargs: Any) -> None:
+        deprecation_with_replacement("PdfFileWriter", "PdfWriter", "3.0.0")
+        super().__init__(*args, **kwargs)