diff --git a/third_party/usvg/PATCHES.md b/third_party/usvg/PATCHES.md new file mode 100644 index 0000000000..45ebf2a1c4 --- /dev/null +++ b/third_party/usvg/PATCHES.md @@ -0,0 +1,49 @@ +# Patches + +This file tracks the upstream sync state of the vendored `usvg` source. + +## Fork Base + +Initially forked from upstream commit [`0ecb332e`](https://github.com/linebender/resvg/commit/0ecb332e51360ed59da2c0e5b1167311f77cac8a) (linebender/resvg, 2025-10-29) — pre-harfrust, pre-edition-2024, with `kurbo 0.12` / `svgtypes 0.16`. + +## Last Synced + +Upstream: [`b3c7f58d`](https://github.com/linebender/resvg/commit/b3c7f58d059da6aa0a25141b1948c61b8c579c12) (linebender/resvg, 2026-04-09). + +## Adopted Upstream Patches + +Cherry-picked from `0ecb332e..b3c7f58d` via `git format-patch` + `git am`. `Cargo.toml`, `tests/`, and dep/edition/MSRV bumps were intentionally excluded. + +| PR | Summary | +| ------------------------------------------------------ | ----------------------------------------------------------- | +| [#980](https://github.com/linebender/resvg/pull/980) | feat: do not write empty `defs` nodes | +| [#981](https://github.com/linebender/resvg/pull/981) | check if text paths need to be written out | +| [#984](https://github.com/linebender/resvg/pull/984) | consolidate `BlendMode::to_string` | +| [#988](https://github.com/linebender/resvg/pull/988) | fix bug in rewriting of clip paths with transformed path | +| [#994](https://github.com/linebender/resvg/pull/994) | don't emit warning for certain attributes with value `none` | +| [#1040](https://github.com/linebender/resvg/pull/1040) | fix: text nodes should inherit absolute transform | +| [#1043](https://github.com/linebender/resvg/pull/1043) | correctly calculate glyph advances | + +## Local Modifications + +Changes made in the fork that are not from upstream: + +- `src/lib.rs`, `src/main.rs` — fork note + `#![allow(clippy::all)]` +- `src/text/mod.rs` — `flatten` is treated as optional; the text node is preserved even when outlining fails +- `tests/files/text-simple-case-expected.svg` and related — snapshot regenerated locally + +## How to Sync + +```sh +# In a local clone of linebender/resvg: +git format-patch ..main -o /tmp/usvg-patches \ + -- crates/usvg/src crates/usvg/codegen + +# In this repo, rewrite paths and apply: +for p in /tmp/usvg-patches/*.patch; do + sed -i '' 's| a/crates/usvg/| a/third_party/usvg/|g; s| b/crates/usvg/| b/third_party/usvg/|g; s|^--- a/crates/usvg/|--- a/third_party/usvg/|g; s|^+++ b/crates/usvg/|+++ b/third_party/usvg/|g' "$p" + git am "$p" +done +``` + +After syncing, update **Last Synced** above and append the new PRs to **Adopted Upstream Patches**. diff --git a/third_party/usvg/README.md b/third_party/usvg/README.md index 2c68793168..9f69524d65 100644 --- a/third_party/usvg/README.md +++ b/third_party/usvg/README.md @@ -51,6 +51,10 @@ This is a fork of the original [usvg](https://github.com/linebender/resvg) libra - **Crates.io:** [usvg](https://crates.io/crates/usvg) - **Documentation:** [docs.rs/usvg](https://docs.rs/usvg) +### Sync State + +See [PATCHES.md](PATCHES.md) for the fork base, last-synced upstream commit, and the list of adopted patches. + ### Why This Fork? This fork includes planned modifications to align with Grida's architecture, including dependency unification, bundle downsizing, and enhanced tree processing. See [TODO.md](TODO.md) for details on planned changes and development roadmap. diff --git a/third_party/usvg/src/parser/svgtree/mod.rs b/third_party/usvg/src/parser/svgtree/mod.rs index 30308dcb80..3591fd8f3a 100644 --- a/third_party/usvg/src/parser/svgtree/mod.rs +++ b/third_party/usvg/src/parser/svgtree/mod.rs @@ -282,6 +282,23 @@ impl<'a, 'input: 'a> SvgNode<'a, 'input> { .iter() .find(|a| a.name == aid) .map(|a| a.value.as_str())?; + // These AId have an initial value of none + let is_possible_none = matches!( + aid, + AId::Mask + | AId::MarkerStart + | AId::MarkerMid + | AId::MarkerEnd + | AId::ClipPath + | AId::Filter + | AId::FontSizeAdjust + | AId::TextDecoration + | AId::Stroke + | AId::StrokeDasharray + ); + if is_possible_none && value == "none" { + return None; + } match T::parse(*self, aid, value) { Some(v) => Some(v), None => { diff --git a/third_party/usvg/src/text/flatten.rs b/third_party/usvg/src/text/flatten.rs index 89929a08e2..f8e6fb1a3a 100644 --- a/third_party/usvg/src/text/flatten.rs +++ b/third_party/usvg/src/text/flatten.rs @@ -26,6 +26,7 @@ fn push_outline_paths( builder: &mut tiny_skia_path::PathBuilder, new_children: &mut Vec, rendering_mode: ShapeRendering, + abs_transform: Transform, ) { let builder = mem::replace(builder, tiny_skia_path::PathBuilder::new()); @@ -38,7 +39,7 @@ fn push_outline_paths( span.paint_order, rendering_mode, Arc::new(p), - Transform::default(), + abs_transform, ) }) { new_children.push(Node::Path(Box::new(path))); @@ -48,6 +49,7 @@ fn push_outline_paths( pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZeroRect)> { let mut new_children = vec![]; + let abs_transform = text.abs_transform; let rendering_mode = resolve_rendering_mode(text); for span in &text.layouted { @@ -78,7 +80,8 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ transform: glyph.colr_transform(), ..Group::empty() }; - // TODO: Probably need to update abs_transform of children? + // TODO: Probably need to update abs_transform of children? Same + // for SVG and bitmap glyphs. group.children.push(Node::Group(Box::new(tree.root))); group.calculate_bounding_boxes(); @@ -86,13 +89,18 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ } // An SVG glyph. Will return the usvg node containing the glyph descriptions. else if let Some(node) = cache.fontdb_svg(glyph.font, glyph.id) { - push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + push_outline_paths( + span, + &mut span_builder, + &mut new_children, + rendering_mode, + abs_transform, + ); let mut group = Group { transform: glyph.svg_transform(), ..Group::empty() }; - // TODO: Probably need to update abs_transform of children? group.children.push(node); group.calculate_bounding_boxes(); @@ -100,7 +108,13 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ } // A bitmap glyph. else if let Some(img) = cache.fontdb_raster(glyph.font, glyph.id) { - push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + push_outline_paths( + span, + &mut span_builder, + &mut new_children, + rendering_mode, + abs_transform, + ); let transform = if img.is_sbix { glyph.sbix_transform( @@ -136,7 +150,13 @@ pub(crate) fn flatten(text: &mut Text, cache: &mut Cache) -> Option<(Group, NonZ } } - push_outline_paths(span, &mut span_builder, &mut new_children, rendering_mode); + push_outline_paths( + span, + &mut span_builder, + &mut new_children, + rendering_mode, + abs_transform, + ); if let Some(path) = span.line_through.as_ref() { let mut path = path.clone(); diff --git a/third_party/usvg/src/text/layout.rs b/third_party/usvg/src/text/layout.rs index 2261f66bb2..9ab2ef850b 100644 --- a/third_party/usvg/src/text/layout.rs +++ b/third_party/usvg/src/text/layout.rs @@ -1130,8 +1130,9 @@ fn apply_word_spacing(chunk: &TextChunk, clusters: &mut [GlyphCluster]) { fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphCluster { debug_assert!(!glyphs.is_empty()); + let mut x = 0.0; let mut width = 0.0; - let mut x: f32 = 0.0; + let mut advance = 0.0; let mut positioned_glyphs = vec![]; @@ -1162,6 +1163,7 @@ fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphClu x += glyph.width as f32; let glyph_width = glyph.width as f32 * sx; + advance += glyph_width; if glyph_width > width { width = glyph_width; } @@ -1173,7 +1175,7 @@ fn form_glyph_clusters(glyphs: &[Glyph], text: &str, font_size: f32) -> GlyphClu byte_idx, codepoint: byte_idx.char_from(text), width, - advance: width, + advance, ascent: font.ascent(font_size), descent: font.descent(font_size), has_relative_shift: false, diff --git a/third_party/usvg/src/tree/mod.rs b/third_party/usvg/src/tree/mod.rs index e3486663dd..bdb819cf44 100644 --- a/third_party/usvg/src/tree/mod.rs +++ b/third_party/usvg/src/tree/mod.rs @@ -5,6 +5,7 @@ pub mod filter; mod geom; mod text; +use std::fmt::Display; use std::sync::Arc; pub use strict_num::{self, ApproxEqUlps, NonZeroPositiveF32, NormalizedF32, PositiveF32}; @@ -228,6 +229,30 @@ impl Default for BlendMode { } } +impl Display for BlendMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let blend_mode = match self { + BlendMode::Normal => "normal", + BlendMode::Multiply => "multiply", + BlendMode::Screen => "screen", + BlendMode::Overlay => "overlay", + BlendMode::Darken => "darken", + BlendMode::Lighten => "lighten", + BlendMode::ColorDodge => "color-dodge", + BlendMode::ColorBurn => "color-burn", + BlendMode::HardLight => "hard-light", + BlendMode::SoftLight => "soft-light", + BlendMode::Difference => "difference", + BlendMode::Exclusion => "exclusion", + BlendMode::Hue => "hue", + BlendMode::Saturation => "saturation", + BlendMode::Color => "color", + BlendMode::Luminosity => "luminosity", + }; + write!(f, "{blend_mode}") + } +} + /// A spread method. /// /// `spreadMethod` attribute in the SVG. @@ -1594,6 +1619,16 @@ impl Tree { has_text_nodes(&self.root) } + /// Checks if the current tree has any `defs` nodes. + pub fn has_defs_nodes(&self) -> bool { + !self.linear_gradients().is_empty() + || !self.radial_gradients().is_empty() + || !self.patterns().is_empty() + || !self.filters().is_empty() + || !self.clip_paths().is_empty() + || !self.masks().is_empty() + } + /// Returns a list of all unique [`LinearGradient`]s in the tree. pub fn linear_gradients(&self) -> &[Arc] { &self.linear_gradients diff --git a/third_party/usvg/src/writer.rs b/third_party/usvg/src/writer.rs index e475a059bb..afacefcb8d 100644 --- a/third_party/usvg/src/writer.rs +++ b/third_party/usvg/src/writer.rs @@ -153,9 +153,10 @@ pub(crate) fn convert(tree: &Tree, opt: &WriteOptions) -> String { xml.write_attribute("xmlns:xlink", "http://www.w3.org/1999/xlink"); } - xml.start_svg_element(EId::Defs); - write_defs(tree, opt, &mut xml); - xml.end_element(); + let has_text_paths = has_text_paths(&tree.root); + if tree.has_defs_nodes() || has_text_paths { + write_defs(tree, opt, &mut xml, has_text_paths); + } write_elements(&tree.root, false, opt, &mut xml); @@ -227,27 +228,7 @@ fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { xml.write_filter_primitive_attrs(filter.rect(), fe); xml.write_filter_input(AId::In, &blend.input1); xml.write_filter_input(AId::In2, &blend.input2); - xml.write_svg_attribute( - AId::Mode, - match blend.mode { - BlendMode::Normal => "normal", - BlendMode::Multiply => "multiply", - BlendMode::Screen => "screen", - BlendMode::Overlay => "overlay", - BlendMode::Darken => "darken", - BlendMode::Lighten => "lighten", - BlendMode::ColorDodge => "color-dodge", - BlendMode::ColorBurn => "color-burn", - BlendMode::HardLight => "hard-light", - BlendMode::SoftLight => "soft-light", - BlendMode::Difference => "difference", - BlendMode::Exclusion => "exclusion", - BlendMode::Hue => "hue", - BlendMode::Saturation => "saturation", - BlendMode::Color => "color", - BlendMode::Luminosity => "luminosity", - }, - ); + xml.write_svg_attribute(AId::Mode, &blend.mode.to_string()); xml.write_svg_attribute(AId::Result, &fe.result); xml.end_element(); } @@ -507,7 +488,8 @@ fn write_filters(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { } } -fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { +fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter, write_text_paths: bool) { + xml.start_svg_element(EId::Defs); for lg in tree.linear_gradients() { xml.start_svg_element(EId::LinearGradient); xml.write_id_attribute(lg.id(), opt); @@ -548,7 +530,7 @@ fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { xml.end_element(); } - if tree.has_text_nodes() { + if write_text_paths { write_text_path_paths(&tree.root, opt, xml); } @@ -589,8 +571,48 @@ fn write_defs(tree: &Tree, opt: &WriteOptions, xml: &mut XmlWriter) { xml.end_element(); } + xml.end_element(); // end EId::Defs +} + +fn has_text_paths(parent: &Group) -> bool { + for node in &parent.children { + if let Node::Group(ref group) = node { + if has_text_paths(group) { + return true; + } + } else if let Node::Text(ref text) = node { + for chunk in &text.chunks { + if let TextFlow::Path(ref text_path) = chunk.text_flow { + let path = Path::new( + text_path.id().to_string(), + true, + None, + None, + PaintOrder::default(), + ShapeRendering::default(), + text_path.path.clone(), + Transform::default(), + ); + if path.is_some() { + return true; + } + } + } + } + let mut need_path = false; + node.subroots(|subroot| { + if !need_path && has_text_paths(subroot) { + need_path = true; + } + }); + if need_path { + return true; + } + } + false } +/// Write the `path` elements for text paths. fn write_text_path_paths(parent: &Group, opt: &WriteOptions, xml: &mut XmlWriter) { for node in &parent.children { if let Node::Group(ref group) = node { @@ -809,16 +831,22 @@ fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: & // Same with text. Text elements will be converted into groups, // but only the group's children should be written. for child in &g.children { - if let Node::Path(ref path) = child { - let clip_id = g.clip_path.as_ref().map(|cp| cp.id().to_string()); - write_path( - path, - is_clip_path, - g.transform, - clip_id.as_deref(), - opt, - xml, - ); + match child { + Node::Group(child_group) => { + write_group_element(child_group, is_clip_path, opt, xml); + } + Node::Path(child_path) => { + let clip_id = g.clip_path.as_ref().map(|cp| cp.id().to_string()); + write_path( + child_path, + is_clip_path, + g.transform, + clip_id.as_deref(), + opt, + xml, + ); + } + _ => {} } } return; @@ -854,31 +882,12 @@ fn write_group_element(g: &Group, is_clip_path: bool, opt: &WriteOptions, xml: & xml.write_transform(AId::Transform, g.transform, opt); if g.blend_mode != BlendMode::Normal || g.isolate { - let blend_mode = match g.blend_mode { - BlendMode::Normal => "normal", - BlendMode::Multiply => "multiply", - BlendMode::Screen => "screen", - BlendMode::Overlay => "overlay", - BlendMode::Darken => "darken", - BlendMode::Lighten => "lighten", - BlendMode::ColorDodge => "color-dodge", - BlendMode::ColorBurn => "color-burn", - BlendMode::HardLight => "hard-light", - BlendMode::SoftLight => "soft-light", - BlendMode::Difference => "difference", - BlendMode::Exclusion => "exclusion", - BlendMode::Hue => "hue", - BlendMode::Saturation => "saturation", - BlendMode::Color => "color", - BlendMode::Luminosity => "luminosity", - }; - // For reasons unknown, `mix-blend-mode` and `isolation` must be written // as `style` attribute. let isolation = if g.isolate { "isolate" } else { "auto" }; xml.write_attribute_fmt( AId::Style.to_str(), - format_args!("mix-blend-mode:{};isolation:{}", blend_mode, isolation), + format_args!("mix-blend-mode:{};isolation:{}", g.blend_mode, isolation), ); }