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, Pattern, RatioOrAngle}; use typst_utils::hash128; use xmlwriter::XmlWriter; use crate::{Id, SVGRenderer, State, SvgMatrix, SvgPathBuilder}; /// The number of segments in a conic gradient. /// This is a heuristic value that seems to work well. /// Smaller values could be interesting for optimization. const CONIC_SEGMENT: usize = 360; impl SVGRenderer { /// Render a frame to a string. pub(super) 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() } /// Write a fill attribute. pub(super) fn write_fill( &mut self, fill: &Paint, fill_rule: FillRule, size: Size, ts: Transform, ) { match fill { Paint::Solid(color) => self.xml.write_attribute("fill", &color.encode()), Paint::Gradient(gradient) => { 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})")); } } match fill_rule { FillRule::NonZero => self.xml.write_attribute("fill-rule", "nonzero"), FillRule::EvenOdd => self.xml.write_attribute("fill-rule", "evenodd"), } } /// Pushes a gradient to the list of gradients to write SVG file. /// /// If the gradient is already present, returns the id of the existing /// gradient. Otherwise, inserts the gradient and returns the id of the /// inserted gradient. If the transform of the gradient is the identify /// matrix, the returned ID will be the ID of the "source" gradient, /// this is a file size optimization. pub(super) fn push_gradient( &mut self, gradient: &Gradient, size: Size, ts: Transform, ) -> Id { let gradient_id = self .gradients .insert_with(hash128(&(gradient, size.aspect_ratio())), || { (gradient.clone(), size.aspect_ratio()) }); if ts.is_identity() { return gradient_id; } self.gradient_refs .insert_with(hash128(&(gradient_id, ts)), || GradientRef { id: gradient_id, kind: gradient.into(), transform: ts, }) } pub(super) fn push_pattern( &mut self, pattern: &Pattern, size: Size, ts: Transform, ) -> Id { let pattern_size = pattern.size() + pattern.spacing(); // 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 the raw gradients (without transform) to the SVG file. pub(super) fn write_gradients(&mut self) { if self.gradients.is_empty() { return; } self.xml.start_element("defs"); self.xml.write_attribute("id", "gradients"); for (id, (gradient, ratio)) in self.gradients.iter() { match &gradient { Gradient::Linear(linear) => { self.xml.start_element("linearGradient"); self.xml.write_attribute("id", &id); self.xml.write_attribute("spreadMethod", "pad"); self.xml.write_attribute("gradientUnits", "userSpaceOnUse"); let angle = Gradient::correct_aspect_ratio(linear.angle, *ratio); let (sin, cos) = (angle.sin(), angle.cos()); let length = sin.abs() + cos.abs(); let (x1, y1, x2, y2) = match angle.quadrant() { Quadrant::First => (0.0, 0.0, cos * length, sin * length), Quadrant::Second => (1.0, 0.0, cos * length + 1.0, sin * length), Quadrant::Third => { (1.0, 1.0, cos * length + 1.0, sin * length + 1.0) } Quadrant::Fourth => (0.0, 1.0, cos * length, sin * length + 1.0), }; self.xml.write_attribute("x1", &x1); self.xml.write_attribute("y1", &y1); self.xml.write_attribute("x2", &x2); self.xml.write_attribute("y2", &y2); } Gradient::Radial(radial) => { self.xml.start_element("radialGradient"); self.xml.write_attribute("id", &id); self.xml.write_attribute("spreadMethod", "pad"); self.xml.write_attribute("gradientUnits", "userSpaceOnUse"); self.xml.write_attribute("cx", &radial.center.x.get()); self.xml.write_attribute("cy", &radial.center.y.get()); self.xml.write_attribute("r", &radial.radius.get()); self.xml.write_attribute("fx", &radial.focal_center.x.get()); self.xml.write_attribute("fy", &radial.focal_center.y.get()); self.xml.write_attribute("fr", &radial.focal_radius.get()); } Gradient::Conic(conic) => { self.xml.start_element("pattern"); self.xml.write_attribute("id", &id); self.xml.write_attribute("viewBox", "0 0 1 1"); self.xml.write_attribute("preserveAspectRatio", "none"); self.xml.write_attribute("patternUnits", "userSpaceOnUse"); self.xml.write_attribute("width", "2"); self.xml.write_attribute("height", "2"); self.xml.write_attribute("x", "-0.5"); self.xml.write_attribute("y", "-0.5"); // The rotation angle, negated to match rotation in PNG. let angle: f32 = -(Gradient::correct_aspect_ratio(conic.angle, *ratio).to_rad() as f32) .rem_euclid(TAU); let center: (f32, f32) = (conic.center.x.get() as f32, conic.center.y.get() as f32); // We build an arg segment for each segment of a circle. let dtheta = TAU / CONIC_SEGMENT as f32; for i in 0..CONIC_SEGMENT { let theta1 = dtheta * i as f32; let theta2 = dtheta * (i + 1) as f32; // Create the path for the segment. let mut builder = SvgPathBuilder::default(); builder.move_to( correct_pattern_pos(center.0), correct_pattern_pos(center.1), ); builder.line_to( correct_pattern_pos(-2.0 * (theta1 + angle).cos() + center.0), correct_pattern_pos(2.0 * (theta1 + angle).sin() + center.1), ); builder.arc( (2.0, 2.0), 0.0, 0, 1, ( correct_pattern_pos( -2.0 * (theta2 + angle).cos() + center.0, ), correct_pattern_pos( 2.0 * (theta2 + angle).sin() + center.1, ), ), ); builder.close(); let t1 = (i as f32) / CONIC_SEGMENT as f32; let t2 = (i + 1) as f32 / CONIC_SEGMENT as f32; let subgradient = SVGSubGradient { center: conic.center, t0: Angle::rad((theta1 + angle) as f64), t1: Angle::rad((theta2 + angle) as f64), c0: gradient .sample(RatioOrAngle::Ratio(Ratio::new(t1 as f64))), c1: gradient .sample(RatioOrAngle::Ratio(Ratio::new(t2 as f64))), }; let id = self .conic_subgradients .insert_with(hash128(&subgradient), || subgradient); // Add the path to the pattern. self.xml.start_element("path"); self.xml.write_attribute("d", &builder.0); self.xml.write_attribute_fmt("fill", format_args!("url(#{id})")); self.xml .write_attribute_fmt("stroke", format_args!("url(#{id})")); self.xml.write_attribute("stroke-width", "0"); self.xml.write_attribute("shape-rendering", "optimizeSpeed"); self.xml.end_element(); } // We skip the default stop generation code. self.xml.end_element(); continue; } } for window in gradient.stops_ref().windows(2) { let (start_c, start_t) = window[0]; let (end_c, end_t) = window[1]; self.xml.start_element("stop"); self.xml.write_attribute("offset", &start_t.repr()); self.xml.write_attribute("stop-color", &start_c.to_hex()); self.xml.end_element(); // Generate (256 / len) stops between the two stops. // This is a workaround for a bug in many readers: // They tend to just ignore the color space of the gradient. // The goal is to have smooth gradients but not to balloon the file size // too much if there are already a lot of stops as in most presets. let len = if gradient.anti_alias() { (256 / gradient.stops_ref().len() as u32).max(2) } else { 2 }; for i in 1..(len - 1) { let t0 = i as f64 / (len - 1) as f64; let t = start_t + (end_t - start_t) * t0; let c = gradient.sample(RatioOrAngle::Ratio(t)); self.xml.start_element("stop"); self.xml.write_attribute("offset", &t.repr()); self.xml.write_attribute("stop-color", &c.to_hex()); self.xml.end_element(); } self.xml.start_element("stop"); self.xml.write_attribute("offset", &end_t.repr()); self.xml.write_attribute("stop-color", &end_c.to_hex()); self.xml.end_element() } self.xml.end_element(); } self.xml.end_element() } /// Write the sub-gradients that are used for conic gradients. pub(super) fn write_subgradients(&mut self) { if self.conic_subgradients.is_empty() { return; } self.xml.start_element("defs"); self.xml.write_attribute("id", "subgradients"); for (id, gradient) in self.conic_subgradients.iter() { let x1 = 2.0 - gradient.t0.cos() as f32 + gradient.center.x.get() as f32; let y1 = gradient.t0.sin() as f32 + gradient.center.y.get() as f32; let x2 = 2.0 - gradient.t1.cos() as f32 + gradient.center.x.get() as f32; let y2 = gradient.t1.sin() as f32 + gradient.center.y.get() as f32; self.xml.start_element("linearGradient"); self.xml.write_attribute("id", &id); self.xml.write_attribute("gradientUnits", "objectBoundingBox"); self.xml.write_attribute("x1", &x1); self.xml.write_attribute("y1", &y1); self.xml.write_attribute("x2", &x2); self.xml.write_attribute("y2", &y2); self.xml.start_element("stop"); self.xml.write_attribute("offset", "0%"); self.xml.write_attribute("stop-color", &gradient.c0.to_hex()); self.xml.end_element(); self.xml.start_element("stop"); self.xml.write_attribute("offset", "100%"); self.xml.write_attribute("stop-color", &gradient.c1.to_hex()); self.xml.end_element(); self.xml.end_element(); } self.xml.end_element(); } pub(super) fn write_gradient_refs(&mut self) { if self.gradient_refs.is_empty() { return; } self.xml.start_element("defs"); self.xml.write_attribute("id", "gradient-refs"); for (id, gradient_ref) in self.gradient_refs.iter() { match gradient_ref.kind { GradientKind::Linear => { self.xml.start_element("linearGradient"); self.xml.write_attribute( "gradientTransform", &SvgMatrix(gradient_ref.transform), ); } GradientKind::Radial => { self.xml.start_element("radialGradient"); self.xml.write_attribute( "gradientTransform", &SvgMatrix(gradient_ref.transform), ); } GradientKind::Conic => { self.xml.start_element("pattern"); self.xml.write_attribute( "patternTransform", &SvgMatrix(gradient_ref.transform), ); } } self.xml.write_attribute("id", &id); // Writing the href attribute to the "reference" gradient. self.xml .write_attribute_fmt("href", format_args!("#{}", gradient_ref.id)); // Also writing the xlink:href attribute for compatibility. self.xml .write_attribute_fmt("xlink:href", format_args!("#{}", gradient_ref.id)); self.xml.end_element(); } self.xml.end_element(); } /// Write the raw gradients (without transform) to the SVG file. pub(super) 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::>() { let size = pattern.size() + pattern.spacing(); 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. pub(super) 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(); } } /// 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)] pub 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, } /// A reference to a deduplicated gradient, with a transform matrix. /// /// Allows gradients to be reused across multiple invocations, /// simply by changing the transform matrix. #[derive(Hash)] pub struct GradientRef { /// The ID of the deduplicated gradient id: Id, /// The gradient kind (used to determine the SVG element to use) /// but without needing to clone the entire gradient. kind: GradientKind, /// The transform matrix to apply to the gradient. transform: Transform, } /// A subgradient for conic gradients. #[derive(Hash)] pub struct SVGSubGradient { /// The center point of the gradient. center: Axes, /// The start point of the subgradient. t0: Angle, /// The end point of the subgradient. t1: Angle, /// The color at the start point of the subgradient. c0: Color, /// The color at the end point of the subgradient. c1: Color, } /// The kind of linear gradient. #[derive(Hash, Clone, Copy, PartialEq, Eq)] enum GradientKind { /// A linear gradient. Linear, /// A radial gradient. Radial, /// A conic gradient. Conic, } impl From<&Gradient> for GradientKind { fn from(value: &Gradient) -> Self { match value { Gradient::Linear { .. } => GradientKind::Linear, Gradient::Radial { .. } => GradientKind::Radial, Gradient::Conic { .. } => GradientKind::Conic, } } } /// Encode the color as an SVG color. pub trait ColorEncode { /// Encode the color. fn encode(&self) -> EcoString; } impl ColorEncode for Color { fn encode(&self) -> EcoString { match *self { c @ Color::Rgb(_) | c @ Color::Luma(_) | c @ Color::Cmyk(_) | c @ Color::Hsv(_) => c.to_hex(), Color::LinearRgb(rgb) => { if rgb.alpha != 1.0 { eco_format!( "color(srgb-linear {:.5} {:.5} {:.5} / {:.5})", rgb.red, rgb.green, rgb.blue, rgb.alpha ) } else { eco_format!( "color(srgb-linear {:.5} {:.5} {:.5})", rgb.red, rgb.green, rgb.blue, ) } } Color::Oklab(oklab) => { if oklab.alpha != 1.0 { eco_format!( "oklab({:.3}% {:.5} {:.5} / {:.5})", oklab.l * 100.0, oklab.a, oklab.b, oklab.alpha ) } else { eco_format!( "oklab({:.3}% {:.5} {:.5})", oklab.l * 100.0, oklab.a, oklab.b, ) } } Color::Oklch(oklch) => { if oklch.alpha != 1.0 { eco_format!( "oklch({:.3}% {:.5} {:.3}deg / {:.3})", oklch.l * 100.0, oklch.chroma, oklch.hue.into_degrees(), oklch.alpha ) } else { eco_format!( "oklch({:.3}% {:.5} {:.3}deg)", oklch.l * 100.0, oklch.chroma, oklch.hue.into_degrees(), ) } } Color::Hsl(hsl) => { if hsl.alpha != 1.0 { eco_format!( "hsla({:.3}deg {:.3}% {:.3}% / {:.5})", hsl.hue.into_degrees(), hsl.saturation * 100.0, hsl.lightness * 100.0, hsl.alpha, ) } else { eco_format!( "hsl({:.3}deg {:.3}% {:.3}%)", hsl.hue.into_degrees(), hsl.saturation * 100.0, hsl.lightness * 100.0, ) } } } } } /// Maps a coordinate in a unit size square to a coordinate in the pattern. pub fn correct_pattern_pos(x: f32) -> f32 { (x + 0.5) / 2.0 }