diff options
| -rw-r--r-- | Cargo.toml | 4 | ||||
| -rw-r--r-- | src/eval/value.rs | 2 | ||||
| -rw-r--r-- | src/export/pdf.rs | 115 | ||||
| -rw-r--r-- | src/font.rs | 14 | ||||
| -rw-r--r-- | src/frame.rs | 59 | ||||
| -rw-r--r-- | src/geom/paint.rs | 17 | ||||
| -rw-r--r-- | src/geom/path.rs | 25 | ||||
| -rw-r--r-- | src/library/deco.rs | 27 | ||||
| -rw-r--r-- | src/library/page.rs | 8 | ||||
| -rw-r--r-- | src/library/shape.rs | 76 | ||||
| -rw-r--r-- | src/library/text.rs | 32 | ||||
| -rw-r--r-- | src/library/utility.rs | 4 | ||||
| -rw-r--r-- | src/style/mod.rs | 2 | ||||
| -rw-r--r-- | tests/ref/elements/circle.png | bin | 12818 -> 18807 bytes | |||
| -rw-r--r-- | tests/ref/elements/ellipse.png | bin | 7146 -> 9597 bytes | |||
| -rw-r--r-- | tests/ref/elements/rect.png | bin | 2866 -> 2913 bytes | |||
| -rw-r--r-- | tests/ref/elements/square.png | bin | 6983 -> 6921 bytes | |||
| -rw-r--r-- | tests/ref/text/decorations.png | bin | 9194 -> 9668 bytes | |||
| -rw-r--r-- | tests/ref/text/links.png | bin | 9282 -> 9303 bytes | |||
| -rw-r--r-- | tests/typ/elements/circle.typ | 16 | ||||
| -rw-r--r-- | tests/typ/elements/ellipse.typ | 3 | ||||
| -rw-r--r-- | tests/typ/elements/rect.typ | 9 | ||||
| -rw-r--r-- | tests/typ/elements/square.typ | 6 | ||||
| -rw-r--r-- | tests/typeset.rs | 157 |
24 files changed, 332 insertions, 244 deletions
@@ -24,7 +24,7 @@ fxhash = "0.2.1" image = { version = "0.23", default-features = false, features = ["png", "jpeg"] } itertools = "0.10" miniz_oxide = "0.4" -pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "a750b66" } +pdf-writer = { git = "https://github.com/typst/pdf-writer", rev = "f446079" } rustybuzz = "0.4" serde = { version = "1", features = ["derive", "rc"] } ttf-parser = "0.12" @@ -43,7 +43,7 @@ walkdir = { version = "2", optional = true } [dev-dependencies] walkdir = "2" -tiny-skia = "0.5" +tiny-skia = "0.6" usvg = { version = "0.15", default-features = false } iai = { git = "https://github.com/reknih/iai" } diff --git a/src/eval/value.rs b/src/eval/value.rs index 0fcc4bfc..e224438a 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -147,7 +147,7 @@ impl From<usize> for Value { impl From<RgbaColor> for Value { fn from(v: RgbaColor) -> Self { - Self::Color(Color::Rgba(v)) + Self::Color(v.into()) } } diff --git a/src/export/pdf.rs b/src/export/pdf.rs index 05f73e52..b807d059 100644 --- a/src/export/pdf.rs +++ b/src/export/pdf.rs @@ -14,7 +14,7 @@ use ttf_parser::{name_id, GlyphId, Tag}; use super::subset; use crate::font::{find_name, FaceId, FontStore}; -use crate::frame::{Element, Frame, Geometry, Text}; +use crate::frame::{Element, Frame, Geometry, Shape, Stroke, Text}; use crate::geom::{self, Color, Em, Length, Paint, Size}; use crate::image::{Image, ImageId, ImageStore}; use crate::Context; @@ -389,7 +389,7 @@ impl<'a> PageExporter<'a> { self.in_text_state = true; } - Element::Geometry(..) | Element::Image(..) if self.in_text_state => { + Element::Shape(_) | Element::Image(..) if self.in_text_state => { self.content.end_text(); self.in_text_state = false; } @@ -401,19 +401,11 @@ impl<'a> PageExporter<'a> { let y = y - offset.y.to_f32(); match *element { - Element::Text(ref text) => { - self.write_text(x, y, text); - } - Element::Geometry(ref geometry, paint) => { - self.write_geometry(x, y, geometry, paint); - } - Element::Image(id, size) => { - self.write_image(x, y, id, size); - } + Element::Text(ref text) => self.write_text(x, y, text), + Element::Shape(ref shape) => self.write_shape(x, y, shape), + Element::Image(id, size) => self.write_image(x, y, id, size), + Element::Frame(ref frame) => self.write_frame(x, y, frame), Element::Link(_, _) => {} - Element::Frame(ref frame) => { - self.write_frame(x, y, frame); - } } } @@ -482,79 +474,92 @@ impl<'a> PageExporter<'a> { } } - /// Write an image into the content stream. - fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) { - let name = format!("Im{}", self.image_map.map(id)); - let w = size.w.to_f32(); - let h = size.h.to_f32(); - - self.content.save_state(); - self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]); - self.content.x_object(Name(name.as_bytes())); - self.content.restore_state(); - } - /// Write a geometrical shape into the content stream. - fn write_geometry(&mut self, x: f32, y: f32, geometry: &Geometry, paint: Paint) { - self.content.save_state(); + fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { + if shape.fill.is_none() && shape.stroke.is_none() { + return; + } - match *geometry { - Geometry::Rect(Size { w, h }) => { - let w = w.to_f32(); - let h = h.to_f32(); + match shape.geometry { + Geometry::Rect(size) => { + let w = size.w.to_f32(); + let h = size.h.to_f32(); if w > 0.0 && h > 0.0 { - self.write_fill(paint); self.content.rect(x, y - h, w, h); - self.content.fill_nonzero(); } } Geometry::Ellipse(size) => { - let path = geom::Path::ellipse(size); - self.write_fill(paint); - self.write_filled_path(x, y, &path); + let approx = geom::Path::ellipse(size); + self.write_path(x, y, &approx); } - Geometry::Line(target, thickness) => { - self.write_stroke(paint, thickness.to_f32()); + Geometry::Line(target) => { + let dx = target.x.to_f32(); + let dy = target.y.to_f32(); self.content.move_to(x, y); - self.content.line_to(x + target.x.to_f32(), y - target.y.to_f32()); - self.content.stroke(); + self.content.line_to(x + dx, y - dy); } Geometry::Path(ref path) => { - self.write_fill(paint); - self.write_filled_path(x, y, path) + self.write_path(x, y, path); } } + self.content.save_state(); + + if let Some(fill) = shape.fill { + self.write_fill(fill); + } + + if let Some(stroke) = shape.stroke { + self.write_stroke(stroke); + } + + match (shape.fill, shape.stroke) { + (None, None) => unreachable!(), + (Some(_), None) => self.content.fill_nonzero(), + (None, Some(_)) => self.content.stroke(), + (Some(_), Some(_)) => self.content.fill_nonzero_and_stroke(), + }; + + self.content.restore_state(); + } + + /// Write an image into the content stream. + fn write_image(&mut self, x: f32, y: f32, id: ImageId, size: Size) { + let name = format!("Im{}", self.image_map.map(id)); + let w = size.w.to_f32(); + let h = size.h.to_f32(); + self.content.save_state(); + self.content.concat_matrix([w, 0.0, 0.0, h, x, y - h]); + self.content.x_object(Name(name.as_bytes())); self.content.restore_state(); } - /// Write and fill path into a content stream. - fn write_filled_path(&mut self, x: f32, y: f32, path: &geom::Path) { + /// Write a path into a content stream. + fn write_path(&mut self, x: f32, y: f32, path: &geom::Path) { for elem in &path.0 { match elem { geom::PathElement::MoveTo(p) => { - self.content.move_to(x + p.x.to_f32(), y + p.y.to_f32()) + self.content.move_to(x + p.x.to_f32(), y - p.y.to_f32()) } geom::PathElement::LineTo(p) => { - self.content.line_to(x + p.x.to_f32(), y + p.y.to_f32()) + self.content.line_to(x + p.x.to_f32(), y - p.y.to_f32()) } geom::PathElement::CubicTo(p1, p2, p3) => self.content.cubic_to( x + p1.x.to_f32(), - y + p1.y.to_f32(), + y - p1.y.to_f32(), x + p2.x.to_f32(), - y + p2.y.to_f32(), + y - p2.y.to_f32(), x + p3.x.to_f32(), - y + p3.y.to_f32(), + y - p3.y.to_f32(), ), geom::PathElement::ClosePath => self.content.close_path(), }; } - self.content.fill_nonzero(); } /// Write a fill change into a content stream. fn write_fill(&mut self, fill: Paint) { - let Paint::Color(Color::Rgba(c)) = fill; + let Paint::Solid(Color::Rgba(c)) = fill; self.content.set_fill_rgb( c.r as f32 / 255.0, c.g as f32 / 255.0, @@ -563,14 +568,14 @@ impl<'a> PageExporter<'a> { } /// Write a stroke change into a content stream. - fn write_stroke(&mut self, stroke: Paint, thickness: f32) { - let Paint::Color(Color::Rgba(c)) = stroke; + fn write_stroke(&mut self, stroke: Stroke) { + let Paint::Solid(Color::Rgba(c)) = stroke.paint; self.content.set_stroke_rgb( c.r as f32 / 255.0, c.g as f32 / 255.0, c.b as f32 / 255.0, ); - self.content.set_line_width(thickness); + self.content.set_line_width(stroke.thickness.to_f32()); } } diff --git a/src/font.rs b/src/font.rs index fb93d5c9..5afd1aca 100644 --- a/src/font.rs +++ b/src/font.rs @@ -187,11 +187,11 @@ pub struct Face { /// Metrics for a decorative line. #[derive(Debug, Copy, Clone)] pub struct LineMetrics { - /// The thickness of the line. - pub strength: Em, /// The vertical offset of the line from the baseline. Positive goes /// upwards, negative downwards. pub position: Em, + /// The thickness of the line. + pub thickness: Em, } impl Face { @@ -218,22 +218,22 @@ impl Face { let underline = ttf.underline_metrics(); let strikethrough = LineMetrics { - strength: strikeout + position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)), + thickness: strikeout .or(underline) .map_or(Em::new(0.06), |s| to_em(s.thickness)), - position: strikeout.map_or(Em::new(0.25), |s| to_em(s.position)), }; let underline = LineMetrics { - strength: underline + position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)), + thickness: underline .or(strikeout) .map_or(Em::new(0.06), |s| to_em(s.thickness)), - position: underline.map_or(Em::new(-0.2), |s| to_em(s.position)), }; let overline = LineMetrics { - strength: underline.strength, position: cap_height + Em::new(0.1), + thickness: underline.thickness, }; Some(Self { diff --git a/src/frame.rs b/src/frame.rs index 9feb6959..9f1b1c28 100644 --- a/src/frame.rs +++ b/src/frame.rs @@ -128,14 +128,13 @@ impl<'a> Iterator for Elements<'a> { /// The building block frames are composed of. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum Element { - /// Shaped text. + /// A run of shaped text. Text(Text), - /// A geometric shape and the paint which with it should be filled or - /// stroked (which one depends on the kind of geometry). - Geometry(Geometry, Paint), - /// A raster image. + /// A geometric shape with optional fill and stroke. + Shape(Shape), + /// A raster image and its size. Image(ImageId, Size), - /// A link to an external resource. + /// A link to an external resource and its trigger region. Link(String, Size), /// A subframe, which can be a clipping boundary. Frame(Rc<Frame>), @@ -167,15 +166,51 @@ pub struct Glyph { pub x_offset: Em, } -/// A geometric shape. +/// A geometric shape with optional fill and stroke. +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] +pub struct Shape { + /// The shape's geometry. + pub geometry: Geometry, + /// The shape's background fill. + pub fill: Option<Paint>, + /// The shape's border stroke. + pub stroke: Option<Stroke>, +} + +impl Shape { + /// Create a filled shape without a stroke. + pub fn filled(geometry: Geometry, fill: Paint) -> Self { + Self { geometry, fill: Some(fill), stroke: None } + } + + /// Create a stroked shape without a fill. + pub fn stroked(geometry: Geometry, stroke: Stroke) -> Self { + Self { + geometry, + fill: None, + stroke: Some(stroke), + } + } +} + +/// A shape's geometry. #[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] pub enum Geometry { - /// A filled rectangle with its origin in the topleft corner. + /// A line to a point (relative to its position). + Line(Point), + /// A rectangle with its origin in the topleft corner. Rect(Size), - /// A filled ellipse with its origin in the center. + /// A ellipse with its origin in the topleft corner. Ellipse(Size), - /// A stroked line to a point (relative to its position) with a thickness. - Line(Point, Length), - /// A filled bezier path. + /// A bezier path. Path(Path), } + +/// A stroke of a geometric shape. +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] +pub struct Stroke { + /// The stroke's paint. + pub paint: Paint, + /// The stroke's thickness. + pub thickness: Length, +} diff --git a/src/geom/paint.rs b/src/geom/paint.rs index 74d7d147..66bfb17c 100644 --- a/src/geom/paint.rs +++ b/src/geom/paint.rs @@ -7,7 +7,16 @@ use super::*; #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub enum Paint { /// A solid color. - Color(Color), + Solid(Color), +} + +impl<T> From<T> for Paint +where + T: Into<Color>, +{ + fn from(t: T) -> Self { + Self::Solid(t.into()) + } } /// A color in a dynamic format. @@ -25,6 +34,12 @@ impl Debug for Color { } } +impl From<RgbaColor> for Color { + fn from(rgba: RgbaColor) -> Self { + Self::Rgba(rgba) + } +} + /// An 8-bit RGBA color. #[derive(Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)] pub struct RgbaColor { diff --git a/src/geom/path.rs b/src/geom/path.rs index bc0d3f2d..39e75312 100644 --- a/src/geom/path.rs +++ b/src/geom/path.rs @@ -20,22 +20,35 @@ impl Path { Self(vec![]) } + /// Create a path that describes a rectangle. + pub fn rect(size: Size) -> Self { + let z = Length::zero(); + let point = Point::new; + let mut path = Self::new(); + path.move_to(point(z, z)); + path.line_to(point(size.w, z)); + path.line_to(point(size.w, size.h)); + path.line_to(point(z, size.h)); + path.close_path(); + path + } + /// Create a path that approximates an axis-aligned ellipse. pub fn ellipse(size: Size) -> Self { // https://stackoverflow.com/a/2007782 + let z = Length::zero(); let rx = size.w / 2.0; let ry = size.h / 2.0; let m = 0.551784; let mx = m * rx; let my = m * ry; - let z = Length::zero(); - let point = Point::new; + let point = |x, y| Point::new(x + rx, y + ry); let mut path = Self::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.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(-rx, z)); path } diff --git a/src/library/deco.rs b/src/library/deco.rs index 2722fd68..1f8c051f 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -17,20 +17,13 @@ pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { } fn line_impl(args: &mut Args, kind: LineKind) -> TypResult<Value> { - let stroke = args.named("stroke")?.or_else(|| args.find()); + let stroke = args.named("stroke")?.or_else(|| args.find()).map(Paint::Solid); let thickness = args.named::<Linear>("thickness")?.or_else(|| args.find()); let offset = args.named("offset")?; let extent = args.named("extent")?.unwrap_or_default(); let body: Template = args.expect("body")?; - Ok(Value::Template(body.decorate(Decoration::Line( - LineDecoration { - kind, - stroke: stroke.map(Paint::Color), - thickness, - offset, - extent, - }, + LineDecoration { kind, stroke, thickness, offset, extent }, )))) } @@ -112,12 +105,15 @@ impl LineDecoration { LineKind::Overline => face.overline, }; - let stroke = self.stroke.unwrap_or(text.fill); - let thickness = self .thickness .map(|s| s.resolve(text.size)) - .unwrap_or(metrics.strength.to_length(text.size)); + .unwrap_or(metrics.thickness.to_length(text.size)); + + let stroke = Stroke { + paint: self.stroke.unwrap_or(text.fill), + thickness, + }; let offset = self .offset @@ -127,10 +123,9 @@ impl LineDecoration { let extent = self.extent.resolve(text.size); let subpos = Point::new(pos.x - extent, pos.y + offset); - let vector = Point::new(text.width + 2.0 * extent, Length::zero()); - let line = Geometry::Line(vector, thickness); - - frame.push(subpos, Element::Geometry(line, stroke)); + let target = Point::new(text.width + 2.0 * extent, Length::zero()); + let shape = Shape::stroked(Geometry::Line(target), stroke); + frame.push(subpos, Element::Shape(shape)); } } } diff --git a/src/library/page.rs b/src/library/page.rs index b760e76a..20871bd9 100644 --- a/src/library/page.rs +++ b/src/library/page.rs @@ -18,7 +18,7 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let right = args.named("right")?; let bottom = args.named("bottom")?; let flip = args.named("flip")?; - let fill = args.named("fill")?; + let fill = args.named("fill")?.map(Paint::Solid); ctx.template.modify(move |style| { let page = style.page_mut(); @@ -63,7 +63,7 @@ pub fn page(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { } if let Some(fill) = fill { - page.fill = Some(Paint::Color(fill)); + page.fill = Some(fill); } }); @@ -105,8 +105,8 @@ impl PageNode { // Add background fill if requested. if let Some(fill) = self.fill { for frame in &mut frames { - let element = Element::Geometry(Geometry::Rect(frame.size), fill); - Rc::make_mut(frame).prepend(Point::zero(), element); + let shape = Shape::filled(Geometry::Rect(frame.size), fill); + Rc::make_mut(frame).prepend(Point::zero(), Element::Shape(shape)); } } diff --git a/src/library/shape.rs b/src/library/shape.rs index d0df5f48..abf927e4 100644 --- a/src/library/shape.rs +++ b/src/library/shape.rs @@ -7,9 +7,7 @@ use crate::util::RcExt; pub fn rect(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let width = args.named("width")?; let height = args.named("height")?; - let fill = args.named("fill")?; - let body = args.find(); - Ok(shape_impl(ShapeKind::Rect, width, height, fill, body)) + shape_impl(args, ShapeKind::Rect, width, height) } /// `square`: A square with optional content. @@ -23,18 +21,14 @@ pub fn square(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { None => args.named("height")?, size => size, }; - let fill = args.named("fill")?; - let body = args.find(); - Ok(shape_impl(ShapeKind::Square, width, height, fill, body)) + shape_impl(args, ShapeKind::Square, width, height) } /// `ellipse`: An ellipse with optional content. pub fn ellipse(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let width = args.named("width")?; let height = args.named("height")?; - let fill = args.named("fill")?; - let body = args.find(); - Ok(shape_impl(ShapeKind::Ellipse, width, height, fill, body)) + shape_impl(args, ShapeKind::Ellipse, width, height) } /// `circle`: A circle with optional content. @@ -48,30 +42,44 @@ pub fn circle(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { None => args.named("height")?, diameter => diameter, }; - let fill = args.named("fill")?; - let body = args.find(); - Ok(shape_impl(ShapeKind::Circle, width, height, fill, body)) + shape_impl(args, ShapeKind::Circle, width, height) } fn shape_impl( + args: &mut Args, kind: ShapeKind, width: Option<Linear>, height: Option<Linear>, - fill: Option<Color>, - body: Option<Template>, -) -> Value { - // Set default fill if there's no fill. - let fill = fill.unwrap_or(Color::Rgba(RgbaColor::gray(175))); +) -> TypResult<Value> { + // The default appearance of a shape. + let default = Stroke { + paint: RgbaColor::BLACK.into(), + thickness: Length::pt(1.0), + }; + + // Parse fill & stroke. + let fill = args.named("fill")?.map(Paint::Solid); + let stroke = match (args.named("stroke")?, args.named("thickness")?) { + (None, None) => fill.is_none().then(|| default), + (color, thickness) => Some(Stroke { + paint: color.map(Paint::Solid).unwrap_or(default.paint), + thickness: thickness.unwrap_or(default.thickness), + }), + }; - Value::Template(Template::from_inline(move |style| { + let padding = Sides::splat(args.named("padding")?.unwrap_or_default()); + let body = args.find::<Template>(); + + Ok(Value::Template(Template::from_inline(move |style| { ShapeNode { kind, - fill: Some(Paint::Color(fill)), - child: body.as_ref().map(|body| body.pack(style)), + fill, + stroke, + child: body.as_ref().map(|body| body.pack(style).padded(padding)), } .pack() .sized(width, height) - })) + }))) } /// Places its child into a sizable and fillable shape. @@ -79,8 +87,10 @@ fn shape_impl( pub struct ShapeNode { /// Which shape to place the child into. pub kind: ShapeKind, - /// How to fill the shape, if at all. + /// How to fill the shape. pub fill: Option<Paint>, + /// How the stroke the shape. + pub stroke: Option<Stroke>, /// The child node to place into the shape, if any. pub child: Option<PackedNode>, } @@ -160,18 +170,20 @@ impl Layout for ShapeNode { Frame::new(size, size.h) }; - // Add background fill if desired. - if let Some(fill) = self.fill { - let (pos, geometry) = match self.kind { - ShapeKind::Square | ShapeKind::Rect => { - (Point::zero(), Geometry::Rect(frame.size)) - } - ShapeKind::Circle | ShapeKind::Ellipse => { - (frame.size.to_point() / 2.0, Geometry::Ellipse(frame.size)) - } + // Add fill and/or stroke. + if self.fill.is_some() || self.stroke.is_some() { + let geometry = match self.kind { + ShapeKind::Square | ShapeKind::Rect => Geometry::Rect(frame.size), + ShapeKind::Circle | ShapeKind::Ellipse => Geometry::Ellipse(frame.size), + }; + + let shape = Shape { + geometry, + fill: self.fill, + stroke: self.stroke, }; - frame.prepend(pos, Element::Geometry(geometry, fill)); + frame.prepend(Point::zero(), Element::Shape(shape)); } // Ensure frame size matches regions size if expansion is on. diff --git a/src/library/text.rs b/src/library/text.rs index cdcc7509..d0b5c8e6 100644 --- a/src/library/text.rs +++ b/src/library/text.rs @@ -163,12 +163,12 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let fallback = args.named("fallback")?; let style = args.named("style")?; let weight = args.named("weight")?; - let tracking = args.named("tracking")?; let stretch = args.named("stretch")?; let size = args.named::<Linear>("size")?.or_else(|| args.find()); + let tracking = args.named("tracking")?.map(Em::new); let top_edge = args.named("top-edge")?; let bottom_edge = args.named("bottom-edge")?; - let fill = args.named("fill")?.or_else(|| args.find()); + let fill = args.named("fill")?.or_else(|| args.find()).map(Paint::Solid); let kerning = args.named("kerning")?; let smallcaps = args.named("smallcaps")?; let alternates = args.named("alternates")?; @@ -182,7 +182,6 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let slashed_zero = args.named("slashed-zero")?; let fractions = args.named("fractions")?; let features = args.named("features")?; - let body = args.find::<Template>(); macro_rules! set { @@ -195,30 +194,19 @@ pub fn font(ctx: &mut EvalContext, args: &mut Args) -> TypResult<Value> { let f = move |style_: &mut Style| { let text = style_.text_mut(); - - if let Some(size) = size { - text.size = size.resolve(text.size); - } - - if let Some(fill) = fill { - text.fill = Paint::Color(fill); - } - - if let Some(tracking) = tracking { - text.tracking = Em::new(tracking); - } - + set!(text.families_mut().list => list.clone()); + set!(text.families_mut().serif => serif.clone()); + set!(text.families_mut().sans_serif => sans_serif.clone()); + set!(text.families_mut().monospace => monospace.clone()); + set!(text.fallback => fallback); set!(text.variant.style => style); set!(text.variant.weight => weight); set!(text.variant.stretch => stretch); + set!(text.size => size.map(|v| v.resolve(text.size))); + set!(text.tracking => tracking); set!(text.top_edge => top_edge); set!(text.bottom_edge => bottom_edge); - set!(text.fallback => fallback); - - set!(text.families_mut().list => list.clone()); - set!(text.families_mut().serif => serif.clone()); - set!(text.families_mut().sans_serif => sans_serif.clone()); - set!(text.families_mut().monospace => monospace.clone()); + set!(text.fill => fill); set!(text.features_mut().kerning => kerning); set!(text.features_mut().smallcaps => smallcaps); set!(text.features_mut().alternates => alternates); diff --git a/src/library/utility.rs b/src/library/utility.rs index 6d15a823..b2435e8a 100644 --- a/src/library/utility.rs +++ b/src/library/utility.rs @@ -86,7 +86,7 @@ pub fn str(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { /// `rgb`: Create an RGB(A) color. pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { - Ok(Value::Color(Color::Rgba( + Ok(Value::from( if let Some(string) = args.find::<Spanned<EcoString>>() { match RgbaColor::from_str(&string.v) { Ok(color) => color, @@ -106,7 +106,7 @@ pub fn rgb(_: &mut EvalContext, args: &mut Args) -> TypResult<Value> { }; RgbaColor::new(f(r)?, f(g)?, f(b)?, f(a)?) }, - ))) + )) } /// `abs`: The absolute value of a numeric value. diff --git a/src/style/mod.rs b/src/style/mod.rs index 4588163b..4a8830f8 100644 --- a/src/style/mod.rs +++ b/src/style/mod.rs @@ -224,7 +224,7 @@ impl Default for TextStyle { }, top_edge: VerticalFontMetric::CapHeight, bottom_edge: VerticalFontMetric::Baseline, - fill: Paint::Color(Color::Rgba(RgbaColor::BLACK)), + fill: RgbaColor::BLACK.into(), families: Rc::new(FamilyStyle::default()), features: Rc::new(FontFeatures::default()), tracking: Em::zero(), diff --git a/tests/ref/elements/circle.png b/tests/ref/elements/circle.png Binary files differindex f5c0b05a..4ef3ad5e 100644 --- a/tests/ref/elements/circle.png +++ b/tests/ref/elements/circle.png diff --git a/tests/ref/elements/ellipse.png b/tests/ref/elements/ellipse.png Binary files differindex 77124e1d..9a80c4c0 100644 --- a/tests/ref/elements/ellipse.png +++ b/tests/ref/elements/ellipse.png diff --git a/tests/ref/elements/rect.png b/tests/ref/elements/rect.png Binary files differindex b6d40545..a434b81d 100644 --- a/tests/ref/elements/rect.png +++ b/tests/ref/elements/rect.png diff --git a/tests/ref/elements/square.png b/tests/ref/elements/square.png Binary files differindex a12e1a12..12d00d9b 100644 --- a/tests/ref/elements/square.png +++ b/tests/ref/elements/square.png diff --git a/tests/ref/text/decorations.png b/tests/ref/text/decorations.png Binary files differindex 0bcba75a..183dacf9 100644 --- a/tests/ref/text/decorations.png +++ b/tests/ref/text/decorations.png diff --git a/tests/ref/text/links.png b/tests/ref/text/links.png Binary files differindex 43b77c87..c4aeec06 100644 --- a/tests/ref/text/links.png +++ b/tests/ref/text/links.png diff --git a/tests/typ/elements/circle.typ b/tests/typ/elements/circle.typ index 5be9cf56..b89c8a0d 100644 --- a/tests/typ/elements/circle.typ +++ b/tests/typ/elements/circle.typ @@ -3,17 +3,18 @@ --- // Default circle. #circle() +#circle[Hey] --- // Test auto sizing. Auto-sized circle. \ -#circle(fill: rgb("eb5278"))[ - #align(center, center)[But, soft!] -] +#circle(fill: rgb("eb5278"), thickness: 2pt, + align(center, center)[But, soft!] +) Center-aligned rect in auto-sized circle. -#circle(fill: forest, +#circle(fill: forest, stroke: conifer, align(center, center, rect(fill: conifer, pad(5pt)[But, soft!]) ) @@ -21,17 +22,18 @@ Center-aligned rect in auto-sized circle. Rect in auto-sized circle. \ #circle(fill: forest, - rect(fill: conifer)[ + rect(fill: conifer, stroke: white, padding: 4pt)[ + #font(8pt) But, soft! what light through yonder window breaks? ] ) Expanded by height. -#circle(fill: conifer)[A \ B \ C] +#circle(stroke: black, align(center)[A \ B \ C]) --- // Ensure circle directly in rect works. -#rect(width: 40pt, height: 30pt, circle(fill: forest)) +#rect(width: 40pt, height: 30pt, fill: forest, circle(fill: conifer)) --- // Test relative sizing. diff --git a/tests/typ/elements/ellipse.typ b/tests/typ/elements/ellipse.typ index 9c36fb1c..49b1514d 100644 --- a/tests/typ/elements/ellipse.typ +++ b/tests/typ/elements/ellipse.typ @@ -17,6 +17,7 @@ Rect in ellipse in fixed rect. \ ) Auto-sized ellipse. \ -#ellipse(fill: conifer)[ +#ellipse(fill: conifer, stroke: forest, thickness: 3pt, padding: 3pt)[ + #font(8pt) But, soft! what light through yonder window breaks? ] diff --git a/tests/typ/elements/rect.typ b/tests/typ/elements/rect.typ index 59dd4ee0..bb666fb2 100644 --- a/tests/typ/elements/rect.typ +++ b/tests/typ/elements/rect.typ @@ -8,10 +8,15 @@ #page(width: 150pt) // Fit to text. -#rect(fill: conifer)[Textbox] +#rect(fill: conifer, padding: 3pt)[Textbox] // Empty with fixed width and height. -#rect(width: 3cm, height: 12pt, fill: rgb("ed8a4c")) +#block(rect( + height: 15pt, + fill: rgb("46b3c2"), + stroke: rgb("234994"), + thickness: 2pt, +)) // Fixed width, text height. #rect(width: 2cm, fill: rgb("9650d6"), pad(5pt)[Fixed and padded]) diff --git a/tests/typ/elements/square.typ b/tests/typ/elements/square.typ index 3163f872..32a26ea6 100644 --- a/tests/typ/elements/square.typ +++ b/tests/typ/elements/square.typ @@ -7,16 +7,16 @@ --- // Test auto-sized square. -#square(fill: eastern)[ +#square(fill: eastern, padding: 5pt)[ #font(fill: white, weight: "bold") - #align(center, pad(5pt)[Typst]) + Typst ] --- // Test relative-sized child. #square(fill: eastern)[ #rect(width: 10pt, height: 5pt, fill: conifer) \ - #rect(width: 40%, height: 5pt, fill: conifer) + #rect(width: 40%, height: 5pt, stroke: conifer) ] --- diff --git a/tests/typeset.rs b/tests/typeset.rs index 11e0090f..723df4a8 100644 --- a/tests/typeset.rs +++ b/tests/typeset.rs @@ -12,7 +12,7 @@ use walkdir::WalkDir; use typst::diag::Error; use typst::eval::Value; use typst::font::Face; -use typst::frame::{Element, Frame, Geometry, Text}; +use typst::frame::{Element, Frame, Geometry, Shape, Stroke, Text}; use typst::geom::{self, Color, Length, Paint, PathElement, RgbaColor, Sides, Size}; use typst::image::Image; use typst::layout::layout; @@ -388,8 +388,8 @@ fn draw(ctx: &Context, frames: &[Rc<Frame>], dpp: f32) -> sk::Pixmap { let width = 2.0 * pad + frames.iter().map(|l| l.size.w).max().unwrap_or_default(); let height = pad + frames.iter().map(|l| l.size.h + pad).sum::<Length>(); - let pxw = (dpp * width.to_pt() as f32) as u32; - let pxh = (dpp * height.to_pt() as f32) as u32; + let pxw = (dpp * width.to_f32()) as u32; + let pxh = (dpp * height.to_f32()) as u32; if pxw > 4000 || pxh > 4000 { panic!( "overlarge image: {} by {} ({:?} x {:?})", @@ -405,20 +405,20 @@ fn draw(ctx: &Context, frames: &[Rc<Frame>], dpp: f32) -> sk::Pixmap { let path = sk::PathBuilder::from_rect(rect); mask.set_path(pxw, pxh, &path, sk::FillRule::default(), false); - let mut ts = sk::Transform::from_scale(dpp, dpp) - .pre_translate(pad.to_pt() as f32, pad.to_pt() as f32); + let mut ts = + sk::Transform::from_scale(dpp, dpp).pre_translate(pad.to_f32(), pad.to_f32()); for frame in frames { let mut background = sk::Paint::default(); background.set_color(sk::Color::WHITE); - let w = frame.size.w.to_pt() as f32; - let h = frame.size.h.to_pt() as f32; + let w = frame.size.w.to_f32(); + let h = frame.size.h.to_f32(); let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); canvas.fill_rect(rect, &background, ts, None); draw_frame(&mut canvas, ts, &mask, ctx, frame); - ts = ts.pre_translate(0.0, (frame.size.h + pad).to_pt() as f32); + ts = ts.pre_translate(0.0, (frame.size.h + pad).to_f32()); } canvas @@ -433,8 +433,8 @@ fn draw_frame( ) { let mut storage; let mask = if frame.clips { - let w = frame.size.w.to_pt() as f32; - let h = frame.size.h.to_pt() as f32; + let w = frame.size.w.to_f32(); + let h = frame.size.h.to_f32(); let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); let path = sk::PathBuilder::from_rect(rect).transform(ts).unwrap(); let rule = sk::FillRule::default(); @@ -450,24 +450,24 @@ fn draw_frame( }; for (pos, element) in &frame.elements { - let x = pos.x.to_pt() as f32; - let y = pos.y.to_pt() as f32; + let x = pos.x.to_f32(); + let y = pos.y.to_f32(); let ts = ts.pre_translate(x, y); match *element { Element::Text(ref text) => { draw_text(canvas, ts, mask, ctx.fonts.get(text.face_id), text); } - Element::Geometry(ref geometry, paint) => { - draw_geometry(canvas, ts, mask, geometry, paint); + Element::Shape(ref shape) => { + draw_shape(canvas, ts, mask, shape); } Element::Image(id, size) => { draw_image(canvas, ts, mask, ctx.images.get(id), size); } Element::Link(_, s) => { - let outline = Geometry::Rect(s); - let paint = Paint::Color(Color::Rgba(RgbaColor::new(40, 54, 99, 40))); - draw_geometry(canvas, ts, mask, &outline, paint); + let fill = RgbaColor::new(40, 54, 99, 40).into(); + let shape = Shape::filled(Geometry::Rect(s), fill); + draw_shape(canvas, ts, mask, &shape); } Element::Frame(ref frame) => { draw_frame(canvas, ts, mask, ctx, frame); @@ -484,15 +484,15 @@ fn draw_text( text: &Text, ) { let ttf = face.ttf(); - let size = text.size.to_pt() as f32; - let units_per_em = ttf.units_per_em() as f32; - let pixels_per_em = text.size.to_pt() as f32 * ts.sy; + let size = text.size.to_f32(); + let units_per_em = face.units_per_em as f32; + let pixels_per_em = text.size.to_f32() * ts.sy; let scale = size / units_per_em; let mut x = 0.0; for glyph in &text.glyphs { let glyph_id = GlyphId(glyph.id); - let offset = x + glyph.x_offset.to_length(text.size).to_pt() as f32; + let offset = x + glyph.x_offset.to_length(text.size).to_f32(); let ts = ts.pre_translate(offset, 0.0); if let Some(tree) = ttf @@ -535,56 +535,59 @@ fn draw_text( // Otherwise, draw normal outline. let mut builder = WrappedPathBuilder(sk::PathBuilder::new()); if ttf.outline_glyph(glyph_id, &mut builder).is_some() { - // Flip vertically because font designed coordinate system is Y-up. + // Flip vertically because font design coordinate system is Y-up. let ts = ts.pre_scale(scale, -scale); let path = builder.0.finish().unwrap(); - let mut paint = convert_typst_paint(text.fill); - paint.anti_alias = true; + let paint = convert_typst_paint(text.fill); canvas.fill_path(&path, &paint, sk::FillRule::default(), ts, Some(mask)); } } - x += glyph.x_advance.to_length(text.size).to_pt() as f32; + x += glyph.x_advance.to_length(text.size).to_f32(); } } -fn draw_geometry( +fn draw_shape( canvas: &mut sk::Pixmap, ts: sk::Transform, mask: &sk::ClipMask, - geometry: &Geometry, - paint: Paint, + shape: &Shape, ) { - let paint = convert_typst_paint(paint); - let rule = sk::FillRule::default(); - - match *geometry { - Geometry::Rect(Size { w: width, h: height }) => { - let w = width.to_pt() as f32; - let h = height.to_pt() as f32; + let path = match shape.geometry { + Geometry::Rect(size) => { + let w = size.w.to_f32(); + let h = size.h.to_f32(); let rect = sk::Rect::from_xywh(0.0, 0.0, w, h).unwrap(); - canvas.fill_rect(rect, &paint, ts, Some(mask)); + sk::PathBuilder::from_rect(rect) } Geometry::Ellipse(size) => { - let path = convert_typst_path(&geom::Path::ellipse(size)); - canvas.fill_path(&path, &paint, rule, ts, Some(mask)); - } - Geometry::Line(target, thickness) => { - let path = { - let mut builder = sk::PathBuilder::new(); - builder.line_to(target.x.to_pt() as f32, target.y.to_pt() as f32); - builder.finish().unwrap() - }; - - let mut stroke = sk::Stroke::default(); - stroke.width = thickness.to_pt() as f32; - canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask)); + let approx = geom::Path::ellipse(size); + convert_typst_path(&approx) } - Geometry::Path(ref path) => { - let path = convert_typst_path(path); - canvas.fill_path(&path, &paint, rule, ts, Some(mask)); + Geometry::Line(target) => { + let mut builder = sk::PathBuilder::new(); + builder.line_to(target.x.to_f32(), target.y.to_f32()); + builder.finish().unwrap() } + Geometry::Path(ref path) => convert_typst_path(path), }; + + if let Some(fill) = shape.fill { + let mut paint = convert_typst_paint(fill); + if matches!(shape.geometry, Geometry::Rect(_)) { + paint.anti_alias = false; + } + + let rule = sk::FillRule::default(); + canvas.fill_path(&path, &paint, rule, ts, Some(mask)); + } + + if let Some(Stroke { paint, thickness }) = shape.stroke { + let paint = convert_typst_paint(paint); + let mut stroke = sk::Stroke::default(); + stroke.width = thickness.to_f32(); + canvas.stroke_path(&path, &paint, &stroke, ts, Some(mask)); + } } fn draw_image( @@ -600,8 +603,8 @@ fn draw_image( *dest = sk::ColorU8::from_rgba(r, g, b, a).premultiply(); } - let view_width = size.w.to_pt() as f32; - let view_height = size.h.to_pt() as f32; + let view_width = size.w.to_f32(); + let view_height = size.h.to_f32(); let scale_x = view_width as f32 / pixmap.width() as f32; let scale_y = view_height as f32 / pixmap.height() as f32; @@ -619,25 +622,32 @@ fn draw_image( } fn convert_typst_paint(paint: Paint) -> sk::Paint<'static> { - let Paint::Color(Color::Rgba(c)) = paint; + let Paint::Solid(Color::Rgba(c)) = paint; let mut paint = sk::Paint::default(); paint.set_color_rgba8(c.r, c.g, c.b, c.a); + paint.anti_alias = true; paint } fn convert_typst_path(path: &geom::Path) -> sk::Path { let mut builder = sk::PathBuilder::new(); - let f = |v: Length| v.to_pt() as f32; for elem in &path.0 { match elem { PathElement::MoveTo(p) => { - builder.move_to(f(p.x), f(p.y)); + builder.move_to(p.x.to_f32(), p.y.to_f32()); } PathElement::LineTo(p) => { - builder.line_to(f(p.x), f(p.y)); + builder.line_to(p.x.to_f32(), p.y.to_f32()); } PathElement::CubicTo(p1, p2, p3) => { - builder.cubic_to(f(p1.x), f(p1.y), f(p2.x), f(p2.y), f(p3.x), f(p3.y)); + builder.cubic_to( + p1.x.to_f32(), + p1.y.to_f32(), + p2.x.to_f32(), + p2.y.to_f32(), + p3.x.to_f32(), + p3.y.to_f32(), + ); } PathElement::ClosePath => { builder.close(); @@ -648,20 +658,16 @@ fn convert_typst_path(path: &geom::Path) -> sk::Path { } fn convert_usvg_transform(transform: usvg::Transform) -> sk::Transform { - let g = |v: f64| v as f32; let usvg::Transform { a, b, c, d, e, f } = transform; - sk::Transform::from_row(g(a), g(b), g(c), g(d), g(e), g(f)) + sk::Transform::from_row(a as _, b as _, c as _, d as _, e as _, f as _) } fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) { let mut paint = sk::Paint::default(); paint.anti_alias = true; - match fill.paint { - usvg::Paint::Color(usvg::Color { red, green, blue }) => { - paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8()) - } - usvg::Paint::Link(_) => {} + if let usvg::Paint::Color(usvg::Color { red, green, blue }) = fill.paint { + paint.set_color_rgba8(red, green, blue, fill.opacity.to_u8()) } let rule = match fill.rule { @@ -674,17 +680,16 @@ fn convert_usvg_fill(fill: &usvg::Fill) -> (sk::Paint<'static>, sk::FillRule) { fn convert_usvg_path(path: &usvg::PathData) -> sk::Path { let mut builder = sk::PathBuilder::new(); - let f = |v: f64| v as f32; for seg in path.iter() { match *seg { usvg::PathSegment::MoveTo { x, y } => { - builder.move_to(f(x), f(y)); + builder.move_to(x as _, y as _); } usvg::PathSegment::LineTo { x, y } => { - builder.line_to(f(x), f(y)); + builder.line_to(x as _, y as _); } usvg::PathSegment::CurveTo { x1, y1, x2, y2, x, y } => { - builder.cubic_to(f(x1), f(y1), f(x2), f(y2), f(x), f(y)); + builder.cubic_to(x1 as _, y1 as _, x2 as _, y2 as _, x as _, y as _); } usvg::PathSegment::ClosePath => { builder.close(); @@ -717,3 +722,15 @@ impl OutlineBuilder for WrappedPathBuilder { self.0.close(); } } + +/// Additional methods for [`Length`]. +trait LengthExt { + /// Convert an em length to a number of points. + fn to_f32(self) -> f32; +} + +impl LengthExt for Length { + fn to_f32(self) -> f32 { + self.to_pt() as f32 + } +} |
