1use blitz_traits::node_id::NodeId;
2use selectors::context::QuirksMode;
3use std::sync::atomic::Ordering as Ao;
4use std::{
5 io::Cursor,
6 sync::{Arc, atomic::AtomicUsize, mpsc::Sender},
7};
8use style::{
9 font_face::{FontFaceSourceFormat, FontFaceSourceFormatKeyword, FontStyleRange, Source},
10 media_queries::MediaList,
11 servo_arc::Arc as ServoArc,
12 shared_lock::SharedRwLock,
13 shared_lock::{Locked, SharedRwLockReadGuard},
14 stylesheets::{
15 AllowImportRules, CssRule, DocumentStyleSheet, ImportRule, Origin, Stylesheet,
16 StylesheetInDocument, StylesheetLoader as ServoStylesheetLoader, UrlExtraData,
17 import_rule::{ImportLayer, ImportSheet, ImportSupportsCondition},
18 },
19 values::{CssUrl, SourceLocation},
20};
21
22use blitz_traits::net::{AbortSignal, Bytes, NetHandler, NetProvider, Request};
23use blitz_traits::shell::ShellProvider;
24
25use url::Url;
26
27use crate::{document::DocumentEvent, util::ImageType};
28
29pub(crate) fn stamped_request(url: Url, signal: Option<&AbortSignal>) -> Request {
30 let mut req = Request::get(url);
31 if let Some(sig) = signal {
32 req = req.signal(sig.clone());
33 }
34 req
35}
36
37#[derive(Clone, Debug, Default)]
46pub struct FontFaceOverrides {
47 pub family_name: Option<String>,
49 pub weight: Option<f32>,
53 pub style: Option<parley::fontique::FontStyle>,
55}
56
57#[derive(Clone, Debug)]
58pub enum Resource {
59 Image(ImageType, u32, u32, Arc<Vec<u8>>),
60 #[cfg(feature = "svg")]
61 Svg(ImageType, crate::node::SvgImageData),
62 Css(DocumentStyleSheet),
63 Font(Bytes, FontFaceOverrides),
64 DocumentSrc(String),
66 None,
67}
68
69pub(crate) struct ResourceHandler<T: Send + Sync + 'static> {
70 doc_id: usize,
71 request_id: usize,
72 node_id: Option<NodeId>,
73 tx: Sender<DocumentEvent>,
74 shell_provider: Arc<dyn ShellProvider>,
75 data: T,
76}
77
78impl<T: Send + Sync + 'static> ResourceHandler<T> {
79 pub(crate) fn new(
80 tx: Sender<DocumentEvent>,
81 doc_id: usize,
82 node_id: Option<NodeId>,
83 shell_provider: Arc<dyn ShellProvider>,
84 data: T,
85 ) -> Self {
86 static REQUEST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
87 Self {
88 request_id: REQUEST_ID_COUNTER.fetch_add(1, Ao::Relaxed),
89 doc_id,
90 node_id,
91 tx,
92 shell_provider,
93 data,
94 }
95 }
96
97 pub(crate) fn boxed(
98 tx: Sender<DocumentEvent>,
99 doc_id: usize,
100 node_id: Option<NodeId>,
101 shell_provider: Arc<dyn ShellProvider>,
102 data: T,
103 ) -> Box<dyn NetHandler>
104 where
105 ResourceHandler<T>: NetHandler,
106 {
107 Box::new(Self::new(tx, doc_id, node_id, shell_provider, data)) as _
108 }
109
110 pub(crate) fn request_id(&self) -> usize {
111 self.request_id
112 }
113
114 fn respond(&self, resolved_url: String, result: Result<Resource, String>) {
115 let response = ResourceLoadResponse {
116 request_id: self.request_id,
117 node_id: self.node_id,
118 resolved_url: Some(resolved_url),
119 result,
120 };
121 let _ = self.tx.send(DocumentEvent::ResourceLoad(response));
122 self.shell_provider.request_redraw();
123 }
124}
125
126#[allow(unused)]
127pub struct ResourceLoadResponse {
128 pub request_id: usize,
129 pub node_id: Option<NodeId>,
130 pub resolved_url: Option<String>,
131 pub result: Result<Resource, String>,
132}
133
134pub struct StylesheetHandler {
135 pub source_url: Url,
136 pub guard: SharedRwLock,
137 pub net_provider: Arc<dyn NetProvider>,
138 pub abort_signal: Option<AbortSignal>,
139}
140
141impl NetHandler for ResourceHandler<StylesheetHandler> {
142 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
143 let Ok(css) = std::str::from_utf8(&bytes) else {
144 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
145 };
146
147 let sheet = Stylesheet::from_str(
151 css,
152 self.data.source_url.clone().into(),
153 Origin::Author,
154 ServoArc::new(self.data.guard.wrap(MediaList::empty())),
155 self.data.guard.clone(),
156 Some(&StylesheetLoader {
157 tx: self.tx.clone(),
158 doc_id: self.doc_id,
159 net_provider: self.data.net_provider.clone(),
160 shell_provider: self.shell_provider.clone(),
161 abort_signal: self.data.abort_signal.clone(),
162 }),
163 None, QuirksMode::NoQuirks,
165 AllowImportRules::Yes,
166 );
167
168 self.respond(
169 resolved_url,
170 Ok(Resource::Css(DocumentStyleSheet(ServoArc::new(sheet)))),
171 );
172 }
173}
174
175#[derive(Clone)]
176pub(crate) struct StylesheetLoader {
177 pub(crate) tx: Sender<DocumentEvent>,
178 pub(crate) doc_id: usize,
179 pub(crate) net_provider: Arc<dyn NetProvider>,
180 pub(crate) shell_provider: Arc<dyn ShellProvider>,
181 pub(crate) abort_signal: Option<AbortSignal>,
182}
183impl ServoStylesheetLoader for StylesheetLoader {
184 fn request_stylesheet(
185 &self,
186 url: CssUrl,
187 location: SourceLocation,
188 lock: &SharedRwLock,
189 media: ServoArc<Locked<MediaList>>,
190 supports: Option<ImportSupportsCondition>,
191 layer: ImportLayer,
192 ) -> ServoArc<Locked<ImportRule>> {
193 if !supports.as_ref().is_none_or(|s| s.enabled) {
194 return ServoArc::new(lock.wrap(ImportRule {
195 url,
196 stylesheet: ImportSheet::new_refused(),
197 supports,
198 layer,
199 source_location: location,
200 }));
201 }
202
203 let import = ImportRule {
204 url,
205 stylesheet: ImportSheet::new_pending(),
206 supports,
207 layer,
208 source_location: location,
209 };
210
211 let url = import.url.url().unwrap().clone();
212 let import = ServoArc::new(lock.wrap(import));
213 self.net_provider.fetch(
214 self.doc_id,
215 stamped_request(url.as_ref().clone(), self.abort_signal.as_ref()),
216 ResourceHandler::boxed(
217 self.tx.clone(),
218 self.doc_id,
219 None, self.shell_provider.clone(),
221 NestedStylesheetHandler {
222 url: url.clone(),
223 loader: self.clone(),
224 lock: lock.clone(),
225 media,
226 import_rule: import.clone(),
227 net_provider: self.net_provider.clone(),
228 },
229 ),
230 );
231
232 import
233 }
234}
235
236struct NestedStylesheetHandler {
237 loader: StylesheetLoader,
238 lock: SharedRwLock,
239 url: ServoArc<Url>,
240 media: ServoArc<Locked<MediaList>>,
241 import_rule: ServoArc<Locked<ImportRule>>,
242 net_provider: Arc<dyn NetProvider>,
243}
244
245impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
246 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
247 let Ok(css) = std::str::from_utf8(&bytes) else {
248 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
249 };
250
251 let sheet = ServoArc::new(Stylesheet::from_str(
255 css,
256 UrlExtraData(self.data.url.clone()),
257 Origin::Author,
258 self.data.media.clone(),
259 self.data.lock.clone(),
260 Some(&self.data.loader),
261 None, QuirksMode::NoQuirks,
263 AllowImportRules::Yes,
264 ));
265
266 fetch_font_face(
268 self.tx.clone(),
269 self.doc_id,
270 self.node_id,
271 &sheet,
272 &self.data.net_provider,
273 &self.shell_provider,
274 &self.data.lock.read(),
275 self.data.loader.abort_signal.as_ref(),
276 );
277
278 let mut guard = self.data.lock.write();
279 self.data.import_rule.write_with(&mut guard).stylesheet = ImportSheet::Sheet(sheet);
280 drop(guard);
281
282 self.respond(resolved_url, Ok(Resource::None))
283 }
284}
285
286struct FontFaceHandler {
287 format: FontFaceSourceFormatKeyword,
288 overrides: FontFaceOverrides,
289}
290impl NetHandler for ResourceHandler<FontFaceHandler> {
291 fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
292 let result = self.data.parse(bytes);
293 self.respond(resolved_url, result)
294 }
295}
296impl FontFaceHandler {
297 fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
298 if self.format == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
299 self.format = match &bytes.as_ref()[0..4] {
300 #[cfg(feature = "woff")]
303 b"wOFF" => FontFaceSourceFormatKeyword::Woff,
304 #[cfg(feature = "woff")]
307 b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
308 b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
311 &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
314 b"true" => FontFaceSourceFormatKeyword::Truetype,
317 _ => FontFaceSourceFormatKeyword::None,
318 }
319 }
320
321 #[cfg(feature = "woff")]
323 let mut bytes = bytes;
324
325 match self.format {
326 #[cfg(feature = "woff")]
327 FontFaceSourceFormatKeyword::Woff => {
328 #[cfg(feature = "tracing")]
329 tracing::info!("Decompressing woff1 font");
330
331 let decompressed = wuff::decompress_woff1(&bytes).ok();
333
334 if let Some(decompressed) = decompressed {
335 bytes = Bytes::from(decompressed);
336 } else {
337 #[cfg(feature = "tracing")]
338 tracing::warn!("Failed to decompress woff1 font");
339 }
340 }
341 #[cfg(feature = "woff")]
342 FontFaceSourceFormatKeyword::Woff2 => {
343 #[cfg(feature = "tracing")]
344 tracing::info!("Decompressing woff2 font");
345
346 let decompressed = wuff::decompress_woff2(&bytes).ok();
348
349 if let Some(decompressed) = decompressed {
350 bytes = Bytes::from(decompressed);
351 } else {
352 #[cfg(feature = "tracing")]
353 tracing::warn!("Failed to decompress woff2 font");
354 }
355 }
356 FontFaceSourceFormatKeyword::None => {
357 return Ok(Resource::None);
359 }
360 _ => {}
361 }
362
363 Ok(Resource::Font(bytes, std::mem::take(&mut self.overrides)))
364 }
365}
366
367#[allow(clippy::too_many_arguments)]
368pub(crate) fn fetch_font_face(
369 tx: Sender<DocumentEvent>,
370 doc_id: usize,
371 node_id: Option<NodeId>,
372 sheet: &Stylesheet,
373 network_provider: &Arc<dyn NetProvider>,
374 shell_provider: &Arc<dyn ShellProvider>,
375 read_guard: &SharedRwLockReadGuard,
376 abort_signal: Option<&AbortSignal>,
377) {
378 sheet
379 .contents(read_guard)
380 .rules(read_guard)
381 .iter()
382 .filter_map(|rule| match rule {
383 CssRule::FontFace(font_face) => {
384 let descriptor = &font_face.read_with(read_guard).descriptors;
385 let family = descriptor.font_family.as_ref()?;
386 let src = descriptor.src.as_ref()?;
387 let overrides = FontFaceOverrides {
391 family_name: Some(family.name.to_string()),
392 weight: descriptor
393 .font_weight
394 .as_ref()
395 .and_then(|range| range.0.compute().map(|w| w.value())),
396 style: descriptor.font_style.as_ref().map(stylo_to_fontique_style),
397 };
398 Some((src, overrides))
399 }
400 _ => None,
401 })
402 .for_each(|(source_list, overrides)| {
403 let preferred_source = source_list
406 .0
407 .iter()
408 .filter_map(|source| match source {
409 Source::Url(url_source) => Some(url_source),
410 Source::Local(_) => None,
412 })
413 .find_map(|url_source| {
414 let mut format = match &url_source.format_hint {
415 Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
416 Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
417 "woff2" => FontFaceSourceFormatKeyword::Woff2,
418 "ttf" => FontFaceSourceFormatKeyword::Truetype,
419 "otf" => FontFaceSourceFormatKeyword::Opentype,
420 _ => FontFaceSourceFormatKeyword::None,
421 },
422 _ => FontFaceSourceFormatKeyword::None,
423 };
424 if format == FontFaceSourceFormatKeyword::None {
425 let (_, end) = url_source.url.as_str().rsplit_once('.')?;
426 format = match end {
427 "woff2" => FontFaceSourceFormatKeyword::Woff2,
428 "woff" => FontFaceSourceFormatKeyword::Woff,
429 "ttf" => FontFaceSourceFormatKeyword::Truetype,
430 "otf" => FontFaceSourceFormatKeyword::Opentype,
431 "svg" => FontFaceSourceFormatKeyword::Svg,
432 "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
433 _ => FontFaceSourceFormatKeyword::None,
434 }
435 }
436
437 if matches!(
438 format,
439 FontFaceSourceFormatKeyword::Svg
440 | FontFaceSourceFormatKeyword::EmbeddedOpentype
441 ) {
442 #[cfg(feature = "tracing")]
443 tracing::warn!("Skipping unsupported font of type {:?}", format);
444 return None;
445 }
446
447 #[cfg(not(feature = "woff"))]
448 if matches!(
449 format,
450 FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
451 ) {
452 #[cfg(feature = "tracing")]
453 tracing::warn!("Skipping unsupported font of type {:?}", format);
454 return None;
455 }
456
457 let Some(url) = url_source.url.url() else {
460 #[cfg(feature = "tracing")]
461 tracing::warn!("Skipping @font-face source with unresolvable url");
462 return None;
463 };
464 let url = url.as_ref().clone();
465 Some((url, format))
466 });
467
468 if let Some((url, format)) = preferred_source {
469 network_provider.fetch(
470 doc_id,
471 stamped_request(url, abort_signal),
472 ResourceHandler::boxed(
473 tx.clone(),
474 doc_id,
475 node_id,
476 shell_provider.clone(),
477 FontFaceHandler { format, overrides },
478 ),
479 );
480 }
481 })
482}
483
484fn stylo_to_fontique_style(style: &FontStyleRange) -> parley::fontique::FontStyle {
490 use parley::fontique::FontStyle as Fq;
491 match style {
492 FontStyleRange::Italic => Fq::Italic,
493 FontStyleRange::Oblique(min, max) => {
494 let angle = min.degrees();
495 if angle.is_none_or(|a| a == 0.0) && max.degrees().is_none_or(|a| a == 0.0) {
499 Fq::Normal
500 } else {
501 Fq::Oblique(angle)
502 }
503 }
504 }
505}
506
507pub(crate) struct DocumentSrcHandler;
509
510impl NetHandler for ResourceHandler<DocumentSrcHandler> {
511 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
512 let html = String::from_utf8_lossy(&bytes).into_owned();
513 self.respond(resolved_url, Ok(Resource::DocumentSrc(html)));
514 }
515}
516
517pub struct ImageHandler {
518 kind: ImageType,
519}
520impl ImageHandler {
521 pub fn new(kind: ImageType) -> Self {
522 Self { kind }
523 }
524}
525
526impl NetHandler for ResourceHandler<ImageHandler> {
527 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
528 let result = self.data.parse(bytes);
529 self.respond(resolved_url, result)
530 }
531}
532
533impl ImageHandler {
534 fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
535 let image_err = match image::ImageReader::new(Cursor::new(&bytes))
536 .with_guessed_format()
537 .expect("IO errors impossible with Cursor")
538 .decode()
539 {
540 Ok(image) => {
541 let raw_rgba8_data = image.clone().into_rgba8().into_raw();
542 return Ok(Resource::Image(
543 self.kind,
544 image.width(),
545 image.height(),
546 Arc::new(raw_rgba8_data),
547 ));
548 }
549 Err(e) => e.to_string(),
550 };
551
552 #[cfg(feature = "svg")]
553 let svg_err = {
554 use crate::util::parse_svg_image;
555 match parse_svg_image(&bytes) {
556 Ok(svg) => return Ok(Resource::Svg(self.kind, svg)),
557 Err(e) => e.to_string(),
558 }
559 };
560 #[cfg(not(feature = "svg"))]
561 let svg_err = "svg feature disabled";
562
563 Err(format!(
564 "Could not parse image ({} bytes): image-crate error: {image_err}; svg fallback error: {svg_err}",
565 bytes.len()
566 ))
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573 use parley::fontique::FontStyle as Fq;
574 use style::values::specified::Angle;
575
576 fn oblique(min_deg: f32, max_deg: f32) -> FontStyleRange {
577 FontStyleRange::Oblique(Angle::from_degrees(min_deg), Angle::from_degrees(max_deg))
578 }
579
580 #[test]
581 fn italic_maps_to_italic() {
582 assert_eq!(stylo_to_fontique_style(&FontStyleRange::Italic), Fq::Italic,);
583 }
584
585 #[test]
586 fn oblique_zero_zero_maps_to_normal() {
587 assert_eq!(stylo_to_fontique_style(&oblique(0.0, 0.0)), Fq::Normal);
591 }
592
593 #[test]
594 fn oblique_single_angle_maps_to_oblique_with_min() {
595 assert_eq!(
596 stylo_to_fontique_style(&oblique(14.0, 14.0)),
597 Fq::Oblique(Some(14.0)),
598 );
599 }
600
601 #[test]
602 fn oblique_range_uses_min_angle() {
603 assert_eq!(
606 stylo_to_fontique_style(&oblique(10.0, 20.0)),
607 Fq::Oblique(Some(10.0)),
608 );
609 }
610}