Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions third_party/usvg/PATCHES.md
Original file line number Diff line number Diff line change
@@ -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 <last-synced>..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**.
4 changes: 4 additions & 0 deletions third_party/usvg/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 17 additions & 0 deletions third_party/usvg/src/parser/svgtree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down
32 changes: 26 additions & 6 deletions third_party/usvg/src/text/flatten.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ fn push_outline_paths(
builder: &mut tiny_skia_path::PathBuilder,
new_children: &mut Vec<Node>,
rendering_mode: ShapeRendering,
abs_transform: Transform,
) {
let builder = mem::replace(builder, tiny_skia_path::PathBuilder::new());

Expand All @@ -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)));
Expand All @@ -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 {
Expand Down Expand Up @@ -78,29 +80,41 @@ 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();

new_children.push(Node::Group(Box::new(group)));
}
// 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();

new_children.push(Node::Group(Box::new(group)));
}
// 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(
Expand Down Expand Up @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions third_party/usvg/src/text/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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![];

Expand Down Expand Up @@ -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;
}
Expand All @@ -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,
Expand Down
35 changes: 35 additions & 0 deletions third_party/usvg/src/tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<LinearGradient>] {
&self.linear_gradients
Expand Down
Loading
Loading