Skip to content
Open
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ This changelog also contains important changes in dependencies.

This release has an MSRV of 1.87.0 for `usvg` and `resvg` and the C API.

### Fixed
- Discard invalid `fill`/`stroke`/color declarations in a `style` attribute or CSS rule instead of
dropping the property, so a previously declared valid value (or the presentation attribute) is kept.
This fixes elements becoming invisible when an unsupported color syntax such as
`color(display-p3 ...)` is used alongside a hex fallback (e.g. Figma exports). (#914)

## [0.47.0] 2026-02-05

This release has an MSRV of 1.87.0 for `usvg` and `resvg` and the C API.
Expand Down
38 changes: 38 additions & 0 deletions crates/usvg/src/parser/svgtree/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0 OR MIT

use std::collections::HashMap;
use std::str::FromStr;

use roxmltree::Error;
use simplecss::Declaration;
Expand Down Expand Up @@ -275,6 +276,21 @@ pub(crate) fn parse_svg_element<'input>(
.iter_mut()
.position(|a| a.name == aid);

// Per CSS error recovery, a declaration with an invalid value must be discarded so that a
// previously declared valid value (or the presentation attribute) is kept. We only need to
// validate when this declaration would override an existing one; a lone invalid value is
// handled, as before, by the fallback in the lazy conversion step. Restricting the check to
// the override case also keeps the common path free of a redundant parse.
// See https://github.com/linebender/resvg/issues/914
if idx.is_some() && !is_valid_declaration_value(aid, value) {
log::warn!(
"Failed to parse {} value: '{}'. Ignoring declaration.",
aid,
value
);
return;
}

// Append an attribute as usual.
let added = append_attribute(
parent_id,
Expand Down Expand Up @@ -404,6 +420,28 @@ pub(crate) fn parse_svg_element<'input>(
Ok(node_id)
}

/// Checks whether a CSS declaration value is valid for the given presentation attribute.
///
/// Only the color-bearing properties are validated, since those are the ones that can carry
/// color syntaxes we don't support yet (e.g. `color(display-p3 ...)`). Returning `true` for
/// everything else preserves the previous, permissive behavior for properties we don't validate
/// here (they are validated lazily during conversion). This is only consulted when a declaration
/// would override an existing attribute, so the common, conflict-free path stays parse-free here.
fn is_valid_declaration_value(aid: AId, value: &str) -> bool {
match aid {
// `<paint>` properties. `Paint` covers `none`, `inherit`, `currentColor`, `url(...)`
// and plain colors, so this rejects only genuinely unparsable values.
AId::Fill | AId::Stroke => svgtypes::Paint::from_str(value).is_ok(),
// Plain `<color>` properties. They also accept the `inherit` and `currentColor`
// keywords, which `Color::from_str` doesn't handle on its own.
AId::Color | AId::FloodColor | AId::LightingColor | AId::StopColor => {
matches!(value.trim(), "inherit" | "currentColor")
|| svgtypes::Color::from_str(value).is_ok()
}
_ => true,
}
}

fn append_attribute<'input>(
parent_id: NodeId,
tag_name: EId,
Expand Down
22 changes: 22 additions & 0 deletions crates/usvg/tests/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,28 @@ fn stylesheet_injection_with_important() {
);
}

#[test]
fn invalid_style_declaration_falls_back_to_valid_one() {
// Figma exports a valid hex stroke followed by an unsupported `color(display-p3 ...)`
// declaration. Per CSS error recovery, the invalid declaration must be discarded and
// the previous valid one kept, instead of dropping the stroke entirely.
// See https://github.com/linebender/resvg/issues/914
let svg = "<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'>
<path d='M13 13L9 9' stroke='#FAFAFA' \
style='stroke:#FAFAFA;stroke:color(display-p3 0.9804 0.9804 0.9804);stroke-opacity:1;' \
stroke-width='1'/>
</svg>";

let tree = usvg::Tree::from_str(svg, &usvg::Options::default()).unwrap();
let usvg::Node::Path(path) = &tree.root().children()[0] else {
unreachable!()
};
assert_eq!(
path.stroke().unwrap().paint(),
&usvg::Paint::Color(Color::new_rgb(250, 250, 250))
);
}

#[test]
fn simplify_paths() {
let svg = "
Expand Down
Loading