use std::collections::{HashMap, HashSet};
use pdf_writer::{Content, Filter, Name, Pdf, Rect, Ref, Str, TextStr};
use crate::fonts::FontEntry;
use crate::model::{Document, PageBorderDisplay, PageBorders, ParagraphBorder, SectionProperties};
use super::comments::{BODY_SCALE, BODY_TX, BODY_TY, render_comment_pane};
use super::layout::LinkAnnotation;
use super::GradientSpec;
pub(crate) struct HeadingEntry {
pub(super) title: String,
pub(super) level: u8,
pub(super) page_idx: usize,
pub(super) y_position: f32,
}
fn render_page_borders(pb: &PageBorders, sp: &SectionProperties) -> Content {
let mut content = Content::new();
let edge_pos = |b: &ParagraphBorder, margin: f32| {
let inset = b.space_pt + b.width_pt / 2.0;
if pb.offset_from_page {
inset
} else {
margin - inset
}
};
let left_x = pb.left.as_ref().map(|b| edge_pos(b, sp.margin_left));
let right_x = pb
.right
.as_ref()
.map(|b| sp.page_width - edge_pos(b, sp.margin_right));
let top_y = pb
.top
.as_ref()
.map(|b| sp.page_height - edge_pos(b, sp.margin_top));
let bottom_y = pb.bottom.as_ref().map(|b| edge_pos(b, sp.margin_bottom));
let lx = left_x.unwrap_or(0.0);
let rx = right_x.unwrap_or(sp.page_width);
let ty = top_y.unwrap_or(sp.page_height);
let by = bottom_y.unwrap_or(0.0);
let l_ext = pb.left.as_ref().map(|b| b.width_pt / 2.0).unwrap_or(0.0);
let r_ext = pb.right.as_ref().map(|b| b.width_pt / 2.0).unwrap_or(0.0);
let mut draw = |b: &ParagraphBorder, x0: f32, y0: f32, x1: f32, y1: f32| {
content.save_state();
content.set_line_width(b.width_pt);
super::color::stroke_rgb(&mut content, b.color);
content.move_to(x0, y0);
content.line_to(x1, y1);
content.stroke();
content.restore_state();
};
if let (Some(b), Some(y)) = (&pb.top, top_y) {
draw(b, lx - l_ext, y, rx + r_ext, y);
}
if let (Some(b), Some(y)) = (&pb.bottom, bottom_y) {
draw(b, lx - l_ext, y, rx + r_ext, y);
}
if let (Some(b), Some(x)) = (&pb.left, left_x) {
draw(b, x, ty, x, by);
}
if let (Some(b), Some(x)) = (&pb.right, right_x) {
draw(b, x, ty, x, by);
}
content
}
fn srgb_to_linear(s: f32) -> f32 {
if s <= 0.04045 {
s / 12.92
} else {
((s + 0.055) / 1.055).powf(2.4)
}
}
fn srgb_to_linear_rgb(c: [u8; 3]) -> [f32; 3] {
[
srgb_to_linear(c[0] as f32 / 255.0),
srgb_to_linear(c[1] as f32 / 255.0),
srgb_to_linear(c[2] as f32 / 255.0),
]
}
#[allow(clippy::too_many_arguments)]
pub(super) fn assemble_pdf_pages(
pdf: &mut Pdf,
alloc: &mut impl FnMut() -> Ref,
catalog_id: Ref,
pages_id: Ref,
valign_offsets: Vec<f32>,
all_contents: Vec<Content>,
all_deferred_shapes: Vec<Vec<(u32, Content)>>,
all_hf_contents: &mut Vec<Option<Content>>,
all_page_links: &[Vec<LinkAnnotation>],
all_page_comment_anchors: &[Vec<(u32, f32, f32, f32)>],
all_page_alpha_states: &[HashSet<u8>],
all_page_gradient_specs: &[Vec<GradientSpec>],
page_section_indices: &[(usize, bool, usize)],
seen_fonts: &HashMap<String, FontEntry>,
font_order: &[String],
image_xobjects: &[(String, Ref)],
doc: &Document,
bookmark_positions: &HashMap<String, (usize, f32)>,
heading_entries: &[HeadingEntry],
) {
let n = all_contents.len();
let page_ids: Vec<Ref> = (0..n).map(|_| alloc()).collect();
let content_ids: Vec<Ref> = (0..n).map(|_| alloc()).collect();
let has_any_comments = !doc.comments.is_empty()
&& all_page_comment_anchors.iter().any(|p| !p.is_empty());
let page_annot_refs: Vec<Vec<Ref>> = all_page_links
.iter()
.map(|links| {
links
.iter()
.filter_map(|link| {
let annot_ref = alloc();
let mut annot = pdf.annotation(annot_ref);
annot
.subtype(pdf_writer::types::AnnotationType::Link)
.rect(link.rect)
.border(0.0, 0.0, 0.0, None);
if let Some(anchor) = link.url.strip_prefix('#') {
if let Some(&(dest_page_idx, dest_y)) = bookmark_positions.get(anchor) {
debug_assert!(
dest_page_idx < page_ids.len(),
"bookmark '{anchor}' points to page {dest_page_idx} but only {} pages exist",
page_ids.len(),
);
let safe_idx = dest_page_idx.min(page_ids.len().saturating_sub(1));
annot
.action()
.action_type(pdf_writer::types::ActionType::GoTo)
.destination()
.page(page_ids[safe_idx])
.xyz(0.0, dest_y, None);
Some(annot_ref)
} else {
None
}
} else {
annot
.action()
.action_type(pdf_writer::types::ActionType::Uri)
.uri(Str(link.url.as_bytes()));
Some(annot_ref)
}
})
.collect()
})
.collect();
let all_alpha_values: HashSet<u8> = all_page_alpha_states
.iter()
.flat_map(|s| s.iter().copied())
.collect();
let alpha_gs_refs: HashMap<u8, Ref> = all_alpha_values
.iter()
.map(|&pct| {
let gs_ref = alloc();
pdf.ext_graphics(gs_ref)
.non_stroking_alpha(pct as f32 / 100.0);
(pct, gs_ref)
})
.collect();
let all_page_pattern_refs: Vec<Vec<(String, Ref)>> = all_page_gradient_specs
.iter()
.map(|specs| {
specs
.iter()
.map(|spec| {
let func_ref = if spec.stops.len() <= 2 {
let (c0, c1) = if spec.stops.len() >= 2 {
(spec.stops[0].0, spec.stops[spec.stops.len() - 1].0)
} else {
(spec.stops[0].0, spec.stops[0].0)
};
let fref = alloc();
pdf.exponential_function(fref)
.domain([0.0, 1.0])
.c0(srgb_to_linear_rgb(c0))
.c1(srgb_to_linear_rgb(c1))
.n(1.0);
fref
} else {
let sub_refs: Vec<Ref> = spec
.stops
.windows(2)
.map(|pair| {
let fref = alloc();
pdf.exponential_function(fref)
.domain([0.0, 1.0])
.c0(srgb_to_linear_rgb(pair[0].0))
.c1(srgb_to_linear_rgb(pair[1].0))
.n(1.0);
fref
})
.collect();
let bounds: Vec<f32> = spec.stops[1..spec.stops.len() - 1]
.iter()
.map(|s| s.1)
.collect();
let encode: Vec<f32> =
sub_refs.iter().flat_map(|_| [0.0, 1.0]).collect();
let stitch_ref = alloc();
pdf.stitching_function(stitch_ref)
.domain([0.0, 1.0])
.functions(sub_refs)
.bounds(bounds)
.encode(encode);
stitch_ref
};
let ang_rad = spec.angle_deg.to_radians();
let (sin_a, cos_a) = ang_rad.sin_cos();
let cx = spec.x + spec.w / 2.0;
let cy = spec.y + spec.h / 2.0;
let half_len = ((spec.w / 2.0 * cos_a).powi(2)
+ (spec.h / 2.0 * sin_a).powi(2))
.sqrt();
let x0 = cx - half_len * cos_a;
let y0 = cy + half_len * sin_a;
let x1 = cx + half_len * cos_a;
let y1 = cy - half_len * sin_a;
let pat_ref = alloc();
let mut pattern = pdf.shading_pattern(pat_ref);
let mut shading = pattern.function_shading();
shading
.shading_type(pdf_writer::types::FunctionShadingType::Axial)
.color_space()
.cal_rgb(
[0.9505, 1.0, 1.0890],
None,
None,
Some([
0.4124, 0.2126, 0.0193, 0.3576, 0.7152, 0.1192, 0.1805, 0.0722,
0.9505,
]),
);
shading
.function(func_ref)
.coords([x0, y0, x1, y1])
.extend([true, true]);
(spec.pattern_name.clone(), pat_ref)
})
.collect()
})
.collect();
let mut all_deferred_shapes = all_deferred_shapes.into_iter();
for (i, c) in all_contents.into_iter().enumerate() {
let mut body_raw = c.finish().to_vec();
for (_, shape) in all_deferred_shapes.next().into_iter().flatten() {
body_raw.push(b'\n');
body_raw.extend_from_slice(shape.finish().as_slice());
}
let (scale_prefix, scale_suffix) = if has_any_comments {
(
format!("q {BODY_SCALE} 0 0 {BODY_SCALE} {BODY_TX} {BODY_TY} cm\n").into_bytes(),
b"\nQ\n".to_vec(),
)
} else {
(Vec::new(), Vec::new())
};
let valign_off = valign_offsets.get(i).copied().unwrap_or(0.0);
let (valign_prefix, valign_suffix) = if valign_off > 0.01 {
(
format!("q 1 0 0 1 0 {:.3} cm\n", -valign_off).into_bytes(),
b"\nQ\n".to_vec(),
)
} else {
(Vec::new(), Vec::new())
};
let mut pane_raw = Vec::new();
if has_any_comments {
let (.., si) = page_section_indices[i];
let sp = &doc.sections[si].properties;
let transformed: Vec<(u32, f32, f32, f32)> = all_page_comment_anchors[i]
.iter()
.map(|(id, x, y, fs)| {
(
*id,
BODY_TX + BODY_SCALE * x,
BODY_TY + BODY_SCALE * y,
BODY_SCALE * fs,
)
})
.collect();
let mut pane_content = Content::new();
render_comment_pane(
&mut pane_content,
&doc.comments,
&transformed,
sp.page_width,
sp.page_height,
seen_fonts,
);
pane_raw = pane_content.finish().to_vec();
}
let mut combined: Vec<u8> = Vec::new();
if let Some(hf) = all_hf_contents[i].take() {
let hf_raw = hf.finish();
combined.extend_from_slice(hf_raw.as_slice());
combined.push(b'\n');
}
{
let (_, is_first, content_si) = page_section_indices[i];
let sp = &doc.sections[content_si].properties;
if let Some(pb) = &sp.page_borders {
let show = match pb.display {
PageBorderDisplay::AllPages => true,
PageBorderDisplay::FirstPage => is_first,
PageBorderDisplay::NotFirstPage => !is_first,
};
if show {
combined.extend_from_slice(render_page_borders(pb, sp).finish().as_slice());
combined.push(b'\n');
}
}
}
combined.extend_from_slice(&valign_prefix);
combined.extend_from_slice(&scale_prefix);
combined.extend_from_slice(body_raw.as_slice());
combined.extend_from_slice(&scale_suffix);
combined.extend_from_slice(&valign_suffix);
if !pane_raw.is_empty() {
combined.push(b'\n');
combined.extend_from_slice(&pane_raw);
}
let compressed = miniz_oxide::deflate::compress_to_vec_zlib(&combined, 6);
pdf.stream(content_ids[i], &compressed)
.filter(Filter::FlateDecode);
}
let outline_id = if !heading_entries.is_empty() {
let oid = alloc();
let item_refs: Vec<Ref> = heading_entries.iter().map(|_| alloc()).collect();
let mut parent_idx: Vec<Option<usize>> = vec![None; heading_entries.len()];
let mut stack: Vec<usize> = Vec::new(); for (i, entry) in heading_entries.iter().enumerate() {
while let Some(&top) = stack.last() {
if heading_entries[top].level < entry.level {
break;
}
stack.pop();
}
parent_idx[i] = stack.last().copied();
stack.push(i);
}
let mut children_of: Vec<Vec<usize>> = vec![Vec::new(); heading_entries.len()];
let mut root_children: Vec<usize> = Vec::new();
for (i, parent) in parent_idx.iter().enumerate() {
match parent {
Some(p) => children_of[*p].push(i),
None => root_children.push(i),
}
}
fn count_descendants(idx: usize, children_of: &[Vec<usize>]) -> i32 {
let mut total = children_of[idx].len() as i32;
for &child in &children_of[idx] {
total += count_descendants(child, children_of);
}
total
}
let total_visible: i32 = heading_entries.len() as i32;
for (i, entry) in heading_entries.iter().enumerate() {
let parent_ref = match parent_idx[i] {
Some(p) => item_refs[p],
None => oid,
};
let siblings = match parent_idx[i] {
Some(p) => &children_of[p],
None => &root_children,
};
let pos_in_siblings = siblings.iter().position(|&s| s == i).unwrap();
let mut item = pdf.outline_item(item_refs[i]);
item.title(TextStr(&entry.title))
.parent(parent_ref);
if pos_in_siblings > 0 {
item.prev(item_refs[siblings[pos_in_siblings - 1]]);
}
if pos_in_siblings + 1 < siblings.len() {
item.next(item_refs[siblings[pos_in_siblings + 1]]);
}
if !children_of[i].is_empty() {
item.first(item_refs[*children_of[i].first().unwrap()])
.last(item_refs[*children_of[i].last().unwrap()])
.count(count_descendants(i, &children_of));
}
item.dest().page(page_ids[entry.page_idx]).xyz(0.0, entry.y_position, None);
}
pdf.outline(oid)
.first(item_refs[root_children[0]])
.last(item_refs[*root_children.last().unwrap()])
.count(total_visible);
Some(oid)
} else {
None
};
{
let mut catalog = pdf.catalog(catalog_id);
catalog.pages(pages_id);
if let Some(oid) = outline_id {
catalog.outlines(oid)
.page_mode(pdf_writer::types::PageMode::UseOutlines);
}
}
if doc.title.is_some() || doc.author.is_some() || doc.subject.is_some() || doc.keywords.is_some() {
let info_id = alloc();
let mut info = pdf.document_info(info_id);
if let Some(ref t) = doc.title {
info.title(TextStr(t));
}
if let Some(ref a) = doc.author {
info.author(TextStr(a));
}
if let Some(ref s) = doc.subject {
info.subject(TextStr(s));
}
if let Some(ref k) = doc.keywords {
info.keywords(TextStr(k));
}
info.producer(TextStr("docxside-pdf"));
}
pdf.pages(pages_id)
.kids(page_ids.iter().copied())
.count(n as i32);
let font_pairs: Vec<(String, Ref)> = font_order
.iter()
.map(|name| (seen_fonts[name].pdf_name.clone(), seen_fonts[name].font_ref))
.collect();
for i in 0..n {
let (.., si) = page_section_indices[i];
let sp = &doc.sections[si].properties;
let mut page = pdf.page(page_ids[i]);
page.media_box(Rect::new(0.0, 0.0, sp.page_width, sp.page_height))
.parent(pages_id)
.contents(content_ids[i]);
if !page_annot_refs[i].is_empty() {
page.annotations(page_annot_refs[i].iter().copied());
}
{
let mut resources = page.resources();
{
let mut fonts = resources.fonts();
for (name, font_ref) in &font_pairs {
fonts.pair(Name(name.as_bytes()), *font_ref);
}
}
if !image_xobjects.is_empty() {
let mut xobjects = resources.x_objects();
for (name, xobj_ref) in image_xobjects {
xobjects.pair(Name(name.as_bytes()), *xobj_ref);
}
}
if let Some(alpha_set) = all_page_alpha_states.get(i).filter(|s| !s.is_empty()) {
let mut gs_dict = resources.ext_g_states();
for &pct in alpha_set {
let gs_name = format!("GSa{pct}");
let gs_ref = alpha_gs_refs[&pct];
gs_dict.pair(Name(gs_name.as_bytes()), gs_ref);
}
}
if let Some(pat_refs) = all_page_pattern_refs.get(i).filter(|p| !p.is_empty()) {
let mut patterns = resources.patterns();
for (name, pat_ref) in pat_refs {
patterns.pair(Name(name.as_bytes()), *pat_ref);
}
}
}
}
}