diff options
author | 2022-11-13 23:46:45 +0530 | |
---|---|---|
committer | 2022-11-13 23:46:45 +0530 | |
commit | 9468226a9e2e2ab8cdd599f1d8538e860ca86120 (patch) | |
tree | 0a77ada226d6db80639f96b438bf83e4e756edb5 /env/lib/python3.10/site-packages/pikepdf/models/outlines.py | |
download | idcard-9468226a9e2e2ab8cdd599f1d8538e860ca86120.tar.gz idcard-9468226a9e2e2ab8cdd599f1d8538e860ca86120.tar.bz2 idcard-9468226a9e2e2ab8cdd599f1d8538e860ca86120.zip |
id card generator
Diffstat (limited to 'env/lib/python3.10/site-packages/pikepdf/models/outlines.py')
-rw-r--r-- | env/lib/python3.10/site-packages/pikepdf/models/outlines.py | 421 |
1 files changed, 421 insertions, 0 deletions
diff --git a/env/lib/python3.10/site-packages/pikepdf/models/outlines.py b/env/lib/python3.10/site-packages/pikepdf/models/outlines.py new file mode 100644 index 0000000..1143de6 --- /dev/null +++ b/env/lib/python3.10/site-packages/pikepdf/models/outlines.py @@ -0,0 +1,421 @@ +# SPDX-FileCopyrightText: 2022 James R. Barlow, 2020 Matthias Erll + +# SPDX-License-Identifier: MPL-2.0 + +"""Support for document outlines (e.g. table of contents).""" + +from __future__ import annotations + +from enum import Enum +from itertools import chain +from typing import Iterable, List, cast + +from pikepdf import Array, Dictionary, Name, Object, Page, Pdf, String + + +class PageLocation(Enum): + """Page view location definitions, from PDF spec.""" + + XYZ = 1 + Fit = 2 + FitH = 3 + FitV = 4 + FitR = 5 + FitB = 6 + FitBH = 7 + FitBV = 8 + + +PAGE_LOCATION_ARGS = { + PageLocation.XYZ: ('left', 'top', 'zoom'), + PageLocation.FitH: ('top',), + PageLocation.FitV: ('left',), + PageLocation.FitR: ('left', 'bottom', 'right', 'top'), + PageLocation.FitBH: ('top',), + PageLocation.FitBV: ('left',), +} +ALL_PAGE_LOCATION_KWARGS = set(chain.from_iterable(PAGE_LOCATION_ARGS.values())) + + +def make_page_destination( + pdf: Pdf, + page_num: int, + page_location: PageLocation | str | None = None, + *, + left: float | None = None, + top: float | None = None, + right: float | None = None, + bottom: float | None = None, + zoom: float | None = None, +) -> Array: + """ + Create a destination ``Array`` with reference to a Pdf document's page number. + + Arguments: + pdf: PDF document object. + page_num: Page number (zero-based). + page_location: Optional page location, as a string or :enum:`PageLocation`. + left: Specify page viewport rectangle. + top: Specify page viewport rectangle. + right: Specify page viewport rectangle. + bottom: Specify page viewport rectangle. + zoom: Specify page viewport rectangle's zoom level. + + left, top, right, bottom, zoom are used in conjunction with the page fit style + specified by *page_location*. + """ + return _make_page_destination( + pdf, + page_num, + page_location=page_location, + left=left, + top=top, + right=right, + bottom=bottom, + zoom=zoom, + ) + + +def _make_page_destination( + pdf: Pdf, + page_num: int, + page_location: PageLocation | str | None = None, + **kwargs, +) -> Array: + kwargs = {k: v for k, v in kwargs.items() if v is not None} + + res: list[Dictionary | Name] = [pdf.pages[page_num].obj] + if page_location: + if isinstance(page_location, PageLocation): + loc_key = page_location + loc_str = loc_key.name + else: + loc_str = page_location + try: + loc_key = PageLocation[loc_str] + except KeyError: + raise ValueError( + f"Invalid or unsupported page location type {loc_str}" + ) from None + res.append(Name(f'/{loc_str}')) + dest_arg_names = PAGE_LOCATION_ARGS.get(loc_key) + if dest_arg_names: + res.extend(kwargs.get(k, 0) for k in dest_arg_names) + else: + res.append(Name.Fit) + return Array(res) + + +class OutlineStructureError(Exception): + """Indicates an error in the outline data structure.""" + + +class OutlineItem: + """Manage a single item in a PDF document outlines structure. + + Includes nested items. + + Arguments: + title: Title of the outlines item. + destination: Page number, destination name, or any other PDF object + to be used as a reference when clicking on the outlines entry. Note + this should be ``None`` if an action is used instead. If set to a + page number, it will be resolved to a reference at the time of + writing the outlines back to the document. + page_location: Supplemental page location for a page number + in ``destination``, e.g. ``PageLocation.Fit``. May also be + a simple string such as ``'FitH'``. + action: Action to perform when clicking on this item. Will be ignored + during writing if ``destination`` is also set. + obj: ``Dictionary`` object representing this outlines item in a ``Pdf``. + May be ``None`` for creating a new object. If present, an existing + object is modified in-place during writing and original attributes + are retained. + left, top, bottom, right, zoom: Describes the viewport position associated + with a destination. + + This object does not contain any information about higher-level or + neighboring elements. + + Valid destination arrays: + [page /XYZ left top zoom] + generally + [page, PageLocationEntry, 0 to 4 ints] + """ + + def __init__( + self, + title: str, + destination: Array | String | Name | int | None = None, + page_location: PageLocation | str | None = None, + action: Dictionary | None = None, + obj: Dictionary | None = None, + *, + left: float | None = None, + top: float | None = None, + right: float | None = None, + bottom: float | None = None, + zoom: float | None = None, + ): + self.title = title + self.destination = destination + self.page_location = page_location + self.page_location_kwargs = {} + self.action = action + if self.destination is not None and self.action is not None: + raise ValueError("Only one of destination and action may be set") + self.obj = obj + kwargs = dict(left=left, top=top, right=right, bottom=bottom, zoom=zoom) + self.page_location_kwargs = {k: v for k, v in kwargs.items() if v is not None} + self.is_closed = False + self.children: list[OutlineItem] = [] + + def __str__(self): + if self.children: + if self.is_closed: + oc_indicator = '[+]' + else: + oc_indicator = '[-]' + else: + oc_indicator = '[ ]' + if self.destination is not None: + if isinstance(self.destination, Array): + # 12.3.2.2 Explicit destination + # [raw_page, /PageLocation.SomeThing, integer parameters for viewport] + raw_page = self.destination[0] + page = Page(raw_page) + dest = page.label + elif isinstance(self.destination, String): + # 12.3.2.2 Named destination, byte string reference to Names + dest = f'<Named Destination in document .Root.Names dictionary: {self.destination}>' + elif isinstance(self.destination, Name): + # 12.3.2.2 Named destination, name object (PDF 1.1) + dest = f'<Named Destination in document .Root.Dests dictionary: {self.destination}>' + elif isinstance(self.destination, int): + # Page number + dest = f'<Page {self.destination}>' + else: + dest = '<Action>' + return f'{oc_indicator} {self.title} -> {dest}' + + def __repr__(self): + return f'<pikepdf.{self.__class__.__name__}: "{self.title}">' + + @classmethod + def from_dictionary_object(cls, obj: Dictionary): + """Creates a ``OutlineItem`` from a ``Dictionary``. + + Does not process nested items. + + Arguments: + obj: ``Dictionary`` object representing a single outline node. + """ + title = str(obj.Title) + destination = obj.get(Name.Dest) + if destination is not None and not isinstance( + destination, (Array, String, Name) + ): + # 12.3.3: /Dest may be a name, byte string or array + raise OutlineStructureError( + f"Unexpected object type in Outline's /Dest: {destination!r}" + ) + action = obj.get(Name.A) + if action is not None and not isinstance(action, Dictionary): + raise OutlineStructureError( + f"Unexpected object type in Outline's /A: {action!r}" + ) + return cls(title, destination=destination, action=action, obj=obj) + + def to_dictionary_object(self, pdf: Pdf, create_new: bool = False) -> Dictionary: + """Creates/updates a ``Dictionary`` object from this outline node. + + Page numbers are resolved to a page reference on the input + ``Pdf`` object. + + Arguments: + pdf: PDF document object. + create_new: If set to ``True``, creates a new object instead of + modifying an existing one in-place. + """ + if create_new or self.obj is None: + self.obj = obj = pdf.make_indirect(Dictionary()) + else: + obj = self.obj + obj.Title = self.title + if self.destination is not None: + if isinstance(self.destination, int): + self.destination = make_page_destination( + pdf, + self.destination, + self.page_location, + **self.page_location_kwargs, + ) + obj.Dest = self.destination + if Name.A in obj: + del obj.A + elif self.action is not None: + obj.A = self.action + if Name.Dest in obj: + del obj.Dest + return obj + + +class Outline: + """Maintains a intuitive interface for creating and editing PDF document outlines. + + See |pdfrm| section 12.3. + + Arguments: + pdf: PDF document object. + max_depth: Maximum recursion depth to consider when reading the outline. + strict: If set to ``False`` (default) silently ignores structural errors. + Setting it to ``True`` raises a + :class:`pikepdf.OutlineStructureError` + if any object references re-occur while the outline is being read or + written. + + See Also: + :meth:`pikepdf.Pdf.open_outline` + """ + + def __init__(self, pdf: Pdf, max_depth: int = 15, strict: bool = False): + self._root: list[OutlineItem] | None = None + self._pdf = pdf + self._max_depth = max_depth + self._strict = strict + self._updating = False + + def __str__(self): + return str(self.root) + + def __repr__(self): + return f'<pikepdf.{self.__class__.__name__}: {len(self.root)} items>' + + def __enter__(self): + self._updating = True + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + try: + if exc_type is not None: + return + self._save() + finally: + self._updating = False + + def _save_level_outline( + self, + parent: Dictionary, + outline_items: Iterable[OutlineItem], + level: int, + visited_objs: set[tuple[int, int]], + ): + count = 0 + prev: Dictionary | None = None + first: Dictionary | None = None + for item in outline_items: + out_obj = item.to_dictionary_object(self._pdf) + objgen = out_obj.objgen + if objgen in visited_objs: + if self._strict: + raise OutlineStructureError( + f"Outline object {objgen} reoccurred in structure" + ) + out_obj = item.to_dictionary_object(self._pdf, create_new=True) + else: + visited_objs.add(objgen) + + out_obj.Parent = parent + count += 1 + if prev is not None: + prev.Next = out_obj + out_obj.Prev = prev + else: + first = out_obj + if Name.Prev in out_obj: + del out_obj.Prev + prev = out_obj + if level < self._max_depth: + sub_items: Iterable[OutlineItem] = item.children + else: + sub_items = () + self._save_level_outline(out_obj, sub_items, level + 1, visited_objs) + if item.is_closed: + out_obj.Count = -cast(int, out_obj.Count) + else: + count += cast(int, out_obj.Count) + if count: + assert prev is not None and first is not None + if Name.Next in prev: + del prev.Next + parent.First = first + parent.Last = prev + else: + if Name.First in parent: + del parent.First + if Name.Last in parent: + del parent.Last + parent.Count = count + + def _load_level_outline( + self, + first_obj: Dictionary, + outline_items: list[Object], + level: int, + visited_objs: set[tuple[int, int]], + ): + current_obj: Dictionary | None = first_obj + while current_obj: + objgen = current_obj.objgen + if objgen in visited_objs: + if self._strict: + raise OutlineStructureError( + f"Outline object {objgen} reoccurred in structure" + ) + return + visited_objs.add(objgen) + + item = OutlineItem.from_dictionary_object(current_obj) + first_child = current_obj.get(Name.First) + if isinstance(first_child, Dictionary) and level < self._max_depth: + self._load_level_outline( + first_child, item.children, level + 1, visited_objs + ) + count = current_obj.get(Name.Count) + if isinstance(count, int) and count < 0: + item.is_closed = True + outline_items.append(item) + next_obj = current_obj.get(Name.Next) + if next_obj is None or isinstance(next_obj, Dictionary): + current_obj = next_obj + else: + raise OutlineStructureError( + f"Outline object {objgen} points to non-dictionary" + ) + + def _save(self): + if self._root is None: + return + if Name.Outlines in self._pdf.Root: + outlines = self._pdf.Root.Outlines + else: + self._pdf.Root.Outlines = outlines = self._pdf.make_indirect( + Dictionary(Type=Name.Outlines) + ) + self._save_level_outline(outlines, self._root, 0, set()) + + def _load(self): + self._root = root = [] + if Name.Outlines not in self._pdf.Root: + return + outlines = self._pdf.Root.Outlines or {} + first_obj = outlines.get(Name.First) + if first_obj: + self._load_level_outline(first_obj, root, 0, set()) + + @property + def root(self) -> list[OutlineItem]: + """Return the root node of the outline.""" + if self._root is None: + self._load() + return cast(List[OutlineItem], self._root) |