diff --git a/src/exec/mod.rs b/src/exec/mod.rs index 90e5a2255..5a2ff6984 100644 --- a/src/exec/mod.rs +++ b/src/exec/mod.rs @@ -122,6 +122,7 @@ impl Exec for RawNode { ctx.push(FixedNode { width: None, height: None, + aspect: None, child: StackNode { dirs: ctx.state.dirs, aligns: ctx.state.aligns, diff --git a/src/export/pdf.rs b/src/export/pdf.rs index d8391e2dd..6881188d9 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -15,8 +15,8 @@ use ttf_parser::{name_id, GlyphId}; use crate::color::Color; use crate::env::{Env, ImageResource, ResourceId}; -use crate::geom::Length; -use crate::layout::{Element, Fill, Frame, Shape}; +use crate::geom::{self, Length, Size}; +use crate::layout::{Element, Fill, Frame, Image, Shape}; /// Export a collection of frames into a _PDF_ document. /// @@ -134,63 +134,59 @@ impl<'a> PdfExporter<'a> { let mut face = FaceId::MAX; let mut size = Length::ZERO; let mut fill: Option = None; - let mut change_color = |content: &mut Content, new_fill: Fill| { - if fill != Some(new_fill) { - match new_fill { - Fill::Color(Color::Rgba(c)) => { - content.fill_rgb( - c.r as f32 / 255.0, - c.g as f32 / 255.0, - c.b as f32 / 255.0, - ); - } - Fill::Image(_) => todo!(), - } - fill = Some(new_fill); - } - }; for (pos, element) in &page.elements { let x = pos.x.to_pt() as f32; + let y = (page.size.height - pos.y).to_pt() as f32; + match element { - Element::Image(image) => { - let name = format!("Im{}", self.images.map(image.res)); - let size = image.size; - let y = (page.size.height - pos.y - size.height).to_pt() as f32; - let w = size.width.to_pt() as f32; - let h = size.height.to_pt() as f32; + &Element::Image(Image { res, size: Size { width, height } }) => { + let name = format!("Im{}", self.images.map(res)); + let w = width.to_pt() as f32; + let h = height.to_pt() as f32; content.save_state(); - content.matrix(w, 0.0, 0.0, h, x, y); + content.matrix(w, 0.0, 0.0, h, x, y - h); content.x_object(Name(name.as_bytes())); content.restore_state(); } Element::Geometry(geometry) => { content.save_state(); - change_color(&mut content, geometry.fill); + write_fill(&mut content, geometry.fill); - match &geometry.shape { - Shape::Rect(r) => { - let w = r.width.to_pt() as f32; - let h = r.height.to_pt() as f32; - let y = (page.size.height - pos.y - r.height).to_pt() as f32; + match geometry.shape { + Shape::Rect(Size { width, height }) => { + let w = width.to_pt() as f32; + let h = height.to_pt() as f32; if w > 0.0 && h > 0.0 { - content.rect(x, y, w, h, false, true); + content.rect(x, y - h, w, h, false, true); } } + + Shape::Ellipse(size) => { + let path = geom::ellipse_path(size); + write_path(&mut content, x, y, &path, false, true); + } + + Shape::Path(ref path) => { + write_path(&mut content, x, y, path, false, true) + } } content.restore_state(); } Element::Text(shaped) => { - change_color(&mut content, shaped.color); + if fill != Some(shaped.color) { + write_fill(&mut content, shaped.color); + fill = Some(shaped.color); + } let mut text = content.text(); - // Then, also check if we need to - // issue a font switching action. + // Then, also check if we need to issue a font switching + // action. if shaped.face != face || shaped.font_size != size { face = shaped.face; size = shaped.font_size; @@ -199,8 +195,6 @@ impl<'a> PdfExporter<'a> { text.font(Name(name.as_bytes()), size.to_pt() as f32); } - let x = pos.x.to_pt() as f32; - let y = (page.size.height - pos.y).to_pt() as f32; text.matrix(1.0, 0.0, 0.0, 1.0, x, y); text.show(&shaped.encode_glyphs_be()); } @@ -365,6 +359,97 @@ impl<'a> PdfExporter<'a> { } } +/// Write a fill change into a content stream. +fn write_fill(content: &mut Content, fill: Fill) { + match fill { + Fill::Color(Color::Rgba(c)) => { + content.fill_rgb(c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0); + } + Fill::Image(_) => todo!(), + } +} + +/// Write a path into a content stream. +fn write_path( + content: &mut Content, + x: f32, + y: f32, + path: &geom::Path, + stroke: bool, + fill: bool, +) { + let f = |length: Length| length.to_pt() as f32; + let mut builder = content.path(stroke, fill); + for elem in &path.0 { + match elem { + geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)), + geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)), + geom::PathElement::CubicTo(p1, p2, p3) => builder.cubic_to( + x + f(p1.x), + y + f(p1.y), + x + f(p2.x), + y + f(p2.y), + x + f(p3.x), + y + f(p3.y), + ), + geom::PathElement::ClosePath => builder.close_path(), + }; + } +} + +/// The compression level for the deflating. +const DEFLATE_LEVEL: u8 = 6; + +/// Encode an image with a suitable filter. +/// +/// Skips the alpha channel as that's encoded separately. +fn encode_image(img: &ImageResource) -> ImageResult<(Vec, Filter, ColorSpace)> { + let mut data = vec![]; + let (filter, space) = match (img.format, &img.buf) { + // 8-bit gray JPEG. + (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => { + img.buf.write_to(&mut data, img.format)?; + (Filter::DctDecode, ColorSpace::DeviceGray) + } + + // 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier). + (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => { + img.buf.write_to(&mut data, img.format)?; + (Filter::DctDecode, ColorSpace::DeviceRgb) + } + + // TODO: Encode flate streams with PNG-predictor? + + // 8-bit gray PNG. + (ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => { + data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL); + (Filter::FlateDecode, ColorSpace::DeviceGray) + } + + // Anything else (including Rgb(a) PNGs). + (_, buf) => { + let (width, height) = buf.dimensions(); + let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); + for (_, _, Rgba([r, g, b, _])) in buf.pixels() { + pixels.push(r); + pixels.push(g); + pixels.push(b); + } + + data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); + (Filter::FlateDecode, ColorSpace::DeviceRgb) + } + }; + Ok((data, filter, space)) +} + +/// Encode an image's alpha channel if present. +fn encode_alpha(img: &ImageResource) -> (Vec, Filter) { + let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); + let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); + (data, Filter::FlateDecode) +} + /// We need to know exactly which indirect reference id will be used for which /// objects up-front to correctly declare the document catalogue, page tree and /// so on. These offsets are computed in the beginning and stored here. @@ -485,56 +570,3 @@ where self.to_layout.iter().copied() } } - -/// The compression level for the deflating. -const DEFLATE_LEVEL: u8 = 6; - -/// Encode an image with a suitable filter. -/// -/// Skips the alpha channel as that's encoded separately. -fn encode_image(img: &ImageResource) -> ImageResult<(Vec, Filter, ColorSpace)> { - let mut data = vec![]; - let (filter, space) = match (img.format, &img.buf) { - // 8-bit gray JPEG. - (ImageFormat::Jpeg, DynamicImage::ImageLuma8(_)) => { - img.buf.write_to(&mut data, img.format)?; - (Filter::DctDecode, ColorSpace::DeviceGray) - } - - // 8-bit Rgb JPEG (Cmyk JPEGs get converted to Rgb earlier). - (ImageFormat::Jpeg, DynamicImage::ImageRgb8(_)) => { - img.buf.write_to(&mut data, img.format)?; - (Filter::DctDecode, ColorSpace::DeviceRgb) - } - - // TODO: Encode flate streams with PNG-predictor? - - // 8-bit gray PNG. - (ImageFormat::Png, DynamicImage::ImageLuma8(luma)) => { - data = deflate::compress_to_vec_zlib(&luma.as_raw(), DEFLATE_LEVEL); - (Filter::FlateDecode, ColorSpace::DeviceGray) - } - - // Anything else (including Rgb(a) PNGs). - (_, buf) => { - let (width, height) = buf.dimensions(); - let mut pixels = Vec::with_capacity(3 * width as usize * height as usize); - for (_, _, Rgba([r, g, b, _])) in buf.pixels() { - pixels.push(r); - pixels.push(g); - pixels.push(b); - } - - data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); - (Filter::FlateDecode, ColorSpace::DeviceRgb) - } - }; - Ok((data, filter, space)) -} - -/// Encode an image's alpha channel if present. -fn encode_alpha(img: &ImageResource) -> (Vec, Filter) { - let pixels: Vec<_> = img.buf.pixels().map(|(_, _, Rgba([_, _, _, a]))| a).collect(); - let data = deflate::compress_to_vec_zlib(&pixels, DEFLATE_LEVEL); - (data, Filter::FlateDecode) -} diff --git a/src/geom/length.rs b/src/geom/length.rs index 419da5c37..1175876c7 100644 --- a/src/geom/length.rs +++ b/src/geom/length.rs @@ -32,7 +32,7 @@ impl Length { } /// Create a length from a number of raw units. - pub fn raw(raw: f64) -> Self { + pub const fn raw(raw: f64) -> Self { Self { raw } } @@ -57,7 +57,7 @@ impl Length { } /// Get the value of this length in raw units. - pub fn to_raw(self) -> f64 { + pub const fn to_raw(self) -> f64 { self.raw } diff --git a/src/geom/mod.rs b/src/geom/mod.rs index 5d9068348..5099c6b06 100644 --- a/src/geom/mod.rs +++ b/src/geom/mod.rs @@ -8,6 +8,7 @@ mod dir; mod gen; mod length; mod linear; +mod path; mod point; mod relative; mod sides; @@ -20,6 +21,7 @@ pub use dir::*; pub use gen::*; pub use length::*; pub use linear::*; +pub use path::*; pub use point::*; pub use relative::*; pub use sides::*; diff --git a/src/geom/path.rs b/src/geom/path.rs new file mode 100644 index 000000000..c9fcf1c09 --- /dev/null +++ b/src/geom/path.rs @@ -0,0 +1,60 @@ +use super::*; + +/// A bezier path. +#[derive(Default, Debug, Clone, PartialEq)] +pub struct Path(pub Vec); + +/// An element in a bezier path. +#[derive(Debug, Clone, PartialEq)] +pub enum PathElement { + MoveTo(Point), + LineTo(Point), + CubicTo(Point, Point, Point), + ClosePath, +} + +impl Path { + /// Create an empty path. + pub fn new() -> Self { + Self(vec![]) + } + + /// Push a [`MoveTo`](PathElement::MoveTo) element. + pub fn move_to(&mut self, p: Point) { + self.0.push(PathElement::MoveTo(p)); + } + + /// Push a [`LineTo`](PathElement::LineTo) element. + pub fn line_to(&mut self, p: Point) { + self.0.push(PathElement::LineTo(p)); + } + + /// Push a [`CubicTo`](PathElement::CubicTo) element. + pub fn cubic_to(&mut self, p1: Point, p2: Point, p3: Point) { + self.0.push(PathElement::CubicTo(p1, p2, p3)); + } + + /// Push a [`ClosePath`](PathElement::ClosePath) element. + pub fn close_path(&mut self) { + self.0.push(PathElement::ClosePath); + } +} + +/// Create a path that approximates an axis-aligned ellipse. +pub fn ellipse_path(size: Size) -> Path { + // https://stackoverflow.com/a/2007782 + let rx = size.width / 2.0; + let ry = size.height / 2.0; + let m = 0.551784; + let mx = m * rx; + let my = m * ry; + let z = Length::ZERO; + let point = Point::new; + let mut path = Path::new(); + path.move_to(point(-rx, z)); + path.cubic_to(point(-rx, my), point(-mx, ry), point(z, ry)); + path.cubic_to(point(mx, ry), point(rx, my), point(rx, z)); + path.cubic_to(point(rx, -my), point(mx, -ry), point(z, -ry)); + path.cubic_to(point(-mx, -ry), point(-rx, -my), point(z - rx, z)); + path +} diff --git a/src/geom/relative.rs b/src/geom/relative.rs index 9d7b3d3e1..65312e999 100644 --- a/src/geom/relative.rs +++ b/src/geom/relative.rs @@ -26,8 +26,8 @@ impl Relative { /// Resolve this relative to the given `length`. pub fn resolve(self, length: Length) -> Length { - // Zero wins over infinity. - if self.is_zero() { + // We don't want NaNs. + if length.is_infinite() { Length::ZERO } else { self.get() * length diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 292f00c45..deeced452 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -34,7 +34,7 @@ impl Sides { } impl Sides { - /// Resolve the linear margins relative to the given `size`. + /// Resolve the linear sides relative to the given `size`. pub fn resolve(self, size: Size) -> Sides { Sides { left: self.left.resolve(size.width), diff --git a/src/geom/size.rs b/src/geom/size.rs index 67e5643d6..2feaa950e 100644 --- a/src/geom/size.rs +++ b/src/geom/size.rs @@ -28,7 +28,8 @@ impl Size { /// Whether the other size fits into this one (smaller width and height). pub fn fits(self, other: Self) -> bool { - self.width >= other.width && self.height >= other.height + const EPS: Length = Length::raw(1e-6); + self.width + EPS >= other.width && self.height + EPS >= other.height } /// Whether both components are finite. @@ -45,6 +46,11 @@ impl Size { pub fn is_nan(self) -> bool { self.width.is_nan() || self.height.is_nan() } + + /// Convert to a point. + pub fn to_point(self) -> Point { + Point::new(self.width, self.height) + } } impl Get for Size { diff --git a/src/layout/background.rs b/src/layout/background.rs index bb155073f..17280a86a 100644 --- a/src/layout/background.rs +++ b/src/layout/background.rs @@ -3,25 +3,38 @@ use super::*; /// A node that places a rectangular filled background behind its child. #[derive(Debug, Clone, PartialEq)] pub struct BackgroundNode { + /// The kind of shape to use as a background. + pub shape: BackgroundShape, /// The background fill. pub fill: Fill, /// The child node to be filled. pub child: Node, } +/// The kind of shape to use as a background. +#[derive(Debug, Clone, PartialEq)] +pub enum BackgroundShape { + Rect, + Ellipse, +} + impl Layout for BackgroundNode { fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - let mut layouted = self.child.layout(ctx, areas); + let mut fragment = self.child.layout(ctx, areas); - for frame in layouted.frames_mut() { - let element = Element::Geometry(Geometry { - shape: Shape::Rect(frame.size), - fill: self.fill, - }); - frame.elements.insert(0, (Point::ZERO, element)); + for frame in fragment.frames_mut() { + let (point, shape) = match self.shape { + BackgroundShape::Rect => (Point::ZERO, Shape::Rect(frame.size)), + BackgroundShape::Ellipse => { + (frame.size.to_point() / 2.0, Shape::Ellipse(frame.size)) + } + }; + + let element = Element::Geometry(Geometry { shape, fill: self.fill }); + frame.elements.insert(0, (point, element)); } - layouted + fragment } } diff --git a/src/layout/fixed.rs b/src/layout/fixed.rs index e3365668a..22c45ef12 100644 --- a/src/layout/fixed.rs +++ b/src/layout/fixed.rs @@ -7,6 +7,10 @@ pub struct FixedNode { pub width: Option, /// The fixed height, if any. pub height: Option, + /// The fixed aspect ratio between width and height, if any. + /// + /// The resulting frame will satisfy `width = aspect * height`. + pub aspect: Option, /// The child node whose size to fix. pub child: Node, } @@ -14,18 +18,26 @@ pub struct FixedNode { impl Layout for FixedNode { fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { let Areas { current, full, .. } = areas; - let size = Size::new( + + let full = Size::new( self.width.map(|w| w.resolve(full.width)).unwrap_or(current.width), self.height.map(|h| h.resolve(full.height)).unwrap_or(current.height), ); + let mut size = full; + if let Some(aspect) = self.aspect { + // Shrink the size to ensure that the aspect ratio can be satisfied. + let width = size.width.min(aspect * size.height); + size = Size::new(width, width / aspect); + } + let fill_if = |cond| if cond { Expand::Fill } else { Expand::Fit }; let expand = Spec::new( fill_if(self.width.is_some()), fill_if(self.height.is_some()), ); - let areas = Areas::once(size, expand); + let areas = Areas::once(size, full, expand).with_aspect(self.aspect); self.child.layout(ctx, &areas) } } diff --git a/src/layout/frame.rs b/src/layout/frame.rs index c85d75392..6e8761514 100644 --- a/src/layout/frame.rs +++ b/src/layout/frame.rs @@ -1,7 +1,7 @@ use super::Shaped; use crate::color::Color; use crate::env::ResourceId; -use crate::geom::{Point, Size}; +use crate::geom::{Path, Point, Size}; /// A finished layout with elements at fixed positions. #[derive(Debug, Clone, PartialEq)] @@ -59,8 +59,12 @@ pub struct Geometry { /// Some shape. #[derive(Debug, Clone, PartialEq)] pub enum Shape { - /// A rectangle. + /// A rectangle with its origin in the topleft corner. Rect(Size), + /// An ellipse with its origin in the center. + Ellipse(Size), + /// A bezier path. + Path(Path), } /// The kind of graphic fill to be applied to a [`Shape`]. diff --git a/src/layout/mod.rs b/src/layout/mod.rs index ae4ab89d3..360c9d84b 100644 --- a/src/layout/mod.rs +++ b/src/layout/mod.rs @@ -58,8 +58,7 @@ impl PageRun { /// Layout the page run. pub fn layout(&self, ctx: &mut LayoutContext) -> Vec { let areas = Areas::repeat(self.size, Spec::uniform(Expand::Fill)); - let layouted = self.child.layout(ctx, &areas); - layouted.into_frames() + self.child.layout(ctx, &areas).into_frames() } } @@ -89,21 +88,17 @@ pub struct Areas { pub last: Option, /// Whether the frames resulting from layouting into this areas should be /// shrunk to fit their content or expanded to fill the area. + /// + /// This property is handled partially by the par layouter and fully by the + /// stack layouter. pub expand: Spec, + /// The aspect ratio the resulting frame should respect. + /// + /// This property is only handled by the stack layouter. + pub aspect: Option, } impl Areas { - /// Create a new length-1 sequence of areas with just one `area`. - pub fn once(size: Size, expand: Spec) -> Self { - Self { - current: size, - full: size, - backlog: vec![], - last: None, - expand, - } - } - /// Create a new sequence of areas that repeats `area` indefinitely. pub fn repeat(size: Size, expand: Spec) -> Self { Self { @@ -112,6 +107,40 @@ impl Areas { backlog: vec![], last: Some(size), expand, + aspect: None, + } + } + + /// Create a new length-1 sequence of areas with just one `area`. + pub fn once(size: Size, full: Size, expand: Spec) -> Self { + Self { + current: size, + full, + backlog: vec![], + last: None, + expand, + aspect: None, + } + } + + /// Builder-style method for setting the aspect ratio. + pub fn with_aspect(mut self, aspect: Option) -> Self { + self.aspect = aspect; + self + } + + /// Map all areas. + pub fn map(&self, mut f: F) -> Self + where + F: FnMut(Size) -> Size, + { + Self { + current: f(self.current), + full: f(self.full), + backlog: self.backlog.iter().copied().map(|s| f(s)).collect(), + last: self.last.map(f), + expand: self.expand, + aspect: self.aspect, } } diff --git a/src/layout/pad.rs b/src/layout/pad.rs index 33ce217d9..fb0389965 100644 --- a/src/layout/pad.rs +++ b/src/layout/pad.rs @@ -13,12 +13,12 @@ impl Layout for PadNode { fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { let areas = shrink(areas, self.padding); - let mut layouted = self.child.layout(ctx, &areas); - for frame in layouted.frames_mut() { + let mut fragment = self.child.layout(ctx, &areas); + for frame in fragment.frames_mut() { pad(frame, self.padding); } - layouted + fragment } } @@ -30,23 +30,30 @@ impl From for AnyNode { /// Shrink all areas by the padding. fn shrink(areas: &Areas, padding: Sides) -> Areas { - let shrink = |size| size - padding.resolve(size).size(); - Areas { - current: shrink(areas.current), - full: shrink(areas.full), - backlog: areas.backlog.iter().copied().map(shrink).collect(), - last: areas.last.map(shrink), - expand: areas.expand, - } + areas.map(|size| size - padding.resolve(size).size()) } -/// Enlarge the frame and move all elements inwards. +/// Pad the frame and move all elements inwards. fn pad(frame: &mut Frame, padding: Sides) { - let padding = padding.resolve(frame.size); + let padded = solve(padding, frame.size); + let padding = padding.resolve(padded); let origin = Point::new(padding.left, padding.top); - frame.size += padding.size(); + frame.size = padded; for (point, _) in &mut frame.elements { *point += origin; } } + +/// Solve for the size `padded` that satisfies (approximately): +/// `padded - padding.resolve(padded).size() == size` +fn solve(padding: Sides, size: Size) -> Size { + fn solve_axis(length: Length, padding: Linear) -> Length { + (length + padding.abs) / (1.0 - padding.rel.get()) + } + + Size::new( + solve_axis(size.width, padding.left + padding.right), + solve_axis(size.height, padding.top + padding.bottom), + ) +} diff --git a/src/layout/par.rs b/src/layout/par.rs index e9fda015a..0364a03a0 100644 --- a/src/layout/par.rs +++ b/src/layout/par.rs @@ -18,7 +18,7 @@ pub struct ParNode { impl Layout for ParNode { fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - let mut layouter = ParLayouter::new(self, areas.clone()); + let mut layouter = ParLayouter::new(self.dirs, self.line_spacing, areas.clone()); for child in &self.children { match child.layout(ctx, &layouter.areas) { Fragment::Spacing(spacing) => layouter.push_spacing(spacing), @@ -57,12 +57,12 @@ struct ParLayouter { } impl ParLayouter { - fn new(par: &ParNode, areas: Areas) -> Self { + fn new(dirs: LayoutDirs, line_spacing: Length, areas: Areas) -> Self { Self { - main: par.dirs.main.axis(), - cross: par.dirs.cross.axis(), - dirs: par.dirs, - line_spacing: par.line_spacing, + main: dirs.main.axis(), + cross: dirs.cross.axis(), + dirs, + line_spacing, areas, finished: vec![], lines: vec![], @@ -134,12 +134,12 @@ impl ParLayouter { } fn finish_line(&mut self) { - let expand = self.areas.expand.switch(self.dirs); let full_size = { + let expand = self.areas.expand.switch(self.dirs); let full = self.areas.full.switch(self.dirs); Gen::new( self.line_size.main, - expand.cross.resolve(self.line_size.cross.min(full.cross), full.cross), + expand.cross.resolve(self.line_size.cross, full.cross), ) }; diff --git a/src/layout/stack.rs b/src/layout/stack.rs index 32eba6765..6a87290ea 100644 --- a/src/layout/stack.rs +++ b/src/layout/stack.rs @@ -16,7 +16,7 @@ pub struct StackNode { impl Layout for StackNode { fn layout(&self, ctx: &mut LayoutContext, areas: &Areas) -> Fragment { - let mut layouter = StackLayouter::new(self, areas.clone()); + let mut layouter = StackLayouter::new(self.dirs, areas.clone()); for child in &self.children { match child.layout(ctx, &layouter.areas) { Fragment::Spacing(spacing) => layouter.push_spacing(spacing), @@ -49,10 +49,10 @@ struct StackLayouter { } impl StackLayouter { - fn new(stack: &StackNode, areas: Areas) -> Self { + fn new(dirs: LayoutDirs, areas: Areas) -> Self { Self { - main: stack.dirs.main.axis(), - dirs: stack.dirs, + main: dirs.main.axis(), + dirs, areas, finished: vec![], frames: vec![], @@ -93,12 +93,27 @@ impl StackLayouter { fn finish_area(&mut self) { let full_size = { - let expand = self.areas.expand.switch(self.dirs); - let full = self.areas.full.switch(self.dirs); - Gen::new( - expand.main.resolve(self.used.main.min(full.main), full.main), - expand.cross.resolve(self.used.cross.min(full.cross), full.cross), - ) + let expand = self.areas.expand; + let full = self.areas.full; + let current = self.areas.current; + let used = self.used.switch(self.dirs).to_size(); + + let mut size = Size::new( + expand.horizontal.resolve(used.width, full.width), + expand.vertical.resolve(used.height, full.height), + ); + + if let Some(aspect) = self.areas.aspect { + let width = size + .width + .max(aspect * size.height) + .min(current.width) + .min((current.height + used.height) / aspect); + + size = Size::new(width, width / aspect); + } + + size.switch(self.dirs) }; let mut output = Frame::new(full_size.switch(self.dirs).to_size()); diff --git a/src/library/font.rs b/src/library/font.rs index 0993f7f0f..ed2c0ef3c 100644 --- a/src/library/font.rs +++ b/src/library/font.rs @@ -16,7 +16,7 @@ use super::*; /// - Font Stretch: `stretch`, of type `relative`, between 0.5 and 2.0. /// - Top edge of the font: `top-edge`, of type `vertical-font-metric`. /// - Bottom edge of the font: `bottom-edge`, of type `vertical-font-metric`. -/// - Fill color the glyphs: `color`, of type `color`. +/// - Color the glyphs: `color`, of type `color`. /// - Serif family definition: `serif`, of type `font-familiy-list`. /// - Sans-serif family definition: `sans-serif`, of type `font-familiy-list`. /// - Monospace family definition: `monospace`, of type `font-familiy-list`. diff --git a/src/library/image.rs b/src/library/image.rs index 8cb094630..10217e313 100644 --- a/src/library/image.rs +++ b/src/library/image.rs @@ -25,7 +25,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let loaded = ctx.env.resources.load(&path.v, ImageResource::parse); if let Some((res, img)) = loaded { let dimensions = img.buf.dimensions(); - ctx.push(NodeImage { + ctx.push(ImageNode { res, dimensions, width, @@ -41,7 +41,7 @@ pub fn image(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// An image node. #[derive(Debug, Clone, PartialEq)] -struct NodeImage { +struct ImageNode { /// How to align this image node in its parent. aligns: LayoutAligns, /// The resource id of the image file. @@ -54,7 +54,7 @@ struct NodeImage { height: Option, } -impl Layout for NodeImage { +impl Layout for ImageNode { fn layout(&self, _: &mut LayoutContext, areas: &Areas) -> Fragment { let Areas { current, full, .. } = areas; @@ -90,8 +90,8 @@ impl Layout for NodeImage { } } -impl From for AnyNode { - fn from(image: NodeImage) -> Self { +impl From for AnyNode { + fn from(image: ImageNode) -> Self { Self::new(image) } } diff --git a/src/library/mod.rs b/src/library/mod.rs index d0920cf1c..b09f94a0c 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -51,6 +51,8 @@ pub fn new() -> Scope { } func!("align", align); + func!("circle", circle); + func!("ellipse", ellipse); func!("font", font); func!("h", h); func!("image", image); @@ -58,6 +60,7 @@ pub fn new() -> Scope { func!("page", page); func!("pagebreak", pagebreak); func!("paragraph", par); + func!("square", square); func!("rect", rect); func!("repr", repr); func!("rgb", rgb); diff --git a/src/library/shapes.rs b/src/library/shapes.rs index 211a4f2e9..a5faf73ec 100644 --- a/src/library/shapes.rs +++ b/src/library/shapes.rs @@ -1,49 +1,162 @@ -use super::*; -use crate::layout::{BackgroundNode, Fill, FixedNode}; +use std::f64::consts::SQRT_2; -/// `rect`: Create a rectangular box. +use super::*; +use crate::color::Color; +use crate::layout::{BackgroundNode, BackgroundShape, Fill, FixedNode, PadNode}; + +/// `rect`: Create a rectangle. /// /// # Positional parameters /// - Body: optional, of type `template`. /// /// # Named parameters -/// - Width of the box: `width`, of type `linear` relative to parent width. -/// - Height of the box: `height`, of type `linear` relative to parent height. -/// - Main layouting direction: `main-dir`, of type `direction`. -/// - Cross layouting direction: `cross-dir`, of type `direction`. -/// - Fill color of the box: `fill`, of type `color`. +/// - Width: `width`, of type `linear` relative to parent width. +/// - Height: `height`, of type `linear` relative to parent height. +/// - Fill color: `fill`, of type `color`. /// /// # Return value /// A template that places the body into a rectangle. -/// -/// # Relevant types and constants -/// - Type `direction` -/// - `ltr` (left to right) -/// - `rtl` (right to left) -/// - `ttb` (top to bottom) -/// - `btt` (bottom to top) pub fn rect(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { let width = args.get(ctx, "width"); let height = args.get(ctx, "height"); - let main = args.get(ctx, "main-dir"); - let cross = args.get(ctx, "cross-dir"); let fill = args.get(ctx, "fill"); let body = args.find::(ctx).unwrap_or_default(); + rect_impl("rect", width, height, None, fill, body) +} - Value::template("box", move |ctx| { +/// `square`: Create a square. +/// +/// # Positional parameters +/// - Body: optional, of type `template`. +/// +/// # Named parameters +/// - Side length: `length`, of type `length`. +/// - Width: `width`, of type `linear` relative to parent width. +/// - Height: `height`, of type `linear` relative to parent height. +/// - Fill color: `fill`, of type `color`. +/// +/// Note that you can specify only one of `length`, `width` and `height`. The +/// width and height parameters exist so that you can size the square relative +/// to its parent's size, which isn't possible by setting the side length. +/// +/// # Return value +/// A template that places the body into a square. +pub fn square(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let length = args.get::(ctx, "length").map(Linear::from); + let width = length.or_else(|| args.get(ctx, "width")); + let height = if width.is_none() { args.get(ctx, "height") } else { None }; + let fill = args.get(ctx, "fill"); + let body = args.find::(ctx).unwrap_or_default(); + rect_impl("square", width, height, Some(1.0), fill, body) +} + +fn rect_impl( + name: &str, + width: Option, + height: Option, + aspect: Option, + fill: Option, + body: TemplateValue, +) -> Value { + Value::template(name, move |ctx| { let snapshot = ctx.state.clone(); - - ctx.set_dirs(Gen::new(main, cross)); - let child = ctx.exec(&body).into(); - let fixed = FixedNode { width, height, child }; + let node = FixedNode { width, height, aspect, child }; + if let Some(color) = fill { ctx.push(BackgroundNode { + shape: BackgroundShape::Rect, fill: Fill::Color(color), - child: fixed.into(), + child: node.into(), }); } else { - ctx.push(fixed); + ctx.push(node); + } + + ctx.state = snapshot; + }) +} + +/// `ellipse`: Create an ellipse. +/// +/// # Positional parameters +/// - Body: optional, of type `template`. +/// +/// # Named parameters +/// - Width: `width`, of type `linear` relative to parent width. +/// - Height: `height`, of type `linear` relative to parent height. +/// - Fill color: `fill`, of type `color`. +/// +/// # Return value +/// A template that places the body into an ellipse. +pub fn ellipse(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let width = args.get(ctx, "width"); + let height = args.get(ctx, "height"); + let fill = args.get(ctx, "fill"); + let body = args.find::(ctx).unwrap_or_default(); + ellipse_impl("ellipse", width, height, None, fill, body) +} + +/// `circle`: Create a circle. +/// +/// # Positional parameters +/// - Body: optional, of type `template`. +/// +/// # Named parameters +/// - Radius: `radius`, of type `length`. +/// - Width: `width`, of type `linear` relative to parent width. +/// - Height: `height`, of type `linear` relative to parent height. +/// - Fill color: `fill`, of type `color`. +/// +/// Note that you can specify only one of `radius`, `width` and `height`. The +/// width and height parameters exist so that you can size the circle relative +/// to its parent's size, which isn't possible by setting the radius. +/// +/// # Return value +/// A template that places the body into a circle. +pub fn circle(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { + let radius = args.get::(ctx, "radius").map(|r| 2.0 * Linear::from(r)); + let width = radius.or_else(|| args.get(ctx, "width")); + let height = if width.is_none() { args.get(ctx, "height") } else { None }; + let fill = args.get(ctx, "fill"); + let body = args.find::(ctx).unwrap_or_default(); + ellipse_impl("circle", width, height, Some(1.0), fill, body) +} + +fn ellipse_impl( + name: &str, + width: Option, + height: Option, + aspect: Option, + fill: Option, + body: TemplateValue, +) -> Value { + Value::template(name, move |ctx| { + // This padding ratio ensures that the rectangular padded area fits + // perfectly into the ellipse. + const PAD: f64 = 0.5 - SQRT_2 / 4.0; + + let snapshot = ctx.state.clone(); + let child = ctx.exec(&body).into(); + let node = FixedNode { + width, + height, + aspect, + child: PadNode { + padding: Sides::uniform(Relative::new(PAD).into()), + child, + } + .into(), + }; + + if let Some(color) = fill { + ctx.push(BackgroundNode { + shape: BackgroundShape::Ellipse, + fill: Fill::Color(color), + child: node.into(), + }); + } else { + ctx.push(node); } ctx.state = snapshot; diff --git a/src/library/spacing.rs b/src/library/spacing.rs index fee802fac..c96b8be48 100644 --- a/src/library/spacing.rs +++ b/src/library/spacing.rs @@ -9,7 +9,7 @@ use crate::layout::SpacingNode; /// # Return value /// A template that adds horizontal spacing. pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing(ctx, args, SpecAxis::Horizontal) + spacing_impl(ctx, args, SpecAxis::Horizontal) } /// `v`: Add vertical spacing. @@ -20,11 +20,10 @@ pub fn h(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { /// # Return value /// A template that adds vertical spacing. pub fn v(ctx: &mut EvalContext, args: &mut FuncArgs) -> Value { - spacing(ctx, args, SpecAxis::Vertical) + spacing_impl(ctx, args, SpecAxis::Vertical) } -/// Apply spacing along a specific axis. -fn spacing(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value { +fn spacing_impl(ctx: &mut EvalContext, args: &mut FuncArgs, axis: SpecAxis) -> Value { let spacing: Option = args.require(ctx, "spacing"); Value::template("spacing", move |ctx| { if let Some(linear) = spacing { diff --git a/tests/ref/library/circle.png b/tests/ref/library/circle.png new file mode 100644 index 000000000..7244d0dce Binary files /dev/null and b/tests/ref/library/circle.png differ diff --git a/tests/ref/library/ellipse.png b/tests/ref/library/ellipse.png new file mode 100644 index 000000000..de178d60e Binary files /dev/null and b/tests/ref/library/ellipse.png differ diff --git a/tests/ref/library/pagebreak.png b/tests/ref/library/pagebreak.png index f0052f9e1..c5ac1f544 100644 Binary files a/tests/ref/library/pagebreak.png and b/tests/ref/library/pagebreak.png differ diff --git a/tests/ref/library/rect.png b/tests/ref/library/rect.png new file mode 100644 index 000000000..81ee91d70 Binary files /dev/null and b/tests/ref/library/rect.png differ diff --git a/tests/ref/library/shapes.png b/tests/ref/library/shapes.png deleted file mode 100644 index a244feb1a..000000000 Binary files a/tests/ref/library/shapes.png and /dev/null differ diff --git a/tests/ref/library/square.png b/tests/ref/library/square.png new file mode 100644 index 000000000..401b1ab2a Binary files /dev/null and b/tests/ref/library/square.png differ diff --git a/tests/typ/library/base.typ b/tests/typ/library/base.typ index 29c976c2a..cc9f14a03 100644 --- a/tests/typ/library/base.typ +++ b/tests/typ/library/base.typ @@ -3,7 +3,7 @@ --- #test(type("hi"), "string") -#test(repr([Hi #rect[there]]), "[Hi []]") +#test(repr([Hi #rect[there]]), "[Hi []]") --- // Check the output. diff --git a/tests/typ/library/circle.typ b/tests/typ/library/circle.typ new file mode 100644 index 000000000..b395ee2be --- /dev/null +++ b/tests/typ/library/circle.typ @@ -0,0 +1,41 @@ +// Test the `circle` function. + +--- +// Test auto sizing. + +Auto-sized circle. \ +#circle(fill: #eb5278, align(center, center, [But, soft!])) + +Center-aligned rect in auto-sized circle. +#circle(fill: #43a127)[ + #align(center, center) + #rect(fill: #9feb52, pad(5pt)[But, soft!]) +] + +100%-width rect in auto-sized circle. \ +#circle(fill: #43a127, rect(width: 100%, fill: #9feb52)[ + But, soft! what light through yonder window breaks? +]) + +Expanded by height. +#circle(fill: #9feb52)[A \ B \ C] + +--- +// Test relative sizing. +#rect(width: 100%, height: 50pt, fill: #aaa)[ + #align(center, center) + #font(color: #fff) + #circle(radius: 10pt, fill: #239DAD)[A] + #circle(height: 60%, fill: #239DAD)[B] + #circle(width: 20% + 20pt, fill: #239DAD)[C] +] + +--- +// Radius wins over width and height. +// Error: 2:23-2:34 unexpected argument +// Error: 1:36-1:49 unexpected argument +#circle(radius: 10pt, width: 50pt, height: 100pt, fill: #239DAD) + +// Width wins over height. +// Error: 22-34 unexpected argument +#circle(width: 20pt, height: 50pt, fill: #239DAD) diff --git a/tests/typ/library/ellipse.typ b/tests/typ/library/ellipse.typ new file mode 100644 index 000000000..06d84a114 --- /dev/null +++ b/tests/typ/library/ellipse.typ @@ -0,0 +1,16 @@ +// Test the `ellipse` function. + +--- +100% rect in 100% ellipse in fixed rect. \ +#rect(width: 3cm, height: 2cm, fill: #2a631a)[ + #ellipse(width: 100%, height: 100%, fill: #43a127)[ + #rect(width: 100%, height: 100%, fill: #9feb52)[ + #align(center, center)[Stuff inside an ellipse!] + ] + ] +] + +Auto-sized ellipse. \ +#ellipse(fill: #9feb52)[ + But, soft! what light through yonder window breaks? +] diff --git a/tests/typ/library/pagebreak.typ b/tests/typ/library/pagebreak.typ index 37a544cfa..26629f4bd 100644 --- a/tests/typ/library/pagebreak.typ +++ b/tests/typ/library/pagebreak.typ @@ -4,3 +4,20 @@ First of two #pagebreak() #page(height: 40pt) + +--- +// Make sure that you can't do page related stuff in a shape. +A +#rect[ + B + // Error: 16 cannot modify page from here + #pagebreak() + + // Error: 11-15 cannot modify page from here + #page("a4") +] +C + +// No consequences from the page("A4") call here. +#pagebreak() +D diff --git a/tests/typ/library/rect.typ b/tests/typ/library/rect.typ new file mode 100644 index 000000000..407134114 --- /dev/null +++ b/tests/typ/library/rect.typ @@ -0,0 +1,27 @@ +// Test shapes. + +--- +// Test the `rect` function. + +#page(width: 150pt) + +// Fit to text. +#rect(fill: #9feb52)[Textbox] + +// Empty with fixed width and height. +#rect(width: 3cm, height: 12pt, fill: #CB4CED) + +// Fixed width, text height. +#rect(width: 2cm, fill: #9650D6, pad(5pt)[Fixed and padded]) + +// Page width, fixed height. +#rect(height: 1cm, width: 100%, fill: #734CED)[Topleft] + +// Not visible, but creates a gap between the boxes above and below +// due to line spacing. +#rect(width: 2in, fill: #ff0000) + +// These are in a row! +#rect(width: 0.5in, height: 10pt, fill: #D6CD67) +#rect(width: 0.5in, height: 10pt, fill: #EDD466) +#rect(width: 0.5in, height: 10pt, fill: #E3BE62) diff --git a/tests/typ/library/shapes.typ b/tests/typ/library/shapes.typ deleted file mode 100644 index c5a8abf95..000000000 --- a/tests/typ/library/shapes.typ +++ /dev/null @@ -1,42 +0,0 @@ -// Test shapes. - ---- -// Test `rect` function. - -#page("a8", flip: true) - -// Fixed width, should have text height. -#rect(width: 2cm, fill: #9650D6)[Legal] - -Sometimes there is no box. - -// Fixed height, should span line. -#rect(height: 1cm, width: 100%, fill: #734CED)[B] - -// Empty with fixed width and height. -#rect(width: 6cm, height: 12pt, fill: #CB4CED) - -// Not visible, but creates a gap between the boxes above and below. -#rect(width: 2in, fill: #ff0000) - -// These are in a row! -#rect(width: 0.5in, height: 10pt, fill: #D6CD67) -#rect(width: 0.5in, height: 10pt, fill: #EDD466) -#rect(width: 0.5in, height: 10pt, fill: #E3BE62) - ---- -// Make sure that you can't do page related stuff in a shape. -A -#rect[ - B - // Error: 16 cannot modify page from here - #pagebreak() - - // Error: 11-15 cannot modify page from here - #page("a4") -] -C - -// No consequences from the page("A4") call here. -#pagebreak() -D diff --git a/tests/typ/library/square.typ b/tests/typ/library/square.typ new file mode 100644 index 000000000..5f224b569 --- /dev/null +++ b/tests/typ/library/square.typ @@ -0,0 +1,31 @@ +// Test the `square` function. + +--- +Auto-sized square. \ +#square(fill: #239DAD)[ + #align(center) + #pad(5pt)[ + #font(color: #fff, weight: bold) + Typst \ + ] +] + +--- +// Length wins over width and height. +// Error: 2:9-2:20 unexpected argument +// Error: 1:22-1:34 unexpected argument +#square(width: 10cm, height: 20cm, length: 1cm, fill: #eb5278) + +--- +// Test height overflow. +#page(width: 75pt, height: 100pt) +#square(fill: #9feb52)[ + But, soft! what light through yonder window breaks? +] + +--- +// Test width overflow. +#page(width: 100pt, height: 75pt) +#square(fill: #9feb52)[ + But, soft! what light through yonder window breaks? +] diff --git a/tests/typeset.rs b/tests/typeset.rs index 653470339..2cf6bfb6e 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -8,18 +8,19 @@ use std::rc::Rc; use fontdock::fs::FsIndex; use image::{GenericImageView, Rgba}; use tiny_skia::{ - Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, PathBuilder, Pattern, Pixmap, - Rect, SpreadMode, Transform, + Canvas, Color, ColorU8, FillRule, FilterQuality, Paint, Pattern, Pixmap, Rect, + SpreadMode, Transform, }; use ttf_parser::OutlineBuilder; use walkdir::WalkDir; +use typst::color; use typst::diag::{Diag, DiagSet, Level, Pass}; use typst::env::{Env, FsIndexExt, ImageResource, ResourceLoader}; use typst::eval::{EvalContext, FuncArgs, FuncValue, Scope, Value}; use typst::exec::State; use typst::export::pdf; -use typst::geom::{Length, Point, Sides, Size}; +use typst::geom::{self, Length, Point, Sides, Size}; use typst::layout::{Element, Fill, Frame, Geometry, Image, Shape, Shaped}; use typst::library; use typst::parse::{LineMap, Scanner}; @@ -418,7 +419,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) { let y = pos.y.to_pt() as f32; let scale = (shaped.font_size / units_per_em as f64).to_pt() as f32; - let mut builder = WrappedPathBuilder(PathBuilder::new()); + let mut builder = WrappedPathBuilder::default(); face.outline_glyph(glyph, &mut builder); if let Some(path) = builder.0.finish() { @@ -426,7 +427,7 @@ fn draw_text(env: &Env, canvas: &mut Canvas, pos: Point, shaped: &Shaped) { .transform(&Transform::from_row(scale, 0.0, 0.0, -scale, x, y).unwrap()) .unwrap(); - let mut paint = paint_from_fill(shaped.color); + let mut paint = convert_fill(shaped.color); paint.anti_alias = true; canvas.fill_path(&placed, &paint, FillRule::default()); @@ -438,28 +439,27 @@ fn draw_geometry(_: &Env, canvas: &mut Canvas, pos: Point, element: &Geometry) { let x = pos.x.to_pt() as f32; let y = pos.y.to_pt() as f32; - let paint = paint_from_fill(element.fill); + let paint = convert_fill(element.fill); + let rule = FillRule::default(); - match &element.shape { - Shape::Rect(s) => { - let (w, h) = (s.width.to_pt() as f32, s.height.to_pt() as f32); - canvas.fill_rect(Rect::from_xywh(x, y, w, h).unwrap(), &paint); + match element.shape { + Shape::Rect(Size { width, height }) => { + let w = width.to_pt() as f32; + let h = height.to_pt() as f32; + let rect = Rect::from_xywh(x, y, w, h).unwrap(); + canvas.fill_rect(rect, &paint); + } + Shape::Ellipse(size) => { + let path = convert_path(x, y, &geom::ellipse_path(size)); + canvas.fill_path(&path, &paint, rule); + } + Shape::Path(ref path) => { + let path = convert_path(x, y, path); + canvas.fill_path(&path, &paint, rule); } }; } -fn paint_from_fill(fill: Fill) -> Paint<'static> { - let mut paint = Paint::default(); - match fill { - Fill::Color(c) => match c { - typst::color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a), - }, - Fill::Image(_) => todo!(), - } - - paint -} - fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) { let img = &env.resources.loaded::(element.res); @@ -492,7 +492,40 @@ fn draw_image(env: &Env, canvas: &mut Canvas, pos: Point, element: &Image) { ); } -struct WrappedPathBuilder(PathBuilder); +fn convert_fill(fill: Fill) -> Paint<'static> { + let mut paint = Paint::default(); + match fill { + Fill::Color(c) => match c { + color::Color::Rgba(c) => paint.set_color_rgba8(c.r, c.g, c.b, c.a), + }, + Fill::Image(_) => todo!(), + } + paint +} + +fn convert_path(x: f32, y: f32, path: &geom::Path) -> tiny_skia::Path { + let f = |length: Length| length.to_pt() as f32; + let mut builder = tiny_skia::PathBuilder::new(); + for elem in &path.0 { + match elem { + geom::PathElement::MoveTo(p) => builder.move_to(x + f(p.x), y + f(p.y)), + geom::PathElement::LineTo(p) => builder.line_to(x + f(p.x), y + f(p.y)), + geom::PathElement::CubicTo(p1, p2, p3) => builder.cubic_to( + x + f(p1.x), + y + f(p1.y), + x + f(p2.x), + y + f(p2.y), + x + f(p3.x), + y + f(p3.y), + ), + geom::PathElement::ClosePath => builder.close(), + }; + } + builder.finish().unwrap() +} + +#[derive(Default)] +struct WrappedPathBuilder(tiny_skia::PathBuilder); impl OutlineBuilder for WrappedPathBuilder { fn move_to(&mut self, x: f32, y: f32) {