From f2f05e07b0ff2d98e3c822b2618d02281ed1078c Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 16 Oct 2019 21:31:14 +0200 Subject: [PATCH] =?UTF-8?q?Implement=20space=20extension=20(multipage)=20?= =?UTF-8?q?=E2=9E=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 +- Cargo.toml | 3 + src/export/pdf.rs | 14 +- src/layout/flex.rs | 222 ++++++++---------- src/layout/mod.rs | 18 +- src/layout/stacked.rs | 135 ++++++++--- src/layout/tree.rs | 53 +++-- src/macros.rs | 2 + src/size.rs | 14 ++ tests/layouting.rs | 114 +++++++-- tests/layouts/pagebreaks.typ | 2 + tests/layouts/shakespeare-right.tps | 88 ------- .../{shakespeare.tps => shakespeare.tpl} | 0 tests/layouts/shakespeare.typ | 8 + tests/layouts/{styles.tps => styles.typ} | 5 +- tests/render.py | 75 ++++-- 16 files changed, 451 insertions(+), 304 deletions(-) create mode 100644 tests/layouts/pagebreaks.typ delete mode 100644 tests/layouts/shakespeare-right.tps rename tests/layouts/{shakespeare.tps => shakespeare.tpl} (100%) create mode 100644 tests/layouts/shakespeare.typ rename tests/layouts/{styles.tps => styles.typ} (51%) diff --git a/.gitignore b/.gitignore index 83240860d..773446c87 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,4 @@ **/*.rs.bk Cargo.lock things -test-cache +tests/cache diff --git a/Cargo.toml b/Cargo.toml index ee945338e..c2430db0d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,9 @@ byteorder = "1" smallvec = "0.6.10" unicode-xid = "0.1.0" +[dev-dependencies] +regex = "1" + [[bin]] name = "typstc" path = "src/bin/main.rs" diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 3c718c2ea..f029a37ff 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -80,7 +80,7 @@ impl<'d, W: Write> ExportProcess<'d, W> { ) -> PdfResult> { let (fonts, font_remap) = Self::subset_fonts(layouts, font_loader)?; - let offsets = Self::calculate_offset(layouts.count(), fonts.len()); + let offsets = Self::calculate_offsets(layouts.count(), fonts.len()); Ok(ExportProcess { writer: PdfWriter::new(target), @@ -155,7 +155,7 @@ impl<'d, W: Write> ExportProcess<'d, W> { /// We need to know in advance which IDs to use for which objects to cross-reference them. /// Therefore, we calculate them in the beginning. - fn calculate_offset(layout_count: usize, font_count: usize) -> Offsets { + fn calculate_offsets(layout_count: usize, font_count: usize) -> Offsets { let catalog = 1; let page_tree = catalog + 1; let pages = (page_tree + 1, page_tree + layout_count as Ref); @@ -203,7 +203,11 @@ impl<'d, W: Write> ExportProcess<'d, W> { )?; // The page objects (non-root nodes in the page tree). - for (id, page) in ids(self.offsets.pages).zip(self.layouts) { + let iter = ids(self.offsets.pages) + .zip(ids(self.offsets.contents)) + .zip(self.layouts); + + for ((page_id, content_id), page) in iter { let rect = Rect::new( 0.0, 0.0, @@ -212,10 +216,10 @@ impl<'d, W: Write> ExportProcess<'d, W> { ); self.writer.write_obj( - id, + page_id, Page::new(self.offsets.page_tree) .media_box(rect) - .contents(ids(self.offsets.contents)), + .content(content_id), )?; } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index a4b3ed6d9..80cc2074b 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -21,22 +21,19 @@ pub struct FlexLayouter { ctx: FlexContext, units: Vec, - actions: LayoutActionList, - usable: Size2D, - dimensions: Size2D, - cursor: Size2D, - + stack: StackLayouter, + usable_width: Size, run: FlexRun, - next_glue: Option, + cached_glue: Option, } /// The context for flex layouting. #[derive(Debug, Copy, Clone)] pub struct FlexContext { - /// The space to layout the boxes in. pub space: LayoutSpace, /// The spacing between two lines of boxes. pub flex_spacing: Size, + pub extra_space: Option, } enum FlexUnit { @@ -49,7 +46,7 @@ enum FlexUnit { } struct FlexRun { - content: Vec<(Size2D, Layout)>, + content: Vec<(Size, Layout)>, size: Size2D, } @@ -60,17 +57,17 @@ impl FlexLayouter { ctx, units: vec![], - actions: LayoutActionList::new(), - usable: ctx.space.usable(), - dimensions: match ctx.space.alignment { - Alignment::Left => Size2D::zero(), - Alignment::Right => Size2D::with_x(ctx.space.usable().x), + stack: StackLayouter::new(StackContext { + space: ctx.space, + extra_space: ctx.extra_space, + }), + + usable_width: ctx.space.usable().x, + run: FlexRun { + content: vec![], + size: Size2D::zero() }, - - cursor: Size2D::new(ctx.space.padding.left, ctx.space.padding.top), - - run: FlexRun::new(), - next_glue: None, + cached_glue: None, } } @@ -90,12 +87,14 @@ impl FlexLayouter { } /// Compute the justified layout. - pub fn finish(mut self) -> LayoutResult { + /// + /// The layouter is not consumed by this to prevent ownership problems + /// with borrowed layouters. The state of the layouter is not reset. + /// Therefore, it should not be further used after calling `finish`. + pub fn finish(&mut self) -> LayoutResult { // Move the units out of the layout because otherwise, we run into // ownership problems. - let units = self.units; - self.units = Vec::new(); - + let units = std::mem::replace(&mut self.units, vec![]); for unit in units { match unit { FlexUnit::Boxed(boxed) => self.layout_box(boxed)?, @@ -104,17 +103,88 @@ impl FlexLayouter { } // Finish the last flex run. - self.finish_flex_run(); + self.finish_run()?; - Ok(Layout { - dimensions: if self.ctx.space.shrink_to_fit { - self.dimensions.padded(self.ctx.space.padding) - } else { - self.ctx.space.dimensions - }, - actions: self.actions.into_vec(), - debug_render: true, - }) + self.stack.finish() + } + + /// Layout a content box into the current flex run or start a new run if + /// it does not fit. + fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> { + let glue_width = self + .cached_glue + .as_ref() + .map(|layout| layout.dimensions.x) + .unwrap_or(Size::zero()); + + let new_line_width = self.run.size.x + glue_width + boxed.dimensions.x; + + if self.overflows_line(new_line_width) { + self.cached_glue = None; + + // If the box does not even fit on its own line, then we try + // it in the next space, or we have to give up if there is none. + if self.overflows_line(boxed.dimensions.x) { + if self.ctx.extra_space.is_some() { + self.stack.finish_layout(true)?; + return self.layout_box(boxed); + } else { + return Err(LayoutError::NotEnoughSpace("cannot fit box into flex run")); + } + } + + self.finish_run()?; + } else { + // Only add the glue if we did not move to a new line. + self.flush_glue(); + } + + self.add_to_run(boxed); + + Ok(()) + } + + fn layout_glue(&mut self, glue: Layout) { + self.flush_glue(); + self.cached_glue = Some(glue); + } + + fn flush_glue(&mut self) { + if let Some(glue) = self.cached_glue.take() { + let new_line_width = self.run.size.x + glue.dimensions.x; + if !self.overflows_line(new_line_width) { + self.add_to_run(glue); + } + } + } + + fn add_to_run(&mut self, layout: Layout) { + let x = self.run.size.x; + + self.run.size.x += layout.dimensions.x; + self.run.size.y = crate::size::max(self.run.size.y, layout.dimensions.y); + + self.run.content.push((x, layout)); + } + + fn finish_run(&mut self) -> LayoutResult<()> { + self.run.size.y += self.ctx.flex_spacing; + + let mut actions = LayoutActionList::new(); + for (x, layout) in self.run.content.drain(..) { + let position = Size2D::with_x(x); + actions.add_layout(position, layout); + } + + self.stack.add(Layout { + dimensions: self.run.size, + actions: actions.into_vec(), + debug_render: false, + })?; + + self.run.size = Size2D::zero(); + + Ok(()) } /// Whether this layouter contains any items. @@ -122,91 +192,7 @@ impl FlexLayouter { self.units.is_empty() } - fn layout_box(&mut self, boxed: Layout) -> LayoutResult<()> { - let next_glue_width = self - .next_glue - .as_ref() - .map(|g| g.dimensions.x) - .unwrap_or(Size::zero()); - - let new_line_width = self.run.size.x + next_glue_width + boxed.dimensions.x; - - if self.overflows(new_line_width) { - // If the box does not even fit on its own line, then - // we can't do anything. - if self.overflows(boxed.dimensions.x) { - return Err(LayoutError::NotEnoughSpace); - } - - self.next_glue = None; - self.finish_flex_run(); - } else { - // Only add the glue if we did not move to a new line. - self.flush_glue(); - } - - self.add_to_flex_run(boxed); - - Ok(()) - } - - fn layout_glue(&mut self, glue: Layout) { - self.flush_glue(); - self.next_glue = Some(glue); - } - - fn flush_glue(&mut self) { - if let Some(glue) = self.next_glue.take() { - self.add_to_flex_run(glue); - } - } - - fn add_to_flex_run(&mut self, layout: Layout) { - let position = self.cursor; - - self.cursor.x += layout.dimensions.x; - self.run.size.x += layout.dimensions.x; - self.run.size.y = crate::size::max(self.run.size.y, layout.dimensions.y); - - self.run.content.push((position, layout)); - } - - fn finish_flex_run(&mut self) { - // Add all layouts from the current flex run at the correct positions. - match self.ctx.space.alignment { - Alignment::Left => { - for (position, layout) in self.run.content.drain(..) { - self.actions.add_layout(position, layout); - } - } - - Alignment::Right => { - let extra_space = Size2D::with_x(self.usable.x - self.run.size.x); - for (position, layout) in self.run.content.drain(..) { - self.actions.add_layout(position + extra_space, layout); - } - } - } - - self.dimensions.x = crate::size::max(self.dimensions.x, self.run.size.x); - self.dimensions.y += self.ctx.flex_spacing; - self.dimensions.y += self.run.size.y; - - self.cursor.x = self.ctx.space.padding.left; - self.cursor.y += self.run.size.y + self.ctx.flex_spacing; - self.run.size = Size2D::zero(); - } - - fn overflows(&self, line: Size) -> bool { - line > self.usable.x - } -} - -impl FlexRun { - fn new() -> FlexRun { - FlexRun { - content: vec![], - size: Size2D::zero() - } + fn overflows_line(&self, line: Size) -> bool { + line > self.usable_width } } diff --git a/src/layout/mod.rs b/src/layout/mod.rs index 031226b98..b0fba4f2c 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -48,6 +48,7 @@ impl Layout { self.dimensions.x.to_pt(), self.dimensions.y.to_pt() )?; + writeln!(f, "{}", self.actions.len())?; for action in &self.actions { action.serialize(f)?; writeln!(f)?; @@ -93,6 +94,17 @@ impl MultiLayout { } } +impl MultiLayout { + /// Serialize this collection of layouts into an output buffer. + pub fn serialize(&self, f: &mut W) -> io::Result<()> { + writeln!(f, "{}", self.count())?; + for layout in self { + layout.serialize(f)?; + } + Ok(()) + } +} + impl IntoIterator for MultiLayout { type Item = Layout; type IntoIter = std::vec::IntoIter; @@ -112,7 +124,7 @@ impl<'a> IntoIterator for &'a MultiLayout { } /// The general context for layouting. -#[derive(Copy, Clone)] +#[derive(Debug, Copy, Clone)] pub struct LayoutContext<'a, 'p> { pub loader: &'a SharedFontLoader<'p>, pub style: &'a TextStyle, @@ -154,7 +166,7 @@ pub enum Alignment { /// The error type for layouting. pub enum LayoutError { /// There is not enough space to add an item. - NotEnoughSpace, + NotEnoughSpace(&'static str), /// There was no suitable font for the given character. NoSuitableFont(char), /// An error occured while gathering font data. @@ -167,7 +179,7 @@ pub type LayoutResult = Result; error_type! { err: LayoutError, show: f => match err { - LayoutError::NotEnoughSpace => write!(f, "not enough space"), + LayoutError::NotEnoughSpace(desc) => write!(f, "not enough space: {}", desc), LayoutError::NoSuitableFont(c) => write!(f, "no suitable font for '{}'", c), LayoutError::Font(err) => write!(f, "font error: {}", err), }, diff --git a/src/layout/stacked.rs b/src/layout/stacked.rs index bfca4e3ef..367a03d77 100644 --- a/src/layout/stacked.rs +++ b/src/layout/stacked.rs @@ -5,44 +5,38 @@ use super::*; /// The boxes are arranged vertically, each layout gettings it's own "line". pub struct StackLayouter { ctx: StackContext, + layouts: MultiLayout, actions: LayoutActionList, + + space: LayoutSpace, usable: Size2D, dimensions: Size2D, cursor: Size2D, + in_extra_space: bool, + started: bool, } /// The context for stack layouting. #[derive(Debug, Copy, Clone)] pub struct StackContext { - /// The space to layout the boxes in. pub space: LayoutSpace, + pub extra_space: Option, } impl StackLayouter { /// Create a new stack layouter. pub fn new(ctx: StackContext) -> StackLayouter { - let space = ctx.space; - StackLayouter { ctx, + layouts: MultiLayout::new(), actions: LayoutActionList::new(), + space: ctx.space, usable: ctx.space.usable(), - dimensions: match ctx.space.alignment { - Alignment::Left => Size2D::zero(), - Alignment::Right => Size2D::with_x(space.usable().x), - }, - - cursor: Size2D::new( - // If left-align, the cursor points to the top-left corner of - // each box. If we right-align, it points to the top-right - // corner. - match ctx.space.alignment { - Alignment::Left => space.padding.left, - Alignment::Right => space.dimensions.x - space.padding.right, - }, - space.padding.top, - ), + dimensions: start_dimensions(ctx.space), + cursor: start_cursor(ctx.space), + in_extra_space: false, + started: true, } } @@ -53,19 +47,30 @@ impl StackLayouter { /// Add a sublayout to the bottom. pub fn add(&mut self, layout: Layout) -> LayoutResult<()> { + if !self.started { + self.start_new_space()?; + } + let new_dimensions = Size2D { x: crate::size::max(self.dimensions.x, layout.dimensions.x), y: self.dimensions.y + layout.dimensions.y, }; if self.overflows(new_dimensions) { - return Err(LayoutError::NotEnoughSpace); + if self.ctx.extra_space.is_some() && + !(self.in_extra_space && self.overflows(layout.dimensions)) + { + self.finish_layout(true)?; + return self.add(layout); + } else { + return Err(LayoutError::NotEnoughSpace("cannot fit box into stack")); + } } // Determine where to put the box. When we right-align it, we want the // cursor to point to the top-right corner of the box. Therefore, the // position has to be moved to the left by the width of the box. - let position = match self.ctx.space.alignment { + let position = match self.space.alignment { Alignment::Left => self.cursor, Alignment::Right => self.cursor - Size2D::with_x(layout.dimensions.x), }; @@ -88,26 +93,74 @@ impl StackLayouter { /// Add vertical space after the last layout. pub fn add_space(&mut self, space: Size) -> LayoutResult<()> { - if self.overflows(self.dimensions + Size2D::with_y(space)) { - return Err(LayoutError::NotEnoughSpace); + if !self.started { + self.start_new_space()?; } - self.cursor.y += space; - self.dimensions.y += space; + let new_dimensions = self.dimensions + Size2D::with_y(space); + + if self.overflows(new_dimensions) { + if self.ctx.extra_space.is_some() { + self.finish_layout(false)?; + } else { + return Err(LayoutError::NotEnoughSpace("cannot fit space into stack")); + } + } else { + self.cursor.y += space; + self.dimensions.y += space; + } Ok(()) } /// Finish the layouting. - pub fn finish(self) -> Layout { - Layout { - dimensions: if self.ctx.space.shrink_to_fit { - self.dimensions.padded(self.ctx.space.padding) + /// + /// The layouter is not consumed by this to prevent ownership problems. + /// It should not be used further. + pub fn finish(&mut self) -> LayoutResult { + if self.started { + self.finish_layout(false)?; + } + Ok(std::mem::replace(&mut self.layouts, MultiLayout::new())) + } + + /// Finish the current layout and start a new one in an extra space + /// (if there is an extra space). + /// + /// If `start_new_empty` is true, a new empty layout will be started. Otherwise, + /// the new layout only emerges when new content is added. + pub fn finish_layout(&mut self, start_new_empty: bool) -> LayoutResult<()> { + let actions = std::mem::replace(&mut self.actions, LayoutActionList::new()); + self.layouts.add(Layout { + dimensions: if self.space.shrink_to_fit { + self.dimensions.padded(self.space.padding) } else { - self.ctx.space.dimensions + self.space.dimensions }, - actions: self.actions.into_vec(), + actions: actions.into_vec(), debug_render: true, + }); + + self.started = false; + + if start_new_empty { + self.start_new_space()?; + } + + Ok(()) + } + + pub fn start_new_space(&mut self) -> LayoutResult<()> { + if let Some(space) = self.ctx.extra_space { + self.started = true; + self.space = space; + self.usable = space.usable(); + self.dimensions = start_dimensions(space); + self.cursor = start_cursor(space); + self.in_extra_space = true; + Ok(()) + } else { + Err(LayoutError::NotEnoughSpace("no extra space to start")) } } @@ -121,10 +174,30 @@ impl StackLayouter { /// Whether this layouter contains any items. pub fn is_empty(&self) -> bool { - self.actions.is_empty() + self.layouts.is_empty() && self.actions.is_empty() } fn overflows(&self, dimensions: Size2D) -> bool { !self.usable.fits(dimensions) } } + +fn start_dimensions(space: LayoutSpace) -> Size2D { + match space.alignment { + Alignment::Left => Size2D::zero(), + Alignment::Right => Size2D::with_x(space.usable().x), + } +} + +fn start_cursor(space: LayoutSpace) -> Size2D { + Size2D { + // If left-align, the cursor points to the top-left corner of + // each box. If we right-align, it points to the top-right + // corner. + x: match space.alignment { + Alignment::Left => space.padding.left, + Alignment::Right => space.dimensions.x - space.padding.right, + }, + y: space.padding.top, + } +} diff --git a/src/layout/tree.rs b/src/layout/tree.rs index 506168836..c8695e837 100644 --- a/src/layout/tree.rs +++ b/src/layout/tree.rs @@ -19,14 +19,13 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { fn new(ctx: LayoutContext<'a, 'p>) -> TreeLayouter<'a, 'p> { TreeLayouter { ctx, - stack: StackLayouter::new(StackContext { space: ctx.space }), + stack: StackLayouter::new(StackContext { + space: ctx.space, + extra_space: ctx.extra_space + }), flex: FlexLayouter::new(FlexContext { - space: LayoutSpace { - dimensions: ctx.space.usable(), - padding: SizeBox::zero(), - alignment: ctx.space.alignment, - shrink_to_fit: true, - }, + space: flex_space(ctx.space), + extra_space: ctx.extra_space.map(|s| flex_space(s)), flex_spacing: flex_spacing(&ctx.style), }), style: Cow::Borrowed(ctx.style), @@ -48,10 +47,8 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { // Finish the current flex layouting process. Node::Newline => { - self.layout_flex()?; - let space = paragraph_spacing(&self.style); - self.stack.add_space(space)?; + self.layout_flex(space)?; } // Toggle the text styles. @@ -70,12 +67,10 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { fn finish(mut self) -> LayoutResult { // If there are remainings, add them to the layout. if !self.flex.is_empty() { - self.layout_flex()?; + self.layout_flex(Size::zero())?; } - Ok(MultiLayout { - layouts: vec![self.stack.finish()], - }) + self.stack.finish() } /// Add text to the flex layout. If `glue` is true, the text will be a glue @@ -98,29 +93,38 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { } /// Finish the current flex layout and add it the stack. - fn layout_flex(&mut self) -> LayoutResult<()> { + fn layout_flex(&mut self, after_space: Size) -> LayoutResult<()> { if self.flex.is_empty() { return Ok(()); } + let layouts = self.flex.finish()?; + self.stack.add_many(layouts)?; + self.stack.add_space(after_space)?; + let mut ctx = self.flex.ctx(); ctx.space.dimensions = self.stack.remaining(); ctx.flex_spacing = flex_spacing(&self.style); - let next = FlexLayouter::new(ctx); - let flex = std::mem::replace(&mut self.flex, next); - let boxed = flex.finish()?; + self.flex = FlexLayouter::new(ctx); - self.stack.add(boxed) + Ok(()) } /// Layout a function. fn layout_func(&mut self, func: &FuncCall) -> LayoutResult<()> { let mut ctx = self.ctx; ctx.style = &self.style; + ctx.space.dimensions = self.stack.remaining(); ctx.space.padding = SizeBox::zero(); - ctx.space.shrink_to_fit = true; + ctx.space.shrink_to_fit = false; + + if let Some(space) = ctx.extra_space.as_mut() { + space.dimensions = space.dimensions.unpadded(space.padding); + space.padding = SizeBox::zero(); + space.shrink_to_fit = false; + } let commands = func.body.layout(ctx)?; @@ -137,6 +141,15 @@ impl<'a, 'p> TreeLayouter<'a, 'p> { } } +fn flex_space(space: LayoutSpace) -> LayoutSpace { + LayoutSpace { + dimensions: space.usable(), + padding: SizeBox::zero(), + alignment: space.alignment, + shrink_to_fit: true, + } +} + fn flex_spacing(style: &TextStyle) -> Size { (style.line_spacing - 1.0) * Size::pt(style.font_size) } diff --git a/src/macros.rs b/src/macros.rs index 831a37c8a..a1c182fbe 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -12,6 +12,7 @@ macro_rules! error_type { impl std::fmt::Display for $err { fn fmt(&self, $f: &mut std::fmt::Formatter) -> std::fmt::Result { + #[allow(unused)] let $var = self; $show } @@ -22,6 +23,7 @@ macro_rules! error_type { impl std::error::Error for $err { // The source method is only generated if an implementation was given. $(fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + #[allow(unused)] let $var = self; $source })* diff --git a/src/size.rs b/src/size.rs index c74394397..414fd855a 100644 --- a/src/size.rs +++ b/src/size.rs @@ -131,6 +131,15 @@ impl Size2D { } } + /// Return a [`Size2D`] reduced by the paddings of the given box. + #[inline] + pub fn unpadded(&self, padding: SizeBox) -> Size2D { + Size2D { + x: self.x - padding.left - padding.right, + y: self.y - padding.top - padding.bottom, + } + } + /// Whether the given [`Size2D`] fits into this one, that is, /// both coordinate values are smaller. #[inline] @@ -189,6 +198,11 @@ debug_display!(Size); /// An error which can be returned when parsing a size. pub struct ParseSizeError; +error_type! { + err: ParseSizeError, + show: f => write!(f, "failed to parse size"), +} + impl FromStr for Size { type Err = ParseSizeError; diff --git a/tests/layouting.rs b/tests/layouting.rs index 75aaa66b9..f77c6e850 100644 --- a/tests/layouting.rs +++ b/tests/layouting.rs @@ -3,12 +3,16 @@ use std::io::{BufWriter, Read, Write}; use std::process::Command; use std::time::Instant; +use regex::{Regex, Captures}; + use typst::export::pdf::PdfExporter; use typst::layout::LayoutAction; use typst::toddle::query::FileSystemFontProvider; +use typst::size::{Size, Size2D, SizeBox}; +use typst::style::PageStyle; use typst::Typesetter; -const CACHE_DIR: &str = "test-cache"; +const CACHE_DIR: &str = "tests/cache"; fn main() { let mut perfect_match = false; @@ -31,6 +35,10 @@ fn main() { for entry in fs::read_dir("tests/layouts/").unwrap() { let path = entry.unwrap().path(); + if path.extension() != Some(std::ffi::OsStr::new("typ")) { + continue; + } + let name = path.file_stem().unwrap().to_str().unwrap(); let matches = if perfect_match { @@ -51,36 +59,47 @@ fn main() { /// Create a _PDF_ with a name from the source code. fn test(name: &str, src: &str) { - print!("Testing: {}", name); + println!("Testing: {}", name); + + let (src, size) = preprocess(src); let mut typesetter = Typesetter::new(); let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap(); typesetter.add_font_provider(provider.clone()); + if let Some(dimensions) = size { + typesetter.set_page_style(PageStyle { + dimensions, + margins: SizeBox::zero() + }); + } + let start = Instant::now(); // Layout into box layout. - let tree = typesetter.parse(src).unwrap(); - let layout = typesetter.layout(&tree).unwrap(); + let tree = typesetter.parse(&src).unwrap(); + let layouts = typesetter.layout(&tree).unwrap(); let end = Instant::now(); let duration = end - start; - println!(" [{:?}]", duration); + println!(" => {:?}", duration); + println!(); // Write the serialed layout file. - let path = format!("{}/serialized/{}.box", CACHE_DIR, name); + let path = format!("{}/serialized/{}.lay", CACHE_DIR, name); let mut file = File::create(path).unwrap(); // Find all used fonts and their filenames. let mut map = Vec::new(); let mut loader = typesetter.loader().borrow_mut(); - let single = &layout.layouts[0]; - for action in &single.actions { - if let LayoutAction::SetFont(index, _) = action { - if map.iter().find(|(i, _)| i == index).is_none() { - let (_, provider_index) = loader.get_provider_and_index(*index); - let filename = provider.get_path(provider_index).to_str().unwrap(); - map.push((*index, filename)); + for layout in &layouts { + for action in &layout.actions { + if let LayoutAction::SetFont(index, _) = action { + if map.iter().find(|(i, _)| i == index).is_none() { + let (_, provider_index) = loader.get_provider_and_index(*index); + let filename = provider.get_path(provider_index).to_str().unwrap(); + map.push((*index, filename)); + } } } } @@ -91,7 +110,8 @@ fn test(name: &str, src: &str) { for (index, path) in map { writeln!(file, "{} {}", index, path).unwrap(); } - single.serialize(&mut file).unwrap(); + + layouts.serialize(&mut file).unwrap(); // Render the layout into a PNG. Command::new("python") @@ -104,5 +124,69 @@ fn test(name: &str, src: &str) { let path = format!("{}/pdf/{}.pdf", CACHE_DIR, name); let file = BufWriter::new(File::create(path).unwrap()); let exporter = PdfExporter::new(); - exporter.export(&layout, typesetter.loader(), file).unwrap(); + exporter.export(&layouts, typesetter.loader(), file).unwrap(); +} + +fn preprocess<'a>(src: &'a str) -> (String, Option) { + let include_regex = Regex::new(r"\{include:((.|\.|\-)*)\}").unwrap(); + let lorem_regex = Regex::new(r"\{lorem:(\d*)\}").unwrap(); + let size_regex = Regex::new(r"\{(size:(([\d\w]*)\*([\d\w]*)))\}").unwrap(); + + let mut size = None; + + let mut preprocessed = size_regex.replace_all(&src, |cap: &Captures| { + let width_str = cap.get(3).unwrap().as_str(); + let height_str = cap.get(4).unwrap().as_str(); + + let width = width_str.parse::().unwrap(); + let height = height_str.parse::().unwrap(); + + size = Some(Size2D::new(width, height)); + + "".to_string() + }).to_string(); + + let mut changed = true; + while changed { + changed = false; + preprocessed = include_regex.replace_all(&preprocessed, |cap: &Captures| { + changed = true; + let filename = cap.get(1).unwrap().as_str(); + + let path = format!("tests/layouts/{}", filename); + let mut file = File::open(path).unwrap(); + let mut buf = String::new(); + file.read_to_string(&mut buf).unwrap(); + buf + }).to_string(); + } + + preprocessed= lorem_regex.replace_all(&preprocessed, |cap: &Captures| { + let num_str = cap.get(1).unwrap().as_str(); + let num_words = num_str.parse::().unwrap(); + + generate_lorem(num_words) + }).to_string(); + + (preprocessed, size) +} + +fn generate_lorem(num_words: usize) -> String { + const LOREM: [&str; 69] = [ + "Lorem", "ipsum", "dolor", "sit", "amet,", "consectetur", "adipiscing", "elit.", "Etiam", + "suscipit", "porta", "pretium.", "Donec", "eu", "lorem", "hendrerit,", "scelerisque", + "lectus", "at,", "consequat", "ligula.", "Nulla", "elementum", "massa", "et", "viverra", + "consectetur.", "Donec", "blandit", "metus", "ut", "ipsum", "commodo", "congue.", "Nullam", + "auctor,", "mi", "vel", "tristique", "venenatis,", "nisl", "nunc", "tristique", "diam,", + "aliquam", "pellentesque", "lorem", "massa", "vel", "neque.", "Sed", "malesuada", "ante", + "nisi,", "sit", "amet", "auctor", "risus", "fermentum", "in.", "Sed", "blandit", "mollis", + "mi,", "non", "tristique", "nisi", "fringilla", "at." + ]; + + let mut buf = String::new(); + for i in 0 .. num_words { + buf.push_str(LOREM[i % LOREM.len()]); + buf.push(' '); + } + buf } diff --git a/tests/layouts/pagebreaks.typ b/tests/layouts/pagebreaks.typ new file mode 100644 index 000000000..6887613fd --- /dev/null +++ b/tests/layouts/pagebreaks.typ @@ -0,0 +1,2 @@ +{size:200pt*200pt} +{lorem:400} diff --git a/tests/layouts/shakespeare-right.tps b/tests/layouts/shakespeare-right.tps deleted file mode 100644 index db670fdf3..000000000 --- a/tests/layouts/shakespeare-right.tps +++ /dev/null @@ -1,88 +0,0 @@ -[align: right][ - [bold][Scene 5: _The Tower of London_] - - [italic][Enter Mortimer, brought in a chair, and Gaolers.] - - *Mortimer.* Kind keepers of my weak decaying age, - Let dying Mortimer here rest himself. - Even like a man new haled from the rack, - So fare my limbs with long imprisonment; - And these grey locks, the pursuivants of death, - Nestor-like aged in an age of care, - Argue the end of Edmund Mortimer. - These eyes, like lamps whose wasting oil is spent, - Wax dim, as drawing to their exigent; - Weak shoulders, overborne with burdening grief, - And pithless arms, like to a withered vine - That droops his sapless branches to the ground. - Yet are these feet, whose strengthless stay is numb, - Unable to support this lump of clay, - Swift-winged with desire to get a grave, - As witting I no other comfort have. - But tell me, keeper, will my nephew come? - - *First Keeper.* Richard Plantagenet, my lord, will come. - We sent unto the Temple, unto his chamber; - And answer was return'd that he will come. - - *Mortimer.* Enough; my soul shall then be satisfied. - Poor gentleman! his wrong doth equal mine. - Since Henry Monmouth first began to reign, - Before whose glory I was great in arms, - This loathsome sequestration have I had; - And even since then hath Richard been obscur'd, - Depriv'd of honour and inheritance. - But now the arbitrator of despairs, - Just Death, kind umpire of men's miseries, - With sweet enlargement doth dismiss me hence. - I would his troubles likewise were expir'd, - That so he might recover what was lost. - - - [italic][Enter Richard Plantagenet] - - *First Keeper.* My lord, your loving nephew now is come. - - *Mortimer.* Richard Plantagenet, my friend, is he come? - - *Plantagenet.* Ay, noble uncle, thus ignobly us'd, - Your nephew, late despised Richard, comes. - - *Mortimer.* Direct mine arms I may embrace his neck - And in his bosom spend my latter gasp. - O, tell me when my lips do touch his cheeks, - That I may kindly give one fainting kiss. - And now declare, sweet stem from York's great stock, - Why didst thou say of late thou wert despis'd? - - *Plantagenet.* First, lean thine aged back against mine arm; - And, in that ease, I'll tell thee my disease. - This day, in argument upon a case, - Some words there grew 'twixt Somerset and me; - Among which terms he us'd his lavish tongue - And did upbraid me with my father's death; - Which obloquy set bars before my tongue, - Else with the like I had requited him. - Therefore, good uncle, for my father's sake, - In honour of a true Plantagenet, - And for alliance sake, declare the cause - My father, Earl of Cambridge, lost his head. - - *Mortimer.* That cause, fair nephew, that imprison'd me - And hath detain'd me all my flow'ring youth - Within a loathsome dungeon, there to pine, - Was cursed instrument of his decease. - - *Plantagenet.* Discover more at large what cause that was, - For I am ignorant and cannot guess. - - *Mortimer.* I will, if that my fading breath permit - And death approach not ere my tale be done. - Henry the Fourth, grandfather to this king, - Depos'd his nephew Richard, Edward's son, - The first-begotten and the lawful heir - Of Edward king, the third of that descent; - During whose reign the Percies of the north, - Finding his usurpation most unjust, - Endeavour'd my advancement to the throne ... -] diff --git a/tests/layouts/shakespeare.tps b/tests/layouts/shakespeare.tpl similarity index 100% rename from tests/layouts/shakespeare.tps rename to tests/layouts/shakespeare.tpl diff --git a/tests/layouts/shakespeare.typ b/tests/layouts/shakespeare.typ new file mode 100644 index 000000000..09edd3612 --- /dev/null +++ b/tests/layouts/shakespeare.typ @@ -0,0 +1,8 @@ +// Basic unboxed +{include:shakespeare.tpl} + +// Boxed, but still left-aligned +[align: left][{include:shakespeare.tpl}] + +// Boxed, and right-aligned +[align: right][{include:shakespeare.tpl}] diff --git a/tests/layouts/styles.tps b/tests/layouts/styles.typ similarity index 51% rename from tests/layouts/styles.tps rename to tests/layouts/styles.typ index ef5d4e3be..767a0b73a 100644 --- a/tests/layouts/styles.tps +++ b/tests/layouts/styles.typ @@ -1,8 +1,5 @@ _Multiline:_ -Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy -eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam -voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet -clita kasd gubergren, no sea takimata sanctus est. +{lorem:45} _Emoji:_ Hello World! 🌍 diff --git a/tests/render.py b/tests/render.py index e52054c12..4cb1e0002 100644 --- a/tests/render.py +++ b/tests/render.py @@ -5,36 +5,73 @@ from PIL import Image, ImageDraw, ImageFont BASE = os.path.dirname(__file__) -CACHE_DIR = os.path.join(BASE, "../test-cache/"); +CACHE_DIR = os.path.join(BASE, "cache/"); def main(): assert len(sys.argv) == 2, "usage: python render.py " name = sys.argv[1] - filename = os.path.join(CACHE_DIR, f"serialized/{name}.box") + filename = os.path.join(CACHE_DIR, f"serialized/{name}.lay") with open(filename, encoding="utf-8") as file: lines = [line[:-1] for line in file.readlines()] - fonts = {} - font_count = int(lines[0]) - for i in range(font_count): - parts = lines[1 + i].split(' ', 1) - index = int(parts[0]) - path = parts[1] - fonts[index] = os.path.join(BASE, "../fonts", path) - - width, height = (float(s) for s in lines[font_count + 1].split()) - - renderer = Renderer(fonts, width, height) - for command in lines[font_count + 2:]: - renderer.execute(command) + renderer = MultiboxRenderer(lines) + renderer.render() + image = renderer.export() pathlib.Path(os.path.join(CACHE_DIR, "rendered")).mkdir(parents=True, exist_ok=True) - renderer.export(name) + image.save(CACHE_DIR + "rendered/" + name + ".png") -class Renderer: +class MultiboxRenderer: + def __init__(self, lines): + self.combined = None + + self.fonts = {} + font_count = int(lines[0]) + for i in range(font_count): + parts = lines[i + 1].split(' ', 1) + index = int(parts[0]) + path = parts[1] + self.fonts[index] = os.path.join(BASE, "../fonts", path) + + self.content = lines[font_count + 1:] + + def render(self): + images = [] + + layout_count = int(self.content[0]) + start = 1 + + for _ in range(layout_count): + width, height = (float(s) for s in self.content[start].split()) + action_count = int(self.content[start + 1]) + start += 2 + + renderer = BoxRenderer(self.fonts, width, height) + for i in range(action_count): + command = self.content[start + i] + renderer.execute(command) + + images.append(renderer.export()) + start += action_count + + width = max(image.width for image in images) + 20 + height = sum(image.height for image in images) + 10 * (len(images) + 1) + + self.combined = Image.new('RGBA', (width, height)) + + cursor = 10 + for image in images: + self.combined.paste(image, (10, cursor)) + cursor += 10 + image.height + + def export(self): + return self.combined + + +class BoxRenderer: def __init__(self, fonts, width, height): self.fonts = fonts self.size = (pix(width), pix(height)) @@ -102,8 +139,8 @@ class Renderer: else: raise Exception("invalid command") - def export(self, name): - self.img.save(CACHE_DIR + "rendered/" + name + ".png") + def export(self): + return self.img def pix(points):