use super::document::reserve_bounded;
use super::types::PdfRef;
use crate::fallible::len_u64;
use crate::{Error, Limits, Result};
use std::fmt::Write as _;
use std::mem::size_of;
pub(super) const MAX_OUTLINE_DEPTH: usize = 256;
const HEX: &[u8; 16] = b"0123456789ABCDEF";
const OUTLINE_STACK: &str = "bookmark stack allocation";
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum BookmarkView {
Fit,
Xyz,
}
pub(super) trait ObjectSink {
type Ref: Copy + Into<PdfRef>;
fn begin_object(&mut self, reference: Self::Ref) -> Result<()>;
fn write(&mut self, bytes: &[u8]) -> Result<()>;
fn end_object(&mut self) -> Result<()>;
}
pub(super) trait ObjectAllocator: ObjectSink {
fn reserve(&mut self) -> Result<Self::Ref>;
}
#[derive(Clone, Copy)]
pub(super) struct OutlineItem<R> {
pub(super) reference: R,
pub(super) parent: R,
pub(super) page: R,
pub(super) view: BookmarkView,
pub(super) previous: Option<R>,
pub(super) next: Option<R>,
pub(super) first_child: Option<R>,
pub(super) last_child: Option<R>,
pub(super) descendants: u32,
}
impl<R: Copy> OutlineItem<R> {
pub(super) fn new(
reference: R,
parent: R,
previous: Option<R>,
page: R,
view: BookmarkView,
) -> Self {
Self {
reference,
parent,
page,
view,
previous,
next: None,
first_child: None,
last_child: None,
descendants: 0,
}
}
}
fn pdf_ref(reference: impl Into<PdfRef>) -> String {
let reference = reference.into();
format!("{} {} R", reference.number, reference.generation)
}
pub(super) fn write_utf16_hex<O: ObjectSink>(out: &mut O, text: &str) -> Result<()> {
let mut hex = [0_u8; 4096];
let mut used = 0;
for unit in text.encode_utf16() {
if used == hex.len() {
out.write(&hex)?;
used = 0;
}
for byte in unit.to_be_bytes() {
hex[used] = HEX[(byte >> 4) as usize];
hex[used + 1] = HEX[(byte & 0x0f) as usize];
used += 2;
}
}
if used > 0 {
out.write(&hex[..used])?;
}
Ok(())
}
pub(super) fn write_item<O: ObjectSink>(
out: &mut O,
item: &OutlineItem<O::Ref>,
title: &str,
) -> Result<()> {
out.begin_object(item.reference)?;
out.write(b"<< /Title <FEFF")?;
write_utf16_hex(out, title)?;
let view = match item.view {
BookmarkView::Fit => "/Fit",
BookmarkView::Xyz => "/XYZ null null null",
};
let mut text = format!(
"> /Parent {} /Dest [{} {view}]",
pdf_ref(item.parent),
pdf_ref(item.page)
);
if let Some(previous) = item.previous {
let _ = write!(text, " /Prev {}", pdf_ref(previous));
}
if let Some(next) = item.next {
let _ = write!(text, " /Next {}", pdf_ref(next));
}
if let (Some(first), Some(last)) = (item.first_child, item.last_child) {
let _ = write!(
text,
" /First {} /Last {} /Count {}",
pdf_ref(first),
pdf_ref(last),
item.descendants
);
}
text.push_str(" >>");
out.write(text.as_bytes())?;
out.end_object()
}
pub(super) fn write_root<O: ObjectSink>(
out: &mut O,
root: O::Ref,
first: O::Ref,
last: O::Ref,
count: u32,
) -> Result<()> {
out.begin_object(root)?;
out.write(
format!(
"<< /Type /Outlines /First {} /Last {} /Count {count} >>",
pdf_ref(first),
pdf_ref(last)
)
.as_bytes(),
)?;
out.end_object()
}
struct OpenItem<R> {
item: OutlineItem<R>,
title: String,
ordinal: u32,
}
struct ClosedItem<R> {
item: OutlineItem<R>,
title: String,
}
pub(super) struct OutlineBuilder<R> {
root: Option<R>,
first: Option<R>,
last: Option<R>,
open: Vec<OpenItem<R>>,
written: u32,
retained_titles: u64,
retained_nodes: u32,
failed: bool,
}
impl<R: Copy + Into<PdfRef>> OutlineBuilder<R> {
pub(super) const fn new() -> Self {
Self {
root: None,
first: None,
last: None,
open: Vec::new(),
written: 0,
retained_titles: 0,
retained_nodes: 0,
failed: false,
}
}
pub(super) const fn written(&self) -> u32 {
self.written
}
pub(super) fn ensure_intact(&self) -> Result<()> {
if self.failed {
Err(Error::invalid(
"PDF outline cannot continue after a failed bookmark operation",
))
} else {
Ok(())
}
}
pub(super) fn check_depth(&self, depth: usize) -> Result<()> {
if depth > self.open.len() {
return Err(Error::invalid("bookmark depth skips a parent"));
}
Ok(())
}
pub(super) fn add<O: ObjectAllocator<Ref = R>>(
&mut self,
out: &mut O,
limits: &Limits,
depth: usize,
page: R,
view: BookmarkView,
title: String,
) -> Result<()> {
self.check_depth(depth)?;
let title_capacity = len_u64(title.capacity());
let (next_titles, next_nodes) = self.preflight_memory(depth, title_capacity, limits)?;
let root = match self.root {
Some(root) => root,
None => {
let root = out.reserve()?;
self.root = Some(root);
root
}
};
let reference = out.reserve()?;
self.failed = true;
let previous_item = self.close_to(out, depth)?;
let (parent, links) = match self.open.last_mut() {
Some(active) => (
active.item.reference,
(&mut active.item.first_child, &mut active.item.last_child),
),
None => (root, (&mut self.first, &mut self.last)),
};
let previous = *links.1;
links.0.get_or_insert(reference);
*links.1 = Some(reference);
if let Some(previous_item) = previous_item {
self.emit(out, previous_item, Some(reference))?;
}
self.open.push(OpenItem {
item: OutlineItem::new(reference, parent, previous, page, view),
title,
ordinal: self.written,
});
debug_assert_eq!(
self.retained_titles.checked_add(title_capacity),
Some(next_titles)
);
debug_assert_eq!(self.retained_nodes.checked_add(1), Some(next_nodes));
self.retained_titles = next_titles;
self.retained_nodes = next_nodes;
self.written += 1;
self.failed = false;
Ok(())
}
pub(super) fn finish<O: ObjectSink<Ref = R>>(&mut self, out: &mut O) -> Result<Option<R>> {
if let Some(last_root) = self.close_to(out, 0)? {
self.emit(out, last_root, None)?;
}
if let Some(root) = self.root {
let first = self
.first
.ok_or(Error::invalid("outline root has no first child"))?;
let last = self
.last
.ok_or(Error::invalid("outline root has no last child"))?;
write_root(out, root, first, last, self.written)?;
}
Ok(self.root)
}
fn preflight_memory(
&mut self,
depth: usize,
title_capacity: u64,
limits: &Limits,
) -> Result<(u64, u32)> {
let mut released_titles = 0_u64;
let mut released_nodes = 0_u32;
for item in self.open.iter().skip(depth) {
released_titles = released_titles
.checked_add(len_u64(item.title.capacity()))
.ok_or(Error::invalid("released bookmark title bytes overflow"))?;
released_nodes = released_nodes
.checked_add(1)
.ok_or(Error::invalid("released bookmark count overflows"))?;
}
let next_titles = self
.retained_titles
.checked_sub(released_titles)
.and_then(|retained| retained.checked_add(title_capacity))
.ok_or(Error::invalid("retained bookmark title bytes overflow"))?;
let next_nodes = self
.retained_nodes
.checked_sub(released_nodes)
.and_then(|retained| retained.checked_add(1))
.ok_or(Error::invalid("retained bookmark count overflows"))?;
let attempted = u64::from(next_nodes)
.checked_mul(size_of::<OpenItem<R>>() as u64)
.and_then(|node_bytes| node_bytes.checked_add(next_titles))
.ok_or(Error::invalid("retained bookmark allocation overflows"))?;
limits.check_allocation(attempted)?;
if depth == self.open.len() {
let maximum = u64::from(limits.max_bookmarks);
reserve_bounded(&mut self.open, maximum, limits, OUTLINE_STACK)?;
}
Ok((next_titles, next_nodes))
}
fn close_to<O: ObjectSink<Ref = R>>(
&mut self,
out: &mut O,
depth: usize,
) -> Result<Option<ClosedItem<R>>> {
let mut closed = None;
while self.open.len() > depth {
let Some(open) = self.open.pop() else {
break;
};
if let Some(last_child) = closed.take() {
self.emit(out, last_child, None)?;
}
let mut item = open.item;
item.descendants = self
.written
.checked_sub(open.ordinal + 1)
.ok_or(Error::invalid("bookmark descendant count underflows"))?;
closed = Some(ClosedItem {
item,
title: open.title,
});
}
Ok(closed)
}
fn emit<O: ObjectSink<Ref = R>>(
&mut self,
out: &mut O,
closed: ClosedItem<R>,
next: Option<R>,
) -> Result<()> {
let mut item = closed.item;
item.next = next;
write_item(out, &item, &closed.title)?;
self.retained_titles = self
.retained_titles
.checked_sub(len_u64(closed.title.capacity()))
.ok_or(Error::invalid("retained bookmark title bytes underflow"))?;
self.retained_nodes = self
.retained_nodes
.checked_sub(1)
.ok_or(Error::invalid("retained bookmark count underflows"))?;
Ok(())
}
}