""" Quoting section 8.2.2 "Document Outline" of the 2006 PDF spec 1.7: > The document outline consists of a tree-structured hierarchy of outline items (sometimes called bookmarks), > which serve as a visual table of contents to display the document’s structure to the user. """ from typing import NamedTuple, Optional from .syntax import Destination, PDFObject, PDFString from .structure_tree import StructElem class OutlineSection(NamedTuple): name: str level: str page_number: int dest: Destination struct_elem: Optional[StructElem] = None class OutlineItemDictionary(PDFObject): __slots__ = ( "_id", "title", "parent", "prev", "next", "first", "last", "count", "dest", "struct_elem", ) def __init__( self, title: str, dest: str = None, struct_elem: StructElem = None, **kwargs ): super().__init__(**kwargs) self.title = PDFString(title) self.parent = None self.prev = None self.next = None self.first = None self.last = None self.count = 0 self.dest = dest self.struct_elem = struct_elem class OutlineDictionary(PDFObject): __slots__ = ("_id", "type", "first", "last", "count") def __init__(self, **kwargs): super().__init__(**kwargs) self.type = "/Outlines" self.first = None self.last = None self.count = 0 def serialize_outline(sections, first_object_id=1, fpdf=None): """ Assign object IDs & output the whole outline hierarchy serialized as a multi-lines string in PDF syntax, ready to be embedded. Objects ID assignement will start with the provided first ID, that will be assigned to the Outlines object. Apart from that, assignement is made in an arbitrary order. All PDF objects must have assigned IDs before proceeding to output generation though, as they have many references to each others. If a FPDF instance provided, its `_newobj` & `_out` methods will be called and this method output will be meaningless. Args: sections (sequence): list of OutlineSection """ outline = OutlineDictionary(id=first_object_id) n = first_object_id + 1 outline_items = [] last_outline_item_per_level = {} for section in sections: outline_item = OutlineItemDictionary( title=section.name, dest=section.dest.as_str(fpdf), struct_elem=section.struct_elem, id=n, ) n += 1 if section.level in last_outline_item_per_level: last_outline_item_at_level = last_outline_item_per_level[section.level] last_outline_item_at_level.next = outline_item outline_item.prev = last_outline_item_at_level if section.level - 1 in last_outline_item_per_level: parent_outline_item = last_outline_item_per_level[section.level - 1] else: parent_outline_item = outline outline_item.parent = parent_outline_item if parent_outline_item.first is None: parent_outline_item.first = outline_item parent_outline_item.last = outline_item parent_outline_item.count += 1 outline_items.append(outline_item) last_outline_item_per_level[section.level] = outline_item last_outline_item_per_level = { level: oitem for level, oitem in last_outline_item_per_level.items() if level <= section.level } output = [] output.append(outline.serialize(fpdf)) for outline_item in outline_items: output.append(outline_item.serialize(fpdf)) return "\n".join(output)