From 8973f7465154d1daf3a6469bd169a952f0aed63b Mon Sep 17 00:00:00 2001 From: Stefano Di Martino Date: Sun, 14 Jun 2026 15:55:29 +0200 Subject: [PATCH] Discard invalid color declarations instead of dropping the property (#914) Figma exports SVGs with a dual stroke definition: a valid hex fallback followed by a wide-gamut value, e.g. style="stroke:#FAFAFA;stroke:color(display-p3 0.9804 0.9804 0.9804);" When splitting a `style` attribute (or applying a CSS rule), the last declaration for a property always won, regardless of validity. The unsupported `color(display-p3 ...)` value thus overwrote the valid `#FAFAFA` one, and since stroke parsing later fell back to "no stroke", the element was rendered invisibly. Per CSS error recovery, a declaration with an invalid value must be discarded so a previously declared valid value (or the presentation attribute) is kept. We validate the color-bearing presentation properties (`fill`, `stroke`, `color`, `flood-color`, `lighting-color`, `stop-color`) and skip an invalid declaration, but only when it would override an existing attribute. A lone invalid value keeps the previous behavior (fallback at conversion), so the common, conflict-free path incurs no extra parse. Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 6 ++++ crates/usvg/src/parser/svgtree/parse.rs | 38 +++++++++++++++++++++++++ crates/usvg/tests/parser.rs | 22 ++++++++++++++ 3 files changed, 66 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index cb2e52f17..82dfc3461 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/crates/usvg/src/parser/svgtree/parse.rs b/crates/usvg/src/parser/svgtree/parse.rs index 2eae321b6..2489775e8 100644 --- a/crates/usvg/src/parser/svgtree/parse.rs +++ b/crates/usvg/src/parser/svgtree/parse.rs @@ -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; @@ -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, @@ -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 { + // `` 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 `` 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, diff --git a/crates/usvg/tests/parser.rs b/crates/usvg/tests/parser.rs index 905bbc0db..fd462f9ec 100644 --- a/crates/usvg/tests/parser.rs +++ b/crates/usvg/tests/parser.rs @@ -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 = " + + "; + + 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 = "