pdfox 0.1.0

A pure-Rust PDF library — create, parse, and render PDF documents with zero C dependencies
Documentation
/// PDF Outline (Bookmarks) support.
///
/// Outlines appear in the left-hand panel of PDF viewers and let readers jump
/// directly to sections. They form a tree: each item can have children.
///
/// PDF spec: § 12.3.3 Document outline

use crate::color::Color;
use crate::object::{ObjRef, PdfDict, PdfObject};
use crate::writer::PdfWriter;

/// Text style flags for an outline item
#[derive(Debug, Clone, Copy, Default)]
pub struct OutlineFlags {
    pub italic: bool,
    pub bold: bool,
}

impl OutlineFlags {
    /// Encode as the PDF /F integer bitmask
    fn to_pdf_flags(&self) -> i64 {
        let mut f = 0i64;
        if self.italic { f |= 1; }
        if self.bold   { f |= 2; }
        f
    }
}

/// A destination within the document
#[derive(Debug, Clone)]
pub enum Destination {
    /// Jump to the top of a page (0-indexed page number)
    Page(usize),
    /// Jump to a specific position on a page
    PageXY { page: usize, x: f64, y: f64, zoom: Option<f64> },
    /// Fit the entire page in view
    PageFit(usize),
}

/// A single outline (bookmark) item
#[derive(Debug, Clone)]
pub struct OutlineItem {
    pub title: String,
    pub dest: Destination,
    pub color: Option<Color>,
    pub flags: OutlineFlags,
    pub children: Vec<OutlineItem>,
    /// Whether children are initially collapsed
    pub collapsed: bool,
}

impl OutlineItem {
    pub fn new(title: impl Into<String>, dest: Destination) -> Self {
        Self {
            title: title.into(),
            dest,
            color: None,
            flags: OutlineFlags::default(),
            children: Vec::new(),
            collapsed: false,
        }
    }

    pub fn color(mut self, c: Color) -> Self {
        self.color = Some(c);
        self
    }

    pub fn bold(mut self) -> Self {
        self.flags.bold = true;
        self
    }

    pub fn italic(mut self) -> Self {
        self.flags.italic = true;
        self
    }

    pub fn collapsed(mut self) -> Self {
        self.collapsed = true;
        self
    }

    pub fn child(mut self, item: OutlineItem) -> Self {
        self.children.push(item);
        self
    }
}

/// The complete outline tree for a document
#[derive(Debug, Default)]
pub struct Outline {
    pub items: Vec<OutlineItem>,
}

impl Outline {
    pub fn new() -> Self {
        Self { items: Vec::new() }
    }

    pub fn add(mut self, item: OutlineItem) -> Self {
        self.items.push(item);
        self
    }

    pub fn is_empty(&self) -> bool {
        self.items.is_empty()
    }

    /// Write the entire outline tree to the PDF writer.
    /// Returns the ObjRef of the /Outlines root dictionary.
    pub fn write(
        &self,
        writer: &mut PdfWriter,
        page_refs: &[ObjRef],
    ) -> ObjRef {
        let root_ref = writer.reserve();

        // Write all items recursively, collecting first/last child refs
        let (first, last, count) = self.write_items(writer, &self.items, page_refs, root_ref);

        // Write the root /Outlines dict
        let mut root = PdfDict::new();
        root.set("Type", PdfObject::name("Outlines"));
        if let (Some(f), Some(l)) = (first, last) {
            root.set("First", PdfObject::Reference(f));
            root.set("Last",  PdfObject::Reference(l));
        }
        root.set("Count", PdfObject::Integer(count as i64));
        writer.write_object(root_ref, &PdfObject::Dictionary(root));

        root_ref
    }

    /// Recursively write a list of sibling items.
    /// Returns (first_ref, last_ref, open_count) for the parent to use.
    fn write_items(
        &self,
        writer: &mut PdfWriter,
        items: &[OutlineItem],
        page_refs: &[ObjRef],
        parent_ref: ObjRef,
    ) -> (Option<ObjRef>, Option<ObjRef>, usize) {
        if items.is_empty() {
            return (None, None, 0);
        }

        // Pre-allocate all refs at this level so we can set Prev/Next correctly
        let refs: Vec<ObjRef> = items.iter().map(|_| writer.reserve()).collect();
        let mut open_count = 0usize;

        for (i, (item, &item_ref)) in items.iter().zip(refs.iter()).enumerate() {
            let prev = if i > 0 { Some(refs[i - 1]) } else { None };
            let next = refs.get(i + 1).copied();

            // Recursively write children
            let (child_first, child_last, child_count) =
                self.write_items(writer, &item.children, page_refs, item_ref);

            // Count: positive = open (children visible), negative = closed
            let this_count = if item.collapsed {
                -(child_count as i64)
            } else {
                child_count as i64
            };

            if !item.collapsed {
                open_count += 1 + child_count;
            } else {
                open_count += 1;
            }

            let mut dict = PdfDict::new();
            dict.set("Title", PdfObject::string(item.title.as_str()));
            dict.set("Parent", PdfObject::Reference(parent_ref));
            if let Some(p) = prev { dict.set("Prev", PdfObject::Reference(p)); }
            if let Some(n) = next { dict.set("Next", PdfObject::Reference(n)); }
            if let (Some(f), Some(l)) = (child_first, child_last) {
                dict.set("First", PdfObject::Reference(f));
                dict.set("Last",  PdfObject::Reference(l));
                dict.set("Count", PdfObject::Integer(this_count));
            }

            // Destination
            let dest = build_dest(&item.dest, page_refs);
            if !dest.is_empty() {
                dict.set("Dest", PdfObject::Array(dest));
            }

            // Optional styling
            if let Some(col) = item.color {
                if let Color::Rgb(r, g, b) = col {
                    dict.set("C", PdfObject::Array(vec![
                        PdfObject::Real(r),
                        PdfObject::Real(g),
                        PdfObject::Real(b),
                    ]));
                }
            }
            let flags = item.flags.to_pdf_flags();
            if flags != 0 {
                dict.set("F", PdfObject::Integer(flags));
            }

            writer.write_object(item_ref, &PdfObject::Dictionary(dict));
        }

        (refs.first().copied(), refs.last().copied(), open_count)
    }
}

fn build_dest(dest: &Destination, page_refs: &[ObjRef]) -> Vec<PdfObject> {
    match dest {
        Destination::Page(n) => {
            if let Some(&r) = page_refs.get(*n) {
                vec![
                    PdfObject::Reference(r),
                    PdfObject::name("XYZ"),
                    PdfObject::Null,
                    PdfObject::Null,
                    PdfObject::Null,
                ]
            } else { vec![] }
        }
        Destination::PageXY { page, x, y, zoom } => {
            if let Some(&r) = page_refs.get(*page) {
                vec![
                    PdfObject::Reference(r),
                    PdfObject::name("XYZ"),
                    PdfObject::Real(*x),
                    PdfObject::Real(*y),
                    zoom.map(PdfObject::Real).unwrap_or(PdfObject::Null),
                ]
            } else { vec![] }
        }
        Destination::PageFit(n) => {
            if let Some(&r) = page_refs.get(*n) {
                vec![PdfObject::Reference(r), PdfObject::name("Fit")]
            } else { vec![] }
        }
    }
}