diff --git a/Cargo.toml b/Cargo.toml index c7fa703c4..c31d1d89f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 0fcc4bfc7..e224438ac 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -147,7 +147,7 @@ impl From for Value { impl From 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 05f73e52e..b807d0598 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 a geometrical shape into the content stream. + fn write_shape(&mut self, x: f32, y: f32, shape: &Shape) { + if shape.fill.is_none() && shape.stroke.is_none() { + return; + } + + 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.content.rect(x, y - h, w, h); + } + } + Geometry::Ellipse(size) => { + let approx = geom::Path::ellipse(size); + self.write_path(x, y, &approx); + } + 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 + dx, y - dy); + } + Geometry::Path(ref 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 a geometrical shape into the content stream. - fn write_geometry(&mut self, x: f32, y: f32, geometry: &Geometry, paint: Paint) { - self.content.save_state(); - - match *geometry { - Geometry::Rect(Size { w, h }) => { - let w = w.to_f32(); - let h = 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); - } - Geometry::Line(target, thickness) => { - self.write_stroke(paint, thickness.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(); - } - Geometry::Path(ref path) => { - self.write_fill(paint); - self.write_filled_path(x, y, path) - } - } - - 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 fb93d5c96..5afd1acad 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 9feb69595..9f1b1c284 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), @@ -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, + /// The shape's border stroke. + pub stroke: Option, +} + +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 74d7d1470..66bfb17c0 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 From for Paint +where + T: Into, +{ + 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 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 bc0d3f2d4..39e753122 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 2722fd687..1f8c051f2 100644 --- a/src/library/deco.rs +++ b/src/library/deco.rs @@ -17,20 +17,13 @@ pub fn overline(_: &mut EvalContext, args: &mut Args) -> TypResult { } fn line_impl(args: &mut Args, kind: LineKind) -> TypResult { - 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::("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 b760e76a7..20871bd9b 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 { 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 { } 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 d0df5f480..abf927e46 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 { 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 { 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 { 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 { 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, height: Option, - fill: Option, - body: Option