Add Page struct

To get rid of the Meta hack where numbering and things like that are stored in the frame.
This commit is contained in:
Laurenz 2024-01-17 21:50:35 +01:00
parent 50741209a8
commit 6ac71eeaf7
17 changed files with 203 additions and 199 deletions

View File

@ -219,7 +219,7 @@ fn export_image(
.pages .pages
.par_iter() .par_iter()
.enumerate() .enumerate()
.map(|(i, frame)| { .map(|(i, page)| {
let storage; let storage;
let path = if numbered { let path = if numbered {
storage = string.replace("{n}", &format!("{:0width$}", i + 1)); storage = string.replace("{n}", &format!("{:0width$}", i + 1));
@ -231,20 +231,23 @@ fn export_image(
// If we are not watching, don't use the cache. // If we are not watching, don't use the cache.
// If the frame is in the cache, skip it. // If the frame is in the cache, skip it.
// If the file does not exist, always create it. // If the file does not exist, always create it.
if watching && cache.is_cached(i, frame) && path.exists() { if watching && cache.is_cached(i, &page.frame) && path.exists() {
return Ok(()); return Ok(());
} }
match fmt { match fmt {
ImageExportFormat::Png => { ImageExportFormat::Png => {
let pixmap = let pixmap = typst_render::render(
typst_render::render(frame, command.ppi / 72.0, Color::WHITE); &page.frame,
command.ppi / 72.0,
Color::WHITE,
);
pixmap pixmap
.save_png(path) .save_png(path)
.map_err(|err| eco_format!("failed to write PNG file ({err})"))?; .map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
} }
ImageExportFormat::Svg => { ImageExportFormat::Svg => {
let svg = typst_svg::svg(frame); let svg = typst_svg::svg(&page.frame);
fs::write(path, svg.as_bytes()) fs::write(path, svg.as_bytes())
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?; .map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
} }

View File

@ -388,8 +388,8 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
let world = DocWorld(source); let world = DocWorld(source);
let mut tracer = Tracer::new(); let mut tracer = Tracer::new();
let mut frames = match typst::compile(&world, &mut tracer) { let mut document = match typst::compile(&world, &mut tracer) {
Ok(doc) => doc.pages, Ok(doc) => doc,
Err(err) => { Err(err) => {
let msg = &err[0].message; let msg = &err[0].message;
panic!("while trying to compile:\n{text}:\n\nerror: {msg}"); panic!("while trying to compile:\n{text}:\n\nerror: {msg}");
@ -397,16 +397,16 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
}; };
if let Some([x, y, w, h]) = zoom { if let Some([x, y, w, h]) = zoom {
frames[0].translate(Point::new(-x, -y)); document.pages[0].frame.translate(Point::new(-x, -y));
*frames[0].size_mut() = Size::new(w, h); *document.pages[0].frame.size_mut() = Size::new(w, h);
} }
if single { if single {
frames.truncate(1); document.pages.truncate(1);
} }
let hash = typst::util::hash128(text); let hash = typst::util::hash128(text);
resolver.example(hash, highlighted, &frames) resolver.example(hash, highlighted, &document)
} }
/// Extract an attribute value from an HTML element. /// Extract an attribute value from an HTML element.

View File

@ -25,9 +25,10 @@ use typst::foundations::{
FOUNDATIONS, FOUNDATIONS,
}; };
use typst::introspection::INTROSPECTION; use typst::introspection::INTROSPECTION;
use typst::layout::{Abs, Frame, Margin, PageElem, LAYOUT}; use typst::layout::{Abs, Margin, PageElem, LAYOUT};
use typst::loading::DATA_LOADING; use typst::loading::DATA_LOADING;
use typst::math::MATH; use typst::math::MATH;
use typst::model::Document;
use typst::model::MODEL; use typst::model::MODEL;
use typst::symbols::SYMBOLS; use typst::symbols::SYMBOLS;
use typst::text::{Font, FontBook, TEXT}; use typst::text::{Font, FontBook, TEXT};
@ -97,7 +98,7 @@ pub trait Resolver {
fn image(&self, filename: &str, data: &[u8]) -> String; fn image(&self, filename: &str, data: &[u8]) -> String;
/// Produce HTML for an example. /// Produce HTML for an example.
fn example(&self, hash: u128, source: Option<Html>, frames: &[Frame]) -> Html; fn example(&self, hash: u128, source: Option<Html>, document: &Document) -> Html;
/// Determine the commits between two tags. /// Determine the commits between two tags.
fn commits(&self, from: &str, to: &str) -> Vec<Commit>; fn commits(&self, from: &str, to: &str) -> Vec<Commit>;
@ -789,7 +790,7 @@ mod tests {
None None
} }
fn example(&self, _: u128, _: Option<Html>, _: &[Frame]) -> Html { fn example(&self, _: u128, _: Option<Html>, _: &Document) -> Html {
Html::new(String::new()) Html::new(String::new())
} }

View File

@ -121,8 +121,8 @@ pub fn jump_from_cursor(
} }
let span = node.span(); let span = node.span();
for (i, frame) in document.pages.iter().enumerate() { for (i, page) in document.pages.iter().enumerate() {
if let Some(pos) = find_in_frame(frame, span) { if let Some(pos) = find_in_frame(&page.frame, span) {
return Some(Position { return Some(Position {
page: NonZeroUsize::new(i + 1).unwrap(), page: NonZeroUsize::new(i + 1).unwrap(),
point: pos, point: pos,

View File

@ -30,7 +30,7 @@ use crate::color::ColorSpaces;
use crate::extg::ExtGState; use crate::extg::ExtGState;
use crate::gradient::PdfGradient; use crate::gradient::PdfGradient;
use crate::image::EncodedImage; use crate::image::EncodedImage;
use crate::page::Page; use crate::page::EncodedPage;
use crate::pattern::PdfPattern; use crate::pattern::PdfPattern;
/// Export a document into a PDF file. /// Export a document into a PDF file.
@ -72,7 +72,7 @@ struct PdfContext<'a> {
/// The writer we are writing the PDF into. /// The writer we are writing the PDF into.
pdf: Pdf, pdf: Pdf,
/// Content of exported pages. /// Content of exported pages.
pages: Vec<Page>, pages: Vec<EncodedPage>,
/// For each font a mapping from used glyphs to their text representation. /// For each font a mapping from used glyphs to their text representation.
/// May contain multiple chars in case of ligatures or similar things. The /// May contain multiple chars in case of ligatures or similar things. The
/// same glyph can have a different text representation within one document, /// same glyph can have a different text representation within one document,

View File

@ -10,11 +10,10 @@ use pdf_writer::writers::PageLabel;
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr}; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr};
use typst::introspection::Meta; use typst::introspection::Meta;
use typst::layout::{ use typst::layout::{
Abs, Em, Frame, FrameItem, GroupItem, PdfPageLabel, PdfPageLabelStyle, Point, Ratio, Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
Size, Transform,
}; };
use typst::model::Destination; use typst::model::{Destination, Numbering};
use typst::text::{Font, TextItem}; use typst::text::{Case, Font, TextItem};
use typst::util::{Deferred, Numeric}; use typst::util::{Deferred, Numeric};
use typst::visualize::{ use typst::visualize::{
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape, FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
@ -27,34 +26,36 @@ use crate::{deflate_deferred, AbsExt, EmExt, PdfContext};
/// Construct page objects. /// Construct page objects.
#[typst_macros::time(name = "construct pages")] #[typst_macros::time(name = "construct pages")]
pub(crate) fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) { pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) {
for frame in frames { for page in pages {
let (page_ref, page) = construct_page(ctx, frame); let (page_ref, mut encoded) = construct_page(ctx, &page.frame);
encoded.label = page
.numbering
.as_ref()
.and_then(|num| PdfPageLabel::generate(num, page.number));
ctx.page_refs.push(page_ref); ctx.page_refs.push(page_ref);
ctx.pages.push(page); ctx.pages.push(encoded);
} }
} }
/// Construct a page object. /// Construct a page object.
#[typst_macros::time(name = "construct page")] #[typst_macros::time(name = "construct page")]
pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page) { pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, EncodedPage) {
let page_ref = ctx.alloc.bump(); let page_ref = ctx.alloc.bump();
let size = frame.size();
let mut ctx = PageContext { let mut ctx = PageContext {
parent: ctx, parent: ctx,
page_ref, page_ref,
label: None,
uses_opacities: false, uses_opacities: false,
content: Content::new(), content: Content::new(),
state: State::new(frame.size()), state: State::new(size),
saves: vec![], saves: vec![],
bottom: 0.0, bottom: 0.0,
links: vec![], links: vec![],
resources: HashMap::default(), resources: HashMap::default(),
}; };
let size = frame.size();
// Make the coordinate system start at the top-left. // Make the coordinate system start at the top-left.
ctx.bottom = size.y.to_f32(); ctx.bottom = size.y.to_f32();
ctx.transform(Transform { ctx.transform(Transform {
@ -69,13 +70,13 @@ pub(crate) fn construct_page(ctx: &mut PdfContext, frame: &Frame) -> (Ref, Page)
// Encode the page into the content stream. // Encode the page into the content stream.
write_frame(&mut ctx, frame); write_frame(&mut ctx, frame);
let page = Page { let page = EncodedPage {
size, size,
content: deflate_deferred(ctx.content.finish()), content: deflate_deferred(ctx.content.finish()),
id: ctx.page_ref, id: ctx.page_ref,
uses_opacities: ctx.uses_opacities, uses_opacities: ctx.uses_opacities,
links: ctx.links, links: ctx.links,
label: ctx.label, label: None,
resources: ctx.resources, resources: ctx.resources,
}; };
@ -249,8 +250,86 @@ pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)
result result
} }
/// Specification for a PDF page label.
#[derive(Debug, Clone, PartialEq, Hash, Default)]
struct PdfPageLabel {
/// Can be any string or none. Will always be prepended to the numbering style.
prefix: Option<EcoString>,
/// Based on the numbering pattern.
///
/// If `None` or numbering is a function, the field will be empty.
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)
offset: Option<NonZeroUsize>,
}
/// A PDF page label number style.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
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 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 {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 })
}
}
/// Data for an exported page. /// Data for an exported page.
pub struct Page { pub struct EncodedPage {
/// The indirect object id of the page. /// The indirect object id of the page.
pub id: Ref, pub id: Ref,
/// The page's dimensions. /// The page's dimensions.
@ -261,10 +340,10 @@ pub struct Page {
pub uses_opacities: bool, pub uses_opacities: bool,
/// Links in the PDF coordinate system. /// Links in the PDF coordinate system.
pub links: Vec<(Destination, Rect)>, pub links: Vec<(Destination, Rect)>,
/// The page's PDF label.
pub label: Option<PdfPageLabel>,
/// The page's used resources /// The page's used resources
pub resources: HashMap<PageResource, usize>, pub resources: HashMap<PageResource, usize>,
/// The page's PDF label.
label: Option<PdfPageLabel>,
} }
/// Represents a resource being used in a PDF page by its name. /// Represents a resource being used in a PDF page by its name.
@ -326,7 +405,6 @@ impl PageResource {
pub struct PageContext<'a, 'b> { pub struct PageContext<'a, 'b> {
pub(crate) parent: &'a mut PdfContext<'b>, pub(crate) parent: &'a mut PdfContext<'b>,
page_ref: Ref, page_ref: Ref,
label: Option<PdfPageLabel>,
pub content: Content, pub content: Content,
state: State, state: State,
saves: Vec<State>, saves: Vec<State>,
@ -557,7 +635,6 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
for &(pos, ref item) in frame.items() { for &(pos, ref item) in frame.items() {
let x = pos.x.to_f32(); let x = pos.x.to_f32();
let y = pos.y.to_f32(); let y = pos.y.to_f32();
match item { match item {
FrameItem::Group(group) => write_group(ctx, pos, group), FrameItem::Group(group) => write_group(ctx, pos, group),
FrameItem::Text(text) => write_text(ctx, pos, text), FrameItem::Text(text) => write_text(ctx, pos, text),
@ -567,8 +644,6 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
Meta::Link(dest) => write_link(ctx, pos, dest, *size), Meta::Link(dest) => write_link(ctx, pos, dest, *size),
Meta::Elem(_) => {} Meta::Elem(_) => {}
Meta::Hide => {} Meta::Hide => {}
Meta::PageNumbering(_) => {}
Meta::PdfPageLabel(label) => ctx.label = Some(label.clone()),
}, },
} }
} }

View File

@ -13,6 +13,7 @@ use typst::introspection::Meta;
use typst::layout::{ use typst::layout::{
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform, Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
}; };
use typst::model::Document;
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::visualize::{ use typst::visualize::{
Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap, Color, DashPattern, FixedStroke, Geometry, Gradient, Image, ImageKind, LineCap,
@ -39,19 +40,20 @@ pub fn render(frame: &Frame, pixel_per_pt: f32, fill: Color) -> sk::Pixmap {
canvas canvas
} }
/// Export multiple frames into a single raster image. /// Export a document with potentially multiple pages into a single raster image.
/// ///
/// The padding will be added around and between the individual frames. /// The padding will be added around and between the individual frames.
pub fn render_merged( pub fn render_merged(
frames: &[Frame], document: &Document,
pixel_per_pt: f32, pixel_per_pt: f32,
frame_fill: Color, frame_fill: Color,
padding: Abs, padding: Abs,
padding_fill: Color, padding_fill: Color,
) -> sk::Pixmap { ) -> sk::Pixmap {
let pixmaps: Vec<_> = frames let pixmaps: Vec<_> = document
.pages
.iter() .iter()
.map(|frame| render(frame, pixel_per_pt, frame_fill)) .map(|page| render(&page.frame, pixel_per_pt, frame_fill))
.collect(); .collect();
let padding = (pixel_per_pt * padding.to_f32()).round() as u32; let padding = (pixel_per_pt * padding.to_f32()).round() as u32;
@ -165,8 +167,6 @@ fn render_frame(canvas: &mut sk::Pixmap, state: State, frame: &Frame) {
FrameItem::Meta(meta, _) => match meta { FrameItem::Meta(meta, _) => match meta {
Meta::Link(_) => {} Meta::Link(_) => {}
Meta::Elem(_) => {} Meta::Elem(_) => {}
Meta::PageNumbering(_) => {}
Meta::PdfPageLabel(_) => {}
Meta::Hide => {} Meta::Hide => {}
}, },
} }

View File

@ -11,6 +11,7 @@ use typst::layout::{
Abs, Angle, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Quadrant, Ratio, Abs, Angle, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Quadrant, Ratio,
Size, Transform, Size, Transform,
}; };
use typst::model::Document;
use typst::text::{Font, TextItem}; use typst::text::{Font, TextItem};
use typst::util::hash128; use typst::util::hash128;
use typst::visualize::{ use typst::visualize::{
@ -35,24 +36,33 @@ pub fn svg(frame: &Frame) -> String {
renderer.finalize() renderer.finalize()
} }
/// Export multiple frames into a single SVG file. /// Export a document with potentially multiple pages into a single SVG file.
/// ///
/// The padding will be added around and between the individual frames. /// The padding will be added around and between the individual frames.
pub fn svg_merged(frames: &[Frame], padding: Abs) -> String { pub fn svg_merged(document: &Document, padding: Abs) -> String {
let width = 2.0 * padding let width = 2.0 * padding
+ frames.iter().map(|frame| frame.width()).max().unwrap_or_default(); + document
let height = padding + frames.iter().map(|page| page.height() + padding).sum::<Abs>(); .pages
let size = Size::new(width, height); .iter()
.map(|page| page.frame.width())
.max()
.unwrap_or_default();
let height = padding
+ document
.pages
.iter()
.map(|page| page.frame.height() + padding)
.sum::<Abs>();
let mut renderer = SVGRenderer::new(); let mut renderer = SVGRenderer::new();
renderer.write_header(size); renderer.write_header(Size::new(width, height));
let [x, mut y] = [padding; 2]; let [x, mut y] = [padding; 2];
for frame in frames { for page in &document.pages {
let ts = Transform::translate(x, y); let ts = Transform::translate(x, y);
let state = State::new(frame.size(), Transform::identity()); let state = State::new(page.frame.size(), Transform::identity());
renderer.render_frame(state, ts, frame); renderer.render_frame(state, ts, &page.frame);
y += frame.height() + padding; y += page.frame.height() + padding;
} }
renderer.finalize() renderer.finalize()

View File

@ -12,7 +12,7 @@ use smallvec::SmallVec;
use crate::diag::{bail, StrResult}; use crate::diag::{bail, StrResult};
use crate::foundations::{Content, Label, Repr, Selector}; use crate::foundations::{Content, Label, Repr, Selector};
use crate::introspection::{Location, Meta}; use crate::introspection::{Location, Meta};
use crate::layout::{Frame, FrameItem, Point, Position, Transform}; use crate::layout::{Frame, FrameItem, Page, Point, Position, Transform};
use crate::model::Numbering; use crate::model::Numbering;
use crate::util::NonZeroExt; use crate::util::NonZeroExt;
@ -38,16 +38,17 @@ pub struct Introspector {
impl Introspector { impl Introspector {
/// Applies new frames in-place, reusing the existing allocations. /// Applies new frames in-place, reusing the existing allocations.
#[typst_macros::time(name = "introspect")] #[typst_macros::time(name = "introspect")]
pub fn rebuild(&mut self, frames: &[Frame]) { pub fn rebuild(&mut self, pages: &[Page]) {
self.pages = frames.len(); self.pages = pages.len();
self.elems.clear(); self.elems.clear();
self.labels.clear(); self.labels.clear();
self.page_numberings.clear(); self.page_numberings.clear();
self.queries.clear(); self.queries.clear();
for (i, frame) in frames.iter().enumerate() { for (i, page) in pages.iter().enumerate() {
let page = NonZeroUsize::new(1 + i).unwrap(); let page_nr = NonZeroUsize::new(1 + i).unwrap();
self.extract(frame, page, Transform::identity()); self.extract(&page.frame, page_nr, Transform::identity());
self.page_numberings.push(page.numbering.clone());
} }
} }
@ -77,9 +78,6 @@ impl Introspector {
self.labels.entry(label).or_default().push(self.elems.len() - 1); self.labels.entry(label).or_default().push(self.elems.len() - 1);
} }
} }
FrameItem::Meta(Meta::PageNumbering(numbering), _) => {
self.page_numberings.push(numbering.clone());
}
_ => {} _ => {}
} }
} }

View File

@ -29,8 +29,7 @@ use crate::foundations::Packed;
use crate::foundations::{ use crate::foundations::{
category, elem, ty, Behave, Behaviour, Category, Content, Repr, Scope, Unlabellable, category, elem, ty, Behave, Behaviour, Category, Content, Repr, Scope, Unlabellable,
}; };
use crate::layout::PdfPageLabel; use crate::model::Destination;
use crate::model::{Destination, Numbering};
/// Interactions between document parts. /// Interactions between document parts.
/// ///
@ -88,10 +87,6 @@ pub enum Meta {
/// An identifiable element that produces something within the area this /// An identifiable element that produces something within the area this
/// metadata is attached to. /// metadata is attached to.
Elem(Content), Elem(Content),
/// The numbering of the current page.
PageNumbering(Option<Numbering>),
/// A PDF page label of the current page.
PdfPageLabel(PdfPageLabel),
/// Indicates that content should be hidden. This variant doesn't appear /// Indicates that content should be hidden. This variant doesn't appear
/// in the final frames as it is removed alongside the content that should /// in the final frames as it is removed alongside the content that should
/// be hidden. /// be hidden.
@ -103,8 +98,6 @@ impl Debug for Meta {
match self { match self {
Self::Link(dest) => write!(f, "Link({dest:?})"), Self::Link(dest) => write!(f, "Link({dest:?})"),
Self::Elem(content) => write!(f, "Elem({:?})", content.func()), Self::Elem(content) => write!(f, "Elem({:?})", content.func()),
Self::PageNumbering(value) => write!(f, "PageNumbering({value:?})"),
Self::PdfPageLabel(label) => write!(f, "PdfPageLabel({label:?})"),
Self::Hide => f.pad("Hide"), Self::Hide => f.pad("Hide"),
} }
} }

View File

@ -4,9 +4,7 @@ use std::fmt::{self, Debug, Formatter};
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::sync::Arc; use std::sync::Arc;
use ecow::{eco_format, EcoString}; use crate::foundations::{cast, dict, Dict, StyleChain, Value};
use crate::foundations::{cast, dict, Dict, Repr, StyleChain, Value};
use crate::introspection::{Meta, MetaElem}; use crate::introspection::{Meta, MetaElem};
use crate::layout::{ use crate::layout::{
Abs, Axes, Corners, FixedAlignment, Length, Point, Rel, Sides, Size, Transform, Abs, Axes, Corners, FixedAlignment, Length, Point, Rel, Sides, Size, Transform,
@ -552,42 +550,3 @@ impl From<Position> for Dict {
} }
} }
} }
/// Specification for a PDF page label.
#[derive(Debug, Clone, PartialEq, Hash, Default)]
pub 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>,
}
impl Repr for PdfPageLabel {
fn repr(&self) -> EcoString {
eco_format!("{self:?}")
}
}
/// 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,
}

View File

@ -9,10 +9,10 @@ use crate::foundations::{
cast, elem, AutoValue, Cast, Content, Dict, Fold, Func, NativeElement, Packed, cast, elem, AutoValue, Cast, Content, Dict, Fold, Func, NativeElement, Packed,
Resolve, Smart, StyleChain, Value, Resolve, Smart, StyleChain, Value,
}; };
use crate::introspection::{Counter, CounterKey, ManualPageCounter, Meta}; use crate::introspection::{Counter, CounterKey, ManualPageCounter};
use crate::layout::{ use crate::layout::{
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Fragment, Frame, HAlignment, Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, Layout, Length,
Layout, Length, Point, Ratio, Regions, Rel, Sides, Size, VAlignment, Point, Ratio, Regions, Rel, Sides, Size, VAlignment,
}; };
use crate::model::Numbering; use crate::model::Numbering;
@ -347,7 +347,7 @@ impl Packed<PageElem> {
styles: StyleChain, styles: StyleChain,
page_counter: &mut ManualPageCounter, page_counter: &mut ManualPageCounter,
extend_to: Option<Parity>, extend_to: Option<Parity>,
) -> SourceResult<Fragment> { ) -> SourceResult<Vec<Page>> {
// When one of the lengths is infinite the page fits its content along // When one of the lengths is infinite the page fits its content along
// that axis. // that axis.
let width = self.width(styles).unwrap_or(Abs::inf()); let width = self.width(styles).unwrap_or(Abs::inf());
@ -413,7 +413,6 @@ impl Packed<PageElem> {
let header_ascent = self.header_ascent(styles); let header_ascent = self.header_ascent(styles);
let footer_descent = self.footer_descent(styles); let footer_descent = self.footer_descent(styles);
let numbering = self.numbering(styles); let numbering = self.numbering(styles);
let numbering_meta = Meta::PageNumbering(numbering.clone());
let number_align = self.number_align(styles); let number_align = self.number_align(styles);
let mut header = Cow::Borrowed(self.header(styles)); let mut header = Cow::Borrowed(self.header(styles));
let mut footer = Cow::Borrowed(self.footer(styles)); let mut footer = Cow::Borrowed(self.footer(styles));
@ -447,7 +446,8 @@ impl Packed<PageElem> {
} }
// Post-process pages. // Post-process pages.
for frame in frames.iter_mut() { let mut pages = Vec::with_capacity(frames.len());
for mut frame in frames {
// The padded width of the page's content without margins. // The padded width of the page's content without margins.
let pw = frame.width(); let pw = frame.width();
@ -462,7 +462,6 @@ impl Packed<PageElem> {
// Realize margins. // Realize margins.
frame.set_size(frame.size() + margin.sum_by_axis()); frame.set_size(frame.size() + margin.sum_by_axis());
frame.translate(Point::new(margin.left, margin.top)); frame.translate(Point::new(margin.left, margin.top));
frame.push_positionless_meta(numbering_meta.clone());
// The page size with margins. // The page size with margins.
let size = frame.size(); let size = frame.size();
@ -506,22 +505,32 @@ impl Packed<PageElem> {
frame.fill(fill.clone()); frame.fill(fill.clone());
} }
page_counter.visit(engine, frame)?; page_counter.visit(engine, &frame)?;
pages.push(Page {
// Add a PDF page label if there is a numbering. frame,
if let Some(num) = numbering { numbering: numbering.clone(),
if let Some(page_label) = num.apply_pdf(page_counter.logical()) { number: page_counter.logical(),
frame.push_positionless_meta(Meta::PdfPageLabel(page_label)); });
}
}
page_counter.step(); page_counter.step();
} }
Ok(Fragment::frames(frames)) Ok(pages)
} }
} }
/// A finished page.
#[derive(Debug, Default, Clone)]
pub struct Page {
/// The frame that defines the page.
pub frame: Frame,
/// The page's numbering.
pub numbering: Option<Numbering>,
/// The logical page number (controlled by `counter(page)` and may thus not
/// match the physical number).
pub number: usize,
}
/// Specification of the page's margins. /// Specification of the page's margins.
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
pub struct Margin { pub struct Margin {

View File

@ -7,7 +7,7 @@ use crate::foundations::{
Value, Value,
}; };
use crate::introspection::{Introspector, ManualPageCounter}; use crate::introspection::{Introspector, ManualPageCounter};
use crate::layout::{Frame, LayoutRoot, PageElem}; use crate::layout::{LayoutRoot, Page, PageElem};
/// The root element of a document and its metadata. /// The root element of a document and its metadata.
/// ///
@ -95,9 +95,8 @@ impl LayoutRoot for Packed<DocumentElem> {
.to_packed::<PageElem>()? .to_packed::<PageElem>()?
.clear_to(styles) .clear_to(styles)
}); });
let fragment = let run = page.layout(engine, styles, &mut page_counter, extend_to)?;
page.layout(engine, styles, &mut page_counter, extend_to)?; pages.extend(run);
pages.extend(fragment);
} else { } else {
bail!(child.span(), "unexpected document child"); bail!(child.span(), "unexpected document child");
} }
@ -139,8 +138,8 @@ cast! {
/// A finished document with metadata and page frames. /// A finished document with metadata and page frames.
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct Document { pub struct Document {
/// The page frames. /// The document's finished pages.
pub pages: Vec<Frame>, pub pages: Vec<Page>,
/// The document's title. /// The document's title.
pub title: Option<EcoString>, pub title: Option<EcoString>,
/// The document's author. /// The document's author.

View File

@ -1,4 +1,3 @@
use std::num::NonZeroUsize;
use std::str::FromStr; use std::str::FromStr;
use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese}; use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
@ -7,7 +6,6 @@ use ecow::{eco_format, EcoString, EcoVec};
use crate::diag::SourceResult; use crate::diag::SourceResult;
use crate::engine::Engine; use crate::engine::Engine;
use crate::foundations::{cast, func, Func, Str, Value}; use crate::foundations::{cast, func, Func, Str, Value};
use crate::layout::{PdfPageLabel, PdfPageLabelStyle};
use crate::text::Case; use crate::text::Case;
/// Applies a numbering to a sequence of numbers. /// Applies a numbering to a sequence of numbers.
@ -89,49 +87,6 @@ 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, 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. /// Trim the prefix suffix if this is a pattern.
pub fn trimmed(mut self) -> Self { pub fn trimmed(mut self) -> Self {
if let Self::Pattern(pattern) = &mut self { if let Self::Pattern(pattern) = &mut self {

View File

@ -67,7 +67,7 @@ fuzz_target!(|text: &str| {
let mut tracer = Tracer::new(); let mut tracer = Tracer::new();
if let Ok(document) = typst::compile(&world, &mut tracer) { if let Ok(document) = typst::compile(&world, &mut tracer) {
if let Some(page) = document.pages.first() { if let Some(page) = document.pages.first() {
std::hint::black_box(typst_render::render(page, 1.0, Color::WHITE)); std::hint::black_box(typst_render::render(&page.frame, 1.0, Color::WHITE));
} }
} }
comemo::evict(10); comemo::evict(10);

View File

@ -75,7 +75,7 @@ fn bench_render(iai: &mut Iai) {
let world = BenchWorld::new(); let world = BenchWorld::new();
let mut tracer = Tracer::new(); let mut tracer = Tracer::new();
let document = typst::compile(&world, &mut tracer).unwrap(); let document = typst::compile(&world, &mut tracer).unwrap();
iai.run(|| typst_render::render(&document.pages[0], 1.0, Color::WHITE)) iai.run(|| typst_render::render(&document.pages[0].frame, 1.0, Color::WHITE))
} }
struct BenchWorld { struct BenchWorld {

View File

@ -33,7 +33,7 @@ use typst::diag::{bail, FileError, FileResult, Severity, SourceDiagnostic, StrRe
use typst::eval::Tracer; use typst::eval::Tracer;
use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value}; use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
use typst::introspection::Meta; use typst::introspection::Meta;
use typst::layout::{Abs, Frame, FrameItem, Margin, PageElem, Transform}; use typst::layout::{Abs, Frame, FrameItem, Margin, Page, PageElem, Transform};
use typst::model::Document; use typst::model::Document;
use typst::syntax::{FileId, Source, SyntaxNode, VirtualPath}; use typst::syntax::{FileId, Source, SyntaxNode, VirtualPath};
use typst::text::{Font, FontBook, TextElem, TextSize}; use typst::text::{Font, FontBook, TextElem, TextSize};
@ -423,7 +423,7 @@ fn test(
let mut output = String::new(); let mut output = String::new();
let mut ok = true; let mut ok = true;
let mut updated = false; let mut updated = false;
let mut frames = vec![]; let mut pages = vec![];
let mut line = 0; let mut line = 0;
let mut header_configuration = None; let mut header_configuration = None;
let mut compare_ever = false; let mut compare_ever = false;
@ -490,13 +490,13 @@ fn test(
ok &= part_ok; ok &= part_ok;
compare_ever |= compare_here; compare_ever |= compare_here;
frames.extend(part_frames); pages.extend(part_frames);
} }
line += part.lines().count() + 1; line += part.lines().count() + 1;
} }
let document = Document { pages: frames, ..Default::default() }; let document = Document { pages, ..Default::default() };
if compare_ever { if compare_ever {
if let Some(pdf_path) = pdf_path { if let Some(pdf_path) = pdf_path {
let pdf_data = typst_pdf::pdf( let pdf_data = typst_pdf::pdf(
@ -514,11 +514,12 @@ fn test(
} }
} }
let canvas = render(&document.pages); let canvas = render(&document);
fs::create_dir_all(png_path.parent().unwrap()).unwrap(); fs::create_dir_all(png_path.parent().unwrap()).unwrap();
canvas.save_png(png_path).unwrap(); canvas.save_png(png_path).unwrap();
let svg = typst_svg::svg_merged(&document.pages, Abs::pt(5.0)); let svg = typst_svg::svg_merged(&document, Abs::pt(5.0));
fs::create_dir_all(svg_path.parent().unwrap()).unwrap(); fs::create_dir_all(svg_path.parent().unwrap()).unwrap();
std::fs::write(svg_path, svg.as_bytes()).unwrap(); std::fs::write(svg_path, svg.as_bytes()).unwrap();
@ -595,7 +596,7 @@ fn test_part(
header_configuration: &TestConfig, header_configuration: &TestConfig,
rng: &mut LinearShift, rng: &mut LinearShift,
verbose: bool, verbose: bool,
) -> (bool, bool, Vec<Frame>) { ) -> (bool, bool, Vec<Page>) {
let source = world.set(src_path, text); let source = world.set(src_path, text);
if world.print.syntax { if world.print.syntax {
writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap(); writeln!(output, "Syntax Tree:\n{:#?}\n", source.root()).unwrap();
@ -1038,19 +1039,19 @@ fn test_spans_impl(output: &mut String, node: &SyntaxNode, within: Range<u64>) -
} }
/// Draw all frames into one image with padding in between. /// Draw all frames into one image with padding in between.
fn render(frames: &[Frame]) -> sk::Pixmap { fn render(document: &Document) -> sk::Pixmap {
let pixel_per_pt = 2.0; let pixel_per_pt = 2.0;
let padding = Abs::pt(5.0); let padding = Abs::pt(5.0);
for frame in frames { for page in &document.pages {
let limit = Abs::cm(100.0); let limit = Abs::cm(100.0);
if frame.width() > limit || frame.height() > limit { if page.frame.width() > limit || page.frame.height() > limit {
panic!("overlarge frame: {:?}", frame.size()); panic!("overlarge frame: {:?}", page.frame.size());
} }
} }
let mut pixmap = typst_render::render_merged( let mut pixmap = typst_render::render_merged(
frames, document,
pixel_per_pt, pixel_per_pt,
Color::WHITE, Color::WHITE,
padding, padding,
@ -1059,11 +1060,12 @@ fn render(frames: &[Frame]) -> sk::Pixmap {
let padding = (pixel_per_pt * padding.to_pt() as f32).round(); let padding = (pixel_per_pt * padding.to_pt() as f32).round();
let [x, mut y] = [padding; 2]; let [x, mut y] = [padding; 2];
for frame in frames { for page in &document.pages {
let ts = let ts =
sk::Transform::from_scale(pixel_per_pt, pixel_per_pt).post_translate(x, y); sk::Transform::from_scale(pixel_per_pt, pixel_per_pt).post_translate(x, y);
render_links(&mut pixmap, ts, frame); render_links(&mut pixmap, ts, &page.frame);
y += (pixel_per_pt * frame.height().to_pt() as f32).round().max(1.0) + padding; y += (pixel_per_pt * page.frame.height().to_pt() as f32).round().max(1.0)
+ padding;
} }
pixmap pixmap