From 6f22e4f13c42f06b686a01fbdd28a0163e88ae77 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Fri, 11 Oct 2019 20:28:22 +0200 Subject: [PATCH] =?UTF-8?q?Render=20debug=20boxes=20=F0=9F=A7=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 5 ++ src/doc.rs | 27 +------- src/export/pdf.rs | 7 +- src/layout/actions.rs | 131 ++++++++++++++++++++++++++++++++++++ src/layout/boxed.rs | 14 ++-- src/layout/flex.rs | 9 +-- src/layout/mod.rs | 66 ++---------------- src/layout/text.rs | 2 +- src/lib.rs | 2 +- src/{error.rs => macros.rs} | 17 +++-- src/size.rs | 20 ++---- tests/layouting.rs | 30 ++++++--- tests/layouts/styles.tps | 4 +- tests/render.py | 48 ++++++++++++- 14 files changed, 251 insertions(+), 131 deletions(-) create mode 100644 src/layout/actions.rs rename src/{error.rs => macros.rs} (88%) diff --git a/Cargo.toml b/Cargo.toml index a82af22ec..ee945338e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,8 @@ unicode-xid = "0.1.0" [[bin]] name = "typstc" path = "src/bin/main.rs" + +[[test]] +name = "layouting" +path = "tests/layouting.rs" +harness = false diff --git a/src/doc.rs b/src/doc.rs index d83ae6356..9a287913d 100644 --- a/src/doc.rs +++ b/src/doc.rs @@ -1,7 +1,7 @@ //! Representation of typesetted documents. -use std::io::{self, Write}; -use crate::size::{Size, Size2D}; +use crate::layout::LayoutAction; +use crate::size::Size; /// A complete typesetted document, which can be exported. @@ -21,26 +21,3 @@ pub struct Page { /// Layouting actions specifying how to draw content on the page. pub actions: Vec, } - -/// A layouting action. -#[derive(Debug, Clone)] -pub enum LayoutAction { - /// Move to an absolute position. - MoveAbsolute(Size2D), - /// Set the font by index and font size. - SetFont(usize, f32), - /// Write text starting at the current position. - WriteText(String), -} - -impl LayoutAction { - /// Serialize this layout action into a string representation. - pub fn serialize(&self, f: &mut W) -> io::Result<()> { - use LayoutAction::*; - match self { - MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()), - SetFont(i, s) => write!(f, "f {} {}", i, s), - WriteText(s) => write!(f, "w {}", s), - } - } -} diff --git a/src/export/pdf.rs b/src/export/pdf.rs index d601f6b34..3a3f2a9c7 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -14,7 +14,8 @@ use toddle::font::OwnedFont; use toddle::query::SharedFontLoader; use toddle::Error as FontError; -use crate::doc::{Document, Page as DocPage, LayoutAction}; +use crate::layout::LayoutAction; +use crate::doc::{Document, Page as DocPage}; use crate::size::{Size, Size2D}; @@ -192,16 +193,16 @@ impl<'d, W: Write> PdfEngine<'d, W> { } // Flush the position. - if let Some(pos) = next_pos { + if let Some(pos) = next_pos.take() { let x = pos.x.to_pt(); let y = (page.height - pos.y - Size::pt(active_font.1)).to_pt(); text.tm(1.0, 0.0, 0.0, 1.0, x, y); - next_pos = None; } // Write the text. text.tj(self.fonts[active_font.0].encode_text(&string)?); }, + LayoutAction::DebugBox(_, _) => {}, } } diff --git a/src/layout/actions.rs b/src/layout/actions.rs new file mode 100644 index 000000000..f76b61c3a --- /dev/null +++ b/src/layout/actions.rs @@ -0,0 +1,131 @@ +//! Drawing and cofiguration actions used by layouts. + +use std::fmt::{self, Display, Formatter}; +use std::io::{self, Write}; +use crate::size::Size2D; +use super::boxed::BoxLayout; +use LayoutAction::*; + + +/// A layouting action. +#[derive(Clone)] +pub enum LayoutAction { + /// Move to an absolute position. + MoveAbsolute(Size2D), + /// Set the font by index and font size. + SetFont(usize, f32), + /// Write text starting at the current position. + WriteText(String), + /// Visualize a box for debugging purposes. + /// Arguments are position and size. + DebugBox(Size2D, Size2D), +} + +impl LayoutAction { + /// Serialize this layout action into a string representation. + pub fn serialize(&self, f: &mut W) -> io::Result<()> { + use LayoutAction::*; + match self { + MoveAbsolute(s) => write!(f, "m {:.4} {:.4}", s.x.to_pt(), s.y.to_pt()), + SetFont(i, s) => write!(f, "f {} {}", i, s), + WriteText(s) => write!(f, "w {}", s), + DebugBox(p, s) => write!(f, "b {} {} {} {}", + p.x.to_pt(), p.y.to_pt(), s.x.to_pt(), s.y.to_pt()) + } + } +} + +impl Display for LayoutAction { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + use LayoutAction::*; + match self { + MoveAbsolute(s) => write!(f, "move {} {}", s.x, s.y), + SetFont(i, s) => write!(f, "font {} {}", i, s), + WriteText(s) => write!(f, "write \"{}\"", s), + DebugBox(p, s) => write!(f, "box {} {}", p, s), + } + } +} + +debug_display!(LayoutAction); + +/// Unifies and otimizes lists of actions. +#[derive(Debug, Clone)] +pub struct LayoutActionList { + pub origin: Size2D, + actions: Vec, + active_font: (usize, f32), + next_pos: Option, + next_font: Option<(usize, f32)>, +} + +impl LayoutActionList { + /// Create a new action list. + pub fn new() -> LayoutActionList { + LayoutActionList { + actions: vec![], + origin: Size2D::zero(), + active_font: (std::usize::MAX, 0.0), + next_pos: None, + next_font: None, + } + } + + /// Add an action to the list if it is not useless + /// (like changing to a font that is already active). + pub fn add(&mut self, action: LayoutAction) { + match action { + MoveAbsolute(pos) => self.next_pos = Some(self.origin + pos), + DebugBox(pos, size) => self.actions.push(DebugBox(self.origin + pos, size)), + + SetFont(index, size) if (index, size) != self.active_font => { + self.next_font = Some((index, size)); + }, + + _ => { + if let Some(target) = self.next_pos.take() { + self.actions.push(MoveAbsolute(target)); + } + if let Some((index, size)) = self.next_font.take() { + self.actions.push(SetFont(index, size)); + } + + self.actions.push(action); + }, + } + } + + /// Add a series of actions. + pub fn extend(&mut self, actions: I) where I: IntoIterator { + for action in actions.into_iter() { + self.add(action); + } + } + + /// Add all actions from a box layout at a position. A move to the position + /// is generated and all moves inside the box layout are translated as necessary. + pub fn add_box(&mut self, position: Size2D, layout: BoxLayout) { + if let Some(target) = self.next_pos.take() { + self.actions.push(MoveAbsolute(target)); + } + + self.next_pos = Some(position); + self.origin = position; + + if layout.debug_render { + self.actions.push(LayoutAction::DebugBox(position, layout.dimensions)); + } + + self.extend(layout.actions); + } + + /// Whether there are any actions in this list. + pub fn is_empty(&self) -> bool { + self.actions.is_empty() + } + + /// Return the list of actions as a vector. + pub fn into_vec(self) -> Vec { + self.actions + } +} diff --git a/src/layout/boxed.rs b/src/layout/boxed.rs index 5bd909d43..7ea9e4fb3 100644 --- a/src/layout/boxed.rs +++ b/src/layout/boxed.rs @@ -1,9 +1,9 @@ //! Block-style layouting of boxes. use std::io::{self, Write}; -use crate::doc::{Document, Page, LayoutAction}; +use crate::doc::{Document, Page}; use crate::size::{Size, Size2D}; -use super::{ActionList, LayoutSpace, Alignment, LayoutResult, LayoutError}; +use super::*; /// A box layout has a fixed width and height and composes of actions. @@ -13,6 +13,8 @@ pub struct BoxLayout { pub dimensions: Size2D, /// The actions composing this layout. pub actions: Vec, + /// Whether to debug-render this box. + pub debug_render: bool, } impl BoxLayout { @@ -49,7 +51,7 @@ pub struct BoxContext { #[derive(Debug)] pub struct BoxLayouter { pub ctx: BoxContext, - actions: ActionList, + actions: LayoutActionList, dimensions: Size2D, usable: Size2D, cursor: Size2D, @@ -59,9 +61,10 @@ impl BoxLayouter { /// Create a new box layouter. pub fn new(ctx: BoxContext) -> BoxLayouter { let space = ctx.space; + BoxLayouter { ctx, - actions: ActionList::new(), + actions: LayoutActionList::new(), dimensions: match ctx.space.alignment { Alignment::Left => Size2D::zero(), Alignment::Right => Size2D::with_x(space.usable().x), @@ -109,7 +112,7 @@ impl BoxLayouter { /// Add a sublayout at an absolute position. pub fn add_box_absolute(&mut self, position: Size2D, layout: BoxLayout) { - self.actions.add_box_absolute(position, layout); + self.actions.add_box(position, layout); } /// Add some space in between two boxes. @@ -148,6 +151,7 @@ impl BoxLayouter { self.ctx.space.dimensions }, actions: self.actions.into_vec(), + debug_render: true, } } diff --git a/src/layout/flex.rs b/src/layout/flex.rs index 8c099553b..75674e8f9 100644 --- a/src/layout/flex.rs +++ b/src/layout/flex.rs @@ -1,7 +1,7 @@ //! Flexible and lazy layouting of boxes. use crate::size::{Size, Size2D}; -use super::{BoxLayout, ActionList, LayoutSpace, Alignment, LayoutResult, LayoutError}; +use super::*; /// A flex layout consists of a yet unarranged list of boxes. @@ -76,7 +76,7 @@ pub struct FlexContext { struct FlexFinisher { units: Vec, ctx: FlexContext, - actions: ActionList, + actions: LayoutActionList, dimensions: Size2D, usable: Size2D, cursor: Size2D, @@ -92,7 +92,7 @@ impl FlexFinisher { FlexFinisher { units: layout.units, ctx, - actions: ActionList::new(), + actions: LayoutActionList::new(), dimensions: match ctx.space.alignment { Alignment::Left => Size2D::zero(), Alignment::Right => Size2D::with_x(space.usable().x), @@ -129,6 +129,7 @@ impl FlexFinisher { self.ctx.space.dimensions }, actions: self.actions.into_vec(), + debug_render: true, }) } @@ -187,7 +188,7 @@ impl FlexFinisher { }, }; - self.actions.add_box_absolute(position, layout); + self.actions.add_box(position, layout); } // Stretch the dimensions to at least the line width. diff --git a/src/layout/mod.rs b/src/layout/mod.rs index e5fdc42d7..5cc8d8f25 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -6,7 +6,6 @@ use std::mem; use toddle::query::{SharedFontLoader, FontClass}; use toddle::Error as FontError; -use crate::doc::LayoutAction; use crate::size::{Size, Size2D, SizeBox}; use crate::syntax::{SyntaxTree, Node, FuncCall}; use crate::style::TextStyle; @@ -18,6 +17,9 @@ use self::text::TextContext; pub mod text; pub mod boxed; pub mod flex; +mod actions; + +pub use actions::{LayoutAction, LayoutActionList}; /// A collection of layouted content. @@ -159,6 +161,10 @@ impl<'a, 'p> Layouter<'a, 'p> { /// Finish the current flex run and return the resulting box. fn layout_flex(&mut self) -> LayoutResult<()> { + if self.flex_layout.is_empty() { + return Ok(()); + } + let mut layout = FlexLayout::new(); mem::swap(&mut layout, &mut self.flex_layout); @@ -204,64 +210,6 @@ impl<'a, 'p> Layouter<'a, 'p> { } } -/// Manipulates and optimizes a list of actions. -#[derive(Debug, Clone)] -pub struct ActionList { - pub origin: Size2D, - actions: Vec, - active_font: (usize, f32), -} - -impl ActionList { - /// Create a new action list. - pub fn new() -> ActionList { - ActionList { - actions: vec![], - origin: Size2D::zero(), - active_font: (std::usize::MAX, 0.0), - } - } - - /// Add an action to the list if it is not useless - /// (like changing to a font that is already active). - pub fn add(&mut self, action: LayoutAction) { - use LayoutAction::*; - match action { - MoveAbsolute(pos) => self.actions.push(MoveAbsolute(self.origin + pos)), - SetFont(index, size) => if (index, size) != self.active_font { - self.active_font = (index, size); - self.actions.push(action); - }, - _ => self.actions.push(action), - } - } - - /// Add a series of actions. - pub fn extend(&mut self, actions: I) where I: IntoIterator { - for action in actions.into_iter() { - self.add(action); - } - } - - /// Add all actions from a box layout at a position. A move to the position - /// is generated and all moves inside the box layout are translated as necessary. - pub fn add_box_absolute(&mut self, position: Size2D, layout: BoxLayout) { - self.actions.push(LayoutAction::MoveAbsolute(position)); - self.origin = position; - self.extend(layout.actions); - } - - /// Whether there are any actions in this list. - pub fn is_empty(&self) -> bool { - self.actions.is_empty() - } - - /// Return the list of actions as a vector. - pub fn into_vec(self) -> Vec { - self.actions - } -} - /// The error type for layouting. pub enum LayoutError { /// There is not enough space to add an item. diff --git a/src/layout/text.rs b/src/layout/text.rs index c1f4e464b..ecec02208 100644 --- a/src/layout/text.rs +++ b/src/layout/text.rs @@ -3,7 +3,6 @@ use toddle::query::{FontQuery, SharedFontLoader}; use toddle::tables::{Header, CharMap, HorizontalMetrics}; -use crate::doc::LayoutAction; use crate::size::{Size, Size2D}; use super::*; @@ -92,5 +91,6 @@ pub fn layout(text: &str, ctx: TextContext) -> LayoutResult { Ok(BoxLayout { dimensions: Size2D::new(width, Size::pt(ctx.style.font_size)), actions, + debug_render: false, }) } diff --git a/src/lib.rs b/src/lib.rs index 26543b1d6..2fe2f94fe 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,7 +26,7 @@ use crate::style::{PageStyle, TextStyle}; use crate::syntax::SyntaxTree; #[macro_use] -mod error; +mod macros; pub mod doc; pub mod export; pub mod func; diff --git a/src/error.rs b/src/macros.rs similarity index 88% rename from src/error.rs rename to src/macros.rs index 514eb1a89..6aab95cf5 100644 --- a/src/error.rs +++ b/src/macros.rs @@ -24,11 +24,7 @@ macro_rules! error_type { } } - impl std::fmt::Debug for $err { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - std::fmt::Display::fmt(self, f) - } - } + debug_display!($err); impl std::error::Error for $err { // The source method is only generated if an implementation was given. @@ -46,3 +42,14 @@ macro_rules! error_type { })* }; } + +/// Create a `Debug` implementation from a display implementation. +macro_rules! debug_display { + ($type:ident) => { + impl std::fmt::Debug for $type { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(self, f) + } + } + }; +} diff --git a/src/size.rs b/src/size.rs index a6d38d636..b9db382e5 100644 --- a/src/size.rs +++ b/src/size.rs @@ -1,7 +1,7 @@ //! General spacing types. use std::cmp::Ordering; -use std::fmt::{self, Display, Debug, Formatter}; +use std::fmt::{self, Display, Formatter}; use std::iter::Sum; use std::ops::*; use std::str::FromStr; @@ -133,11 +133,7 @@ impl Display for Size { } } -impl Debug for Size { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(self, f) - } -} +debug_display!(Size); /// An error which can be returned when parsing a size. pub struct ParseSizeError; @@ -254,11 +250,7 @@ impl Display for Size2D { } } -impl Debug for Size2D { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(self, f) - } -} +debug_display!(Size2D); impl Neg for Size2D { type Output = Size2D; @@ -349,8 +341,4 @@ impl Display for SizeBox { } } -impl Debug for SizeBox { - fn fmt(&self, f: &mut Formatter) -> fmt::Result { - Display::fmt(self, f) - } -} +debug_display!(SizeBox); diff --git a/tests/layouting.rs b/tests/layouting.rs index b748748f8..2ac92b8e4 100644 --- a/tests/layouting.rs +++ b/tests/layouting.rs @@ -3,15 +3,25 @@ use std::io::{Write, Read, BufWriter}; use std::process::Command; use typst::Typesetter; +use typst::layout::LayoutAction; use typst::toddle::query::FileSystemFontProvider; use typst::export::pdf::PdfExporter; -use typst::doc::LayoutAction; const CACHE_DIR: &str = "test-cache"; -#[test] -fn layouting() { +fn main() { + let mut filter = Vec::new(); + for arg in std::env::args().skip(1) { + if arg.as_str() != "--nocapture" { + filter.push(arg); + } + } + + if !filter.is_empty() { + println!("Using filter: {:?}", filter); + } + fs::create_dir_all(format!("{}/serialized", CACHE_DIR)).unwrap(); fs::create_dir_all(format!("{}/rendered", CACHE_DIR)).unwrap(); fs::create_dir_all(format!("{}/pdf", CACHE_DIR)).unwrap(); @@ -19,20 +29,24 @@ fn layouting() { for entry in fs::read_dir("tests/layouts/").unwrap() { let path = entry.unwrap().path(); - let mut file = File::open(&path).unwrap(); - let mut src = String::new(); - file.read_to_string(&mut src).unwrap(); - let name = path .file_stem().unwrap() .to_str().unwrap(); - test(name, &src); + if filter.is_empty() || filter.iter().any(|pattern| name.contains(pattern)) { + let mut file = File::open(&path).unwrap(); + let mut src = String::new(); + file.read_to_string(&mut src).unwrap(); + + test(name, &src); + } } } /// Create a _PDF_ with a name from the source code. fn test(name: &str, src: &str) { + println!("Testing: {}", name); + let mut typesetter = Typesetter::new(); let provider = FileSystemFontProvider::from_listing("fonts/fonts.toml").unwrap(); typesetter.add_font_provider(provider.clone()); diff --git a/tests/layouts/styles.tps b/tests/layouts/styles.tps index 790d2b38e..ef5d4e3be 100644 --- a/tests/layouts/styles.tps +++ b/tests/layouts/styles.tps @@ -9,5 +9,5 @@ _Emoji:_ Hello World! 🌍 _Styles:_ This is made *bold*, that _italic_ and this one `monospace` using the built-in syntax! -_Styles with functions:_ This [bold][word] is made bold and [italic][that] is italic -using the standard library functions [mono][bold] and `italic`! +_Styles with functions:_ This [bold][word] is made bold and [italic][that] italic +using the standard library functions `bold` and `italic`! diff --git a/tests/render.py b/tests/render.py index 02c2693ff..e52054c12 100644 --- a/tests/render.py +++ b/tests/render.py @@ -37,10 +37,25 @@ def main(): class Renderer: def __init__(self, fonts, width, height): self.fonts = fonts - self.img = Image.new("RGBA", (pix(width), pix(height)), (255, 255, 255)) + self.size = (pix(width), pix(height)) + self.img = Image.new("RGBA", self.size, (255, 255, 255, 255)) self.draw = ImageDraw.Draw(self.img) self.cursor = (0, 0) + self.colors = [ + (176, 264, 158), + (274, 173, 207), + (158, 252, 264), + (285, 275, 187), + (132, 217, 136), + (236, 177, 246), + (174, 232, 279), + (285, 234, 158) + ] + + self.rects = [] + self.color_index = 0 + def execute(self, command): cmd = command[0] parts = command.split()[1:] @@ -56,7 +71,33 @@ class Renderer: elif cmd == 'w': text = command[2:] - self.draw.text(self.cursor, text, (0, 0, 0), font=self.font) + self.draw.text(self.cursor, text, (0, 0, 0, 255), font=self.font) + + elif cmd == 'b': + x, y, w, h = (pix(float(s)) for s in parts) + rect = [x, y, x+w, y+h] + + forbidden_colors = set() + for other_rect, other_color in self.rects: + if rect == other_rect: + return + + if overlap(rect, other_rect) or overlap(other_rect, rect): + forbidden_colors.add(other_color) + + for color in self.colors[self.color_index:] + self.colors[:self.color_index]: + self.color_index = (self.color_index + 1) % len(self.colors) + if color not in forbidden_colors: + break + + overlay = Image.new("RGBA", self.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(overlay) + draw.rectangle(rect, fill=color + (255,)) + + self.img = Image.alpha_composite(self.img, overlay) + self.draw = ImageDraw.Draw(self.img) + + self.rects.append((rect, color)) else: raise Exception("invalid command") @@ -68,6 +109,9 @@ class Renderer: def pix(points): return int(2 * points) +def overlap(a, b): + return (a[0] < b[2] and b[0] < a[2]) and (a[1] < b[3] and b[1] < a[3]) + if __name__ == "__main__": main()