Resolve computed SVG root paint for inline resources

This commit is contained in:
ldm0
2026-08-18 01:56:15 +08:00
committed by Donough Liu
parent acda5d7b2d
commit 5785cbeb8b
4 changed files with 196 additions and 26 deletions
@@ -7,7 +7,10 @@
use std::sync::Arc;
use moli_layout::{LayoutImageResource, PaintColor, ReplacedMetrics};
use moli_layout::{LayoutImageResource, PaintColor, ReplacedMetrics, ResolvedLayoutStyle};
use style::color::ColorSpace;
use style::values::computed::{SVGPaint, SVGPaintKind};
use style_traits::ToCss;
use crate::{
document_runtime::DomHandle,
@@ -15,7 +18,7 @@ use crate::{
};
const SVG_NAMESPACE_ATTRIBUTE: &str = " xmlns=\"http://www.w3.org/2000/svg\"";
const SERIALIZED_SOURCE_INJECTION_RESERVE: usize = 256;
const SERIALIZED_SOURCE_FIXED_INJECTION_RESERVE: usize = 512;
pub(super) fn replaced_metrics(element: &Element) -> ReplacedMetrics {
let metadata = moli_image::svg_image_metadata_from_root_attributes(
@@ -35,11 +38,12 @@ pub(super) fn replaced_metrics(element: &Element) -> ReplacedMetrics {
pub(super) fn replaced_resource(
host: &DomHost,
node: DomHandle,
current_color: PaintColor,
font_size: f32,
style: &ResolvedLayoutStyle,
) -> Option<LayoutImageResource> {
let source_limit =
moli_image::MAX_ENCODED_SVG_BYTES.saturating_sub(SERIALIZED_SOURCE_INJECTION_RESERVE);
let declarations = computed_root_declarations(style);
let source_limit = moli_image::MAX_ENCODED_SVG_BYTES
.saturating_sub(declarations.len())
.saturating_sub(SERIALIZED_SOURCE_FIXED_INJECTION_RESERVE);
let source = match host.dom().outer_html_with_limit(node, source_limit) {
Ok(Some(source)) => source,
Ok(None) => return None,
@@ -52,7 +56,7 @@ pub(super) fn replaced_resource(
return None;
}
};
let Some(source) = prepare_source(source, current_color, font_size) else {
let Some(source) = prepare_source(source, &declarations) else {
tracing::debug!(
node = node.index(),
"inline SVG serialization did not produce an SVG root"
@@ -83,7 +87,7 @@ pub(super) fn replaced_resource(
})
}
fn prepare_source(mut source: String, current_color: PaintColor, font_size: f32) -> Option<String> {
fn prepare_source(mut source: String, declarations: &str) -> Option<String> {
if !source.starts_with("<svg")
|| !source
.as_bytes()
@@ -109,14 +113,13 @@ fn prepare_source(mut source: String, current_color: PaintColor, font_size: f32)
// Appending an important declaration also wins over an earlier authored
// declaration in the same serialized style attribute; the sampled value
// is already the final computed value, so this does not re-run cascade.
let declaration = computed_root_declarations(current_color, font_size);
let root_end = source.find('>')?;
if let Some(style_start) = source[..root_end].find(" style=\"") {
let value_start = style_start + " style=\"".len();
let value_end = value_start + source[value_start..root_end].find('"')?;
source.insert_str(value_end, &format!(";{declaration}"));
source.insert_str(value_end, &format!(";{declarations}"));
} else {
source.insert_str(root_end, &format!(" style=\"{declaration}\""));
source.insert_str(root_end, &format!(" style=\"{declarations}\""));
}
// HTML serialization uses this named entity while XML has no predefined
@@ -127,7 +130,125 @@ fn prepare_source(mut source: String, current_color: PaintColor, font_size: f32)
Some(source)
}
fn computed_root_declarations(color: PaintColor, font_size: f32) -> String {
fn computed_root_declarations(style: &ResolvedLayoutStyle) -> String {
let mut declarations = inherited_context_declarations(style.current_color(), style.font_size());
let Some(computed) = style.stylo_computed_values() else {
return declarations;
};
// Blink paints every SVG LayoutObject from its ComputedStyle. Moli's
// bounded usvg bridge instead serializes the live subtree, so document
// stylesheets are no longer present when usvg reparses it. Snapshot the
// root's inherited SVG paint group into that isolated document. Child
// declarations still override inherited values normally.
let current_color = computed.clone_color();
append_svg_paint(
&mut declarations,
"fill",
&computed.clone_fill(),
&current_color,
);
append_svg_paint(
&mut declarations,
"stroke",
&computed.clone_stroke(),
&current_color,
);
for (name, value) in [
(
"fill-opacity",
computed.clone_fill_opacity().to_css_string(),
),
("fill-rule", computed.clone_fill_rule().to_css_string()),
(
"stroke-opacity",
computed.clone_stroke_opacity().to_css_string(),
),
(
"stroke-width",
computed.clone_stroke_width().to_css_string(),
),
(
"stroke-dasharray",
computed.clone_stroke_dasharray().to_css_string(),
),
(
"stroke-dashoffset",
computed.clone_stroke_dashoffset().to_css_string(),
),
(
"stroke-linecap",
computed.clone_stroke_linecap().to_css_string(),
),
(
"stroke-linejoin",
computed.clone_stroke_linejoin().to_css_string(),
),
(
"stroke-miterlimit",
computed.clone_stroke_miterlimit().to_css_string(),
),
("clip-rule", computed.clone_clip_rule().to_css_string()),
("paint-order", computed.clone_paint_order().to_css_string()),
(
"shape-rendering",
computed.clone_shape_rendering().to_css_string(),
),
] {
append_serialized_declaration(&mut declarations, name, &value);
}
declarations
}
fn inherited_context_declarations(color: PaintColor, font_size: f32) -> String {
let font_size = if font_size.is_finite() {
font_size.max(0.0)
} else {
16.0
};
format!(
"color:{} !important;font-size:{font_size:.6}px !important;",
rgba_css(color),
)
}
fn append_svg_paint(
declarations: &mut String,
name: &str,
paint: &SVGPaint,
current_color: &style::color::AbsoluteColor,
) {
// A computed external paint-server URL needs document URL/resource state
// that the isolated usvg document does not own. Preserve the serialized
// authored value until that resource bridge exists. Blink resolves a
// color paint through VisitedDependentColor() immediately before drawing;
// resolve currentColor at this equivalent paint-snapshot boundary.
match &paint.kind {
SVGPaintKind::Color(color) => {
let absolute = color
.resolve_to_absolute(current_color)
.to_color_space(ColorSpace::Srgb);
let [red, green, blue, alpha] = *absolute.raw_components();
declarations.push_str(name);
declarations.push(':');
declarations.push_str(&rgba_css(PaintColor::new(red, green, blue, alpha)));
declarations.push_str(" !important;");
}
SVGPaintKind::PaintServer(_) => {}
SVGPaintKind::None | SVGPaintKind::ContextFill | SVGPaintKind::ContextStroke => {
append_serialized_declaration(declarations, name, &paint.to_css_string());
}
}
}
fn append_serialized_declaration(declarations: &mut String, name: &str, value: &str) {
declarations.push_str(name);
declarations.push(':');
declarations.push_str(value);
declarations.push_str(" !important;");
}
fn rgba_css(color: PaintColor) -> String {
let channel = |value: f32| {
let value = if value.is_finite() { value } else { 0.0 };
(value.clamp(0.0, 1.0) * 255.0).round() as u8
@@ -137,13 +258,8 @@ fn computed_root_declarations(color: PaintColor, font_size: f32) -> String {
} else {
0.0
};
let font_size = if font_size.is_finite() {
font_size.max(0.0)
} else {
16.0
};
format!(
"color:rgba({},{},{},{alpha:.6}) !important;font-size:{font_size:.6}px !important",
"rgba({},{},{},{alpha:.6})",
channel(color.red),
channel(color.green),
channel(color.blue),
@@ -156,10 +272,11 @@ mod tests {
#[test]
fn source_bridge_adds_xml_namespace_and_resolved_current_color() {
let declarations =
inherited_context_declarations(PaintColor::new(1.0, 0.0, 0.0, 1.0), 16.0);
let source = prepare_source(
"<svg viewBox=\"0 0 2 1\" style=\"display:block\"><rect width=\"2\" height=\"1\" fill=\"currentColor\"></rect></svg>".to_owned(),
PaintColor::new(1.0, 0.0, 0.0, 1.0),
16.0,
&declarations,
)
.unwrap();
assert!(source.starts_with("<svg xmlns=\"http://www.w3.org/2000/svg\""));
@@ -167,12 +167,9 @@ impl LayoutSource for NativeLayoutSourceView<'_> {
svg: ready.svg,
})
}
Some(LayoutReplacedKind::Svg) => super::inline_svg::replaced_resource(
self.host(),
node,
style.current_color(),
style.font_size(),
),
Some(LayoutReplacedKind::Svg) => {
super::inline_svg::replaced_resource(self.host(), node, style)
}
Some(LayoutReplacedKind::Canvas) => {
let pixels = self.runtime.canvas_pixels_for_layout(node)?;
Some(LayoutImageResource {
@@ -0,0 +1,55 @@
use super::*;
#[tokio::test(flavor = "current_thread")]
async fn screenshot_projects_external_svg_root_paint_like_chromium() {
run_page_vm_async_test(async move {
let loader =
crate::network::ResourceRequestClient::new(&FetchConfig::default()).expect("loader");
let mut page_vm = test_page_vm_with_loader_and_document_url(
&loader,
Vec::new(),
Url::parse("https://example.com/inline-svg-computed-paint.html")?,
);
page_vm.vm_mut().eval(
r#"
document.head.innerHTML = `<style>
html,body{margin:0;background:white}
#run{position:absolute;left:0;top:0;width:80px;height:40px;margin:0;padding:0;border:0;background:rgb(174,67,28);color:white}
.icon{position:absolute;left:30px;top:10px;display:block;width:20px;height:20px;fill:currentcolor}
#stroke{position:absolute;left:100px;top:10px;display:block;width:20px;height:20px;color:rgb(0,128,0);fill:none;stroke:currentcolor;stroke-width:4px;stroke-linecap:butt}
</style>`;
document.body.innerHTML = `
<button id=run><svg id=run-icon class=icon viewBox="0 0 20 20"><path d="M2 2v16l16-8z"></path></svg></button>
<svg id=stroke viewBox="0 0 20 20"><path d="M2 10h16"></path></svg>`;
'installed'
"#,
)?;
page_vm.vm_mut().sync_live_document_style_sources();
assert_eq!(
page_vm.vm_mut().eval(
"[getComputedStyle(document.getElementById('run-icon')).fill,getComputedStyle(document.getElementById('stroke')).stroke].join('|')",
)?,
"rgb(255, 255, 255)|rgb(0, 128, 0)",
"Stylo must resolve the external SVG paint rules before the paint bridge snapshots them",
);
let snapshot = page_vm
.vm_mut()
.screenshot_layout_snapshot(moli_layout::PaintViewport::new(140, 50, 1.0))?
.expect("inline SVG computed-paint fixture must retain a layout root");
let raster = moli_paint::raster_snapshot(&snapshot)?;
let pixel = |x: u32, y: u32| {
let index = ((y * raster.width + x) * 4) as usize;
<[u8; 4]>::try_from(&raster.rgba[index..index + 4]).expect("RGBA pixel")
};
assert_eq!(pixel(40, 20), [255, 255, 255, 255]);
assert_eq!(pixel(15, 20), [174, 67, 28, 255]);
assert_eq!(pixel(110, 20), [0, 128, 0, 255]);
assert_eq!(pixel(90, 20), [255, 255, 255, 255]);
Ok::<_, anyhow::Error>(())
})
.await
.expect("inline SVG computed-paint fixture should run");
}
@@ -126,6 +126,7 @@ mod hash_change_delivery;
mod history_traversal;
mod image_load_event;
mod indexed_db;
mod inline_svg_paint;
mod internal_loading_completion;
mod lifecycle;
mod main_document_post_parse_completion;