From 397f4871ff5b14998333e0dad3a720ac6ba7047e Mon Sep 17 00:00:00 2001 From: Martin Slachta Date: Sun, 4 May 2025 16:21:19 +0200 Subject: [PATCH] SVG Export: Removed groups around every single element to reduce size. Every element like path had group around it that defined it's transform. These changes move the transformations to the element itself. This reduces the overall size of the exported SVG. Added new SVG path builder using relative coordinates. The previous with global coordinates is still used for glyph paths. Using relative coordinates allows to transform the entire element without changing the entire path. --- crates/typst-svg/src/image.rs | 11 ++- crates/typst-svg/src/lib.rs | 119 ++++++++++++++++++++++------- crates/typst-svg/src/paint.rs | 5 +- crates/typst-svg/src/shape.rs | 20 ++--- crates/typst-svg/src/text.rs | 13 +++- tests/ref/svg-relative-paths.png | Bin 0 -> 246 bytes tests/suite/svg/relative-paths.typ | 13 ++++ 7 files changed, 137 insertions(+), 44 deletions(-) create mode 100644 tests/ref/svg-relative-paths.png create mode 100644 tests/suite/svg/relative-paths.typ 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 0000000000000000000000000000000000000000..9c2f3d49218e0fe738f06980bebe192c7bc7769e GIT binary patch literal 246 zcmV0{{R3MxSzZ0000gP)t-s|NsB- z@$v8P@9ysI?Ck82kdTj$kAQ%He}8{XO-)QpOiN2kN=iyVKtQ`4hO7Vp0EbCLK~#9! z?bb00!cY)|;Q^Jj9>L0+cmi7stv$e0!Yik-^#-0PIZhxa2n$Id{9p09sUEvK0_L%8 ztEL;0d|NTxBz1&$AMR~`nrf0HOep}4G)I~+M;ZVCx*%PUFkKLs;b!};n&U6Y`=XN5 wb9z`qYlb)Ys>9#*