servo/components/script/drag_data_store.rs
Mukilan Thiyagarajan 8a20e42de4
Add support for static SVG images using resvg crate (#36721)
This change adds support for rendering static SVG images using the
`resvg` crate, allowing svg sources in the `img` tag and in CSS
`background` and `content` properties. There are some limitations in
using resvg:

1. There is no support for animations or interactivity as these would
require implementing the full DOM layer of SVG specification.
2. Only system fonts can be used for text rendering. There is some
mechanism to provide a custom font resolver to usvg, but that is not
explored in this change.
3. resvg's handling of certain edge cases involving lack of explicit
`width` and `height` on the root svg element deviates from what the
specification expects from browsers. For example, resvg uses the values
in `viewBox` to derive the missing width or height dimension, but
without scaling that dimension to preserve the aspect ratio. It also
doesn't allow overriding this behavior.

Demo screenshot:
![servo - resvg
img](https://github.com/user-attachments/assets/8ecb2de2-ab7c-48e2-9f08-2d09d2cb8791)

<details>
<summary>Source</summary>

```
<style>
 #svg1 {
   border: 1px solid red;
 }

 #svg2 {
   border: 1px solid red;
   width: 300px;
 }
 #svg3 {
   border: 1px solid red;
   width: 300px;
   height: 200px;
   object-fit: contain;
 }
 #svg4 {
   border: 1px solid red;
   width: 300px;
   height: 200px;
   object-fit: cover;
 }
 #svg5 {
   border: 1px solid red;
   width: 300px;
   height: 200px;
   object-fit: fill;
 }
 #svg6 {
   border: 1px solid red;
   width: 300px;
   height: 200px;
   object-fit: none;
 }
</style>
</head>
<body>
        <div>
          <img id="svg1" src="https://raw.githubusercontent.com/servo/servo/refs/heads/main/resources/servo.svg" alt="Servo logo">
        </div>
        <div>
          <img id="svg2" src="https://raw.githubusercontent.com/servo/servo/refs/heads/main/resources/servo.svg" alt="Servo logo">
          <img id="svg3" src="https://raw.githubusercontent.com/servo/servo/refs/heads/main/resources/servo.svg" alt="Servo logo">
          <img id="svg4" src="https://raw.githubusercontent.com/servo/servo/refs/heads/main/resources/servo.svg" alt="Servo logo">
        </div>
        <div>
          <img id="svg5" src="https://raw.githubusercontent.com/servo/servo/refs/heads/main/resources/servo.svg" alt="Servo logo">
          <img id="svg6" src="https://raw.githubusercontent.com/servo/servo/refs/heads/main/resources/servo.svg" alt="Servo logo">
        </div>
</body>
```

</details>

---------

Signed-off-by: Mukilan Thiyagarajan <mukilan@igalia.com>
Signed-off-by: Martin Robinson <mrobinson@igalia.com>
Co-authored-by: Martin Robinson <mrobinson@igalia.com>
2025-05-27 11:02:40 +00:00

287 lines
8.8 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::sync::Arc;
use constellation_traits::BlobImpl;
use indexmap::IndexMap;
use pixels::RasterImage;
use crate::dom::bindings::error::{Error, Fallible};
use crate::dom::bindings::root::DomRoot;
use crate::dom::bindings::str::DOMString;
use crate::dom::file::File;
use crate::dom::globalscope::GlobalScope;
use crate::script_runtime::CanGc;
/// <https://html.spec.whatwg.org/multipage/#the-drag-data-item-kind>
pub(crate) enum Kind {
Text {
data: DOMString,
type_: DOMString,
},
File {
bytes: Vec<u8>,
name: DOMString,
type_: String,
},
}
impl Kind {
pub(crate) fn type_(&self) -> DOMString {
match self {
Kind::Text { type_, .. } => type_.clone(),
Kind::File { type_, .. } => DOMString::from(type_.clone()),
}
}
pub(crate) fn as_string(&self) -> Option<String> {
match self {
Kind::Text { data, .. } => Some(data.to_string()),
Kind::File { .. } => None,
}
}
// TODO for now we create a new BlobImpl
// since File constructor requires moving it.
pub(crate) fn as_file(&self, global: &GlobalScope, can_gc: CanGc) -> Option<DomRoot<File>> {
match self {
Kind::Text { .. } => None,
Kind::File { bytes, name, type_ } => Some(File::new(
global,
BlobImpl::new_from_bytes(bytes.clone(), type_.clone()),
name.clone(),
None,
can_gc,
)),
}
}
fn text_type_matches(&self, text_type: &str) -> bool {
matches!(self, Kind::Text { type_, .. } if type_.eq(text_type))
}
fn is_file(&self) -> bool {
matches!(self, Kind::File { .. })
}
}
/// <https://html.spec.whatwg.org/multipage/#drag-data-store-bitmap>
#[allow(dead_code)] // TODO this used by DragEvent.
struct Bitmap {
image: Option<Arc<RasterImage>>,
x: i32,
y: i32,
}
/// Control the behaviour of the drag data store
#[derive(Clone, Copy, Eq, PartialEq)]
pub(crate) enum Mode {
/// <https://html.spec.whatwg.org/multipage/#concept-dnd-rw>
ReadWrite,
/// <https://html.spec.whatwg.org/multipage/#concept-dnd-ro>
ReadOnly,
/// <https://html.spec.whatwg.org/multipage/#concept-dnd-p>
Protected,
}
#[allow(dead_code)] // TODO some fields are used by DragEvent.
pub(crate) struct DragDataStore {
/// <https://html.spec.whatwg.org/multipage/#drag-data-store-item-list>
item_list: IndexMap<u16, Kind>,
next_item_id: u16,
/// <https://html.spec.whatwg.org/multipage/#drag-data-store-default-feedback>
default_feedback: Option<String>,
bitmap: Option<Bitmap>,
mode: Mode,
/// <https://html.spec.whatwg.org/multipage/#drag-data-store-allowed-effects-state>
allowed_effects_state: String,
pub clear_was_called: bool,
}
impl DragDataStore {
/// <https://html.spec.whatwg.org/multipage/#create-a-drag-data-store>
// We don't really need it since it's only instantiated by DataTransfer.
#[allow(clippy::new_without_default)]
pub(crate) fn new() -> DragDataStore {
DragDataStore {
item_list: IndexMap::new(),
next_item_id: 0,
default_feedback: None,
bitmap: None,
mode: Mode::Protected,
allowed_effects_state: String::from("uninitialized"),
clear_was_called: false,
}
}
/// Get the drag data store mode
pub(crate) fn mode(&self) -> Mode {
self.mode
}
/// Set the drag data store mode
pub(crate) fn set_mode(&mut self, mode: Mode) {
self.mode = mode;
}
pub(crate) fn set_bitmap(&mut self, image: Option<Arc<RasterImage>>, x: i32, y: i32) {
self.bitmap = Some(Bitmap { image, x, y });
}
/// <https://html.spec.whatwg.org/multipage/#concept-datatransfer-types>
pub(crate) fn types(&self) -> Vec<DOMString> {
let mut types = Vec::new();
let has_files = self.item_list.values().fold(false, |has_files, item| {
// Step 2.1 For each item in the item list whose kind is text,
// add an entry to L consisting of the item's type string.
match item {
Kind::Text { type_, .. } => types.push(type_.clone()),
Kind::File { .. } => return true,
}
has_files
});
// Step 2.2 If there are any items in the item list whose kind is File,
// add an entry to L consisting of the string "Files".
if has_files {
types.push(DOMString::from("Files"));
}
types
}
pub(crate) fn find_matching_text(&self, type_: &str) -> Option<DOMString> {
self.item_list
.values()
.find(|item| item.text_type_matches(type_))
.and_then(|item| item.as_string())
.map(DOMString::from)
}
pub(crate) fn add(&mut self, kind: Kind) -> Fallible<u16> {
if let Kind::Text { ref type_, .. } = kind {
// Step 2.1 If there is already an item in the item list whose kind is text
// and whose type string is equal to the method's second argument, throw "NotSupportedError".
if self
.item_list
.values()
.any(|item| item.text_type_matches(type_))
{
return Err(Error::NotSupported);
}
}
let item_id = self.next_item_id;
// Step 2.2
self.item_list.insert(item_id, kind);
self.next_item_id += 1;
Ok(item_id)
}
pub(crate) fn set_data(&mut self, format: DOMString, data: DOMString) {
// Step 3-4
let type_ = normalize_mime(format);
// Step 5 Remove the item in the drag data store item list whose kind is text
// and whose type string is equal to format, if there is one.
self.item_list
.retain(|_, item| !item.text_type_matches(&type_));
// Step 6 Add an item whose kind is text, whose type is format, and whose data is the method's second argument.
self.item_list
.insert(self.next_item_id, Kind::Text { data, type_ });
self.next_item_id += 1;
}
pub(crate) fn clear_data(&mut self, format: Option<DOMString>) -> bool {
let mut was_modified = false;
if let Some(format) = format {
// Step 4-5
let type_ = normalize_mime(format);
// Step 6 Remove the item in the item list whose kind is text and whose type is format.
self.item_list.retain(|_, item| {
let matches = item.text_type_matches(&type_);
if matches {
was_modified = true;
}
!matches
});
} else {
// Step 3 Remove each item in the item list whose kind is text.
self.item_list.retain(|_, item| {
let matches = item.is_file();
if !matches {
was_modified = true;
}
matches
});
}
was_modified
}
pub(crate) fn files(
&self,
global: &GlobalScope,
can_gc: CanGc,
file_list: &mut Vec<DomRoot<File>>,
) {
// Step 3 If the data store is in the protected mode return the empty list.
if self.mode == Mode::Protected {
return;
}
// Step 4 For each item in the drag data store item list whose kind is File, add the item's data to the list L.
self.item_list
.values()
.filter_map(|item| item.as_file(global, can_gc))
.for_each(|file| file_list.push(file));
}
pub(crate) fn list_len(&self) -> usize {
self.item_list.len()
}
pub(crate) fn iter_item_list(&self) -> indexmap::map::Values<'_, u16, Kind> {
self.item_list.values()
}
pub(crate) fn get_by_index(&self, index: usize) -> Option<(&u16, &Kind)> {
self.item_list.get_index(index)
}
pub(crate) fn get_by_id(&self, id: &u16) -> Option<&Kind> {
self.item_list.get(id)
}
pub(crate) fn remove(&mut self, index: usize) {
self.item_list.shift_remove_index(index);
}
pub(crate) fn clear_list(&mut self) {
self.item_list.clear();
self.clear_was_called = true;
}
}
fn normalize_mime(mut format: DOMString) -> DOMString {
// Convert format to ASCII lowercase.
format.make_ascii_lowercase();
match format.as_ref() {
// If format equals "text", change it to "text/plain".
"text" => DOMString::from("text/plain"),
// If format equals "url", change it to "text/uri-list".
"url" => DOMString::from("text/uri-list"),
s => DOMString::from(s),
}
}