Improve rendered SVG output syntax for better compatibility and terseness (#1880)

* Improve rendered SVG output syntax for better compatibility and terseness

* Fix CI failing on boolean operations without wasm32?

* Attempt 2
This commit is contained in:
Keavon Chambers 2024-07-30 08:28:49 -07:00 committed by GitHub
parent 22ebe9a2cb
commit 44ffb635e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 249 additions and 189 deletions

View file

@ -1,4 +1,5 @@
mod quad;
pub use quad::Quad;
use crate::raster::bbox::Bbox;
use crate::raster::{BlendMode, Image, ImageFrame};
@ -8,12 +9,13 @@ use crate::vector::style::{Fill, Stroke, ViewMode};
use crate::vector::PointId;
use crate::SurfaceFrame;
use crate::{vector::VectorData, Artboard, Color, GraphicElement, GraphicGroup};
pub use quad::Quad;
use bezier_rs::Subpath;
use base64::Engine;
use glam::{DAffine2, DVec2};
use num_traits::Zero;
use std::fmt::Write;
#[cfg(feature = "vello")]
use vello::*;
@ -107,11 +109,10 @@ impl SvgRender {
.map(|size| format!("viewbox=\"0 0 {} {}\" width=\"{}\" height=\"{}\"", size.x, size.y, size.x, size.y))
.unwrap_or_default();
let svg_header = format!(
r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g transform="{}">"#,
view_box,
format_transform_matrix(transform)
);
let matrix = format_transform_matrix(transform);
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
let svg_header = format!(r#"<svg xmlns="http://www.w3.org/2000/svg" {}><defs>{defs}</defs><g{transform}>"#, view_box);
self.svg.insert(0, svg_header.into());
self.svg.push("</g></svg>".into());
}
@ -193,17 +194,16 @@ impl RenderParams {
}
pub fn format_transform_matrix(transform: DAffine2) -> String {
use std::fmt::Write;
let mut result = "matrix(".to_string();
let cols = transform.to_cols_array();
for (index, item) in cols.iter().enumerate() {
write!(result, "{item}").unwrap();
if index != cols.len() - 1 {
result.push_str(", ");
}
if transform == DAffine2::IDENTITY {
return String::new();
}
result.push(')');
result
transform.to_cols_array().iter().enumerate().fold("matrix(".to_string(), |val, (i, num)| {
let num = if num.abs() < 1_000_000_000. { (num * 1_000_000_000.).round() / 1_000_000_000. } else { *num };
let num = if num.is_zero() { "0".to_string() } else { num.to_string() };
let comma = if i == 5 { "" } else { "," };
val + &(num + comma)
}) + ")"
}
pub fn to_transform(transform: DAffine2) -> usvg::Transform {
@ -234,7 +234,10 @@ impl GraphicElementRendered for GraphicGroup {
render.parent_tag(
"g",
|attributes| {
attributes.push("transform", format_transform_matrix(self.transform));
let matrix = format_transform_matrix(self.transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.alpha_blending.opacity < 1. {
attributes.push("opacity", self.alpha_blending.opacity.to_string());
@ -305,8 +308,6 @@ impl GraphicElementRendered for VectorData {
}
render.leaf_tag("path", |attributes| {
attributes.push("class", "vector-data");
attributes.push("d", path);
let fill_and_stroke = self
@ -443,8 +444,10 @@ impl GraphicElementRendered for Artboard {
if !render_params.hide_artboards {
// Background
render.leaf_tag("rect", |attributes| {
attributes.push("class", "artboard-bg");
attributes.push("fill", format!("#{}", self.background.rgba_hex()));
attributes.push("fill", format!("#{}", self.background.rgb_hex()));
if self.background.a() < 1. {
attributes.push("fill-opacity", ((self.background.a() * 1000.).round() / 1000.).to_string());
}
attributes.push("x", self.location.x.min(self.location.x + self.dimensions.x).to_string());
attributes.push("y", self.location.y.min(self.location.y + self.dimensions.y).to_string());
attributes.push("width", self.dimensions.x.abs().to_string());
@ -456,7 +459,6 @@ impl GraphicElementRendered for Artboard {
render.parent_tag(
"text",
|attributes| {
attributes.push("class", "artboard-label");
attributes.push("fill", "white");
attributes.push("x", (self.location.x.min(self.location.x + self.dimensions.x)).to_string());
attributes.push("y", (self.location.y.min(self.location.y + self.dimensions.y) - 4).to_string());
@ -475,23 +477,22 @@ impl GraphicElementRendered for Artboard {
"g",
// Group tag attributes
|attributes| {
attributes.push("class", "artboard");
attributes.push(
"transform",
format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform),
);
let matrix = format_transform_matrix(DAffine2::from_translation(self.location.as_dvec2()) * self.graphic_group.transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.clip {
let id = format!("artboard-{}", generate_uuid());
let selector = format!("url(#{id})");
use std::fmt::Write;
let matrix = format_transform_matrix(self.graphic_group.transform.inverse());
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{matrix}""#) };
write!(
&mut attributes.0.svg_defs,
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}" transform="{}"/></clipPath>"##,
self.dimensions.x,
self.dimensions.y,
format_transform_matrix(self.graphic_group.transform.inverse())
r##"<clipPath id="{id}"><rect x="0" y="0" width="{}" height="{}"{transform} /></clipPath>"##,
self.dimensions.x, self.dimensions.y
)
.unwrap();
attributes.push("clip-path", selector);
@ -580,18 +581,16 @@ impl GraphicElementRendered for crate::ArtboardGroup {
impl GraphicElementRendered for SurfaceFrame {
fn render_svg(&self, render: &mut SvgRender, _render_params: &RenderParams) {
let transform = self.transform;
let (width, height) = (transform.transform_vector2(DVec2::new(1., 0.)).length(), transform.transform_vector2(DVec2::new(0., 1.)).length());
let matrix = (transform * DAffine2::from_scale((width, height).into()).inverse())
.to_cols_array()
.iter()
.enumerate()
.fold(String::new(), |val, (i, entry)| val + &(entry.to_string() + if i == 5 { "" } else { "," }));
let matrix = format_transform_matrix(transform * DAffine2::from_scale((width, height).into()).inverse());
let transform = if matrix.is_empty() { String::new() } else { format!(r#" transform="{}""#, matrix) };
let canvas = format!(
r#"<foreignObject width="{}" height="{}" transform="matrix({})"><div data-canvas-placeholder="canvas{}"></div></foreignObject>"#,
r#"<foreignObject width="{}" height="{}"{transform}><div data-canvas-placeholder="canvas{}"></div></foreignObject>"#,
width.abs(),
height.abs(),
matrix,
self.surface_id
);
render.svg.push(canvas.into())
@ -617,7 +616,7 @@ impl GraphicElementRendered for SurfaceFrame {
impl GraphicElementRendered for ImageFrame<Color> {
fn render_svg(&self, render: &mut SvgRender, render_params: &RenderParams) {
let transform: String = format_transform_matrix(self.transform * render.transform);
let transform = self.transform * render.transform;
match render_params.image_render_mode {
ImageRenderMode::Base64 => {
@ -638,8 +637,11 @@ impl GraphicElementRendered for ImageFrame<Color> {
attributes.push("width", 1.to_string());
attributes.push("height", 1.to_string());
attributes.push("preserveAspectRatio", "none");
attributes.push("transform", transform);
attributes.push("href", base64_string);
let matrix = format_transform_matrix(transform);
if !matrix.is_empty() {
attributes.push("transform", matrix);
}
if self.alpha_blending.blend_mode != BlendMode::default() {
attributes.push("style", self.alpha_blending.blend_mode.render());
}
@ -765,7 +767,10 @@ impl GraphicElementRendered for Option<Color> {
attributes.push("width", "100");
attributes.push("height", "100");
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex()));
attributes.push("fill", format!("#{}", color.rgb_hex()));
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
});
render.parent_tag("text", text_attributes, |render| render.leaf_node(color_info))
}
@ -785,7 +790,10 @@ impl GraphicElementRendered for Vec<Color> {
attributes.push("height", "100");
attributes.push("x", (index * 120).to_string());
attributes.push("y", "40");
attributes.push("fill", format!("#{}", color.rgba_hex()));
attributes.push("fill", format!("#{}", color.rgb_hex()));
if color.a() < 1. {
attributes.push("fill-opacity", ((color.a() * 1000.).round() / 1000.).to_string());
}
});
}
}

View file

@ -1,20 +1,18 @@
use core::hash::Hash;
use half::f16;
use super::discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float};
use super::{Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB};
use dyn_any::{DynAny, StaticType};
#[cfg(feature = "serde")]
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::float::Float;
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::Euclid;
use bytemuck::{Pod, Zeroable};
use core::hash::Hash;
use half::f16;
use std::fmt::Write;
use super::{
discrete_srgb::{float_to_srgb_u8, srgb_u8_to_float},
Alpha, AssociatedAlpha, Luminance, LuminanceMut, Pixel, RGBMut, Rec709Primaries, RGB, SRGB,
};
#[repr(C)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Default, Clone, Copy, PartialEq, DynAny, Pod, Zeroable)]
@ -813,12 +811,12 @@ impl Color {
/// ```
/// use graphene_core::raster::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240A261", color.rgba_hex())
/// assert_eq!("3240a261", color.rgba_hex())
/// ```
#[cfg(feature = "std")]
pub fn rgba_hex(&self) -> String {
format!(
"{:02X?}{:02X?}{:02X?}{:02X?}",
"{:02x?}{:02x?}{:02x?}{:02x?}",
(self.r() * 255.) as u8,
(self.g() * 255.) as u8,
(self.b() * 255.) as u8,
@ -826,15 +824,34 @@ impl Color {
)
}
/// Return a 6-character RGB, or 8-character RGBA, hex string (without a # prefix). The shorter form is used if the alpha is 1.
///
/// # Examples
/// ```
/// use graphene_core::raster::color::Color;
/// let color1 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240a261", color1.rgb_optional_a_hex())
/// let color2 = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240a2", color2.rgb_optional_a_hex())
/// ```
#[cfg(feature = "std")]
pub fn rgb_optional_a_hex(&self) -> String {
let mut result = format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8);
if self.a() < 1. {
let _ = write!(&mut result, "{:02x?}", (self.a() * 255.) as u8);
}
result
}
/// Return a 6-character RGB hex string (without a # prefix).
/// ```
/// use graphene_core::raster::color::Color;
/// let color = Color::from_rgba8_srgb(0x52, 0x67, 0xFA, 0x61).to_gamma_srgb();
/// assert_eq!("3240A2", color.rgb_hex())
/// assert_eq!("3240a2", color.rgb_hex())
/// ```
#[cfg(feature = "std")]
pub fn rgb_hex(&self) -> String {
format!("{:02X?}{:02X?}{:02X?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
format!("{:02x?}{:02x?}{:02x?}", (self.r() * 255.) as u8, (self.g() * 255.) as u8, (self.b() * 255.) as u8)
}
/// Return the all components as a u8 slice, first component is red, followed by green, followed by blue, followed by alpha.

View file

@ -1,25 +1,13 @@
//! Contains stylistic options for SVG elements.
use crate::consts::{LAYER_OUTLINE_STROKE_COLOR, LAYER_OUTLINE_STROKE_WEIGHT};
use crate::renderer::format_transform_matrix;
use crate::Color;
use dyn_any::{DynAny, StaticType};
use glam::{DAffine2, DVec2};
use std::fmt::{self, Display, Write};
/// Precision of the opacity value in digits after the decimal point.
/// A value of 3 would correspond to a precision of 10^-3.
const OPACITY_PRECISION: usize = 3;
fn format_opacity(attribute: &str, opacity: f32) -> String {
if (opacity - 1.).abs() > 10_f32.powi(-(OPACITY_PRECISION as i32)) {
format!(r#" {attribute}="{opacity:.OPACITY_PRECISION$}""#)
} else {
String::new()
}
}
#[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, serde::Serialize, serde::Deserialize, DynAny, specta::Type)]
pub enum GradientType {
#[default]
@ -134,7 +122,15 @@ impl Gradient {
let mut stop = String::new();
for (position, color) in self.stops.0.iter() {
let _ = write!(stop, r##"<stop offset="{}" stop-color="#{}" />"##, position, color.with_alpha(color.a()).rgba_hex());
stop.push_str("<stop");
if *position != 0. {
let _ = write!(stop, r#" offset="{}""#, (position * 1_000_000.).round() / 1_000_000.);
}
let _ = write!(stop, r##" stop-color="#{}""##, color.rgb_hex());
if color.a() < 1. {
let _ = write!(stop, r#" stop-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
stop.push_str(" />")
}
let mod_gradient = transformed_bound_transform.inverse();
@ -143,28 +139,25 @@ impl Gradient {
let start = mod_points.transform_point2(self.start);
let end = mod_points.transform_point2(self.end);
let transform = mod_gradient
.to_cols_array()
.iter()
.enumerate()
.map(|(i, entry)| entry.to_string() + if i == 5 { "" } else { "," })
.collect::<String>();
let gradient_id = crate::uuid::generate_uuid();
let matrix = format_transform_matrix(mod_gradient);
let gradient_transform = if matrix.is_empty() { String::new() } else { format!(r#" gradientTransform="{}""#, matrix) };
match self.gradient_type {
GradientType::Linear => {
let _ = write!(
svg_defs,
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}" gradientTransform="matrix({})">{}</linearGradient>"#,
gradient_id, start.x, end.x, start.y, end.y, transform, stop
r#"<linearGradient id="{}" x1="{}" x2="{}" y1="{}" y2="{}"{gradient_transform}>{}</linearGradient>"#,
gradient_id, start.x, end.x, start.y, end.y, stop
);
}
GradientType::Radial => {
let radius = (f64::powi(start.x - end.x, 2) + f64::powi(start.y - end.y, 2)).sqrt();
let _ = write!(
svg_defs,
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}" gradientTransform="matrix({})">{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, transform, stop
r#"<radialGradient id="{}" cx="{}" cy="{}" r="{}"{gradient_transform}>{}</radialGradient>"#,
gradient_id, start.x, start.y, radius, stop
);
}
}
@ -277,7 +270,13 @@ impl Fill {
pub fn render(&self, svg_defs: &mut String, multiplied_transform: DAffine2, bounds: [DVec2; 2], transformed_bounds: [DVec2; 2]) -> String {
match self {
Self::None => r#" fill="none""#.to_string(),
Self::Solid(color) => format!(r##" fill="#{}"{}"##, color.rgb_hex(), format_opacity("fill-opacity", color.a())),
Self::Solid(color) => {
let mut result = format!(r##" fill="#{}""##, color.rgb_hex());
if color.a() < 1. {
let _ = write!(result, r#" fill-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
result
}
Self::Gradient(gradient) => {
let gradient_id = gradient.render_defs(svg_defs, multiplied_transform, bounds, transformed_bounds);
format!(r##" fill="url('#{gradient_id}')""##)
@ -505,21 +504,45 @@ impl Stroke {
/// Provide the SVG attributes for the stroke.
pub fn render(&self) -> String {
if let Some(color) = self.color {
format!(
r##" stroke="#{}"{} stroke-width="{}" stroke-dasharray="{}" stroke-dashoffset="{}" stroke-linecap="{}" stroke-linejoin="{}" stroke-miterlimit="{}" "##,
color.rgb_hex(),
format_opacity("stroke-opacity", color.a()),
self.weight,
self.dash_lengths(),
self.dash_offset,
self.line_cap,
self.line_join,
self.line_join_miter_limit
)
} else {
String::new()
// Don't render a stroke at all if it would be invisible
let Some(color) = self.color else { return String::new() };
if self.weight <= 0. || color.a() == 0. {
return String::new();
}
// Set to None if the value is the SVG default
let weight = (self.weight != 1.).then_some(self.weight);
let dash_array = (!self.dash_lengths.is_empty()).then_some(self.dash_lengths());
let dash_offset = (self.dash_offset != 0.).then_some(self.dash_offset);
let line_cap = (self.line_cap != LineCap::Butt).then_some(self.line_cap);
let line_join = (self.line_join != LineJoin::Miter).then_some(self.line_join);
let line_join_miter_limit = (self.line_join_miter_limit != 4.).then_some(self.line_join_miter_limit);
// Render the needed stroke attributes
let mut attributes = format!(r##" stroke="#{}""##, color.rgb_hex());
if color.a() < 1. {
let _ = write!(&mut attributes, r#" stroke-opacity="{}""#, (color.a() * 1000.).round() / 1000.);
}
if let Some(weight) = weight {
let _ = write!(&mut attributes, r#" stroke-width="{}""#, weight);
}
if let Some(dash_array) = dash_array {
let _ = write!(&mut attributes, r#" stroke-dasharray="{}""#, dash_array);
}
if let Some(dash_offset) = dash_offset {
let _ = write!(&mut attributes, r#" stroke-dashoffset="{}""#, dash_offset);
}
if let Some(line_cap) = line_cap {
let _ = write!(&mut attributes, r#" stroke-linecap="{}""#, line_cap);
}
if let Some(line_join) = line_join {
let _ = write!(&mut attributes, r#" stroke-linejoin="{}""#, line_join);
}
if let Some(line_join_miter_limit) = line_join_miter_limit {
let _ = write!(&mut attributes, r#" stroke-miterlimit="{}""#, line_join_miter_limit);
}
attributes
}
pub fn with_color(mut self, color: &Option<Color>) -> Option<Self> {