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