content: import itertools
  import logging
  import pathlib
  from io import BufferedReader, BytesIO
  from types import TracebackType
  from typing import Any, Dict, Generator, List, Literal, Optional, Tuple, Type, Union
  
  from pdfminer.layout import LAParams
  from pdfminer.pdfdocument import PDFDocument
  from pdfminer.pdfinterp import PDFResourceManager
  from pdfminer.pdfpage import PDFPage
  from pdfminer.pdfparser import PDFParser
  
  from ._typing import T_num, T_obj_list
  from .container import Container
  from .page import Page
  from .repair import T_repair_setting, _repair
  from .structure import PDFStructTree, StructTreeMissing
  from .utils import resolve_and_decode
  from .utils.exceptions import PdfminerException
  
  logger = logging.getLogger(__name__)
  
  
  class PDF(Container):
      cached_properties: List[str] = Container.cached_properties + ["_pages"]
  
      def __init__(
          self,
          stream: Union[BufferedReader, BytesIO],
          stream_is_external: bool = False,
          path: Optional[pathlib.Path] = None,
          pages: Optional[Union[List[int], Tuple[int]]] = None,
          laparams: Optional[Dict[str, Any]] = None,
          password: Optional[str] = None,
          strict_metadata: bool = False,
          unicode_norm: Optional[Literal["NFC", "NFKC", "NFD", "NFKD"]] = None,
          raise_unicode_errors: bool = True,
      ):
          self.stream = stream
          self.stream_is_external = stream_is_external
          self.path = path
          self.pages_to_parse = pages
          self.laparams = None if laparams is None else LAParams(**laparams)
          self.password = password
          self.unicode_norm = unicode_norm
          self.raise_unicode_errors = raise_unicode_errors
  
          try:
              self.doc = PDFDocument(PDFParser(stream), password=password or "")
          except Exception as e:
              raise PdfminerException(e)
          self.rsrcmgr = PDFResourceManager()
          self.metadata = {}
  
          for info in self.doc.info:
              self.metadata.update(info)
          for k, v in self.metadata.items():
              try:
                  self.metadata[k] = resolve_and_decode(v)
              except Exception as e:  # pragma: nocover
                  if strict_metadata:
                      # Raise an exception since unable to resolve the metadata value.
                      raise
                  # This metadata value could not be parsed. Instead of failing the PDF
                  # read, treat it as a warning only if `strict_metadata=False`.
                  logger.warning(
                      f'[WARNING] Metadata key "{k}" could not be parsed due to '
                      f"exception: {str(e)}"
                  )
  
      @classmethod
      def open(
          cls,
          path_or_fp: Union[str, pathlib.Path, BufferedReader, BytesIO],
          pages: Optional[Union[List[int], Tuple[int]]] = None,
          laparams: Optional[Dict[str, Any]] = None,
          password: Optional[str] = None,
          strict_metadata: bool = False,
          unicode_norm: Optional[Literal["NFC", "NFKC", "NFD", "NFKD"]] = None,
          repair: bool = False,
          gs_path: Optional[Union[str, pathlib.Path]] = None,
          repair_setting: T_repair_setting = "default",
          raise_unicode_errors: bool = True,
      ) -> "PDF":
  
          stream: Union[BufferedReader, BytesIO]
  
          if repair:
              stream = _repair(
                  path_or_fp, password=password, gs_path=gs_path, setting=repair_setting
              )
              stream_is_external = False
              # Although the original file has a path,
              # the repaired version does not
              path = None
          elif isinstance(path_or_fp, (str, pathlib.Path)):
              stream = open(path_or_fp, "rb")
              stream_is_external = False
              path = pathlib.Path(path_or_fp)
          else:
              stream = path_or_fp
              stream_is_external = True
              path = None
  
          try:
              return cls(
                  stream,
                  path=path,
                  pages=pages,
                  laparams=laparams,
                  password=password,
                  strict_metadata=strict_metadata,
                  unicode_norm=unicode_norm,
                  stream_is_external=stream_is_external,
                  raise_unicode_errors=raise_unicode_errors,
              )
  
          except PdfminerException:
              if not stream_is_external:
                  stream.close()
              raise
  
      def close(self) -> None:
          self.flush_cache()
  
          for page in self.pages:
              page.close()
  
          if not self.stream_is_external:
              self.stream.close()
  
      def __enter__(self) -> "PDF":
          return self
  
      def __exit__(
          self,
          t: Optional[Type[BaseException]],
          value: Optional[BaseException],
          traceback: Optional[TracebackType],
      ) -> None:
          self.close()
  
      @property
      def pages(self) -> List[Page]:
          if hasattr(self, "_pages"):
              return self._pages
  
          doctop: T_num = 0
          pp = self.pages_to_parse
          self._pages: List[Page] = []
  
          def iter_pages() -> Generator[PDFPage, None, None]:
              gen = PDFPage.create_pages(self.doc)
              while True:
                  try:
                      yield next(gen)
                  except StopIteration:
                      break
                  except Exception as e:
                      raise PdfminerException(e)
  
          for i, page in enumerate(iter_pages()):
              page_number = i + 1
              if pp is not None and page_number not in pp:
                  continue
              p = Page(self, page, page_number=page_number, initial_doctop=doctop)
              self._pages.append(p)
              doctop += p.height
          return self._pages
  
      @property
      def objects(self) -> Dict[str, T_obj_list]:
          if hasattr(self, "_objects"):
              return self._objects
          all_objects: Dict[str, T_obj_list] = {}
          for p in self.pages:
              for kind in p.objects.keys():
                  all_objects[kind] = all_objects.get(kind, []) + p.objects[kind]
          self._objects: Dict[str, T_obj_list] = all_objects
          return self._objects
  
      @property
      def annots(self) -> List[Dict[str, Any]]:
          gen = (p.annots for p in self.pages)
          return list(itertools.chain(*gen))
  
      @property
      def hyperlinks(self) -> List[Dict[str, Any]]:
          gen = (p.hyperlinks for p in self.pages)
          return list(itertools.chain(*gen))
  
      @property
      def structure_tree(self) -> List[Dict[str, Any]]:
          """Return the structure tree for the document."""
          try:
              return [elem.to_dict() for elem in PDFStructTree(self)]
          except StructTreeMissing:
              return []
  
      def to_dict(self, object_types: Optional[List[str]] = None) -> Dict[str, Any]:
          return {
              "metadata": self.metadata,
              "pages": [page.to_dict(object_types) for page in self.pages],
          }
  