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