use stet_graphics::document_structure::{
GoToTarget, OutlineAction, OutlineDestination, OutlineNode, ViewSpec,
};
use crate::pdf_objects::PdfObj;
use crate::pdf_writer::PdfWriter;
pub fn write_outline_tree(
writer: &mut PdfWriter,
roots: &[OutlineNode],
page_refs: &[u32],
) -> Option<u32> {
if roots.is_empty() {
return None;
}
let root_ref = writer.alloc_obj();
let total_visible = total_visible_descendants(roots);
let child_refs = emit_children(writer, roots, root_ref, page_refs);
let entries = vec![
(b"Type".to_vec(), PdfObj::name("Outlines")),
(b"First".to_vec(), PdfObj::Ref(*child_refs.first()?)),
(b"Last".to_vec(), PdfObj::Ref(*child_refs.last()?)),
(b"Count".to_vec(), PdfObj::Int(total_visible as i64)),
];
writer.set_object(root_ref, &PdfObj::Dict(entries));
Some(root_ref)
}
fn emit_children(
writer: &mut PdfWriter,
siblings: &[OutlineNode],
parent_ref: u32,
page_refs: &[u32],
) -> Vec<u32> {
let refs: Vec<u32> = (0..siblings.len()).map(|_| writer.alloc_obj()).collect();
for i in 0..siblings.len() {
let prev = if i > 0 { Some(refs[i - 1]) } else { None };
let next = if i + 1 < refs.len() {
Some(refs[i + 1])
} else {
None
};
emit_node(
writer,
&siblings[i],
refs[i],
parent_ref,
prev,
next,
page_refs,
);
}
refs
}
fn emit_node(
writer: &mut PdfWriter,
node: &OutlineNode,
self_ref: u32,
parent_ref: u32,
prev_ref: Option<u32>,
next_ref: Option<u32>,
page_refs: &[u32],
) {
let child_refs = if node.children.is_empty() {
Vec::new()
} else {
emit_children(writer, &node.children, self_ref, page_refs)
};
let mut entries: Vec<(Vec<u8>, PdfObj)> = vec![
(
b"Title".to_vec(),
PdfObj::LitString(node.record.title.clone().into_bytes()),
),
(b"Parent".to_vec(), PdfObj::Ref(parent_ref)),
];
if let Some(p) = prev_ref {
entries.push((b"Prev".to_vec(), PdfObj::Ref(p)));
}
if let Some(n) = next_ref {
entries.push((b"Next".to_vec(), PdfObj::Ref(n)));
}
if !child_refs.is_empty() {
entries.push((b"First".to_vec(), PdfObj::Ref(child_refs[0])));
entries.push((b"Last".to_vec(), PdfObj::Ref(*child_refs.last().unwrap())));
}
let total = total_visible_descendants(&node.children);
if total > 0 {
let signed = match node.record.count {
Some(n) if n > 0 => total as i64,
Some(_) => -(total as i64),
None => -(total as i64),
};
entries.push((b"Count".to_vec(), PdfObj::Int(signed)));
}
if let Some(dest) = &node.record.destination {
match destination_to_pdf(dest, page_refs) {
DestinationEmit::Dest(obj) => entries.push((b"Dest".to_vec(), obj)),
DestinationEmit::Action(obj) => entries.push((b"A".to_vec(), obj)),
DestinationEmit::None => {}
}
}
if let Some([r, g, b]) = node.record.color {
entries.push((
b"C".to_vec(),
PdfObj::Array(vec![PdfObj::Real(r), PdfObj::Real(g), PdfObj::Real(b)]),
));
}
if let Some(f) = node.record.flags {
entries.push((b"F".to_vec(), PdfObj::Int(f as i64)));
}
writer.set_object(self_ref, &PdfObj::Dict(entries));
}
fn total_visible_descendants(nodes: &[OutlineNode]) -> usize {
let mut total = 0;
for n in nodes {
total += 1 + total_visible_descendants(&n.children);
}
total
}
enum DestinationEmit {
Dest(PdfObj),
Action(PdfObj),
None,
}
fn destination_to_pdf(dest: &OutlineDestination, page_refs: &[u32]) -> DestinationEmit {
match dest {
OutlineDestination::PageView { page, view } => match page_to_ref(*page, page_refs) {
Some(page_ref) => DestinationEmit::Dest(page_view_dest_array(page_ref, view)),
None => DestinationEmit::None,
},
OutlineDestination::NamedDest(name) => {
DestinationEmit::Dest(PdfObj::LitString(name.clone().into_bytes()))
}
OutlineDestination::Action(action) => match encode_action(action, page_refs) {
Some(dict) => DestinationEmit::Action(dict),
None => DestinationEmit::None,
},
_ => DestinationEmit::None,
}
}
pub(crate) fn encode_action(action: &OutlineAction, page_refs: &[u32]) -> Option<PdfObj> {
Some(PdfObj::Dict(match action {
OutlineAction::Uri(uri) => vec![
(b"Type".to_vec(), PdfObj::name("Action")),
(b"S".to_vec(), PdfObj::name("URI")),
(b"URI".to_vec(), PdfObj::LitString(uri.clone().into_bytes())),
],
OutlineAction::GoTo(target) => {
let d = match target {
GoToTarget::Named(name) => PdfObj::LitString(name.clone().into_bytes()),
GoToTarget::Explicit { page, view } => match page_to_ref(*page, page_refs) {
Some(page_ref) => page_view_dest_array(page_ref, view),
None => return None,
},
_ => return None,
};
vec![
(b"Type".to_vec(), PdfObj::name("Action")),
(b"S".to_vec(), PdfObj::name("GoTo")),
(b"D".to_vec(), d),
]
}
OutlineAction::JavaScript(js) => vec![
(b"Type".to_vec(), PdfObj::name("Action")),
(b"S".to_vec(), PdfObj::name("JavaScript")),
(b"JS".to_vec(), PdfObj::LitString(js.clone().into_bytes())),
],
OutlineAction::Named(name) => vec![
(b"Type".to_vec(), PdfObj::name("Action")),
(b"S".to_vec(), PdfObj::name("Named")),
(b"N".to_vec(), PdfObj::name(name)),
],
_ => return None,
}))
}
fn page_to_ref(page: u32, page_refs: &[u32]) -> Option<u32> {
if page == 0 {
return None;
}
page_refs.get(page as usize - 1).copied()
}
fn page_view_dest_array(page_ref: u32, view: &ViewSpec) -> PdfObj {
let mut elems: Vec<PdfObj> = vec![PdfObj::Ref(page_ref)];
match view {
ViewSpec::Xyz { left, top, zoom } => {
elems.push(PdfObj::name("XYZ"));
elems.push(opt_real(*left));
elems.push(opt_real(*top));
elems.push(opt_real(*zoom));
}
ViewSpec::Fit => {
elems.push(PdfObj::name("Fit"));
}
ViewSpec::FitH(top) => {
elems.push(PdfObj::name("FitH"));
elems.push(opt_real(*top));
}
ViewSpec::FitV(left) => {
elems.push(PdfObj::name("FitV"));
elems.push(opt_real(*left));
}
ViewSpec::FitR {
left,
bottom,
right,
top,
} => {
elems.push(PdfObj::name("FitR"));
elems.push(PdfObj::Real(*left));
elems.push(PdfObj::Real(*bottom));
elems.push(PdfObj::Real(*right));
elems.push(PdfObj::Real(*top));
}
ViewSpec::FitB => {
elems.push(PdfObj::name("FitB"));
}
ViewSpec::FitBH(top) => {
elems.push(PdfObj::name("FitBH"));
elems.push(opt_real(*top));
}
ViewSpec::FitBV(left) => {
elems.push(PdfObj::name("FitBV"));
elems.push(opt_real(*left));
}
_ => elems.push(PdfObj::name("Fit")),
}
PdfObj::Array(elems)
}
fn opt_real(v: Option<f64>) -> PdfObj {
match v {
Some(v) => PdfObj::Real(v),
None => PdfObj::Null,
}
}