_link.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135
  1. # All rights reserved.
  2. #
  3. # Redistribution and use in source and binary forms, with or without
  4. # modification, are permitted provided that the following conditions are
  5. # met:
  6. #
  7. # * Redistributions of source code must retain the above copyright notice,
  8. # this list of conditions and the following disclaimer.
  9. # * Redistributions in binary form must reproduce the above copyright notice,
  10. # this list of conditions and the following disclaimer in the documentation
  11. # and/or other materials provided with the distribution.
  12. # * The name of the author may not be used to endorse or promote products
  13. # derived from this software without specific prior written permission.
  14. #
  15. # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  16. # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  17. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  18. # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  19. # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  20. # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  21. # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  22. # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  23. # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  24. # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
  25. # POSSIBILITY OF SUCH DAMAGE.
  26. # This module contains code used by _writer.py to track links in pages
  27. # being added to the writer until the links can be resolved.
  28. from typing import TYPE_CHECKING, Optional, Union, cast
  29. from .._utils import logger_warning
  30. from . import ArrayObject, DictionaryObject, IndirectObject, PdfObject, TextStringObject, is_null_or_none
  31. if TYPE_CHECKING:
  32. from .._page import PageObject
  33. from .._reader import PdfReader
  34. from .._writer import PdfWriter
  35. class NamedReferenceLink:
  36. """Named reference link being preserved until we can resolve it correctly."""
  37. def __init__(self, reference: TextStringObject, source_pdf: "PdfReader") -> None:
  38. """reference: TextStringObject with named reference"""
  39. self._reference = reference
  40. self._source_pdf = source_pdf
  41. def find_referenced_page(self) -> Union[IndirectObject, None]:
  42. destination = self._source_pdf.named_destinations.get(str(self._reference))
  43. return destination.page if destination else None
  44. def patch_reference(self, target_pdf: "PdfWriter", new_page: IndirectObject) -> None:
  45. """target_pdf: PdfWriter which the new link went into"""
  46. # point named destination in new PDF to the new page
  47. if str(self._reference) not in target_pdf.named_destinations:
  48. target_pdf.add_named_destination(str(self._reference), new_page.page_number)
  49. class DirectReferenceLink:
  50. """Direct reference link being preserved until we can resolve it correctly."""
  51. def __init__(self, reference: ArrayObject) -> None:
  52. """reference: an ArrayObject whose first element is the Page indirect object"""
  53. self._reference = reference
  54. def find_referenced_page(self) -> IndirectObject:
  55. return self._reference[0]
  56. def patch_reference(self, target_pdf: "PdfWriter", new_page: IndirectObject) -> None:
  57. """target_pdf: PdfWriter which the new link went into"""
  58. self._reference[0] = new_page
  59. ReferenceLink = Union[NamedReferenceLink, DirectReferenceLink]
  60. def extract_links(new_page: "PageObject", old_page: "PageObject") -> list[tuple[ReferenceLink, ReferenceLink]]:
  61. """Extracts links from two pages on the assumption that the two pages are
  62. the same. Produces one list of (new link, old link) tuples.
  63. """
  64. new_annotations = new_page.get("/Annots", ArrayObject()).get_object()
  65. old_annotations = old_page.get("/Annots", ArrayObject()).get_object()
  66. if is_null_or_none(new_annotations):
  67. new_annotations = ArrayObject()
  68. if is_null_or_none(old_annotations):
  69. old_annotations = ArrayObject()
  70. if not isinstance(new_annotations, ArrayObject) or not isinstance(old_annotations, ArrayObject):
  71. logger_warning(
  72. f"Expected annotation arrays: {old_annotations} {new_annotations}. Ignoring annotations.",
  73. __name__
  74. )
  75. return []
  76. # TODO: Investigate in https://github.com/py-pdf/pypdf/issues/3667
  77. # if len(new_annotations) != len(old_annotations):
  78. # logger_warning(f"Annotation sizes differ: {old_annotations} vs. {new_annotations}", __name__)
  79. new_links = [_build_link(link, new_page) for link in new_annotations]
  80. old_links = [_build_link(link, old_page) for link in old_annotations]
  81. return [
  82. (new_link, old_link) for (new_link, old_link)
  83. in zip(new_links, old_links)
  84. if new_link and old_link
  85. ]
  86. def _build_link(indirect_object: IndirectObject, page: "PageObject") -> Optional[ReferenceLink]:
  87. src = cast("PdfReader", page.pdf)
  88. link = cast(DictionaryObject, indirect_object.get_object())
  89. if (not isinstance(link, DictionaryObject)) or link.get("/Subtype") != "/Link":
  90. return None
  91. if "/A" in link:
  92. action = cast(DictionaryObject, link["/A"])
  93. if action.get("/S") != "/GoTo":
  94. return None
  95. if "/D" not in action:
  96. return None
  97. return _create_link(action["/D"], src)
  98. if "/Dest" in link:
  99. return _create_link(link["/Dest"], src)
  100. return None # Nothing to do here
  101. def _create_link(reference: PdfObject, source_pdf: "PdfReader") -> Optional[ReferenceLink]:
  102. if isinstance(reference, TextStringObject):
  103. return NamedReferenceLink(reference, source_pdf)
  104. if isinstance(reference, ArrayObject):
  105. return DirectReferenceLink(reference)
  106. return None