mirror of
https://github.com/typst/typst
synced 2025-05-14 04:56:26 +08:00
298 lines
9.9 KiB
Rust
298 lines
9.9 KiB
Rust
use std::collections::HashMap;
|
|
use std::num::NonZeroUsize;
|
|
|
|
use ecow::EcoString;
|
|
use pdf_writer::{
|
|
types::{ActionType, AnnotationFlags, AnnotationType, NumberingStyle},
|
|
Filter, Finish, Name, Rect, Ref, Str,
|
|
};
|
|
use typst::foundations::Label;
|
|
use typst::introspection::Location;
|
|
use typst::layout::{Abs, Frame};
|
|
use typst::model::{Destination, Numbering};
|
|
use typst::text::Case;
|
|
|
|
use crate::{content, AbsExt, PdfChunk, WithDocument, WithRefs, WithResources};
|
|
use crate::{font::improve_glyph_sets, Resources};
|
|
|
|
/// Construct page objects.
|
|
#[typst_macros::time(name = "construct pages")]
|
|
pub fn traverse_pages(
|
|
state: &WithDocument,
|
|
) -> (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
|
|
.exported_pages
|
|
.as_ref()
|
|
.is_some_and(|ranges| !ranges.includes_page_index(i))
|
|
{
|
|
// Don't export this page.
|
|
pages.push(None);
|
|
skipped_pages += 1;
|
|
} else {
|
|
let mut encoded = construct_page(&mut resources, &page.frame);
|
|
encoded.label = page
|
|
.numbering
|
|
.as_ref()
|
|
.and_then(|num| PdfPageLabel::generate(num, page.number))
|
|
.or_else(|| {
|
|
// When some pages were ignored from export, we show a page label with
|
|
// the correct real (not logical) page number.
|
|
// This is for consistency with normal output when pages have no numbering
|
|
// and all are exported: the final PDF page numbers always correspond to
|
|
// the real (not logical) page numbers. Here, the final PDF page number
|
|
// will differ, but we can at least use labels to indicate what was
|
|
// the corresponding real page number in the Typst document.
|
|
(skipped_pages > 0).then(|| PdfPageLabel::arabic(i + 1))
|
|
});
|
|
pages.push(Some(encoded));
|
|
}
|
|
}
|
|
|
|
improve_glyph_sets(&mut resources.glyph_sets);
|
|
|
|
(PdfChunk::new(), (pages, resources))
|
|
}
|
|
|
|
/// Construct a page object.
|
|
#[typst_macros::time(name = "construct page")]
|
|
fn construct_page(out: &mut Resources<()>, frame: &Frame) -> EncodedPage {
|
|
let content = content::build(out, frame, None);
|
|
|
|
EncodedPage { content, label: None }
|
|
}
|
|
|
|
/// Allocate a reference for each exported page.
|
|
pub fn alloc_page_refs(context: &WithResources) -> (PdfChunk, Vec<Option<Ref>>) {
|
|
let mut chunk = PdfChunk::new();
|
|
let page_refs = context
|
|
.pages
|
|
.iter()
|
|
.map(|p| p.as_ref().map(|_| chunk.alloc()))
|
|
.collect();
|
|
(chunk, page_refs)
|
|
}
|
|
|
|
/// Write the page tree.
|
|
pub fn write_page_tree(ctx: &WithRefs) -> (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,
|
|
);
|
|
}
|
|
|
|
chunk
|
|
.pages(page_tree_ref)
|
|
.count(ctx.pages.len() as i32)
|
|
.kids(ctx.globals.pages.iter().filter_map(Option::as_ref).copied());
|
|
|
|
(chunk, page_tree_ref)
|
|
}
|
|
|
|
/// Write a page tree node.
|
|
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 {
|
|
// Page excluded from export.
|
|
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)
|
|
// `key` must be a `Str`, not a `Name`.
|
|
.pair(Name(b"D"), Str(key.as_str().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());
|
|
|
|
// Don't add links to non-exported pages.
|
|
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);
|
|
}
|
|
|
|
/// Specification for a PDF page label.
|
|
#[derive(Debug, Clone, PartialEq, Hash, Default)]
|
|
pub(crate) struct PdfPageLabel {
|
|
/// Can be any string or none. Will always be prepended to the numbering style.
|
|
pub prefix: Option<EcoString>,
|
|
/// Based on the numbering pattern.
|
|
///
|
|
/// If `None` or numbering is a function, the field will be empty.
|
|
pub style: Option<PdfPageLabelStyle>,
|
|
/// Offset for the page label start.
|
|
///
|
|
/// Describes where to start counting from when setting a style.
|
|
/// (Has to be greater or equal than 1)
|
|
pub offset: Option<NonZeroUsize>,
|
|
}
|
|
|
|
/// A PDF page label number style.
|
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
pub enum PdfPageLabelStyle {
|
|
/// Decimal arabic numerals (1, 2, 3).
|
|
Arabic,
|
|
/// Lowercase roman numerals (i, ii, iii).
|
|
LowerRoman,
|
|
/// Uppercase roman numerals (I, II, III).
|
|
UpperRoman,
|
|
/// Lowercase letters (`a` to `z` for the first 26 pages,
|
|
/// `aa` to `zz` and so on for the next).
|
|
LowerAlpha,
|
|
/// Uppercase letters (`A` to `Z` for the first 26 pages,
|
|
/// `AA` to `ZZ` and so on for the next).
|
|
UpperAlpha,
|
|
}
|
|
|
|
impl PdfPageLabel {
|
|
/// Create a new `PdfNumbering` from a `Numbering` applied to a page
|
|
/// number.
|
|
fn generate(numbering: &Numbering, number: usize) -> Option<PdfPageLabel> {
|
|
let Numbering::Pattern(pat) = numbering else {
|
|
return None;
|
|
};
|
|
|
|
let (prefix, kind, case) = pat.pieces.first()?;
|
|
|
|
// If there is a suffix, we cannot use the common style optimisation,
|
|
// since PDF does not provide a suffix field.
|
|
let mut style = None;
|
|
if pat.suffix.is_empty() {
|
|
use {typst::model::NumberingKind as Kind, PdfPageLabelStyle as Style};
|
|
match (kind, case) {
|
|
(Kind::Arabic, _) => style = Some(Style::Arabic),
|
|
(Kind::Roman, Case::Lower) => style = Some(Style::LowerRoman),
|
|
(Kind::Roman, Case::Upper) => style = Some(Style::UpperRoman),
|
|
(Kind::Letter, Case::Lower) if number <= 26 => {
|
|
style = Some(Style::LowerAlpha)
|
|
}
|
|
(Kind::Letter, Case::Upper) if number <= 26 => {
|
|
style = Some(Style::UpperAlpha)
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Prefix and offset depend on the style: If it is supported by the PDF
|
|
// spec, we use the given prefix and an offset. Otherwise, everything
|
|
// goes into prefix.
|
|
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 })
|
|
}
|
|
|
|
/// Creates an arabic page label with the specified page number.
|
|
/// For example, this will display page label `11` when given the page
|
|
/// number 11.
|
|
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,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Data for an exported page.
|
|
pub struct EncodedPage {
|
|
pub content: content::Encoded,
|
|
pub label: Option<PdfPageLabel>,
|
|
}
|