1use selectors::context::QuirksMode;
2use std::sync::atomic::Ordering as Ao;
3use std::{
4 io::Cursor,
5 sync::{Arc, atomic::AtomicUsize, mpsc::Sender},
6};
7use style::{
8 font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, Source},
9 media_queries::MediaList,
10 servo_arc::Arc as ServoArc,
11 shared_lock::SharedRwLock,
12 shared_lock::{Locked, SharedRwLockReadGuard},
13 stylesheets::{
14 AllowImportRules, CssRule, DocumentStyleSheet, ImportRule, Origin, Stylesheet,
15 StylesheetInDocument, StylesheetLoader as ServoStylesheetLoader, UrlExtraData,
16 import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition},
17 },
18 values::{CssUrl, SourceLocation},
19};
20
21use blitz_traits::net::{Bytes, NetHandler, NetProvider, Request};
22use blitz_traits::shell::ShellProvider;
23
24use url::Url;
25
26use crate::{document::DocumentEvent, util::ImageType};
27
28#[derive(Clone, Debug)]
29pub enum Resource {
30 Image(ImageType, u32, u32, Arc<Vec<u8>>),
31 #[cfg(feature = "svg")]
32 Svg(ImageType, Arc<usvg::Tree>),
33 Css(DocumentStyleSheet),
34 Font(Bytes),
35 None,
36}
37
38pub(crate) struct ResourceHandler<T: Send + Sync + 'static> {
39 doc_id: usize,
40 request_id: usize,
41 node_id: Option<usize>,
42 tx: Sender<DocumentEvent>,
43 shell_provider: Arc<dyn ShellProvider>,
44 data: T,
45}
46
47impl<T: Send + Sync + 'static> ResourceHandler<T> {
48 pub(crate) fn new(
49 tx: Sender<DocumentEvent>,
50 doc_id: usize,
51 node_id: Option<usize>,
52 shell_provider: Arc<dyn ShellProvider>,
53 data: T,
54 ) -> Self {
55 static REQUEST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
56 Self {
57 request_id: REQUEST_ID_COUNTER.fetch_add(1, Ao::Relaxed),
58 doc_id,
59 node_id,
60 tx,
61 shell_provider,
62 data,
63 }
64 }
65
66 pub(crate) fn boxed(
67 tx: Sender<DocumentEvent>,
68 doc_id: usize,
69 node_id: Option<usize>,
70 shell_provider: Arc<dyn ShellProvider>,
71 data: T,
72 ) -> Box<dyn NetHandler>
73 where
74 ResourceHandler<T>: NetHandler,
75 {
76 Box::new(Self::new(tx, doc_id, node_id, shell_provider, data)) as _
77 }
78
79 pub(crate) fn request_id(&self) -> usize {
80 self.request_id
81 }
82
83 fn respond(&self, resolved_url: String, result: Result<Resource, String>) {
84 let response = ResourceLoadResponse {
85 request_id: self.request_id,
86 node_id: self.node_id,
87 resolved_url: Some(resolved_url),
88 result,
89 };
90 let _ = self.tx.send(DocumentEvent::ResourceLoad(response));
91 self.shell_provider.request_redraw();
92 }
93}
94
95#[allow(unused)]
96pub struct ResourceLoadResponse {
97 pub request_id: usize,
98 pub node_id: Option<usize>,
99 pub resolved_url: Option<String>,
100 pub result: Result<Resource, String>,
101}
102
103pub struct StylesheetHandler {
104 pub source_url: Url,
105 pub guard: SharedRwLock,
106 pub net_provider: Arc<dyn NetProvider>,
107}
108
109impl NetHandler for ResourceHandler<StylesheetHandler> {
110 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
111 let Ok(css) = std::str::from_utf8(&bytes) else {
112 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
113 };
114
115 let sheet = Stylesheet::from_str(
119 css,
120 self.data.source_url.clone().into(),
121 Origin::Author,
122 ServoArc::new(self.data.guard.wrap(MediaList::empty())),
123 self.data.guard.clone(),
124 Some(&StylesheetLoader {
125 tx: self.tx.clone(),
126 doc_id: self.doc_id,
127 net_provider: self.data.net_provider.clone(),
128 shell_provider: self.shell_provider.clone(),
129 }),
130 None, QuirksMode::NoQuirks,
132 AllowImportRules::Yes,
133 );
134
135 self.respond(
136 resolved_url,
137 Ok(Resource::Css(DocumentStyleSheet(ServoArc::new(sheet)))),
138 );
139 }
140}
141
142#[derive(Clone)]
143pub(crate) struct StylesheetLoader {
144 pub(crate) tx: Sender<DocumentEvent>,
145 pub(crate) doc_id: usize,
146 pub(crate) net_provider: Arc<dyn NetProvider>,
147 pub(crate) shell_provider: Arc<dyn ShellProvider>,
148}
149impl ServoStylesheetLoader for StylesheetLoader {
150 fn request_stylesheet(
151 &self,
152 url: CssUrl,
153 location: SourceLocation,
154 lock: &SharedRwLock,
155 media: ServoArc<Locked<MediaList>>,
156 supports: Option<ImportSupportsCondition>,
157 layer: ImportLayer,
158 ) -> ServoArc<Locked<ImportRule>> {
159 if !supports.as_ref().is_none_or(|s| s.enabled) {
160 return ServoArc::new(lock.wrap(ImportRule {
161 url,
162 stylesheet: ImportSheet::new_refused(),
163 supports,
164 layer,
165 source_location: location,
166 }));
167 }
168
169 let import = ImportRule {
170 url,
171 stylesheet: ImportSheet::new_pending(),
172 supports,
173 layer,
174 source_location: location,
175 };
176
177 let url = import.url.url().unwrap().clone();
178 let import = ServoArc::new(lock.wrap(import));
179 self.net_provider.fetch(
180 self.doc_id,
181 Request::get(url.as_ref().clone()),
182 ResourceHandler::boxed(
183 self.tx.clone(),
184 self.doc_id,
185 None, self.shell_provider.clone(),
187 NestedStylesheetHandler {
188 url: url.clone(),
189 loader: self.clone(),
190 lock: lock.clone(),
191 media,
192 import_rule: import.clone(),
193 net_provider: self.net_provider.clone(),
194 },
195 ),
196 );
197
198 import
199 }
200}
201
202struct NestedStylesheetHandler {
203 loader: StylesheetLoader,
204 lock: SharedRwLock,
205 url: ServoArc<Url>,
206 media: ServoArc<Locked<MediaList>>,
207 import_rule: ServoArc<Locked<ImportRule>>,
208 net_provider: Arc<dyn NetProvider>,
209}
210
211impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
212 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
213 let Ok(css) = std::str::from_utf8(&bytes) else {
214 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
215 };
216
217 let sheet = ServoArc::new(Stylesheet::from_str(
221 css,
222 UrlExtraData(self.data.url.clone()),
223 Origin::Author,
224 self.data.media.clone(),
225 self.data.lock.clone(),
226 Some(&self.data.loader),
227 None, QuirksMode::NoQuirks,
229 AllowImportRules::Yes,
230 ));
231
232 fetch_font_face(
234 self.tx.clone(),
235 self.doc_id,
236 self.node_id,
237 &sheet,
238 &self.data.net_provider,
239 &self.shell_provider,
240 &self.data.lock.read(),
241 );
242
243 let mut guard = self.data.lock.write();
244 self.data.import_rule.write_with(&mut guard).stylesheet = ImportSheet::Sheet(sheet);
245 drop(guard);
246
247 self.respond(resolved_url, Ok(Resource::None))
248 }
249}
250
251struct FontFaceHandler(FontFaceSourceFormatKeyword);
252impl NetHandler for ResourceHandler<FontFaceHandler> {
253 fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
254 let result = self.data.parse(bytes);
255 self.respond(resolved_url, result)
256 }
257}
258impl FontFaceHandler {
259 fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
260 if self.0 == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
261 self.0 = match &bytes.as_ref()[0..4] {
262 #[cfg(feature = "woff")]
265 b"wOFF" => FontFaceSourceFormatKeyword::Woff,
266 #[cfg(feature = "woff")]
269 b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
270 b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
273 &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
276 b"true" => FontFaceSourceFormatKeyword::Truetype,
279 _ => FontFaceSourceFormatKeyword::None,
280 }
281 }
282
283 #[cfg(feature = "woff")]
285 let mut bytes = bytes;
286
287 match self.0 {
288 #[cfg(feature = "woff")]
289 FontFaceSourceFormatKeyword::Woff => {
290 #[cfg(feature = "tracing")]
291 tracing::info!("Decompressing woff1 font");
292
293 let decompressed = wuff::decompress_woff1(&bytes).ok();
295
296 if let Some(decompressed) = decompressed {
297 bytes = Bytes::from(decompressed);
298 } else {
299 #[cfg(feature = "tracing")]
300 tracing::warn!("Failed to decompress woff1 font");
301 }
302 }
303 #[cfg(feature = "woff")]
304 FontFaceSourceFormatKeyword::Woff2 => {
305 #[cfg(feature = "tracing")]
306 tracing::info!("Decompressing woff2 font");
307
308 let decompressed = wuff::decompress_woff2(&bytes).ok();
310
311 if let Some(decompressed) = decompressed {
312 bytes = Bytes::from(decompressed);
313 } else {
314 #[cfg(feature = "tracing")]
315 tracing::warn!("Failed to decompress woff2 font");
316 }
317 }
318 FontFaceSourceFormatKeyword::None => {
319 return Ok(Resource::None);
321 }
322 _ => {}
323 }
324
325 Ok(Resource::Font(bytes))
326 }
327}
328
329pub(crate) fn fetch_font_face(
330 tx: Sender<DocumentEvent>,
331 doc_id: usize,
332 node_id: Option<usize>,
333 sheet: &Stylesheet,
334 network_provider: &Arc<dyn NetProvider>,
335 shell_provider: &Arc<dyn ShellProvider>,
336 read_guard: &SharedRwLockReadGuard,
337) {
338 sheet
339 .contents(read_guard)
340 .rules(read_guard)
341 .iter()
342 .filter_map(|rule| match rule {
343 CssRule::FontFace(font_face) => {
344 let descriptor = &font_face.read_with(read_guard).descriptors;
346 descriptor
347 .src
348 .as_ref()
349 .filter(|_| descriptor.font_family.is_some())
350 }
351 _ => None,
352 })
353 .for_each(|source_list| {
354 let preferred_source = source_list
357 .0
358 .iter()
359 .filter_map(|source| match source {
360 Source::Url(url_source) => Some(url_source),
361 Source::Local(_) => None,
363 })
364 .find_map(|url_source| {
365 let mut format = match &url_source.format_hint {
366 Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
367 Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
368 "woff2" => FontFaceSourceFormatKeyword::Woff2,
369 "ttf" => FontFaceSourceFormatKeyword::Truetype,
370 "otf" => FontFaceSourceFormatKeyword::Opentype,
371 _ => FontFaceSourceFormatKeyword::None,
372 },
373 _ => FontFaceSourceFormatKeyword::None,
374 };
375 if format == FontFaceSourceFormatKeyword::None {
376 let (_, end) = url_source.url.as_str().rsplit_once('.')?;
377 format = match end {
378 "woff2" => FontFaceSourceFormatKeyword::Woff2,
379 "woff" => FontFaceSourceFormatKeyword::Woff,
380 "ttf" => FontFaceSourceFormatKeyword::Truetype,
381 "otf" => FontFaceSourceFormatKeyword::Opentype,
382 "svg" => FontFaceSourceFormatKeyword::Svg,
383 "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
384 _ => FontFaceSourceFormatKeyword::None,
385 }
386 }
387
388 if matches!(
389 format,
390 FontFaceSourceFormatKeyword::Svg
391 | FontFaceSourceFormatKeyword::EmbeddedOpentype
392 ) {
393 #[cfg(feature = "tracing")]
394 tracing::warn!("Skipping unsupported font of type {:?}", format);
395 return None;
396 }
397
398 #[cfg(not(feature = "woff"))]
399 if matches!(
400 format,
401 FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
402 ) {
403 #[cfg(feature = "tracing")]
404 tracing::warn!("Skipping unsupported font of type {:?}", format);
405 return None;
406 }
407
408 let url = url_source.url.url().unwrap().as_ref().clone();
409 Some((url, format))
410 });
411
412 if let Some((url, format)) = preferred_source {
413 network_provider.fetch(
414 doc_id,
415 Request::get(url),
416 ResourceHandler::boxed(
417 tx.clone(),
418 doc_id,
419 node_id,
420 shell_provider.clone(),
421 FontFaceHandler(format),
422 ),
423 );
424 }
425 })
426}
427
428pub struct ImageHandler {
429 kind: ImageType,
430}
431impl ImageHandler {
432 pub fn new(kind: ImageType) -> Self {
433 Self { kind }
434 }
435}
436
437impl NetHandler for ResourceHandler<ImageHandler> {
438 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
439 let result = self.data.parse(bytes);
440 self.respond(resolved_url, result)
441 }
442}
443
444impl ImageHandler {
445 fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
446 if let Ok(image) = image::ImageReader::new(Cursor::new(&bytes))
448 .with_guessed_format()
449 .expect("IO errors impossible with Cursor")
450 .decode()
451 {
452 let raw_rgba8_data = image.clone().into_rgba8().into_raw();
453 return Ok(Resource::Image(
454 self.kind,
455 image.width(),
456 image.height(),
457 Arc::new(raw_rgba8_data),
458 ));
459 };
460
461 #[cfg(feature = "svg")]
462 {
463 use crate::util::parse_svg;
464 if let Ok(tree) = parse_svg(&bytes) {
465 return Ok(Resource::Svg(self.kind, Arc::new(tree)));
466 }
467 }
468
469 Err(String::from("Could not parse image"))
470 }
471}