diff options
| author | Kevin K <xkeviota@gmail.com> | 2023-09-13 10:18:08 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-09-13 10:18:08 +0200 |
| commit | 8927f3d572100377f2feb466b81a8700f0ee3a28 (patch) | |
| tree | 6823622d765bd995c43c63610e2cac7ab0b2c660 /crates/typst-library/src | |
| parent | c1a8ea68cbdb31dba031c3567a1d7f0447b50bed (diff) | |
Add logical numbering support for PDF export (#1933)
Co-authored-by: Laurenz <laurmaedje@gmail.com>
Diffstat (limited to 'crates/typst-library/src')
| -rw-r--r-- | crates/typst-library/src/layout/page.rs | 30 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/counter.rs | 77 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/document.rs | 5 | ||||
| -rw-r--r-- | crates/typst-library/src/meta/numbering.rs | 51 |
4 files changed, 141 insertions, 22 deletions
diff --git a/crates/typst-library/src/layout/page.rs b/crates/typst-library/src/layout/page.rs index d182a417..05d0731d 100644 --- a/crates/typst-library/src/layout/page.rs +++ b/crates/typst-library/src/layout/page.rs @@ -4,7 +4,7 @@ use std::str::FromStr; use typst::eval::AutoValue; use super::{AlignElem, ColumnsElem}; -use crate::meta::{Counter, CounterKey, Numbering}; +use crate::meta::{Counter, CounterKey, ManualPageCounter, Numbering}; use crate::prelude::*; use crate::text::TextElem; @@ -327,7 +327,7 @@ impl PageElem { &self, vt: &mut Vt, styles: StyleChain, - mut number: NonZeroUsize, + page_counter: &mut ManualPageCounter, ) -> SourceResult<Fragment> { tracing::info!("Page layout"); @@ -378,7 +378,10 @@ impl PageElem { let mut frames = child.layout(vt, styles, regions)?.into_frames(); // Align the child to the pagebreak's parity. - if self.clear_to(styles).is_some_and(|p| !p.matches(number.get())) { + if self + .clear_to(styles) + .is_some_and(|p| !p.matches(page_counter.physical().get())) + { let size = area.map(Abs::is_finite).select(area, Size::zero()); frames.insert(0, Frame::new(size)); } @@ -389,6 +392,7 @@ impl PageElem { let header_ascent = self.header_ascent(styles); let footer_descent = self.footer_descent(styles); let numbering = self.numbering(styles); + let numbering_meta = Meta::PageNumbering(numbering.clone().into_value()); let number_align = self.number_align(styles); let mut header = self.header(styles); let mut footer = self.footer(styles); @@ -418,12 +422,9 @@ impl PageElem { footer = footer.or(numbering_marginal); } - let numbering_meta = - FrameItem::Meta(Meta::PageNumbering(numbering.into_value()), Size::zero()); - // Post-process pages. for frame in frames.iter_mut() { - tracing::info!("Layouting page #{number}"); + tracing::info!("Layouting page #{}", page_counter.physical()); // The padded width of the page's content without margins. let pw = frame.width(); @@ -432,14 +433,14 @@ impl PageElem { // Thus, for left-bound pages, we want to swap on even pages and // for right-bound pages, we want to swap on odd pages. let mut margin = margin; - if two_sided && binding.swap(number) { + if two_sided && binding.swap(page_counter.physical()) { std::mem::swap(&mut margin.left, &mut margin.right); } // Realize margins. frame.set_size(frame.size() + margin.sum_by_axis()); frame.translate(Point::new(margin.left, margin.top)); - frame.push(Point::zero(), numbering_meta.clone()); + frame.push_positionless_meta(numbering_meta.clone()); // The page size with margins. let size = frame.size(); @@ -490,7 +491,16 @@ impl PageElem { frame.fill(fill.clone()); } - number = number.saturating_add(1); + page_counter.visit(vt, frame)?; + + // Add a PDF page label if there is a numbering. + if let Some(num) = &numbering { + if let Some(page_label) = num.apply_pdf(page_counter.logical()) { + frame.push_positionless_meta(Meta::PdfPageLabel(page_label)); + } + } + + page_counter.step(); } Ok(Fragment::frames(frames)) diff --git a/crates/typst-library/src/meta/counter.rs b/crates/typst-library/src/meta/counter.rs index a2a63e81..88bc82bd 100644 --- a/crates/typst-library/src/meta/counter.rs +++ b/crates/typst-library/src/meta/counter.rs @@ -233,7 +233,7 @@ impl Counter { Ok(CounterState(smallvec![at_state.first(), final_state.first()])) } - /// Produces the whole sequence of counter states. + /// Produce the whole sequence of counter states. /// /// This has to happen just once for all counters, cutting down the number /// of counter updates from quadratic to linear. @@ -268,11 +268,8 @@ impl Counter { delayed, tracer, }; - let mut state = CounterState(match &self.0 { - // special case, because pages always start at one. - CounterKey::Page => smallvec![1], - _ => smallvec![0], - }); + + let mut state = CounterState::init(&self.0); let mut page = NonZeroUsize::ONE; let mut stops = eco_vec![(state.clone(), page)]; @@ -543,6 +540,15 @@ pub trait Count { pub struct CounterState(pub SmallVec<[usize; 3]>); impl CounterState { + /// Get the initial counter state for the key. + pub fn init(key: &CounterKey) -> Self { + Self(match key { + // special case, because pages always start at one. + CounterKey::Page => smallvec![1], + _ => smallvec![0], + }) + } + /// Advance the counter and return the numbers for the given heading. pub fn update(&mut self, vt: &mut Vt, update: CounterUpdate) -> SourceResult<()> { match update { @@ -642,7 +648,7 @@ impl Show for DisplayElem { } } -/// Executes a display of a state. +/// Executes an update of a counter. #[elem(Locatable, Show)] struct UpdateElem { /// The key that identifies the counter. @@ -660,3 +666,60 @@ impl Show for UpdateElem { Ok(Content::empty()) } } + +/// An specialized handler of the page counter that tracks both the physical +/// and the logical page counter. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] +pub struct ManualPageCounter { + physical: NonZeroUsize, + logical: usize, +} + +impl ManualPageCounter { + /// Create a new fast page counter, starting at 1. + pub fn new() -> Self { + Self { physical: NonZeroUsize::ONE, logical: 1 } + } + + /// Get the current physical page counter state. + pub fn physical(&self) -> NonZeroUsize { + self.physical + } + + /// Get the current logical page counter state. + pub fn logical(&self) -> usize { + self.logical + } + + /// Advance past a page. + pub fn visit(&mut self, vt: &mut Vt, page: &Frame) -> SourceResult<()> { + for (_, item) in page.items() { + match item { + FrameItem::Group(group) => self.visit(vt, &group.frame)?, + FrameItem::Meta(Meta::Elem(elem), _) => { + let Some(elem) = elem.to::<UpdateElem>() else { continue }; + if elem.key() == CounterKey::Page { + let mut state = CounterState(smallvec![self.logical]); + state.update(vt, elem.update())?; + self.logical = state.first(); + } + } + _ => {} + } + } + + Ok(()) + } + + /// Step past a page _boundary._ + pub fn step(&mut self) { + self.physical = self.physical.saturating_add(1); + self.logical += 1; + } +} + +impl Default for ManualPageCounter { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/typst-library/src/meta/document.rs b/crates/typst-library/src/meta/document.rs index 66f8aeb5..01095dc9 100644 --- a/crates/typst-library/src/meta/document.rs +++ b/crates/typst-library/src/meta/document.rs @@ -1,4 +1,5 @@ use crate::layout::{LayoutRoot, PageElem}; +use crate::meta::ManualPageCounter; use crate::prelude::*; /// The root element of a document and its metadata. @@ -45,6 +46,7 @@ impl LayoutRoot for DocumentElem { tracing::info!("Document layout"); let mut pages = vec![]; + let mut page_counter = ManualPageCounter::new(); for mut child in &self.children() { let outer = styles; @@ -55,8 +57,7 @@ impl LayoutRoot for DocumentElem { } if let Some(page) = child.to::<PageElem>() { - let number = NonZeroUsize::ONE.saturating_add(pages.len()); - let fragment = page.layout(vt, styles, number)?; + let fragment = page.layout(vt, styles, &mut page_counter)?; pages.extend(fragment); } else { bail!(child.span(), "unexpected document child"); diff --git a/crates/typst-library/src/meta/numbering.rs b/crates/typst-library/src/meta/numbering.rs index 40308af0..7843757c 100644 --- a/crates/typst-library/src/meta/numbering.rs +++ b/crates/typst-library/src/meta/numbering.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; use ecow::EcoVec; +use typst::export::{PdfPageLabel, PdfPageLabelStyle}; use crate::prelude::*; use crate::text::Case; @@ -96,6 +97,50 @@ impl Numbering { }) } + /// Create a new `PdfNumbering` from a `Numbering` applied to a page + /// number. + pub fn apply_pdf(&self, number: usize) -> Option<PdfPageLabel> { + let Numbering::Pattern(pat) = self else { + return None; + }; + + let Some((prefix, kind, case)) = pat.pieces.first() else { + return None; + }; + + // 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 NumberingKind as Kind; + use 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 }) + } + /// Trim the prefix suffix if this is a pattern. pub fn trimmed(mut self) -> Self { if let Self::Pattern(pattern) = &mut self { @@ -132,8 +177,8 @@ cast! { /// - `(I)` #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub struct NumberingPattern { - pieces: EcoVec<(EcoString, NumberingKind, Case)>, - suffix: EcoString, + pub pieces: EcoVec<(EcoString, NumberingKind, Case)>, + pub suffix: EcoString, trimmed: bool, } @@ -242,7 +287,7 @@ cast! { /// Different kinds of numberings. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -enum NumberingKind { +pub enum NumberingKind { Arabic, Letter, Roman, |
