mirror of
https://github.com/lexmount/moli.git
synced 2026-10-07 16:01:01 +00:00
Resolve computed SVG root paint for inline resources
This commit is contained in:
@@ -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(),
|
||||
¤t_color,
|
||||
);
|
||||
append_svg_paint(
|
||||
&mut declarations,
|
||||
"stroke",
|
||||
&computed.clone_stroke(),
|
||||
¤t_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;
|
||||
|
||||
Reference in New Issue
Block a user