use std::collections::HashMap;
use std::num::NonZeroUsize;
use ecow::EcoString;
use pdf_writer::types::{ActionType, AnnotationFlags, AnnotationType, NumberingStyle};
use pdf_writer::{Filter, Finish, Name, Rect, Ref, Str};
use typst_library::diag::SourceResult;
use typst_library::foundations::Label;
use typst_library::introspection::Location;
use typst_library::layout::{Abs, Page};
use typst_library::model::{Destination, Numbering};
use crate::{
content, AbsExt, PdfChunk, PdfOptions, Resources, WithDocument, WithRefs,
WithResources,
};
#[typst_macros::time(name = "construct pages")]
#[allow(clippy::type_complexity)]
pub fn traverse_pages(
state: &WithDocument,
) -> SourceResult<(PdfChunk, (Vec<Option<EncodedPage>>, Resources<()>))> {
let mut resources = Resources::default();
let mut pages = Vec::with_capacity(state.document.pages.len());
let mut skipped_pages = 0;
for (i, page) in state.document.pages.iter().enumerate() {
if state
.options
.page_ranges
.as_ref()
.is_some_and(|ranges| !ranges.includes_page_index(i))
{
pages.push(None);
skipped_pages += 1;
} else {
let mut encoded = construct_page(state.options, &mut resources, page)?;
encoded.label = page
.numbering
.as_ref()
.and_then(|num| PdfPageLabel::generate(num, page.number))
.or_else(|| {
(skipped_pages > 0).then(|| PdfPageLabel::arabic(i + 1))
});
pages.push(Some(encoded));
}
}
Ok((PdfChunk::new(), (pages, resources)))
}
#[typst_macros::time(name = "construct page")]
fn construct_page(
options: &PdfOptions,
out: &mut Resources<()>,
page: &Page,
) -> SourceResult<EncodedPage> {
Ok(EncodedPage {
content: content::build(
options,
out,
&page.frame,
page.fill_or_transparent(),
None,
)?,
label: None,
})
}
pub fn alloc_page_refs(
context: &WithResources,
) -> SourceResult<(PdfChunk, Vec<Option<Ref>>)> {
let mut chunk = PdfChunk::new();
let page_refs = context
.pages
.iter()
.map(|p| p.as_ref().map(|_| chunk.alloc()))
.collect();
Ok((chunk, page_refs))
}
pub fn write_page_tree(ctx: &WithRefs) -> SourceResult<(PdfChunk, Ref)> {
let mut chunk = PdfChunk::new();
let page_tree_ref = chunk.alloc.bump();
for i in 0..ctx.pages.len() {
let content_id = chunk.alloc.bump();
write_page(
&mut chunk,
ctx,
content_id,
page_tree_ref,
&ctx.references.named_destinations.loc_to_dest,
i,
);
}
let page_kids = ctx.globals.pages.iter().filter_map(Option::as_ref).copied();
chunk
.pages(page_tree_ref)
.count(page_kids.clone().count() as i32)
.kids(page_kids);
Ok((chunk, page_tree_ref))
}
fn write_page(
chunk: &mut PdfChunk,
ctx: &WithRefs,
content_id: Ref,
page_tree_ref: Ref,
loc_to_dest: &HashMap<Location, Label>,
i: usize,
) {
let Some((page, page_ref)) = ctx.pages[i].as_ref().zip(ctx.globals.pages[i]) else {
return;
};
let mut annotations = Vec::with_capacity(page.content.links.len());
for (dest, rect) in &page.content.links {
let id = chunk.alloc();
annotations.push(id);
let mut annotation = chunk.annotation(id);
annotation.subtype(AnnotationType::Link).rect(*rect);
annotation.border(0.0, 0.0, 0.0, None).flags(AnnotationFlags::PRINT);
let pos = match dest {
Destination::Url(uri) => {
annotation
.action()
.action_type(ActionType::Uri)
.uri(Str(uri.as_bytes()));
continue;
}
Destination::Position(pos) => *pos,
Destination::Location(loc) => {
if let Some(key) = loc_to_dest.get(loc) {
annotation
.action()
.action_type(ActionType::GoTo)
.pair(Name(b"D"), Str(key.resolve().as_bytes()));
continue;
} else {
ctx.document.introspector.position(*loc)
}
}
};
let index = pos.page.get() - 1;
let y = (pos.point.y - Abs::pt(10.0)).max(Abs::zero());
if let Some((Some(page), Some(page_ref))) =
ctx.pages.get(index).zip(ctx.globals.pages.get(index))
{
annotation
.action()
.action_type(ActionType::GoTo)
.destination()
.page(*page_ref)
.xyz(pos.point.x.to_f32(), (page.content.size.y - y).to_f32(), None);
}
}
let mut page_writer = chunk.page(page_ref);
page_writer.parent(page_tree_ref);
let w = page.content.size.x.to_f32();
let h = page.content.size.y.to_f32();
page_writer.media_box(Rect::new(0.0, 0.0, w, h));
page_writer.contents(content_id);
page_writer.pair(Name(b"Resources"), ctx.resources.reference);
if page.content.uses_opacities {
page_writer
.group()
.transparency()
.isolated(false)
.knockout(false)
.color_space()
.srgb();
}
page_writer.annotations(annotations);
page_writer.finish();
chunk
.stream(content_id, page.content.content.wait())
.filter(Filter::FlateDecode);
}
#[derive(Debug, Clone, PartialEq, Hash, Default)]
pub(crate) struct PdfPageLabel {
pub prefix: Option<EcoString>,
pub style: Option<PdfPageLabelStyle>,
pub offset: Option<NonZeroUsize>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum PdfPageLabelStyle {
Arabic,
LowerRoman,
UpperRoman,
LowerAlpha,
UpperAlpha,
}
impl PdfPageLabel {
fn generate(numbering: &Numbering, number: usize) -> Option<PdfPageLabel> {
let Numbering::Pattern(pat) = numbering else {
return None;
};
let (prefix, kind) = pat.pieces.first()?;
let style = if pat.suffix.is_empty() {
use typst_library::model::NumberingKind as Kind;
use PdfPageLabelStyle as Style;
match kind {
Kind::Arabic => Some(Style::Arabic),
Kind::LowerRoman => Some(Style::LowerRoman),
Kind::UpperRoman => Some(Style::UpperRoman),
Kind::LowerLatin if number <= 26 => Some(Style::LowerAlpha),
Kind::LowerLatin if number <= 26 => Some(Style::UpperAlpha),
_ => None,
}
} else {
None
};
let prefix = if style.is_none() {
Some(pat.apply(&[number]))
} else {
(!prefix.is_empty()).then(|| prefix.clone())
};
let offset = style.and(NonZeroUsize::new(number));
Some(PdfPageLabel { prefix, style, offset })
}
fn arabic(number: usize) -> PdfPageLabel {
PdfPageLabel {
prefix: None,
style: Some(PdfPageLabelStyle::Arabic),
offset: NonZeroUsize::new(number),
}
}
}
impl PdfPageLabelStyle {
pub fn to_pdf_numbering_style(self) -> NumberingStyle {
match self {
PdfPageLabelStyle::Arabic => NumberingStyle::Arabic,
PdfPageLabelStyle::LowerRoman => NumberingStyle::LowerRoman,
PdfPageLabelStyle::UpperRoman => NumberingStyle::UpperRoman,
PdfPageLabelStyle::LowerAlpha => NumberingStyle::LowerAlpha,
PdfPageLabelStyle::UpperAlpha => NumberingStyle::UpperAlpha,
}
}
}
pub struct EncodedPage {
pub content: content::Encoded,
pub label: Option<PdfPageLabel>,
}