summaryrefslogtreecommitdiff
path: root/crates/typst-library/src
diff options
context:
space:
mode:
authorKevin K <xkeviota@gmail.com>2023-09-13 10:18:08 +0200
committerGitHub <noreply@github.com>2023-09-13 10:18:08 +0200
commit8927f3d572100377f2feb466b81a8700f0ee3a28 (patch)
tree6823622d765bd995c43c63610e2cac7ab0b2c660 /crates/typst-library/src
parentc1a8ea68cbdb31dba031c3567a1d7f0447b50bed (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.rs30
-rw-r--r--crates/typst-library/src/meta/counter.rs77
-rw-r--r--crates/typst-library/src/meta/document.rs5
-rw-r--r--crates/typst-library/src/meta/numbering.rs51
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,