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 },
237 ),
238 );
239
240 import
241 }
242}
243
244struct NestedStylesheetHandler {
245 loader: StylesheetLoader,
246 lock: SharedRwLock,
247 url: ServoArc<Url>,
248 media: ServoArc<Locked<MediaList>>,
249 import_rule: ServoArc<Locked<ImportRule>>,
250}
251
252impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
253 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
254 let Ok(css) = std::str::from_utf8(&bytes) else {
255 return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
256 };
257
258 let sheet = ServoArc::new(Stylesheet::from_str(
262 css,
263 UrlExtraData(self.data.url.clone()),
264 Origin::Author,
265 self.data.media.clone(),
266 self.data.lock.clone(),
267 Some(&self.data.loader),
268 None, QuirksMode::NoQuirks,
270 AllowImportRules::Yes,
271 ));
272
273 let import_rule = self.data.import_rule.clone();
280 self.respond(resolved_url, Ok(Resource::ImportSheet(import_rule, sheet)))
281 }
282}
283
284struct FontFaceHandler {
285 format: FontFaceSourceFormatKeyword,
286 overrides: FontFaceOverrides,
287}
288impl NetHandler for ResourceHandler<FontFaceHandler> {
289 fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
290 let result = self.data.parse(bytes);
291 self.respond(resolved_url, result)
292 }
293}
294impl FontFaceHandler {
295 fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
296 if self.format == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
297 self.format = match &bytes.as_ref()[0..4] {
298 #[cfg(feature = "woff")]
301 b"wOFF" => FontFaceSourceFormatKeyword::Woff,
302 #[cfg(feature = "woff")]
305 b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
306 b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
309 &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
312 b"true" => FontFaceSourceFormatKeyword::Truetype,
315 _ => FontFaceSourceFormatKeyword::None,
316 }
317 }
318
319 #[cfg(feature = "woff")]
321 let mut bytes = bytes;
322
323 match self.format {
324 #[cfg(feature = "woff")]
325 FontFaceSourceFormatKeyword::Woff => {
326 #[cfg(feature = "tracing")]
327 tracing::info!("Decompressing woff1 font");
328
329 let decompressed = wuff::decompress_woff1(&bytes).ok();
331
332 if let Some(decompressed) = decompressed {
333 bytes = Bytes::from(decompressed);
334 } else {
335 #[cfg(feature = "tracing")]
336 tracing::warn!("Failed to decompress woff1 font");
337 }
338 }
339 #[cfg(feature = "woff")]
340 FontFaceSourceFormatKeyword::Woff2 => {
341 #[cfg(feature = "tracing")]
342 tracing::info!("Decompressing woff2 font");
343
344 let decompressed = wuff::decompress_woff2(&bytes).ok();
346
347 if let Some(decompressed) = decompressed {
348 bytes = Bytes::from(decompressed);
349 } else {
350 #[cfg(feature = "tracing")]
351 tracing::warn!("Failed to decompress woff2 font");
352 }
353 }
354 FontFaceSourceFormatKeyword::None => {
355 return Ok(Resource::None);
357 }
358 _ => {}
359 }
360
361 Ok(Resource::Font(bytes, std::mem::take(&mut self.overrides)))
362 }
363}
364
365#[allow(clippy::too_many_arguments)]
366pub(crate) fn fetch_font_face(
367 tx: Sender<DocumentEvent>,
368 doc_id: usize,
369 node_id: Option<NodeId>,
370 sheet: &Stylesheet,
371 network_provider: &Arc<dyn NetProvider>,
372 shell_provider: &Arc<dyn ShellProvider>,
373 read_guard: &SharedRwLockReadGuard,
374 abort_signal: Option<&AbortSignal>,
375) {
376 sheet
377 .contents(read_guard)
378 .rules(read_guard)
379 .iter()
380 .filter_map(|rule| match rule {
381 CssRule::FontFace(font_face) => {
382 let descriptor = &font_face.read_with(read_guard).descriptors;
383 let family = descriptor.font_family.as_ref()?;
384 let src = descriptor.src.as_ref()?;
385 let overrides = FontFaceOverrides {
389 family_name: Some(family.name.to_string()),
390 weight: descriptor
391 .font_weight
392 .as_ref()
393 .and_then(|range| range.0.compute().map(|w| w.value())),
394 style: descriptor.font_style.as_ref().map(stylo_to_fontique_style),
395 };
396 Some((src, overrides))
397 }
398 _ => None,
399 })
400 .for_each(|(source_list, overrides)| {
401 let preferred_source = source_list
404 .0
405 .iter()
406 .filter_map(|source| match source {
407 Source::Url(url_source) => Some(url_source),
408 Source::Local(_) => None,
410 })
411 .find_map(|url_source| {
412 let mut format = match &url_source.format_hint {
413 Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
414 Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
415 "woff2" => FontFaceSourceFormatKeyword::Woff2,
416 "ttf" => FontFaceSourceFormatKeyword::Truetype,
417 "otf" => FontFaceSourceFormatKeyword::Opentype,
418 _ => FontFaceSourceFormatKeyword::None,
419 },
420 _ => FontFaceSourceFormatKeyword::None,
421 };
422 if format == FontFaceSourceFormatKeyword::None {
423 let (_, end) = url_source.url.as_str().rsplit_once('.')?;
424 format = match end {
425 "woff2" => FontFaceSourceFormatKeyword::Woff2,
426 "woff" => FontFaceSourceFormatKeyword::Woff,
427 "ttf" => FontFaceSourceFormatKeyword::Truetype,
428 "otf" => FontFaceSourceFormatKeyword::Opentype,
429 "svg" => FontFaceSourceFormatKeyword::Svg,
430 "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
431 _ => FontFaceSourceFormatKeyword::None,
432 }
433 }
434
435 if matches!(
436 format,
437 FontFaceSourceFormatKeyword::Svg
438 | FontFaceSourceFormatKeyword::EmbeddedOpentype
439 ) {
440 #[cfg(feature = "tracing")]
441 tracing::warn!("Skipping unsupported font of type {:?}", format);
442 return None;
443 }
444
445 #[cfg(not(feature = "woff"))]
446 if matches!(
447 format,
448 FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
449 ) {
450 #[cfg(feature = "tracing")]
451 tracing::warn!("Skipping unsupported font of type {:?}", format);
452 return None;
453 }
454
455 let Some(url) = url_source.url.url() else {
458 #[cfg(feature = "tracing")]
459 tracing::warn!("Skipping @font-face source with unresolvable url");
460 return None;
461 };
462 let url = url.as_ref().clone();
463 Some((url, format))
464 });
465
466 if let Some((url, format)) = preferred_source {
467 network_provider.fetch(
468 doc_id,
469 stamped_request(url, abort_signal),
470 ResourceHandler::boxed(
471 tx.clone(),
472 doc_id,
473 node_id,
474 shell_provider.clone(),
475 FontFaceHandler { format, overrides },
476 ),
477 );
478 }
479 })
480}
481
482fn stylo_to_fontique_style(style: &FontStyleRange) -> parley::fontique::FontStyle {
488 use parley::fontique::FontStyle as Fq;
489 match style {
490 FontStyleRange::Italic => Fq::Italic,
491 FontStyleRange::Oblique(min, max) => {
492 let angle = min.degrees();
493 if angle.is_none_or(|a| a == 0.0) && max.degrees().is_none_or(|a| a == 0.0) {
497 Fq::Normal
498 } else {
499 Fq::Oblique(angle)
500 }
501 }
502 }
503}
504
505pub(crate) struct DocumentSrcHandler;
507
508impl NetHandler for ResourceHandler<DocumentSrcHandler> {
509 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
510 let html = String::from_utf8_lossy(&bytes).into_owned();
511 self.respond(resolved_url, Ok(Resource::DocumentSrc(html)));
512 }
513}
514
515pub struct ImageHandler {
516 kind: ImageType,
517}
518impl ImageHandler {
519 pub fn new(kind: ImageType) -> Self {
520 Self { kind }
521 }
522}
523
524impl NetHandler for ResourceHandler<ImageHandler> {
525 fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
526 let result = self.data.parse(bytes);
527 self.respond(resolved_url, result)
528 }
529}
530
531impl ImageHandler {
532 fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
533 let image_err = match image::ImageReader::new(Cursor::new(&bytes))
534 .with_guessed_format()
535 .expect("IO errors impossible with Cursor")
536 .decode()
537 {
538 Ok(image) => {
539 let raw_rgba8_data = image.clone().into_rgba8().into_raw();
540 return Ok(Resource::Image(
541 self.kind,
542 image.width(),
543 image.height(),
544 Arc::new(raw_rgba8_data),
545 ));
546 }
547 Err(e) => e.to_string(),
548 };
549
550 #[cfg(feature = "svg")]
551 let svg_err = {
552 use crate::util::parse_svg_image;
553 match parse_svg_image(&bytes) {
554 Ok(svg) => return Ok(Resource::Svg(self.kind, svg)),
555 Err(e) => e.to_string(),
556 }
557 };
558 #[cfg(not(feature = "svg"))]
559 let svg_err = "svg feature disabled";
560
561 Err(format!(
562 "Could not parse image ({} bytes): image-crate error: {image_err}; svg fallback error: {svg_err}",
563 bytes.len()
564 ))
565 }
566}
567
568#[cfg(test)]
569mod tests {
570 use super::*;
571 use parley::fontique::FontStyle as Fq;
572 use style::values::specified::Angle;
573
574 fn oblique(min_deg: f32, max_deg: f32) -> FontStyleRange {
575 FontStyleRange::Oblique(Angle::from_degrees(min_deg), Angle::from_degrees(max_deg))
576 }
577
578 #[test]
579 fn italic_maps_to_italic() {
580 assert_eq!(stylo_to_fontique_style(&FontStyleRange::Italic), Fq::Italic,);
581 }
582
583 #[test]
584 fn oblique_zero_zero_maps_to_normal() {
585 assert_eq!(stylo_to_fontique_style(&oblique(0.0, 0.0)), Fq::Normal);
589 }
590
591 #[test]
592 fn oblique_single_angle_maps_to_oblique_with_min() {
593 assert_eq!(
594 stylo_to_fontique_style(&oblique(14.0, 14.0)),
595 Fq::Oblique(Some(14.0)),
596 );
597 }
598
599 #[test]
600 fn oblique_range_uses_min_angle() {
601 assert_eq!(
604 stylo_to_fontique_style(&oblique(10.0, 20.0)),
605 Fq::Oblique(Some(10.0)),
606 );
607 }
608}