diff options
Diffstat (limited to 'crates/typst-svg/src/lib.rs')
| -rw-r--r-- | crates/typst-svg/src/lib.rs | 296 |
1 files changed, 234 insertions, 62 deletions
diff --git a/crates/typst-svg/src/lib.rs b/crates/typst-svg/src/lib.rs index 7d3a773d..205cff14 100644 --- a/crates/typst-svg/src/lib.rs +++ b/crates/typst-svg/src/lib.rs @@ -14,9 +14,8 @@ use typst::layout::{ use typst::text::{Font, TextItem}; use typst::util::hash128; use typst::visualize::{ - Color, FixedStroke, Geometry, Gradient, GradientRelative, Image, ImageFormat, - LineCap, LineJoin, Paint, Path, PathItem, RasterFormat, RatioOrAngle, Shape, - VectorFormat, + Color, FixedStroke, Geometry, Gradient, Image, ImageFormat, LineCap, LineJoin, Paint, + Path, PathItem, Pattern, RasterFormat, RatioOrAngle, RelativeTo, Shape, VectorFormat, }; use xmlwriter::XmlWriter; @@ -77,6 +76,12 @@ struct SVGRenderer { /// different transforms. Therefore this allows us to reuse the same gradient /// multiple times. gradient_refs: Deduplicator<GradientRef>, + /// Deduplicated patterns with transform matrices. They use a reference + /// (`href`) to a "source" pattern instead of being defined inline. + /// This saves a lot of space since patterns are often reused but with + /// different transforms. Therefore this allows us to reuse the same gradient + /// multiple times. + pattern_refs: Deduplicator<PatternRef>, /// These are the actual gradients being written in the SVG file. /// These gradients are deduplicated because they do not contain the transform /// matrix, allowing them to be reused across multiple invocations. @@ -84,6 +89,12 @@ struct SVGRenderer { /// The `Ratio` is the aspect ratio of the gradient, this is used to correct /// the angle of the gradient. gradients: Deduplicator<(Gradient, Ratio)>, + /// These are the actual patterns being written in the SVG file. + /// These patterns are deduplicated because they do not contain the transform + /// matrix, allowing them to be reused across multiple invocations. + /// + /// The `String` is the rendered pattern frame. + patterns: Deduplicator<Pattern>, /// These are the gradients that compose a conic gradient. conic_subgradients: Deduplicator<SVGSubGradient>, } @@ -141,6 +152,20 @@ struct GradientRef { transform: Transform, } +/// A reference to a deduplicated pattern, with a transform matrix. +/// +/// Allows patterns to be reused across multiple invocations, +/// simply by changing the transform matrix. +#[derive(Hash)] +struct PatternRef { + /// The ID of the deduplicated gradient + id: Id, + /// The transform matrix to apply to the pattern. + transform: Transform, + /// The ratio of the size of the cell to the size of the filled area. + ratio: Axes<Ratio>, +} + /// A subgradient for conic gradients. #[derive(Hash)] struct SVGSubGradient { @@ -199,6 +224,8 @@ impl SVGRenderer { gradient_refs: Deduplicator::new('g'), gradients: Deduplicator::new('f'), conic_subgradients: Deduplicator::new('s'), + pattern_refs: Deduplicator::new('p'), + patterns: Deduplicator::new('t'), } } @@ -219,6 +246,20 @@ impl SVGRenderer { self.xml.write_attribute("xmlns:h5", "http://www.w3.org/1999/xhtml"); } + /// Render a frame to a string. + fn render_pattern_frame( + &mut self, + state: State, + ts: Transform, + frame: &Frame, + ) -> String { + let mut xml = XmlWriter::new(xmlwriter::Options::default()); + std::mem::swap(&mut self.xml, &mut xml); + self.render_frame(state, ts, frame); + std::mem::swap(&mut self.xml, &mut xml); + xml.end_document() + } + /// Render a frame with the given transform. fn render_frame(&mut self, state: State, ts: Transform, frame: &Frame) { self.xml.start_element("g"); @@ -286,37 +327,27 @@ impl SVGRenderer { /// of them works, we will skip the text. fn render_text(&mut self, state: State, text: &TextItem) { let scale: f64 = text.size.to_pt() / text.font.units_per_em(); - let inv_scale: f64 = text.font.units_per_em() / text.size.to_pt(); self.xml.start_element("g"); self.xml.write_attribute("class", "typst-text"); - self.xml.write_attribute_fmt( - "transform", - format_args!("scale({} {})", scale, -scale), - ); + self.xml.write_attribute("transform", "scale(1, -1)"); let mut x: f64 = 0.0; for glyph in &text.glyphs { let id = GlyphId(glyph.id); let offset = x + glyph.x_offset.at(text.size).to_pt(); - self.render_svg_glyph(text, id, offset, inv_scale) - .or_else(|| self.render_bitmap_glyph(text, id, offset, inv_scale)) + self.render_svg_glyph(text, id, offset, scale) + .or_else(|| self.render_bitmap_glyph(text, id, offset)) .or_else(|| { self.render_outline_glyph( state - .pre_concat(Transform::scale( - Ratio::new(scale), - Ratio::new(-scale), - )) - .pre_translate(Point::new( - Abs::pt(offset / scale), - Abs::zero(), - )), + .pre_concat(Transform::scale(Ratio::one(), -Ratio::one())) + .pre_translate(Point::new(Abs::pt(offset), Abs::zero())), text, id, offset, - inv_scale, + scale, ) }); @@ -332,7 +363,7 @@ impl SVGRenderer { text: &TextItem, id: GlyphId, x_offset: f64, - inv_scale: f64, + scale: f64, ) -> Option<()> { let data_url = convert_svg_glyph_to_base64_url(&text.font, id)?; let upem = Abs::raw(text.font.units_per_em()); @@ -344,13 +375,12 @@ impl SVGRenderer { width: upem.to_pt(), height: upem.to_pt(), ts: Transform::translate(Abs::zero(), Abs::pt(-origin_ascender)) - .post_concat(Transform::scale(Ratio::new(1.0), Ratio::new(-1.0))), + .post_concat(Transform::scale(Ratio::new(scale), Ratio::new(-scale))), }); self.xml.start_element("use"); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); - self.xml - .write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale)); + self.xml.write_attribute("x", &x_offset); self.xml.end_element(); Some(()) @@ -362,7 +392,6 @@ impl SVGRenderer { text: &TextItem, id: GlyphId, x_offset: f64, - inv_scale: f64, ) -> Option<()> { let (image, bitmap_x_offset, bitmap_y_offset) = convert_bitmap_glyph_to_image(&text.font, id)?; @@ -390,11 +419,7 @@ impl SVGRenderer { self.xml.write_attribute("x", &(x_offset / scale_factor)); self.xml.write_attribute_fmt( "transform", - format_args!( - "scale({} -{})", - inv_scale * scale_factor, - inv_scale * scale_factor, - ), + format_args!("scale({scale_factor} -{scale_factor})",), ); self.xml.end_element(); @@ -408,19 +433,23 @@ impl SVGRenderer { text: &TextItem, glyph_id: GlyphId, x_offset: f64, - inv_scale: f64, + scale: f64, ) -> Option<()> { - let path = convert_outline_glyph_to_path(&text.font, glyph_id)?; - let hash = hash128(&(&text.font, glyph_id)); + let scale = Ratio::new(scale); + let path = convert_outline_glyph_to_path(&text.font, glyph_id, scale)?; + let hash = hash128(&(&text.font, glyph_id, scale)); let id = self.glyphs.insert_with(hash, || RenderedGlyph::Path(path)); + let glyph_size = text.font.ttf().glyph_bounding_box(glyph_id)?; + let width = glyph_size.width() as f64 * scale.get(); + let height = glyph_size.height() as f64 * scale.get(); + self.xml.start_element("use"); self.xml.write_attribute_fmt("xlink:href", format_args!("#{id}")); - self.xml - .write_attribute_fmt("x", format_args!("{}", x_offset * inv_scale)); + self.xml.write_attribute_fmt("x", format_args!("{}", x_offset)); self.write_fill( &text.fill, - state.size, + Size::new(Abs::pt(width), Abs::pt(height)), self.text_paint_transform(state, &text.fill), ); self.xml.end_element(); @@ -429,17 +458,20 @@ impl SVGRenderer { } fn text_paint_transform(&self, state: State, paint: &Paint) -> Transform { - let Paint::Gradient(gradient) = paint else { - return Transform::identity(); - }; - - match gradient.unwrap_relative(true) { - GradientRelative::Self_ => Transform::scale(Ratio::one(), Ratio::one()), - GradientRelative::Parent => Transform::scale( - Ratio::new(state.size.x.to_pt()), - Ratio::new(state.size.y.to_pt()), - ) - .post_concat(state.transform.invert().unwrap()), + match paint { + Paint::Solid(_) => Transform::identity(), + Paint::Gradient(gradient) => match gradient.unwrap_relative(true) { + RelativeTo::Self_ => Transform::identity(), + RelativeTo::Parent => Transform::scale( + Ratio::new(state.size.x.to_pt()), + Ratio::new(state.size.y.to_pt()), + ) + .post_concat(state.transform.invert().unwrap()), + }, + Paint::Pattern(pattern) => match pattern.unwrap_relative(true) { + RelativeTo::Self_ => Transform::identity(), + RelativeTo::Parent => state.transform.invert().unwrap(), + }, } } @@ -490,16 +522,21 @@ impl SVGRenderer { if let Paint::Gradient(gradient) = paint { match gradient.unwrap_relative(false) { - GradientRelative::Self_ => Transform::scale( + RelativeTo::Self_ => Transform::scale( Ratio::new(shape_size.x.to_pt()), Ratio::new(shape_size.y.to_pt()), ), - GradientRelative::Parent => Transform::scale( + RelativeTo::Parent => Transform::scale( Ratio::new(state.size.x.to_pt()), Ratio::new(state.size.y.to_pt()), ) .post_concat(state.transform.invert().unwrap()), } + } else if let Paint::Pattern(pattern) = paint { + match pattern.unwrap_relative(false) { + RelativeTo::Self_ => Transform::identity(), + RelativeTo::Parent => state.transform.invert().unwrap(), + } } else { Transform::identity() } @@ -519,8 +556,8 @@ impl SVGRenderer { if let Paint::Gradient(gradient) = paint { match gradient.unwrap_relative(false) { - GradientRelative::Self_ => shape_size, - GradientRelative::Parent => state.size, + RelativeTo::Self_ => shape_size, + RelativeTo::Parent => state.size, } } else { shape_size @@ -535,6 +572,10 @@ impl SVGRenderer { let id = self.push_gradient(gradient, size, ts); self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); } + Paint::Pattern(pattern) => { + let id = self.push_pattern(pattern, size, ts); + self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); + } } } @@ -564,6 +605,29 @@ impl SVGRenderer { }) } + fn push_pattern(&mut self, pattern: &Pattern, size: Size, ts: Transform) -> Id { + let pattern_size = pattern.size_abs() + pattern.spacing_abs(); + // Unfortunately due to a limitation of `xmlwriter`, we need to + // render the frame twice: once to allocate all of the resources + // that it needs and once to actually render it. + self.render_pattern_frame( + State::new(pattern_size, Transform::identity()), + Transform::identity(), + pattern.frame(), + ); + + let pattern_id = self.patterns.insert_with(hash128(pattern), || pattern.clone()); + self.pattern_refs + .insert_with(hash128(&(pattern_id, ts)), || PatternRef { + id: pattern_id, + transform: ts, + ratio: Axes::new( + Ratio::new(pattern_size.x.to_pt() / size.x.to_pt()), + Ratio::new(pattern_size.y.to_pt() / size.y.to_pt()), + ), + }) + } + /// Write a stroke attribute. fn write_stroke( &mut self, @@ -577,6 +641,10 @@ impl SVGRenderer { let id = self.push_gradient(gradient, size, fill_transform); self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})")); } + Paint::Pattern(pattern) => { + let id = self.push_pattern(pattern, size, fill_transform); + self.xml.write_attribute_fmt("stroke", format_args!("url(#{id})")); + } } self.xml.write_attribute("stroke-width", &stroke.thickness.to_pt()); @@ -630,6 +698,8 @@ impl SVGRenderer { self.write_gradients(); self.write_gradient_refs(); self.write_subgradients(); + self.write_patterns(); + self.write_pattern_refs(); self.xml.end_document() } @@ -948,12 +1018,78 @@ impl SVGRenderer { self.xml.end_element(); } + + /// Write the raw gradients (without transform) to the SVG file. + fn write_patterns(&mut self) { + if self.patterns.is_empty() { + return; + } + + self.xml.start_element("defs"); + self.xml.write_attribute("id", "patterns"); + + for (id, pattern) in + self.patterns.iter().map(|(i, p)| (i, p.clone())).collect::<Vec<_>>() + { + let size = pattern.size_abs() + pattern.spacing_abs(); + self.xml.start_element("pattern"); + self.xml.write_attribute("id", &id); + self.xml.write_attribute("width", &size.x.to_pt()); + self.xml.write_attribute("height", &size.y.to_pt()); + self.xml.write_attribute("patternUnits", "userSpaceOnUse"); + self.xml.write_attribute_fmt( + "viewBox", + format_args!("0 0 {:.3} {:.3}", size.x.to_pt(), size.y.to_pt()), + ); + + // Render the frame. + let state = State::new(size, Transform::identity()); + let ts = Transform::identity(); + self.render_frame(state, ts, pattern.frame()); + + self.xml.end_element(); + } + + self.xml.end_element() + } + + /// Writes the references to the deduplicated patterns for each usage site. + fn write_pattern_refs(&mut self) { + if self.pattern_refs.is_empty() { + return; + } + + self.xml.start_element("defs"); + self.xml.write_attribute("id", "pattern-refs"); + for (id, pattern_ref) in self.pattern_refs.iter() { + self.xml.start_element("pattern"); + self.xml + .write_attribute("patternTransform", &SvgMatrix(pattern_ref.transform)); + + self.xml.write_attribute("id", &id); + + // Writing the href attribute to the "reference" pattern. + self.xml + .write_attribute_fmt("href", format_args!("#{}", pattern_ref.id)); + + // Also writing the xlink:href attribute for compatibility. + self.xml + .write_attribute_fmt("xlink:href", format_args!("#{}", pattern_ref.id)); + self.xml.end_element(); + } + + self.xml.end_element(); + } } /// Convert an outline glyph to an SVG path. #[comemo::memoize] -fn convert_outline_glyph_to_path(font: &Font, id: GlyphId) -> Option<EcoString> { - let mut builder = SvgPathBuilder::default(); +fn convert_outline_glyph_to_path( + font: &Font, + id: GlyphId, + scale: Ratio, +) -> Option<EcoString> { + let mut builder = SvgPathBuilder::with_scale(scale); font.ttf().outline_glyph(id, &mut builder)?; Some(builder.0) } @@ -1170,10 +1306,17 @@ impl Display for SvgMatrix { } /// A builder for SVG path. -#[derive(Default)] -struct SvgPathBuilder(pub EcoString); +struct SvgPathBuilder(pub EcoString, pub Ratio); impl SvgPathBuilder { + fn with_scale(scale: Ratio) -> Self { + Self(EcoString::new(), scale) + } + + fn scale(&self) -> f32 { + self.1.get() 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) { @@ -1193,34 +1336,63 @@ impl SvgPathBuilder { sweep_flag: u32, pos: (f32, f32), ) { + let scale = self.scale(); write!( &mut self.0, "A {rx} {ry} {x_axis_rot} {large_arc_flag} {sweep_flag} {x} {y} ", - rx = radius.0, - ry = radius.1, - x = pos.0, - y = pos.1, + rx = radius.0 * scale, + ry = radius.1 * scale, + x = pos.0 * scale, + y = pos.1 * scale, ) .unwrap(); } } +impl Default for SvgPathBuilder { + fn default() -> Self { + Self(Default::default(), Ratio::one()) + } +} + /// A builder for SVG path. This is used to build the path for a glyph. impl ttf_parser::OutlineBuilder for SvgPathBuilder { fn move_to(&mut self, x: f32, y: f32) { - write!(&mut self.0, "M {} {} ", x, y).unwrap(); + let scale = self.scale(); + write!(&mut self.0, "M {} {} ", x * scale, y * scale).unwrap(); } fn line_to(&mut self, x: f32, y: f32) { - write!(&mut self.0, "L {} {} ", x, y).unwrap(); + let scale = self.scale(); + write!(&mut self.0, "L {} {} ", x * scale, y * scale).unwrap(); } fn quad_to(&mut self, x1: f32, y1: f32, x: f32, y: f32) { - write!(&mut self.0, "Q {} {} {} {} ", x1, y1, x, y).unwrap(); + let scale = self.scale(); + write!( + &mut self.0, + "Q {} {} {} {} ", + x1 * scale, + y1 * scale, + x * scale, + y * scale + ) + .unwrap(); } fn curve_to(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x: f32, y: f32) { - write!(&mut self.0, "C {} {} {} {} {} {} ", x1, y1, x2, y2, x, y).unwrap(); + let scale = self.scale(); + write!( + &mut self.0, + "C {} {} {} {} {} {} ", + x1 * scale, + y1 * scale, + x2 * scale, + y2 * scale, + x * scale, + y * scale + ) + .unwrap(); } fn close(&mut self) { |
