script: Load and rasterize favicons before passing them to the embedder (#38949)

Currently the embedding API only provides the embedder with the URL for
a favicon. This is not great, for multiple reasons:
* Loading the icon should happen according to the fetch spec which is
not easy for the embedder to recreate (consider CSP, timing information
etc)
* Rasterizing a svg favicon is not trivial

With this change, servo fetches and rasterizes the icon to a bitmap
which is then passed to the embedder.

Testing: I'm not sure how I can write tests for the embedding api. I've
tested the correctness manually using
https://github.com/servo/servo/pull/36680.
Prepares for https://github.com/servo/servo/pull/36680

---------

Signed-off-by: Simon Wülker <simon.wuelker@arcor.de>
This commit is contained in:
Simon Wülker 2025-08-27 16:28:42 +02:00 committed by GitHub
parent a5d890c13a
commit dcd25072d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 373 additions and 159 deletions

View file

@ -11,8 +11,13 @@ use base::id::WebViewId;
use dom_struct::dom_struct;
use embedder_traits::EmbedderMsg;
use html5ever::{LocalName, Prefix, local_name, ns};
use ipc_channel::ipc::IpcSender;
use js::rust::HandleObject;
use mime::Mime;
use net_traits::image_cache::{
Image, ImageCache, ImageCacheResponseMessage, ImageCacheResult, ImageLoadListener,
ImageOrMetadataAvailable, ImageResponse, PendingImageId, UsePlaceholder,
};
use net_traits::mime_classifier::{MediaType, MimeClassifier};
use net_traits::policy_container::PolicyContainer;
use net_traits::request::{
@ -20,15 +25,17 @@ use net_traits::request::{
RequestId,
};
use net_traits::{
FetchMetadata, FetchResponseListener, NetworkError, ReferrerPolicy, ResourceFetchTiming,
ResourceTimingType,
FetchMetadata, FetchResponseListener, FetchResponseMsg, NetworkError, ReferrerPolicy,
ResourceFetchTiming, ResourceTimingType,
};
use pixels::PixelFormat;
use script_bindings::root::Dom;
use servo_arc::Arc;
use servo_url::{ImmutableOrigin, ServoUrl};
use style::attr::AttrValue;
use style::stylesheets::Stylesheet;
use stylo_atoms::Atom;
use webrender_api::units::DeviceIntSize;
use crate::dom::attr::Attr;
use crate::dom::bindings::cell::DomRefCell;
@ -272,8 +279,7 @@ impl VirtualMethods for HTMLLinkElement {
}
if self.relations.get().contains(LinkRelations::ICON) {
let sizes = get_attr(self.upcast(), &local_name!("sizes"));
self.handle_favicon_url(&attr.value(), &sizes);
self.handle_favicon_url();
}
// https://html.spec.whatwg.org/multipage/#link-type-prefetch
@ -291,9 +297,7 @@ impl VirtualMethods for HTMLLinkElement {
}
},
local_name!("sizes") if self.relations.get().contains(LinkRelations::ICON) => {
if let Some(ref href) = get_attr(self.upcast(), &local_name!("href")) {
self.handle_favicon_url(href, &Some(attr.value().to_string()));
}
self.handle_favicon_url();
},
local_name!("crossorigin") => {
// https://html.spec.whatwg.org/multipage/#link-type-prefetch
@ -396,8 +400,7 @@ impl VirtualMethods for HTMLLinkElement {
}
if relations.contains(LinkRelations::ICON) {
let sizes = get_attr(self.upcast(), &local_name!("sizes"));
self.handle_favicon_url(&href, &sizes);
self.handle_favicon_url();
}
if relations.contains(LinkRelations::PREFETCH) {
@ -483,6 +486,48 @@ impl HTMLLinkElement {
options
}
/// <https://html.spec.whatwg.org/multipage/#default-fetch-and-process-the-linked-resource>
///
/// This method does not implement Step 7 (fetching the request) and instead returns the [RequestBuilder],
/// as the fetch context that should be used depends on the link type.
fn default_fetch_and_process_the_linked_resource(&self) -> Option<RequestBuilder> {
// Step 1. Let options be the result of creating link options from el.
let options = self.processing_options();
// Step 2. Let request be the result of creating a link request given options.
let Some(request) = options.create_link_request(self.owner_window().webview_id()) else {
// Step 3. If request is null, then return.
return None;
};
// Step 4. Set request's synchronous flag.
let mut request = request.synchronous(true);
// Step 5. Run the linked resource fetch setup steps, given el and request. If the result is false, then return.
if !self.linked_resource_fetch_setup(&mut request) {
return None;
}
// TODO Step 6. Set request's initiator type to "css" if el's rel attribute
// contains the keyword stylesheet; "link" otherwise.
// Step 7. Fetch request with processResponseConsumeBody set to the following steps given response response and null,
// failure, or a byte sequence bodyBytes: [..]
Some(request)
}
/// <https://html.spec.whatwg.org/multipage/#linked-resource-fetch-setup-steps>
fn linked_resource_fetch_setup(&self, request: &mut RequestBuilder) -> bool {
if self.relations.get().contains(LinkRelations::ICON) {
// Step 1. Set request's destination to "image".
request.destination = Destination::Image;
// Step 2. Return true.
return true;
}
true
}
/// The `fetch and process the linked resource` algorithm for [`rel="prefetch"`](https://html.spec.whatwg.org/multipage/#link-type-prefetch)
fn fetch_and_process_prefetch_link(&self, href: &str) {
// Step 1. If el's href attribute's value is the empty string, then return.
@ -589,17 +634,162 @@ impl HTMLLinkElement {
}
}
fn handle_favicon_url(&self, href: &str, _sizes: &Option<String>) {
let document = self.owner_document();
match document.base_url().join(href) {
Ok(url) => {
let window = document.window();
if window.is_top_level() {
let msg = EmbedderMsg::NewFavicon(document.webview_id(), url.clone());
window.send_to_embedder(msg);
fn handle_favicon_url(&self) {
// The spec does not specify this, but we don't fetch favicons for iframes, as
// they won't be displayed anyways.
let window = self.owner_window();
if !window.is_top_level() {
return;
}
let Ok(href) = self.Href().parse() else {
return;
};
// Ignore all previous fetch operations
self.request_generation_id
.set(self.request_generation_id.get().increment());
let cache_result = window.image_cache().get_cached_image_status(
href,
window.origin().immutable().clone(),
cors_setting_for_element(self.upcast()),
UsePlaceholder::No,
);
match cache_result {
ImageCacheResult::Available(ImageOrMetadataAvailable::ImageAvailable {
image,
is_placeholder,
..
}) => {
debug_assert!(!is_placeholder);
self.process_favicon_response(image);
},
ImageCacheResult::Available(ImageOrMetadataAvailable::MetadataAvailable(_, id)) |
ImageCacheResult::Pending(id) => {
let sender = self.register_image_cache_callback(id);
window.image_cache().add_listener(ImageLoadListener::new(
sender,
window.pipeline_id(),
id,
));
},
ImageCacheResult::ReadyForRequest(id) => {
let Some(request) = self.default_fetch_and_process_the_linked_resource() else {
return;
};
let sender = self.register_image_cache_callback(id);
window.image_cache().add_listener(ImageLoadListener::new(
sender,
window.pipeline_id(),
id,
));
let document = self.upcast::<Node>().owner_doc();
let fetch_context = FaviconFetchContext {
url: self.owner_document().base_url(),
image_cache: window.image_cache(),
id,
link: Trusted::new(self),
resource_timing: ResourceFetchTiming::new(ResourceTimingType::Resource),
};
document.fetch_background(request, fetch_context);
},
ImageCacheResult::LoadError => {},
};
}
fn register_image_cache_callback(
&self,
id: PendingImageId,
) -> IpcSender<ImageCacheResponseMessage> {
let trusted_node = Trusted::new(self);
let window = self.owner_window();
let request_generation_id = self.get_request_generation_id();
window.register_image_cache_listener(id, move |response| {
let trusted_node = trusted_node.clone();
let link_element = trusted_node.root();
let window = link_element.owner_window();
let ImageResponse::Loaded(image, _) = response.response else {
// We don't care about metadata and such for favicons.
return;
};
if request_generation_id != link_element.get_request_generation_id() {
// This load is no longer relevant.
return;
};
window
.as_global_scope()
.task_manager()
.networking_task_source()
.queue(task!(process_favicon_response: move || {
let element = trusted_node.root();
if request_generation_id != element.get_request_generation_id() {
// This load is no longer relevant.
return;
};
element.process_favicon_response(image);
}));
})
}
/// Rasterizes a loaded favicon file if necessary and notifies the embedder about it.
fn process_favicon_response(&self, image: Image) {
// TODO: Include the size attribute here
let window = self.owner_window();
let send_rasterized_favicon_to_embedder = |raster_image: &pixels::RasterImage| {
// Let's not worry about animated favicons...
let frame = raster_image.first_frame();
let format = match raster_image.format {
PixelFormat::K8 => embedder_traits::PixelFormat::K8,
PixelFormat::KA8 => embedder_traits::PixelFormat::KA8,
PixelFormat::RGB8 => embedder_traits::PixelFormat::RGB8,
PixelFormat::RGBA8 => embedder_traits::PixelFormat::RGBA8,
PixelFormat::BGRA8 => embedder_traits::PixelFormat::BGRA8,
};
let embedder_image = embedder_traits::Image::new(
frame.width,
frame.height,
raster_image.bytes.clone(),
raster_image.frames[0].byte_range.clone(),
format,
);
window.send_to_embedder(EmbedderMsg::NewFavicon(window.webview_id(), embedder_image));
};
match image {
Image::Raster(raster_image) => send_rasterized_favicon_to_embedder(&raster_image),
Image::Vector(vector_image) => {
// This size is completely arbitrary.
let size = DeviceIntSize::new(250, 250);
let image_cache = window.image_cache();
if let Some(raster_image) =
image_cache.rasterize_vector_image(vector_image.id, size)
{
send_rasterized_favicon_to_embedder(&raster_image);
} else {
// The rasterization callback will end up calling "process_favicon_response" again,
// but this time with a raster image.
let image_cache_sender = self.register_image_cache_callback(vector_image.id);
image_cache.add_rasterization_complete_listener(
window.pipeline_id(),
vector_image.id,
size,
image_cache_sender,
);
}
},
Err(e) => debug!("Parsing url {} failed: {}", href, e),
}
}
@ -962,6 +1152,93 @@ fn translate_a_preload_destination(potential_destination: &str) -> Destination {
}
}
struct FaviconFetchContext {
/// The `<link>` element that caused this fetch operation
link: Trusted<HTMLLinkElement>,
image_cache: std::sync::Arc<dyn ImageCache>,
id: PendingImageId,
/// The base url of the document that the `<link>` element belongs to.
url: ServoUrl,
resource_timing: ResourceFetchTiming,
}
impl FetchResponseListener for FaviconFetchContext {
fn process_request_body(&mut self, _: RequestId) {}
fn process_request_eof(&mut self, _: RequestId) {}
fn process_response(
&mut self,
request_id: RequestId,
metadata: Result<FetchMetadata, NetworkError>,
) {
self.image_cache.notify_pending_response(
self.id,
FetchResponseMsg::ProcessResponse(request_id, metadata.clone()),
);
}
fn process_response_chunk(&mut self, request_id: RequestId, chunk: Vec<u8>) {
self.image_cache.notify_pending_response(
self.id,
FetchResponseMsg::ProcessResponseChunk(request_id, chunk),
);
}
fn process_response_eof(
&mut self,
request_id: RequestId,
response: Result<ResourceFetchTiming, NetworkError>,
) {
self.image_cache.notify_pending_response(
self.id,
FetchResponseMsg::ProcessResponseEOF(request_id, response),
);
}
fn resource_timing_mut(&mut self) -> &mut ResourceFetchTiming {
&mut self.resource_timing
}
fn resource_timing(&self) -> &ResourceFetchTiming {
&self.resource_timing
}
fn submit_resource_timing(&mut self) {
submit_timing(self, CanGc::note())
}
fn process_csp_violations(&mut self, _request_id: RequestId, violations: Vec<Violation>) {
let global = &self.resource_timing_global();
let link = self.link.root();
let source_position = link
.upcast::<Element>()
.compute_source_position(link.line_number as u32);
global.report_csp_violations(violations, None, Some(source_position));
}
}
impl ResourceTimingListener for FaviconFetchContext {
fn resource_timing_information(&self) -> (InitiatorType, ServoUrl) {
(
InitiatorType::LocalName("link".to_string()),
self.url.clone(),
)
}
fn resource_timing_global(&self) -> DomRoot<GlobalScope> {
self.link.root().upcast::<Node>().owner_doc().global()
}
}
impl PreInvoke for FaviconFetchContext {
fn should_invoke(&self) -> bool {
true
}
}
struct PrefetchContext {
/// The `<link>` element that caused this prefetch operation
link: Trusted<HTMLLinkElement>,

View file

@ -799,9 +799,9 @@ impl Servo {
webview.set_cursor(cursor);
}
},
EmbedderMsg::NewFavicon(webview_id, url) => {
EmbedderMsg::NewFavicon(webview_id, image) => {
if let Some(webview) = self.get_webview_handle(webview_id) {
webview.set_favicon_url(url.into_url());
webview.set_favicon(image);
}
},
EmbedderMsg::NotifyLoadStatusChanged(webview_id, load_status) => {

View file

@ -13,7 +13,7 @@ use compositing_traits::WebViewTrait;
use constellation_traits::{EmbedderToConstellationMessage, TraversalDirection};
use dpi::PhysicalSize;
use embedder_traits::{
Cursor, FocusId, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus,
Cursor, FocusId, Image, InputEvent, JSValue, JavaScriptEvaluationError, LoadStatus,
MediaSessionActionType, ScreenGeometry, Theme, TraversalId, ViewportDetails,
};
use euclid::{Point2D, Scale, Size2D};
@ -84,7 +84,7 @@ pub(crate) struct WebViewInner {
url: Option<Url>,
status_text: Option<String>,
page_title: Option<String>,
favicon_url: Option<Url>,
favicon: Option<Image>,
focused: bool,
animating: bool,
cursor: Cursor,
@ -126,7 +126,7 @@ impl WebView {
url: None,
status_text: None,
page_title: None,
favicon_url: None,
favicon: None,
focused: false,
animating: false,
cursor: Cursor::Pointer,
@ -264,21 +264,13 @@ impl WebView {
self.delegate().notify_page_title_changed(self, new_value);
}
pub fn favicon_url(&self) -> Option<Url> {
self.inner().favicon_url.clone()
pub fn favicon(&self) -> Option<Ref<'_, Image>> {
Ref::filter_map(self.inner(), |inner| inner.favicon.as_ref()).ok()
}
pub(crate) fn set_favicon_url(self, new_value: Url) {
if self
.inner()
.favicon_url
.as_ref()
.is_some_and(|url| url == &new_value)
{
return;
}
self.inner_mut().favicon_url = Some(new_value.clone());
self.delegate().notify_favicon_url_changed(self, new_value);
pub(crate) fn set_favicon(self, new_value: Image) {
self.inner_mut().favicon = Some(new_value);
self.delegate().notify_favicon_changed(self);
}
pub fn focused(&self) -> bool {

View file

@ -432,9 +432,9 @@ pub trait WebViewDelegate {
/// The [`Cursor`] of the currently loaded page in this [`WebView`] has changed. The new
/// cursor can accessed via [`WebView::cursor`].
fn notify_cursor_changed(&self, _webview: WebView, _: Cursor) {}
/// The favicon [`Url`] of the currently loaded page in this [`WebView`] has changed. The new
/// favicon [`Url`] can accessed via [`WebView::favicon_url`].
fn notify_favicon_url_changed(&self, _webview: WebView, _: Url) {}
/// The favicon of the currently loaded page in this [`WebView`] has changed. The new
/// favicon [`Image`] can accessed via [`WebView::favicon`].
fn notify_favicon_changed(&self, _webview: WebView) {}
/// Notify the embedder that it needs to present a new frame.
fn notify_new_frame_ready(&self, _webview: WebView) {}

View file

@ -17,6 +17,7 @@ use std::collections::HashMap;
use std::ffi::c_void;
use std::fmt::{Debug, Display, Error, Formatter};
use std::hash::Hash;
use std::ops::Range;
use std::path::PathBuf;
use std::sync::Arc;
@ -24,7 +25,7 @@ use base::id::{PipelineId, WebViewId};
use crossbeam_channel::Sender;
use euclid::{Point2D, Scale, Size2D};
use http::{HeaderMap, Method, StatusCode};
use ipc_channel::ipc::IpcSender;
use ipc_channel::ipc::{IpcSender, IpcSharedMemory};
use log::warn;
use malloc_size_of::malloc_size_of_is_0;
use malloc_size_of_derive::MallocSizeOf;
@ -358,6 +359,54 @@ impl TraversalId {
}
}
#[derive(Clone, Copy, Deserialize, Eq, Hash, PartialEq, Serialize)]
pub enum PixelFormat {
/// Luminance channel only
K8,
/// Luminance + alpha
KA8,
/// RGB, 8 bits per channel
RGB8,
/// RGB + alpha, 8 bits per channel
RGBA8,
/// BGR + alpha, 8 bits per channel
BGRA8,
}
/// A raster image buffer.
#[derive(Clone, Deserialize, Serialize)]
pub struct Image {
pub width: u32,
pub height: u32,
pub format: PixelFormat,
/// A shared memory block containing the data of one or more image frames.
data: IpcSharedMemory,
range: Range<usize>,
}
impl Image {
pub fn new(
width: u32,
height: u32,
data: IpcSharedMemory,
range: Range<usize>,
format: PixelFormat,
) -> Self {
Self {
width,
height,
format,
data,
range,
}
}
/// Return the bytes belonging to the first image frame.
pub fn data(&self) -> &[u8] {
&self.data[self.range.clone()]
}
}
#[derive(Deserialize, IntoStaticStr, Serialize)]
pub enum EmbedderMsg {
/// A status message to be displayed by the browser chrome.
@ -411,7 +460,7 @@ pub enum EmbedderMsg {
/// Changes the cursor.
SetCursor(WebViewId, Cursor),
/// A favicon was detected
NewFavicon(WebViewId, ServoUrl),
NewFavicon(WebViewId, Image),
/// The history state has changed.
HistoryChanged(WebViewId, Vec<ServoUrl>, usize),
/// A history traversal operation completed.

View file

@ -15,6 +15,7 @@ use std::hash::Hasher;
use std::net::IpAddr;
use std::ops::{Index, Range, RangeFrom, RangeFull, RangeTo};
use std::path::Path;
use std::str::FromStr;
use malloc_size_of_derive::MallocSizeOf;
use serde::{Deserialize, Serialize};
@ -307,3 +308,12 @@ impl From<Arc<Url>> for ServoUrl {
ServoUrl(url)
}
}
impl FromStr for ServoUrl {
type Err = <Url as FromStr>::Err;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let url = Url::from_str(value)?;
Ok(url.into())
}
}

View file

@ -1,4 +0,0 @@
[icon-blocked.sub.html]
expected: TIMEOUT
[Test that spv event is fired]
expected: NOTRUN

View file

@ -1,67 +1,6 @@
[element-link-icon.https.sub.html]
expected: TIMEOUT
[sec-fetch-site - Same origin no attributes]
expected: TIMEOUT
[sec-fetch-site - Cross-site no attributes]
expected: NOTRUN
[sec-fetch-site - Same site no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Origin -> Cross-Site -> Same-Origin redirect no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Origin -> Same-Site -> Same-Origin redirect no attributes]
expected: NOTRUN
[sec-fetch-site - Cross-Site -> Same Origin no attributes]
expected: NOTRUN
[sec-fetch-site - Cross-Site -> Same-Site no attributes]
expected: NOTRUN
[sec-fetch-site - Cross-Site -> Cross-Site no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Origin -> Same Origin no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Origin -> Same-Site no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Origin -> Cross-Site no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Site -> Same Origin no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Site -> Same-Site no attributes]
expected: NOTRUN
[sec-fetch-site - Same-Site -> Cross-Site no attributes]
expected: NOTRUN
[sec-fetch-mode no attributes]
expected: NOTRUN
[sec-fetch-mode attributes: crossorigin]
expected: NOTRUN
[sec-fetch-mode attributes: crossorigin=anonymous]
expected: NOTRUN
[sec-fetch-mode attributes: crossorigin=use-credentials]
expected: NOTRUN
[sec-fetch-dest no attributes]
expected: NOTRUN
[sec-fetch-user no attributes]
expected: NOTRUN
expected: FAIL
[sec-fetch-storage-access - Cross-site no attributes]
expected: NOTRUN
[sec-fetch-storage-access - Same site no attributes]
expected: NOTRUN
expected: FAIL

View file

@ -1,55 +1,6 @@
[element-link-icon.sub.html]
expected: TIMEOUT
[sec-fetch-site - Not sent to non-trustworthy same-origin destination no attributes]
expected: TIMEOUT
[sec-fetch-site - Not sent to non-trustworthy same-site destination no attributes]
expected: NOTRUN
[sec-fetch-site - Not sent to non-trustworthy cross-site destination no attributes]
expected: NOTRUN
[sec-fetch-mode - Not sent to non-trustworthy same-origin destination no attributes]
expected: NOTRUN
[sec-fetch-mode - Not sent to non-trustworthy same-site destination no attributes]
expected: NOTRUN
[sec-fetch-mode - Not sent to non-trustworthy cross-site destination no attributes]
expected: NOTRUN
[sec-fetch-dest - Not sent to non-trustworthy same-origin destination no attributes]
expected: NOTRUN
[sec-fetch-dest - Not sent to non-trustworthy same-site destination no attributes]
expected: NOTRUN
[sec-fetch-dest - Not sent to non-trustworthy cross-site destination no attributes]
expected: NOTRUN
[sec-fetch-user - Not sent to non-trustworthy same-origin destination no attributes]
expected: NOTRUN
[sec-fetch-user - Not sent to non-trustworthy same-site destination no attributes]
expected: NOTRUN
[sec-fetch-user - Not sent to non-trustworthy cross-site destination no attributes]
expected: NOTRUN
[sec-fetch-site - HTTPS downgrade (header not sent) no attributes]
expected: NOTRUN
expected: FAIL
[sec-fetch-site - HTTPS upgrade no attributes]
expected: NOTRUN
[sec-fetch-site - HTTPS downgrade-upgrade no attributes]
expected: NOTRUN
[sec-fetch-storage-access - Not sent to non-trustworthy same-origin destination no attributes]
expected: NOTRUN
[sec-fetch-storage-access - Not sent to non-trustworthy same-site destination no attributes]
expected: NOTRUN
[sec-fetch-storage-access - Not sent to non-trustworthy cross-site destination no attributes]
expected: NOTRUN
expected: FAIL