diff --git a/crates/typst-svg/src/image.rs b/crates/typst-svg/src/image.rs index d74432026..24eadbd01 100644 --- a/crates/typst-svg/src/image.rs +++ b/crates/typst-svg/src/image.rs @@ -2,7 +2,7 @@ use base64::Engine; use ecow::{eco_format, EcoString}; use image::{codecs::png::PngEncoder, ImageEncoder}; use typst_library::foundations::Smart; -use typst_library::layout::{Abs, Axes}; +use typst_library::layout::{Abs, Axes, Transform}; use typst_library::visualize::{ ExchangeFormat, Image, ImageKind, ImageScaling, RasterFormat, }; @@ -11,10 +11,17 @@ use crate::SVGRenderer; impl SVGRenderer { /// Render an image element. - pub(super) fn render_image(&mut self, image: &Image, size: &Axes) { + pub(super) fn render_image( + &mut self, + transform: &Transform, + image: &Image, + size: &Axes, + ) { let url = convert_image_to_base64_url(image); self.xml.start_element("image"); self.xml.write_attribute("xlink:href", &url); + self.xml.write_attribute("x", &transform.tx.to_pt()); + self.xml.write_attribute("y", &transform.ty.to_pt()); self.xml.write_attribute("width", &size.x.to_pt()); self.xml.write_attribute("height", &size.y.to_pt()); self.xml.write_attribute("preserveAspectRatio", "none"); diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index f4e81250f..eefdc277e 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -9,7 +9,6 @@ use std::collections::HashMap; use std::fmt::{self, Display, Formatter, Write}; use ecow::EcoString; -use ttf_parser::OutlineBuilder; use typst_library::layout::{ Abs, Frame, FrameItem, FrameKind, GroupItem, Page, PagedDocument, Point, Ratio, Size, Transform, @@ -211,12 +210,6 @@ impl SVGRenderer { continue; } - let x = pos.x.to_pt(); - let y = pos.y.to_pt(); - self.xml.start_element("g"); - self.xml - .write_attribute_fmt("transform", format_args!("translate({x} {y})")); - match item { FrameItem::Group(group) => { self.render_group(state.pre_translate(*pos), group) @@ -227,12 +220,12 @@ impl SVGRenderer { FrameItem::Shape(shape, _) => { self.render_shape(state.pre_translate(*pos), shape) } - FrameItem::Image(image, size, _) => self.render_image(image, size), + FrameItem::Image(image, size, _) => { + self.render_image(&state.pre_translate(*pos).transform, image, size) + } FrameItem::Link(_, _) => unreachable!(), FrameItem::Tag(_) => unreachable!(), }; - - self.xml.end_element(); } self.xml.end_element(); @@ -244,7 +237,7 @@ impl SVGRenderer { let state = match group.frame.kind() { FrameKind::Soft => state.pre_concat(group.transform), FrameKind::Hard => state - .with_transform(Transform::identity()) + .with_transform(Transform::identity().pre_concat(state.transform)) .with_size(group.frame.size()), }; @@ -257,8 +250,9 @@ impl SVGRenderer { if let Some(clip_curve) = &group.clip { let hash = hash128(&group); - let id = - self.clip_paths.insert_with(hash, || shape::convert_curve(clip_curve)); + let id = self + .clip_paths + .insert_with(hash, || shape::convert_curve(&state.transform, clip_curve)); self.xml.write_attribute_fmt("clip-path", format_args!("url(#{id})")); } @@ -375,18 +369,31 @@ impl Display for SvgMatrix { } } -/// A builder for SVG path. -struct SvgPathBuilder(pub EcoString, pub Ratio); +/// A builder for SVG path using relative coordinates. +struct SvgRelativePathBuilder(pub EcoString, pub Ratio, pub Point); -impl SvgPathBuilder { - fn with_scale(scale: Ratio) -> Self { - Self(EcoString::new(), scale) +impl SvgRelativePathBuilder { + fn with_translate(pos: Point) -> Self { + // add initial M node to transform the entire path + Self( + EcoString::from(format!("M {} {}", pos.x.to_pt(), pos.y.to_pt())), + Ratio::one(), + Point::zero(), + ) } fn scale(&self) -> f32 { self.1.get() as f32 } + fn map_x(&self, x: f32) -> f32 { + x * self.scale() - self.2.x.to_pt() as f32 + } + + fn map_y(&self, y: f32) -> f32 { + y * self.scale() - self.2.y.to_pt() as f32 + } + /// Create a rectangle path. The rectangle is created with the top-left /// corner at (0, 0). The width and height are the size of the rectangle. fn rect(&mut self, width: f32, height: f32) { @@ -406,27 +413,85 @@ impl SvgPathBuilder { sweep_flag: u32, pos: (f32, f32), ) { - let scale = self.scale(); + let rx = self.map_x(radius.0); + let ry = self.map_y(radius.1); + let x = self.map_x(pos.0); + let y = self.map_y(pos.1); write!( &mut self.0, - "A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ", - rx = radius.0 * scale, - ry = radius.1 * scale, - x = pos.0 * scale, - y = pos.1 * scale, + "a {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} " ) .unwrap(); } + + fn move_to(&mut self, x: f32, y: f32) { + let scale = self.scale(); + let _x = self.map_x(x); + let _y = self.map_y(y); + if _x != 0.0 || _y != 0.0 { + write!(&mut self.0, "m {x} {y} ").unwrap(); + } + + self.2 = Point::new(Abs::pt((x * scale) as f64), Abs::pt((y * scale) as f64)); + } + + fn line_to(&mut self, x: f32, y: f32) { + let scale = self.scale(); + let _x = self.map_x(x); + let _y = self.map_y(y); + + if _x != 0.0 && _y != 0.0 { + write!(&mut self.0, "l {_x} {_y} ").unwrap(); + } else if _x != 0.0 { + write!(&mut self.0, "h {_x} ").unwrap(); + } else if _y != 0.0 { + write!(&mut self.0, "v {_y} ").unwrap(); + } + + self.2 = Point::new(Abs::pt((x * scale) as f64), Abs::pt((y * scale) as f64)); + } + + fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { + let scale = self.scale(); + let curve = format!( + "c {} {} {} {} {} {} ", + self.map_x(x1), + self.map_y(y1), + self.map_x(x2), + self.map_y(y2), + self.map_x(x), + self.map_y(y) + ); + write!(&mut self.0, "{curve}").unwrap(); + self.2 = Point::new(Abs::pt((x * scale) as f64), Abs::pt((y * scale) as f64)); + } + + fn close(&mut self) { + write!(&mut self.0, "Z ").unwrap(); + } } -impl Default for SvgPathBuilder { +impl Default for SvgRelativePathBuilder { fn default() -> Self { - Self(Default::default(), Ratio::one()) + Self(Default::default(), Ratio::one(), Point::zero()) } } /// A builder for SVG path. This is used to build the path for a glyph. -impl ttf_parser::OutlineBuilder for SvgPathBuilder { +struct SvgGlyphPathBuilder(pub EcoString, pub Ratio); + +impl SvgGlyphPathBuilder { + fn with_scale(scale: Ratio) -> Self { + Self(EcoString::new(), scale) + } + + fn scale(&self) -> f32 { + self.1.get() as f32 + } +} + +/// A builder for SVG path. This is used to build the path for a glyph. +impl ttf_parser::OutlineBuilder for SvgGlyphPathBuilder { fn move_to(&mut self, x: f32, y: f32) { let scale = self.scale(); write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap(); diff --git a/crates/typst-svg/src/paint.rs b/crates/typst-svg/src/paint.rs index 75ab41cdf..847ad5211 100644 --- a/crates/typst-svg/src/paint.rs +++ b/crates/typst-svg/src/paint.rs @@ -1,14 +1,13 @@ use std::f32::consts::TAU; use ecow::{eco_format, EcoString}; -use ttf_parser::OutlineBuilder; use typst_library::foundations::Repr; use typst_library::layout::{Angle, Axes, Frame, Quadrant, Ratio, Size, Transform}; use typst_library::visualize::{Color, FillRule, Gradient, Paint, RatioOrAngle, Tiling}; use typst_utils::hash128; use xmlwriter::XmlWriter; -use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder}; +use crate::{Id, SVGRenderer, State, SvgMatrix, SvgRelativePathBuilder}; /// The number of segments in a conic gradient. /// This is a heuristic value that seems to work well. @@ -185,7 +184,7 @@ impl SVGRenderer { let theta2 = dtheta * (i + 1) as f32; // Create the path for the segment. - let mut builder = SvgPathBuilder::default(); + let mut builder = SvgRelativePathBuilder::default(); builder.move_to( correct_tiling_pos(center.0), correct_tiling_pos(center.1), diff --git a/crates/typst-svg/src/shape.rs b/crates/typst-svg/src/shape.rs index 981f86a39..6173983e0 100644 --- a/crates/typst-svg/src/shape.rs +++ b/crates/typst-svg/src/shape.rs @@ -1,12 +1,11 @@ use ecow::EcoString; -use ttf_parser::OutlineBuilder; -use typst_library::layout::{Abs, Ratio, Size, Transform}; +use typst_library::layout::{Abs, Point, Ratio, Size, Transform}; use typst_library::visualize::{ Curve, CurveItem, FixedStroke, Geometry, LineCap, LineJoin, Paint, RelativeTo, Shape, }; use crate::paint::ColorEncode; -use crate::{SVGRenderer, State, SvgPathBuilder}; +use crate::{SVGRenderer, State, SvgRelativePathBuilder}; impl SVGRenderer { /// Render a shape element. @@ -33,7 +32,7 @@ impl SVGRenderer { ); } - let path = convert_geometry_to_path(&shape.geometry); + let path = convert_geometry_to_path(&state.transform, &shape.geometry); self.xml.write_attribute("d", &path); self.xml.end_element(); } @@ -154,8 +153,10 @@ impl SVGRenderer { /// Convert a geometry to an SVG path. #[comemo::memoize] -fn convert_geometry_to_path(geometry: &Geometry) -> EcoString { - let mut builder = SvgPathBuilder::default(); +fn convert_geometry_to_path(transform: &Transform, geometry: &Geometry) -> EcoString { + let mut builder = + SvgRelativePathBuilder::with_translate(Point::new(transform.tx, transform.ty)); + match geometry { Geometry::Line(t) => { builder.move_to(0.0, 0.0); @@ -166,13 +167,14 @@ fn convert_geometry_to_path(geometry: &Geometry) -> EcoString { let y = rect.y.to_pt() as f32; builder.rect(x, y); } - Geometry::Curve(p) => return convert_curve(p), + Geometry::Curve(p) => return convert_curve(transform, p), }; builder.0 } -pub fn convert_curve(curve: &Curve) -> EcoString { - let mut builder = SvgPathBuilder::default(); +pub fn convert_curve(transform: &Transform, curve: &Curve) -> EcoString { + let mut builder = + SvgRelativePathBuilder::with_translate(Point::new(transform.tx, transform.ty)); for item in &curve.0 { match item { CurveItem::Move(m) => builder.move_to(m.x.to_pt() as f32, m.y.to_pt() as f32), diff --git a/crates/typst-svg/src/text.rs b/crates/typst-svg/src/text.rs index e6620a59e..d0758a62f 100644 --- a/crates/typst-svg/src/text.rs +++ b/crates/typst-svg/src/text.rs @@ -11,7 +11,7 @@ use typst_library::visualize::{ }; use typst_utils::hash128; -use crate::{SVGRenderer, State, SvgMatrix, SvgPathBuilder}; +use crate::{SVGRenderer, State, SvgGlyphPathBuilder, SvgMatrix}; impl SVGRenderer { /// Render a text item. The text is rendered as a group of glyphs. We will @@ -22,7 +22,14 @@ impl SVGRenderer { self.xml.start_element("g"); self.xml.write_attribute("class", "typst-text"); - self.xml.write_attribute("transform", "scale(1, -1)"); + self.xml.write_attribute( + "transform", + &format!( + "scale(1, -1) translate({} {})", + state.transform.tx.to_pt(), + -state.transform.ty.to_pt() + ), + ); let mut x: f64 = 0.0; for glyph in &text.glyphs { @@ -234,7 +241,7 @@ fn convert_outline_glyph_to_path( id: GlyphId, scale: Ratio, ) -> Option { - let mut builder = SvgPathBuilder::with_scale(scale); + let mut builder = SvgGlyphPathBuilder::with_scale(scale); font.ttf().outline_glyph(id, &mut builder)?; Some(builder.0) } diff --git a/tests/ref/svg-relative-paths.png b/tests/ref/svg-relative-paths.png new file mode 100644 index 000000000..9c2f3d492 Binary files /dev/null and b/tests/ref/svg-relative-paths.png differ diff --git a/tests/suite/svg/relative-paths.typ b/tests/suite/svg/relative-paths.typ new file mode 100644 index 000000000..88478fc9d --- /dev/null +++ b/tests/suite/svg/relative-paths.typ @@ -0,0 +1,13 @@ +--- svg-relative-paths --- +#block[ + #rect(width: 10pt, height: 10pt) + #block(inset: 10pt)[ + #rect(width: 10pt, height: 10pt) + #block(inset: 10pt)[ + #block(inset: 10pt)[ + #rect(width: 10pt, height: 10pt) + #rect(width: 10pt, height: 10pt, radius: 2pt) + ] + ] + ] +]