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:
parent
50741209a8
commit
6ac71eeaf7
@ -219,7 +219,7 @@ fn export_image(
|
||||
.pages
|
||||
.par_iter()
|
||||
.enumerate()
|
||||
.map(|(i, frame)| {
|
||||
.map(|(i, page)| {
|
||||
let storage;
|
||||
let path = if numbered {
|
||||
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 the frame is in the cache, skip 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(());
|
||||
}
|
||||
|
||||
match fmt {
|
||||
ImageExportFormat::Png => {
|
||||
let pixmap =
|
||||
typst_render::render(frame, command.ppi / 72.0, Color::WHITE);
|
||||
let pixmap = typst_render::render(
|
||||
&page.frame,
|
||||
command.ppi / 72.0,
|
||||
Color::WHITE,
|
||||
);
|
||||
pixmap
|
||||
.save_png(path)
|
||||
.map_err(|err| eco_format!("failed to write PNG file ({err})"))?;
|
||||
}
|
||||
ImageExportFormat::Svg => {
|
||||
let svg = typst_svg::svg(frame);
|
||||
let svg = typst_svg::svg(&page.frame);
|
||||
fs::write(path, svg.as_bytes())
|
||||
.map_err(|err| eco_format!("failed to write SVG file ({err})"))?;
|
||||
}
|
||||
|
@ -388,8 +388,8 @@ fn code_block(resolver: &dyn Resolver, lang: &str, text: &str) -> Html {
|
||||
let world = DocWorld(source);
|
||||
|
||||
let mut tracer = Tracer::new();
|
||||
let mut frames = match typst::compile(&world, &mut tracer) {
|
||||
Ok(doc) => doc.pages,
|
||||
let mut document = match typst::compile(&world, &mut tracer) {
|
||||
Ok(doc) => doc,
|
||||
Err(err) => {
|
||||
let msg = &err[0].message;
|
||||
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 {
|
||||
frames[0].translate(Point::new(-x, -y));
|
||||
*frames[0].size_mut() = Size::new(w, h);
|
||||
document.pages[0].frame.translate(Point::new(-x, -y));
|
||||
*document.pages[0].frame.size_mut() = Size::new(w, h);
|
||||
}
|
||||
|
||||
if single {
|
||||
frames.truncate(1);
|
||||
document.pages.truncate(1);
|
||||
}
|
||||
|
||||
let hash = typst::util::hash128(text);
|
||||
resolver.example(hash, highlighted, &frames)
|
||||
resolver.example(hash, highlighted, &document)
|
||||
}
|
||||
|
||||
/// Extract an attribute value from an HTML element.
|
||||
|
@ -25,9 +25,10 @@ use typst::foundations::{
|
||||
FOUNDATIONS,
|
||||
};
|
||||
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::math::MATH;
|
||||
use typst::model::Document;
|
||||
use typst::model::MODEL;
|
||||
use typst::symbols::SYMBOLS;
|
||||
use typst::text::{Font, FontBook, TEXT};
|
||||
@ -97,7 +98,7 @@ pub trait Resolver {
|
||||
fn image(&self, filename: &str, data: &[u8]) -> String;
|
||||
|
||||
/// 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.
|
||||
fn commits(&self, from: &str, to: &str) -> Vec<Commit>;
|
||||
@ -789,7 +790,7 @@ mod tests {
|
||||
None
|
||||
}
|
||||
|
||||
fn example(&self, _: u128, _: Option<Html>, _: &[Frame]) -> Html {
|
||||
fn example(&self, _: u128, _: Option<Html>, _: &Document) -> Html {
|
||||
Html::new(String::new())
|
||||
}
|
||||
|
||||
|
@ -121,8 +121,8 @@ pub fn jump_from_cursor(
|
||||
}
|
||||
|
||||
let span = node.span();
|
||||
for (i, frame) in document.pages.iter().enumerate() {
|
||||
if let Some(pos) = find_in_frame(frame, span) {
|
||||
for (i, page) in document.pages.iter().enumerate() {
|
||||
if let Some(pos) = find_in_frame(&page.frame, span) {
|
||||
return Some(Position {
|
||||
page: NonZeroUsize::new(i + 1).unwrap(),
|
||||
point: pos,
|
||||
|
@ -30,7 +30,7 @@ use crate::color::ColorSpaces;
|
||||
use crate::extg::ExtGState;
|
||||
use crate::gradient::PdfGradient;
|
||||
use crate::image::EncodedImage;
|
||||
use crate::page::Page;
|
||||
use crate::page::EncodedPage;
|
||||
use crate::pattern::PdfPattern;
|
||||
|
||||
/// Export a document into a PDF file.
|
||||
@ -72,7 +72,7 @@ struct PdfContext<'a> {
|
||||
/// The writer we are writing the PDF into.
|
||||
pdf: Pdf,
|
||||
/// Content of exported pages.
|
||||
pages: Vec<Page>,
|
||||
pages: Vec<EncodedPage>,
|
||||
/// For each font a mapping from used glyphs to their text representation.
|
||||
/// May contain multiple chars in case of ligatures or similar things. The
|
||||
/// same glyph can have a different text representation within one document,
|
||||
|
@ -10,11 +10,10 @@ use pdf_writer::writers::PageLabel;
|
||||
use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str, TextStr};
|
||||
use typst::introspection::Meta;
|
||||
use typst::layout::{
|
||||
Abs, Em, Frame, FrameItem, GroupItem, PdfPageLabel, PdfPageLabelStyle, Point, Ratio,
|
||||
Size, Transform,
|
||||
Abs, Em, Frame, FrameItem, GroupItem, Page, Point, Ratio, Size, Transform,
|
||||
};
|
||||
use typst::model::Destination;
|
||||
use typst::text::{Font, TextItem};
|
||||
use typst::model::{Destination, Numbering};
|
||||
use typst::text::{Case, Font, TextItem};
|
||||
use typst::util::{Deferred, Numeric};
|
||||
use typst::visualize::{
|
||||
FixedStroke, Geometry, Image, LineCap, LineJoin, Paint, Path, PathItem, Shape,
|
||||
@ -27,34 +26,36 @@ use crate::{deflate_deferred, AbsExt, EmExt, PdfContext};
|
||||
|
||||
/// Construct page objects.
|
||||
#[typst_macros::time(name = "construct pages")]
|
||||
pub(crate) fn construct_pages(ctx: &mut PdfContext, frames: &[Frame]) {
|
||||
for frame in frames {
|
||||
let (page_ref, page) = construct_page(ctx, frame);
|
||||
pub(crate) fn construct_pages(ctx: &mut PdfContext, pages: &[Page]) {
|
||||
for page in pages {
|
||||
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.pages.push(page);
|
||||
ctx.pages.push(encoded);
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct a page object.
|
||||
#[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 size = frame.size();
|
||||
let mut ctx = PageContext {
|
||||
parent: ctx,
|
||||
page_ref,
|
||||
label: None,
|
||||
uses_opacities: false,
|
||||
content: Content::new(),
|
||||
state: State::new(frame.size()),
|
||||
state: State::new(size),
|
||||
saves: vec![],
|
||||
bottom: 0.0,
|
||||
links: vec![],
|
||||
resources: HashMap::default(),
|
||||
};
|
||||
|
||||
let size = frame.size();
|
||||
|
||||
// Make the coordinate system start at the top-left.
|
||||
ctx.bottom = size.y.to_f32();
|
||||
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.
|
||||
write_frame(&mut ctx, frame);
|
||||
|
||||
let page = Page {
|
||||
let page = EncodedPage {
|
||||
size,
|
||||
content: deflate_deferred(ctx.content.finish()),
|
||||
id: ctx.page_ref,
|
||||
uses_opacities: ctx.uses_opacities,
|
||||
links: ctx.links,
|
||||
label: ctx.label,
|
||||
label: None,
|
||||
resources: ctx.resources,
|
||||
};
|
||||
|
||||
@ -249,8 +250,86 @@ pub(crate) fn write_page_labels(ctx: &mut PdfContext) -> Vec<(NonZeroUsize, Ref)
|
||||
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.
|
||||
pub struct Page {
|
||||
pub struct EncodedPage {
|
||||
/// The indirect object id of the page.
|
||||
pub id: Ref,
|
||||
/// The page's dimensions.
|
||||
@ -261,10 +340,10 @@ pub struct Page {
|
||||
pub uses_opacities: bool,
|
||||
/// Links in the PDF coordinate system.
|
||||
pub links: Vec<(Destination, Rect)>,
|
||||
/// The page's PDF label.
|
||||
pub label: Option<PdfPageLabel>,
|
||||
/// The page's used resources
|
||||
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.
|
||||
@ -326,7 +405,6 @@ impl PageResource {
|
||||
pub struct PageContext<'a, 'b> {
|
||||
pub(crate) parent: &'a mut PdfContext<'b>,
|
||||
page_ref: Ref,
|
||||
label: Option<PdfPageLabel>,
|
||||
pub content: Content,
|
||||
state: State,
|
||||
saves: Vec<State>,
|
||||
@ -557,7 +635,6 @@ fn write_frame(ctx: &mut PageContext, frame: &Frame) {
|
||||
for &(pos, ref item) in frame.items() {
|
||||
let x = pos.x.to_f32();
|
||||
let y = pos.y.to_f32();
|
||||
|
||||
match item {
|
||||
FrameItem::Group(group) => write_group(ctx, pos, group),
|
||||
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::Elem(_) => {}
|
||||
Meta::Hide => {}
|
||||
Meta::PageNumbering(_) => {}
|
||||
Meta::PdfPageLabel(label) => ctx.label = Some(label.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -13,6 +13,7 @@ use typst::introspection::Meta;
|
||||
use typst::layout::{
|
||||
Abs, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Ratio, Size, Transform,
|
||||
};
|
||||
use typst::model::Document;
|
||||
use typst::text::{Font, TextItem};
|
||||
use typst::visualize::{
|
||||
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
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn render_merged(
|
||||
frames: &[Frame],
|
||||
document: &Document,
|
||||
pixel_per_pt: f32,
|
||||
frame_fill: Color,
|
||||
padding: Abs,
|
||||
padding_fill: Color,
|
||||
) -> sk::Pixmap {
|
||||
let pixmaps: Vec<_> = frames
|
||||
let pixmaps: Vec<_> = document
|
||||
.pages
|
||||
.iter()
|
||||
.map(|frame| render(frame, pixel_per_pt, frame_fill))
|
||||
.map(|page| render(&page.frame, pixel_per_pt, frame_fill))
|
||||
.collect();
|
||||
|
||||
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 {
|
||||
Meta::Link(_) => {}
|
||||
Meta::Elem(_) => {}
|
||||
Meta::PageNumbering(_) => {}
|
||||
Meta::PdfPageLabel(_) => {}
|
||||
Meta::Hide => {}
|
||||
},
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use typst::layout::{
|
||||
Abs, Angle, Axes, Frame, FrameItem, FrameKind, GroupItem, Point, Quadrant, Ratio,
|
||||
Size, Transform,
|
||||
};
|
||||
use typst::model::Document;
|
||||
use typst::text::{Font, TextItem};
|
||||
use typst::util::hash128;
|
||||
use typst::visualize::{
|
||||
@ -35,24 +36,33 @@ pub fn svg(frame: &Frame) -> String {
|
||||
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.
|
||||
pub fn svg_merged(frames: &[Frame], padding: Abs) -> String {
|
||||
pub fn svg_merged(document: &Document, padding: Abs) -> String {
|
||||
let width = 2.0 * padding
|
||||
+ frames.iter().map(|frame| frame.width()).max().unwrap_or_default();
|
||||
let height = padding + frames.iter().map(|page| page.height() + padding).sum::<Abs>();
|
||||
let size = Size::new(width, height);
|
||||
+ document
|
||||
.pages
|
||||
.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();
|
||||
renderer.write_header(size);
|
||||
renderer.write_header(Size::new(width, height));
|
||||
|
||||
let [x, mut y] = [padding; 2];
|
||||
for frame in frames {
|
||||
for page in &document.pages {
|
||||
let ts = Transform::translate(x, y);
|
||||
let state = State::new(frame.size(), Transform::identity());
|
||||
renderer.render_frame(state, ts, frame);
|
||||
y += frame.height() + padding;
|
||||
let state = State::new(page.frame.size(), Transform::identity());
|
||||
renderer.render_frame(state, ts, &page.frame);
|
||||
y += page.frame.height() + padding;
|
||||
}
|
||||
|
||||
renderer.finalize()
|
||||
|
@ -12,7 +12,7 @@ use smallvec::SmallVec;
|
||||
use crate::diag::{bail, StrResult};
|
||||
use crate::foundations::{Content, Label, Repr, Selector};
|
||||
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::util::NonZeroExt;
|
||||
|
||||
@ -38,16 +38,17 @@ pub struct Introspector {
|
||||
impl Introspector {
|
||||
/// Applies new frames in-place, reusing the existing allocations.
|
||||
#[typst_macros::time(name = "introspect")]
|
||||
pub fn rebuild(&mut self, frames: &[Frame]) {
|
||||
self.pages = frames.len();
|
||||
pub fn rebuild(&mut self, pages: &[Page]) {
|
||||
self.pages = pages.len();
|
||||
self.elems.clear();
|
||||
self.labels.clear();
|
||||
self.page_numberings.clear();
|
||||
self.queries.clear();
|
||||
|
||||
for (i, frame) in frames.iter().enumerate() {
|
||||
let page = NonZeroUsize::new(1 + i).unwrap();
|
||||
self.extract(frame, page, Transform::identity());
|
||||
for (i, page) in pages.iter().enumerate() {
|
||||
let page_nr = NonZeroUsize::new(1 + i).unwrap();
|
||||
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);
|
||||
}
|
||||
}
|
||||
FrameItem::Meta(Meta::PageNumbering(numbering), _) => {
|
||||
self.page_numberings.push(numbering.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
@ -29,8 +29,7 @@ use crate::foundations::Packed;
|
||||
use crate::foundations::{
|
||||
category, elem, ty, Behave, Behaviour, Category, Content, Repr, Scope, Unlabellable,
|
||||
};
|
||||
use crate::layout::PdfPageLabel;
|
||||
use crate::model::{Destination, Numbering};
|
||||
use crate::model::Destination;
|
||||
|
||||
/// Interactions between document parts.
|
||||
///
|
||||
@ -88,10 +87,6 @@ pub enum Meta {
|
||||
/// An identifiable element that produces something within the area this
|
||||
/// metadata is attached to.
|
||||
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
|
||||
/// in the final frames as it is removed alongside the content that should
|
||||
/// be hidden.
|
||||
@ -103,8 +98,6 @@ impl Debug for Meta {
|
||||
match self {
|
||||
Self::Link(dest) => write!(f, "Link({dest:?})"),
|
||||
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"),
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,7 @@ use std::fmt::{self, Debug, Formatter};
|
||||
use std::num::NonZeroUsize;
|
||||
use std::sync::Arc;
|
||||
|
||||
use ecow::{eco_format, EcoString};
|
||||
|
||||
use crate::foundations::{cast, dict, Dict, Repr, StyleChain, Value};
|
||||
use crate::foundations::{cast, dict, Dict, StyleChain, Value};
|
||||
use crate::introspection::{Meta, MetaElem};
|
||||
use crate::layout::{
|
||||
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,
|
||||
}
|
||||
|
@ -9,10 +9,10 @@ use crate::foundations::{
|
||||
cast, elem, AutoValue, Cast, Content, Dict, Fold, Func, NativeElement, Packed,
|
||||
Resolve, Smart, StyleChain, Value,
|
||||
};
|
||||
use crate::introspection::{Counter, CounterKey, ManualPageCounter, Meta};
|
||||
use crate::introspection::{Counter, CounterKey, ManualPageCounter};
|
||||
use crate::layout::{
|
||||
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Fragment, Frame, HAlignment,
|
||||
Layout, Length, Point, Ratio, Regions, Rel, Sides, Size, VAlignment,
|
||||
Abs, AlignElem, Alignment, Axes, ColumnsElem, Dir, Frame, HAlignment, Layout, Length,
|
||||
Point, Ratio, Regions, Rel, Sides, Size, VAlignment,
|
||||
};
|
||||
|
||||
use crate::model::Numbering;
|
||||
@ -347,7 +347,7 @@ impl Packed<PageElem> {
|
||||
styles: StyleChain,
|
||||
page_counter: &mut ManualPageCounter,
|
||||
extend_to: Option<Parity>,
|
||||
) -> SourceResult<Fragment> {
|
||||
) -> SourceResult<Vec<Page>> {
|
||||
// When one of the lengths is infinite the page fits its content along
|
||||
// that axis.
|
||||
let width = self.width(styles).unwrap_or(Abs::inf());
|
||||
@ -413,7 +413,6 @@ impl Packed<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());
|
||||
let number_align = self.number_align(styles);
|
||||
let mut header = Cow::Borrowed(self.header(styles));
|
||||
let mut footer = Cow::Borrowed(self.footer(styles));
|
||||
@ -447,7 +446,8 @@ impl Packed<PageElem> {
|
||||
}
|
||||
|
||||
// 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.
|
||||
let pw = frame.width();
|
||||
|
||||
@ -462,7 +462,6 @@ impl Packed<PageElem> {
|
||||
// Realize margins.
|
||||
frame.set_size(frame.size() + margin.sum_by_axis());
|
||||
frame.translate(Point::new(margin.left, margin.top));
|
||||
frame.push_positionless_meta(numbering_meta.clone());
|
||||
|
||||
// The page size with margins.
|
||||
let size = frame.size();
|
||||
@ -506,22 +505,32 @@ impl Packed<PageElem> {
|
||||
frame.fill(fill.clone());
|
||||
}
|
||||
|
||||
page_counter.visit(engine, 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.visit(engine, &frame)?;
|
||||
pages.push(Page {
|
||||
frame,
|
||||
numbering: numbering.clone(),
|
||||
number: page_counter.logical(),
|
||||
});
|
||||
|
||||
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.
|
||||
#[derive(Debug, Default, Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub struct Margin {
|
||||
|
@ -7,7 +7,7 @@ use crate::foundations::{
|
||||
Value,
|
||||
};
|
||||
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.
|
||||
///
|
||||
@ -95,9 +95,8 @@ impl LayoutRoot for Packed<DocumentElem> {
|
||||
.to_packed::<PageElem>()?
|
||||
.clear_to(styles)
|
||||
});
|
||||
let fragment =
|
||||
page.layout(engine, styles, &mut page_counter, extend_to)?;
|
||||
pages.extend(fragment);
|
||||
let run = page.layout(engine, styles, &mut page_counter, extend_to)?;
|
||||
pages.extend(run);
|
||||
} else {
|
||||
bail!(child.span(), "unexpected document child");
|
||||
}
|
||||
@ -139,8 +138,8 @@ cast! {
|
||||
/// A finished document with metadata and page frames.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct Document {
|
||||
/// The page frames.
|
||||
pub pages: Vec<Frame>,
|
||||
/// The document's finished pages.
|
||||
pub pages: Vec<Page>,
|
||||
/// The document's title.
|
||||
pub title: Option<EcoString>,
|
||||
/// The document's author.
|
||||
|
@ -1,4 +1,3 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::str::FromStr;
|
||||
|
||||
use chinese_number::{ChineseCase, ChineseCountMethod, ChineseVariant, NumberToChinese};
|
||||
@ -7,7 +6,6 @@ use ecow::{eco_format, EcoString, EcoVec};
|
||||
use crate::diag::SourceResult;
|
||||
use crate::engine::Engine;
|
||||
use crate::foundations::{cast, func, Func, Str, Value};
|
||||
use crate::layout::{PdfPageLabel, PdfPageLabelStyle};
|
||||
use crate::text::Case;
|
||||
|
||||
/// 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.
|
||||
pub fn trimmed(mut self) -> Self {
|
||||
if let Self::Pattern(pattern) = &mut self {
|
||||
|
@ -67,7 +67,7 @@ fuzz_target!(|text: &str| {
|
||||
let mut tracer = Tracer::new();
|
||||
if let Ok(document) = typst::compile(&world, &mut tracer) {
|
||||
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);
|
||||
|
@ -75,7 +75,7 @@ fn bench_render(iai: &mut Iai) {
|
||||
let world = BenchWorld::new();
|
||||
let mut tracer = Tracer::new();
|
||||
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 {
|
||||
|
@ -33,7 +33,7 @@ use typst::diag::{bail, FileError, FileResult, Severity, SourceDiagnostic, StrRe
|
||||
use typst::eval::Tracer;
|
||||
use typst::foundations::{func, Bytes, Datetime, NoneValue, Repr, Smart, Value};
|
||||
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::syntax::{FileId, Source, SyntaxNode, VirtualPath};
|
||||
use typst::text::{Font, FontBook, TextElem, TextSize};
|
||||
@ -423,7 +423,7 @@ fn test(
|
||||
let mut output = String::new();
|
||||
let mut ok = true;
|
||||
let mut updated = false;
|
||||
let mut frames = vec![];
|
||||
let mut pages = vec![];
|
||||
let mut line = 0;
|
||||
let mut header_configuration = None;
|
||||
let mut compare_ever = false;
|
||||
@ -490,13 +490,13 @@ fn test(
|
||||
|
||||
ok &= part_ok;
|
||||
compare_ever |= compare_here;
|
||||
frames.extend(part_frames);
|
||||
pages.extend(part_frames);
|
||||
}
|
||||
|
||||
line += part.lines().count() + 1;
|
||||
}
|
||||
|
||||
let document = Document { pages: frames, ..Default::default() };
|
||||
let document = Document { pages, ..Default::default() };
|
||||
if compare_ever {
|
||||
if let Some(pdf_path) = pdf_path {
|
||||
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();
|
||||
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();
|
||||
std::fs::write(svg_path, svg.as_bytes()).unwrap();
|
||||
|
||||
@ -595,7 +596,7 @@ fn test_part(
|
||||
header_configuration: &TestConfig,
|
||||
rng: &mut LinearShift,
|
||||
verbose: bool,
|
||||
) -> (bool, bool, Vec<Frame>) {
|
||||
) -> (bool, bool, Vec<Page>) {
|
||||
let source = world.set(src_path, text);
|
||||
if world.print.syntax {
|
||||
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.
|
||||
fn render(frames: &[Frame]) -> sk::Pixmap {
|
||||
fn render(document: &Document) -> sk::Pixmap {
|
||||
let pixel_per_pt = 2.0;
|
||||
let padding = Abs::pt(5.0);
|
||||
|
||||
for frame in frames {
|
||||
for page in &document.pages {
|
||||
let limit = Abs::cm(100.0);
|
||||
if frame.width() > limit || frame.height() > limit {
|
||||
panic!("overlarge frame: {:?}", frame.size());
|
||||
if page.frame.width() > limit || page.frame.height() > limit {
|
||||
panic!("overlarge frame: {:?}", page.frame.size());
|
||||
}
|
||||
}
|
||||
|
||||
let mut pixmap = typst_render::render_merged(
|
||||
frames,
|
||||
document,
|
||||
pixel_per_pt,
|
||||
Color::WHITE,
|
||||
padding,
|
||||
@ -1059,11 +1060,12 @@ fn render(frames: &[Frame]) -> sk::Pixmap {
|
||||
|
||||
let padding = (pixel_per_pt * padding.to_pt() as f32).round();
|
||||
let [x, mut y] = [padding; 2];
|
||||
for frame in frames {
|
||||
for page in &document.pages {
|
||||
let ts =
|
||||
sk::Transform::from_scale(pixel_per_pt, pixel_per_pt).post_translate(x, y);
|
||||
render_links(&mut pixmap, ts, frame);
|
||||
y += (pixel_per_pt * frame.height().to_pt() as f32).round().max(1.0) + padding;
|
||||
render_links(&mut pixmap, ts, &page.frame);
|
||||
y += (pixel_per_pt * page.frame.height().to_pt() as f32).round().max(1.0)
|
||||
+ padding;
|
||||
}
|
||||
|
||||
pixmap
|
||||
|
Loading…
x
Reference in New Issue
Block a user