diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/doc.rs | 2 | ||||
| -rw-r--r-- | src/eval/ops.rs | 1 | ||||
| -rw-r--r-- | src/export/pdf/page.rs | 60 | ||||
| -rw-r--r-- | src/export/render.rs | 54 | ||||
| -rw-r--r-- | src/geom/sides.rs | 34 | ||||
| -rw-r--r-- | src/geom/stroke.rs | 285 |
6 files changed, 408 insertions, 28 deletions
@@ -359,6 +359,7 @@ impl Frame { Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { paint: Color::RED.into(), thickness: Abs::pt(1.0), + ..Stroke::default() }), Span::detached(), ), @@ -386,6 +387,7 @@ impl Frame { Geometry::Line(Point::with_x(self.size.x)).stroked(Stroke { paint: Color::GREEN.into(), thickness: Abs::pt(1.0), + ..Stroke::default() }), Span::detached(), ), diff --git a/src/eval/ops.rs b/src/eval/ops.rs index 948243d1..43ee6ceb 100644 --- a/src/eval/ops.rs +++ b/src/eval/ops.rs @@ -108,6 +108,7 @@ pub fn add(lhs: Value, rhs: Value) -> StrResult<Value> { Value::dynamic(PartialStroke { paint: Smart::Custom(color.into()), thickness: Smart::Custom(thickness), + ..PartialStroke::default() }) } diff --git a/src/export/pdf/page.rs b/src/export/pdf/page.rs index 636d42c7..d6ead124 100644 --- a/src/export/pdf/page.rs +++ b/src/export/pdf/page.rs @@ -1,5 +1,7 @@ use ecow::eco_format; -use pdf_writer::types::{ActionType, AnnotationType, ColorSpaceOperand}; +use pdf_writer::types::{ + ActionType, AnnotationType, ColorSpaceOperand, LineCapStyle, LineJoinStyle, +}; use pdf_writer::writers::ColorSpace; use pdf_writer::{Content, Filter, Finish, Name, Rect, Ref, Str}; @@ -7,8 +9,8 @@ use super::{deflate, AbsExt, EmExt, PdfContext, RefExt, D65_GRAY, SRGB}; use crate::doc::{Destination, Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::font::Font; use crate::geom::{ - self, Abs, Color, Em, Geometry, Numeric, Paint, Point, Ratio, Shape, Size, Stroke, - Transform, + self, Abs, Color, Em, Geometry, LineCap, LineJoin, Numeric, Paint, Point, Ratio, + Shape, Size, Stroke, Transform, }; use crate::image::Image; @@ -250,8 +252,17 @@ impl PageContext<'_, '_> { fn set_stroke(&mut self, stroke: &Stroke) { if self.state.stroke.as_ref() != Some(stroke) { + let Stroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = stroke; + let f = |c| c as f32 / 255.0; - let Paint::Solid(color) = stroke.paint; + let Paint::Solid(color) = paint; match color { Color::Luma(c) => { self.set_stroke_color_space(D65_GRAY); @@ -267,7 +278,26 @@ impl PageContext<'_, '_> { } } - self.content.set_line_width(stroke.thickness.to_f32()); + self.content.set_line_width(thickness.to_f32()); + if self.state.stroke.as_ref().map(|s| &s.line_cap) != Some(line_cap) { + self.content.set_line_cap(line_cap.into()); + } + if self.state.stroke.as_ref().map(|s| &s.line_join) != Some(line_join) { + self.content.set_line_join(line_join.into()); + } + if self.state.stroke.as_ref().map(|s| &s.dash_pattern) != Some(dash_pattern) { + if let Some(pattern) = dash_pattern { + self.content.set_dash_pattern( + pattern.array.iter().map(|l| l.to_f32()), + pattern.phase.to_f32(), + ); + } else { + self.content.set_dash_pattern([], 0.0); + } + } + if self.state.stroke.as_ref().map(|s| &s.miter_limit) != Some(miter_limit) { + self.content.set_miter_limit(miter_limit.0 as f32); + } self.state.stroke = Some(stroke.clone()); } } @@ -486,3 +516,23 @@ fn write_link(ctx: &mut PageContext, pos: Point, dest: &Destination, size: Size) ctx.links.push((dest.clone(), rect)); } + +impl From<&LineCap> for LineCapStyle { + fn from(line_cap: &LineCap) -> Self { + match line_cap { + LineCap::Butt => LineCapStyle::ButtCap, + LineCap::Round => LineCapStyle::RoundCap, + LineCap::Square => LineCapStyle::ProjectingSquareCap, + } + } +} + +impl From<&LineJoin> for LineJoinStyle { + fn from(line_join: &LineJoin) -> Self { + match line_join { + LineJoin::Miter => LineJoinStyle::MiterJoin, + LineJoin::Round => LineJoinStyle::RoundJoin, + LineJoin::Bevel => LineJoinStyle::BevelJoin, + } + } +} diff --git a/src/export/render.rs b/src/export/render.rs index 8cee3aa6..f3c72ba0 100644 --- a/src/export/render.rs +++ b/src/export/render.rs @@ -11,7 +11,8 @@ use usvg::{FitTo, NodeExt}; use crate::doc::{Frame, FrameItem, GroupItem, Meta, TextItem}; use crate::geom::{ - self, Abs, Color, Geometry, Paint, PathItem, Shape, Size, Stroke, Transform, + self, Abs, Color, Geometry, LineCap, LineJoin, Paint, PathItem, Shape, Size, Stroke, + Transform, }; use crate::image::{DecodedImage, Image}; @@ -392,9 +393,36 @@ fn render_shape( canvas.fill_path(&path, &paint, rule, ts, mask); } - if let Some(Stroke { paint, thickness }) = &shape.stroke { + if let Some(Stroke { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + }) = &shape.stroke + { + let dash = dash_pattern.as_ref().and_then(|pattern| { + // tiny-skia only allows dash patterns with an even number of elements, + // while pdf allows any number. + let len = if pattern.array.len() % 2 == 1 { + pattern.array.len() * 2 + } else { + pattern.array.len() + }; + let dash_array = + pattern.array.iter().map(|l| l.to_f32()).cycle().take(len).collect(); + + sk::StrokeDash::new(dash_array, pattern.phase.to_f32()) + }); let paint = paint.into(); - let stroke = sk::Stroke { width: thickness.to_f32(), ..Default::default() }; + let stroke = sk::Stroke { + width: thickness.to_f32(), + line_cap: line_cap.into(), + line_join: line_join.into(), + dash, + miter_limit: miter_limit.0 as f32, + }; canvas.stroke_path(&path, &paint, &stroke, ts, mask); } @@ -525,6 +553,26 @@ impl From<Color> for sk::Color { } } +impl From<&LineCap> for sk::LineCap { + fn from(line_cap: &LineCap) -> Self { + match line_cap { + LineCap::Butt => sk::LineCap::Butt, + LineCap::Round => sk::LineCap::Round, + LineCap::Square => sk::LineCap::Square, + } + } +} + +impl From<&LineJoin> for sk::LineJoin { + fn from(line_join: &LineJoin) -> Self { + match line_join { + LineJoin::Miter => sk::LineJoin::Miter, + LineJoin::Round => sk::LineJoin::Round, + LineJoin::Bevel => sk::LineJoin::Bevel, + } + } +} + /// Allows to build tiny-skia paths from glyph outlines. struct WrappedPathBuilder(sk::PathBuilder); diff --git a/src/geom/sides.rs b/src/geom/sides.rs index 25b1fab5..d9a020da 100644 --- a/src/geom/sides.rs +++ b/src/geom/sides.rs @@ -188,22 +188,30 @@ where fn cast(mut value: Value) -> StrResult<Self> { if let Value::Dict(dict) = &mut value { - let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); - - let rest = take("rest")?; - let x = take("x")?.or_else(|| rest.clone()); - let y = take("y")?.or_else(|| rest.clone()); - let sides = Sides { - left: take("left")?.or_else(|| x.clone()), - top: take("top")?.or_else(|| y.clone()), - right: take("right")?.or_else(|| x.clone()), - bottom: take("bottom")?.or_else(|| y.clone()), + let mut try_cast = || -> StrResult<_> { + let mut take = |key| dict.take(key).ok().map(T::cast).transpose(); + + let rest = take("rest")?; + let x = take("x")?.or_else(|| rest.clone()); + let y = take("y")?.or_else(|| rest.clone()); + let sides = Sides { + left: take("left")?.or_else(|| x.clone()), + top: take("top")?.or_else(|| y.clone()), + right: take("right")?.or_else(|| x.clone()), + bottom: take("bottom")?.or_else(|| y.clone()), + }; + + dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + + Ok(sides) }; - dict.finish(&["left", "top", "right", "bottom", "x", "y", "rest"])?; + if let Ok(res) = try_cast() { + return Ok(res); + } + } - Ok(sides) - } else if T::is(&value) { + if T::is(&value) { Ok(Self::splat(Some(T::cast(value)?))) } else { <Self as Cast>::error(value) diff --git a/src/geom/stroke.rs b/src/geom/stroke.rs index 4dba06d9..344da3c5 100644 --- a/src/geom/stroke.rs +++ b/src/geom/stroke.rs @@ -7,6 +7,14 @@ pub struct Stroke { pub paint: Paint, /// The stroke's thickness. pub thickness: Abs, + /// The stroke's line cap. + pub line_cap: LineCap, + /// The stroke's line join. + pub line_join: LineJoin, + /// The stroke's line dash pattern. + pub dash_pattern: Option<DashPattern<Abs, Abs>>, + /// The miter limit. Defaults to 4.0, same as `tiny-skia`. + pub miter_limit: Scalar, } impl Default for Stroke { @@ -14,6 +22,10 @@ impl Default for Stroke { Self { paint: Paint::Solid(Color::BLACK), thickness: Abs::pt(1.0), + line_cap: LineCap::Butt, + line_join: LineJoin::Miter, + dash_pattern: None, + miter_limit: Scalar(4.0), } } } @@ -29,14 +41,41 @@ pub struct PartialStroke<T = Length> { pub paint: Smart<Paint>, /// The stroke's thickness. pub thickness: Smart<T>, + /// The stroke's line cap. + pub line_cap: Smart<LineCap>, + /// The stroke's line join. + pub line_join: Smart<LineJoin>, + /// The stroke's line dash pattern. + pub dash_pattern: Smart<Option<DashPattern<T>>>, + /// The miter limit. + pub miter_limit: Smart<Scalar>, } impl PartialStroke<Abs> { /// Unpack the stroke, filling missing fields from the `default`. pub fn unwrap_or(self, default: Stroke) -> Stroke { + let thickness = self.thickness.unwrap_or(default.thickness); + let dash_pattern = self + .dash_pattern + .map(|pattern| { + pattern.map(|pattern| DashPattern { + array: pattern + .array + .into_iter() + .map(|l| l.finish(thickness)) + .collect(), + phase: pattern.phase, + }) + }) + .unwrap_or(default.dash_pattern); + Stroke { paint: self.paint.unwrap_or(default.paint), - thickness: self.thickness.unwrap_or(default.thickness), + thickness, + line_cap: self.line_cap.unwrap_or(default.line_cap), + line_join: self.line_join.unwrap_or(default.line_join), + dash_pattern, + miter_limit: self.miter_limit.unwrap_or(default.miter_limit), } } @@ -48,13 +87,205 @@ impl PartialStroke<Abs> { impl<T: Debug> Debug for PartialStroke<T> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { - match (&self.paint, &self.thickness) { - (Smart::Custom(paint), Smart::Custom(thickness)) => { - write!(f, "{thickness:?} + {paint:?}") + let Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit, + } = &self; + if line_cap.is_auto() + && line_join.is_auto() + && dash_pattern.is_auto() + && miter_limit.is_auto() + { + match (&self.paint, &self.thickness) { + (Smart::Custom(paint), Smart::Custom(thickness)) => { + write!(f, "{thickness:?} + {paint:?}") + } + (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), + (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), + (Smart::Auto, Smart::Auto) => f.pad("<stroke>"), + } + } else { + write!(f, "(")?; + let mut sep = ""; + if let Smart::Custom(paint) = &paint { + write!(f, "{}color: {:?}", sep, paint)?; + sep = ", "; + } + if let Smart::Custom(thickness) = &thickness { + write!(f, "{}thickness: {:?}", sep, thickness)?; + sep = ", "; + } + if let Smart::Custom(cap) = &line_cap { + write!(f, "{}cap: {:?}", sep, cap)?; + sep = ", "; + } + if let Smart::Custom(join) = &line_join { + write!(f, "{}join: {:?}", sep, join)?; + sep = ", "; + } + if let Smart::Custom(dash) = &dash_pattern { + write!(f, "{}dash: {:?}", sep, dash)?; + sep = ", "; + } + if let Smart::Custom(miter_limit) = &miter_limit { + write!(f, "{}miter-limit: {:?}", sep, miter_limit)?; + } + write!(f, ")")?; + Ok(()) + } + } +} + +/// The line cap of a stroke +#[derive(Cast, Clone, Eq, PartialEq, Hash)] +pub enum LineCap { + Butt, + Round, + Square, +} + +impl Debug for LineCap { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineCap::Butt => write!(f, "\"butt\""), + LineCap::Round => write!(f, "\"round\""), + LineCap::Square => write!(f, "\"square\""), + } + } +} + +/// The line join of a stroke +#[derive(Cast, Clone, Eq, PartialEq, Hash)] +pub enum LineJoin { + Miter, + Round, + Bevel, +} + +impl Debug for LineJoin { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + match self { + LineJoin::Miter => write!(f, "\"miter\""), + LineJoin::Round => write!(f, "\"round\""), + LineJoin::Bevel => write!(f, "\"bevel\""), + } + } +} + +/// A line dash pattern +#[derive(Clone, Eq, PartialEq, Hash)] +pub struct DashPattern<T = Length, DT = DashLength<T>> { + /// The dash array. + pub array: Vec<DT>, + /// The dash phase. + pub phase: T, +} + +impl<T: Debug, DT: Debug> Debug for DashPattern<T, DT> { + fn fmt(&self, f: &mut Formatter) -> fmt::Result { + write!(f, "(array: (")?; + for (i, elem) in self.array.iter().enumerate() { + if i == 0 { + write!(f, "{:?}", elem)?; + } else { + write!(f, ", {:?}", elem)?; } - (Smart::Custom(paint), Smart::Auto) => paint.fmt(f), - (Smart::Auto, Smart::Custom(thickness)) => thickness.fmt(f), - (Smart::Auto, Smart::Auto) => f.pad("<stroke>"), + } + write!(f, "), phase: {:?})", self.phase)?; + Ok(()) + } +} + +impl<T: Default> From<Vec<DashLength<T>>> for DashPattern<T> { + fn from(array: Vec<DashLength<T>>) -> Self { + Self { array, phase: T::default() } + } +} + +/// The length of a dash in a line dash pattern +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum DashLength<T = Length> { + LineWidth, + Length(T), +} + +impl From<Abs> for DashLength { + fn from(l: Abs) -> Self { + DashLength::Length(l.into()) + } +} + +impl<T> DashLength<T> { + fn finish(self, line_width: T) -> T { + match self { + Self::LineWidth => line_width, + Self::Length(l) => l, + } + } +} + +cast_from_value! { + DashLength: "dash length", + "dot" => Self::LineWidth, + l: Length => Self::Length(l), +} + +impl Resolve for DashLength { + type Output = DashLength<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + match self { + Self::LineWidth => DashLength::LineWidth, + Self::Length(l) => DashLength::Length(l.resolve(styles)), + } + } +} + +cast_from_value! { + DashPattern: "dash pattern", + // Use same names as tikz: + // https://tex.stackexchange.com/questions/45275/tikz-get-values-for-predefined-dash-patterns + "solid" => Vec::new().into(), + "dotted" => vec![DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dotted" => vec![DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dotted" => vec![DashLength::LineWidth, Abs::pt(4.0).into()].into(), + "dashed" => vec![Abs::pt(3.0).into(), Abs::pt(3.0).into()].into(), + "densely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into()].into(), + "loosely-dashed" => vec![Abs::pt(3.0).into(), Abs::pt(6.0).into()].into(), + "dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(2.0).into(), DashLength::LineWidth, Abs::pt(2.0).into()].into(), + "densely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(1.0).into(), DashLength::LineWidth, Abs::pt(1.0).into()].into(), + "loosely-dashdotted" => vec![Abs::pt(3.0).into(), Abs::pt(4.0).into(), DashLength::LineWidth, Abs::pt(4.0).into()].into(), + array: Vec<DashLength> => { + Self { + array, + phase: Length::zero(), + } + }, + mut dict: Dict => { + let array: Vec<DashLength> = dict.take("array")?.cast()?; + let phase = dict.take("phase").ok().map(Length::cast) + .transpose()?.unwrap_or(Length::zero()); + + dict.finish(&["array", "phase"])?; + + Self { + array, + phase, + } + }, +} + +impl Resolve for DashPattern { + type Output = DashPattern<Abs>; + + fn resolve(self, styles: StyleChain) -> Self::Output { + DashPattern { + array: self.array.into_iter().map(|l| l.resolve(styles)).collect(), + phase: self.phase.resolve(styles), } } } @@ -64,10 +295,42 @@ cast_from_value! { thickness: Length => Self { paint: Smart::Auto, thickness: Smart::Custom(thickness), + line_cap: Smart::Auto, + line_join: Smart::Auto, + dash_pattern: Smart::Auto, + miter_limit: Smart::Auto, }, color: Color => Self { paint: Smart::Custom(color.into()), thickness: Smart::Auto, + line_cap: Smart::Auto, + line_join: Smart::Auto, + dash_pattern: Smart::Auto, + miter_limit: Smart::Auto, + }, + mut dict: Dict => { + fn take<T: Cast<Value>>(dict: &mut Dict, key: &str) -> StrResult<Smart<T>> { + Ok(dict.take(key).ok().map(T::cast) + .transpose()?.map(Smart::Custom).unwrap_or(Smart::Auto)) + } + + let paint = take::<Paint>(&mut dict, "color")?; + let thickness = take::<Length>(&mut dict, "thickness")?; + let line_cap = take::<LineCap>(&mut dict, "cap")?; + let line_join = take::<LineJoin>(&mut dict, "join")?; + let dash_pattern = take::<Option<DashPattern>>(&mut dict, "dash")?; + let miter_limit = take::<f64>(&mut dict, "miter-limit")?; + + dict.finish(&["color", "thickness", "cap", "join", "dash", "miter-limit"])?; + + Self { + paint, + thickness, + line_cap, + line_join, + dash_pattern, + miter_limit: miter_limit.map(Scalar), + } }, } @@ -78,6 +341,10 @@ impl Resolve for PartialStroke { PartialStroke { paint: self.paint, thickness: self.thickness.resolve(styles), + line_cap: self.line_cap, + line_join: self.line_join, + dash_pattern: self.dash_pattern.resolve(styles), + miter_limit: self.miter_limit, } } } @@ -89,6 +356,10 @@ impl Fold for PartialStroke<Abs> { Self { paint: self.paint.or(outer.paint), thickness: self.thickness.or(outer.thickness), + line_cap: self.line_cap.or(outer.line_cap), + line_join: self.line_join.or(outer.line_join), + dash_pattern: self.dash_pattern.or(outer.dash_pattern), + miter_limit: self.miter_limit, } } } |
