blitz-pdf 0.1.3 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/README.md +6 -6
  2. package/THIRD_PARTY_NOTICES.md +5 -2
  3. package/docs/layout-regressions.md +65 -0
  4. package/docs/pagination-regressions.md +7 -6
  5. package/package.json +1 -1
  6. package/vendor/README.md +35 -0
  7. package/vendor/blitz-dom/Cargo.toml +113 -0
  8. package/vendor/blitz-dom/LICENSE-APACHE +176 -0
  9. package/vendor/blitz-dom/LICENSE-MIT +23 -0
  10. package/vendor/blitz-dom/assets/default.css +1142 -0
  11. package/vendor/blitz-dom/assets/moz-bullet-font.otf +0 -0
  12. package/vendor/blitz-dom/src/accessibility.rs +255 -0
  13. package/vendor/blitz-dom/src/config.rs +97 -0
  14. package/vendor/blitz-dom/src/cssom.rs +971 -0
  15. package/vendor/blitz-dom/src/debug.rs +206 -0
  16. package/vendor/blitz-dom/src/document.rs +3395 -0
  17. package/vendor/blitz-dom/src/events/driver.rs +427 -0
  18. package/vendor/blitz-dom/src/events/focus.rs +40 -0
  19. package/vendor/blitz-dom/src/events/ime.rs +31 -0
  20. package/vendor/blitz-dom/src/events/keyboard.rs +174 -0
  21. package/vendor/blitz-dom/src/events/mod.rs +315 -0
  22. package/vendor/blitz-dom/src/events/pointer.rs +849 -0
  23. package/vendor/blitz-dom/src/font_metrics.rs +177 -0
  24. package/vendor/blitz-dom/src/form.rs +463 -0
  25. package/vendor/blitz-dom/src/html.rs +38 -0
  26. package/vendor/blitz-dom/src/iframe.rs +186 -0
  27. package/vendor/blitz-dom/src/layout/construct.rs +1328 -0
  28. package/vendor/blitz-dom/src/layout/damage.rs +642 -0
  29. package/vendor/blitz-dom/src/layout/inline.rs +1160 -0
  30. package/vendor/blitz-dom/src/layout/list.rs +237 -0
  31. package/vendor/blitz-dom/src/layout/mod.rs +969 -0
  32. package/vendor/blitz-dom/src/layout/paint_tree.rs +462 -0
  33. package/vendor/blitz-dom/src/layout/replaced.rs +439 -0
  34. package/vendor/blitz-dom/src/layout/table.rs +967 -0
  35. package/vendor/blitz-dom/src/layout/text_transform.rs +979 -0
  36. package/vendor/blitz-dom/src/layout/writing_mode.rs +437 -0
  37. package/vendor/blitz-dom/src/lib.rs +160 -0
  38. package/vendor/blitz-dom/src/mutator.rs +1818 -0
  39. package/vendor/blitz-dom/src/net.rs +649 -0
  40. package/vendor/blitz-dom/src/node/attributes.rs +60 -0
  41. package/vendor/blitz-dom/src/node/custom_widget.rs +178 -0
  42. package/vendor/blitz-dom/src/node/element.rs +1106 -0
  43. package/vendor/blitz-dom/src/node/mod.rs +30 -0
  44. package/vendor/blitz-dom/src/node/node.rs +2036 -0
  45. package/vendor/blitz-dom/src/node/scrollbar.rs +237 -0
  46. package/vendor/blitz-dom/src/node/serialize.rs +225 -0
  47. package/vendor/blitz-dom/src/node/stylo_data.rs +142 -0
  48. package/vendor/blitz-dom/src/node/svg.rs +265 -0
  49. package/vendor/blitz-dom/src/node/text.rs +804 -0
  50. package/vendor/blitz-dom/src/query_selector.rs +275 -0
  51. package/vendor/blitz-dom/src/resolve.rs +474 -0
  52. package/vendor/blitz-dom/src/resolved_style.rs +797 -0
  53. package/vendor/blitz-dom/src/scrolling.rs +780 -0
  54. package/vendor/blitz-dom/src/selection.rs +122 -0
  55. package/vendor/blitz-dom/src/stylo.rs +1518 -0
  56. package/vendor/blitz-dom/src/stylo_device.rs +88 -0
  57. package/vendor/blitz-dom/src/stylo_to_cursor_icon.rs +49 -0
  58. package/vendor/blitz-dom/src/stylo_to_kurbo.rs +129 -0
  59. package/vendor/blitz-dom/src/stylo_to_parley.rs +655 -0
  60. package/vendor/blitz-dom/src/traversal.rs +600 -0
  61. package/vendor/blitz-dom/src/tree.rs +136 -0
  62. package/vendor/blitz-dom/src/url.rs +63 -0
  63. package/vendor/blitz-dom/src/util.rs +256 -0
  64. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,649 @@
1
+ use blitz_traits::node_id::NodeId;
2
+ use selectors::context::QuirksMode;
3
+ use std::sync::atomic::Ordering as Ao;
4
+ use std::{
5
+ io::Cursor,
6
+ sync::{Arc, atomic::AtomicUsize, mpsc::Sender},
7
+ };
8
+ use 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
+
22
+ use blitz_traits::net::{AbortSignal, Bytes, NetHandler, NetProvider, Request};
23
+ use blitz_traits::shell::ShellProvider;
24
+
25
+ use url::Url;
26
+
27
+ use crate::{document::DocumentEvent, util::ImageType};
28
+
29
+ pub(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
+ /// Carries `@font-face` descriptors from CSS parsing through to font
38
+ /// registration so `parley::Collection::register_fonts` can alias the bytes
39
+ /// under the `font-family` declared in CSS rather than whatever family name
40
+ /// the TTF's own `name` table reports.
41
+ ///
42
+ /// All fields are `Option` because each descriptor is independently optional
43
+ /// at the CSS level. Missing fields fall back to the values parley reads
44
+ /// from the font's own metadata.
45
+ #[derive(Clone, Debug, Default)]
46
+ pub struct FontFaceOverrides {
47
+ /// `font-family` descriptor (the alias the rest of the stylesheet uses).
48
+ pub family_name: Option<String>,
49
+ /// `font-weight` descriptor as a single CSS weight (100–900). Stylo
50
+ /// parses this as a range; we record the lower bound, which equals the
51
+ /// upper bound in the common single-value case.
52
+ pub weight: Option<f32>,
53
+ /// `font-style` descriptor mapped to fontique's `FontStyle`.
54
+ pub style: Option<parley::fontique::FontStyle>,
55
+ }
56
+
57
+ #[derive(Clone, Debug)]
58
+ pub enum Resource {
59
+ Image(ImageType, u32, u32, Arc<Vec<u8>>),
60
+ #[cfg(feature = "svg")]
61
+ Svg(ImageType, crate::node::SvgImageData),
62
+ Css(DocumentStyleSheet),
63
+ /// Stylesheet loaded for an `@import` rule, to be attached to the rule on the document thread
64
+ ImportedCss(ServoArc<Locked<ImportRule>>, ServoArc<Stylesheet>),
65
+ Font(Bytes, FontFaceOverrides),
66
+ /// HTML fetched for an `<iframe>` element's `src`
67
+ DocumentSrc(String),
68
+ None,
69
+ }
70
+
71
+ pub(crate) struct ResourceHandler<T: Send + Sync + 'static> {
72
+ doc_id: usize,
73
+ request_id: usize,
74
+ node_id: Option<NodeId>,
75
+ tx: Sender<DocumentEvent>,
76
+ shell_provider: Arc<dyn ShellProvider>,
77
+ data: T,
78
+ }
79
+
80
+ impl<T: Send + Sync + 'static> ResourceHandler<T> {
81
+ pub(crate) fn new(
82
+ tx: Sender<DocumentEvent>,
83
+ doc_id: usize,
84
+ node_id: Option<NodeId>,
85
+ shell_provider: Arc<dyn ShellProvider>,
86
+ data: T,
87
+ ) -> Self {
88
+ static REQUEST_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
89
+ Self {
90
+ request_id: REQUEST_ID_COUNTER.fetch_add(1, Ao::Relaxed),
91
+ doc_id,
92
+ node_id,
93
+ tx,
94
+ shell_provider,
95
+ data,
96
+ }
97
+ }
98
+
99
+ pub(crate) fn boxed(
100
+ tx: Sender<DocumentEvent>,
101
+ doc_id: usize,
102
+ node_id: Option<NodeId>,
103
+ shell_provider: Arc<dyn ShellProvider>,
104
+ data: T,
105
+ ) -> Box<dyn NetHandler>
106
+ where
107
+ ResourceHandler<T>: NetHandler,
108
+ {
109
+ Box::new(Self::new(tx, doc_id, node_id, shell_provider, data)) as _
110
+ }
111
+
112
+ pub(crate) fn request_id(&self) -> usize {
113
+ self.request_id
114
+ }
115
+
116
+ fn respond(&self, resolved_url: String, result: Result<Resource, String>) {
117
+ let response = ResourceLoadResponse {
118
+ request_id: self.request_id,
119
+ node_id: self.node_id,
120
+ resolved_url: Some(resolved_url),
121
+ result,
122
+ };
123
+ let _ = self.tx.send(DocumentEvent::ResourceLoad(response));
124
+ self.shell_provider.request_redraw();
125
+ }
126
+ }
127
+
128
+ #[allow(unused)]
129
+ pub struct ResourceLoadResponse {
130
+ pub request_id: usize,
131
+ pub node_id: Option<NodeId>,
132
+ pub resolved_url: Option<String>,
133
+ pub result: Result<Resource, String>,
134
+ }
135
+
136
+ pub struct StylesheetHandler {
137
+ pub source_url: Url,
138
+ pub guard: SharedRwLock,
139
+ pub net_provider: Arc<dyn NetProvider>,
140
+ pub abort_signal: Option<AbortSignal>,
141
+ }
142
+
143
+ impl NetHandler for ResourceHandler<StylesheetHandler> {
144
+ fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
145
+ let Ok(css) = std::str::from_utf8(&bytes) else {
146
+ return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
147
+ };
148
+
149
+ // NOTE(Nico): I don't *think* external stylesheets should have HTML entities escaped
150
+ // let escaped_css = html_escape::decode_html_entities(css);
151
+
152
+ let sheet = Stylesheet::from_str(
153
+ css,
154
+ self.data.source_url.clone().into(),
155
+ Origin::Author,
156
+ ServoArc::new(self.data.guard.wrap(MediaList::empty())),
157
+ self.data.guard.clone(),
158
+ Some(&StylesheetLoader {
159
+ tx: self.tx.clone(),
160
+ doc_id: self.doc_id,
161
+ net_provider: self.data.net_provider.clone(),
162
+ shell_provider: self.shell_provider.clone(),
163
+ abort_signal: self.data.abort_signal.clone(),
164
+ import_depth: 0,
165
+ }),
166
+ None, // error_reporter
167
+ QuirksMode::NoQuirks,
168
+ AllowImportRules::Yes,
169
+ );
170
+
171
+ self.respond(
172
+ resolved_url,
173
+ Ok(Resource::Css(DocumentStyleSheet(ServoArc::new(sheet)))),
174
+ );
175
+ }
176
+ }
177
+
178
+ /// Maximum depth of nested `@import` rules. Imports nested deeper than this
179
+ /// are refused, preventing unbounded recursion (e.g. a stylesheet whose
180
+ /// resolved `@import` URL yields another stylesheet importing in turn, with
181
+ /// the URL growing geometrically at each level).
182
+ const MAX_IMPORT_DEPTH: usize = 16;
183
+
184
+ #[derive(Clone)]
185
+ pub(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
+ /// Depth of `@import` nesting for the stylesheet this loader is parsing
192
+ /// (0 for a top-level stylesheet)
193
+ pub(crate) import_depth: usize,
194
+ }
195
+ impl ServoStylesheetLoader for StylesheetLoader {
196
+ fn request_stylesheet(
197
+ &self,
198
+ url: CssUrl,
199
+ location: SourceLocation,
200
+ lock: &SharedRwLock,
201
+ media: ServoArc<Locked<MediaList>>,
202
+ supports: Option<ImportSupportsCondition>,
203
+ layer: ImportLayer,
204
+ ) -> ServoArc<Locked<ImportRule>> {
205
+ if self.import_depth >= MAX_IMPORT_DEPTH || !supports.as_ref().is_none_or(|s| s.enabled) {
206
+ return ServoArc::new(lock.wrap(ImportRule {
207
+ url,
208
+ stylesheet: ImportSheet::new_refused(),
209
+ supports,
210
+ layer,
211
+ source_location: location,
212
+ }));
213
+ }
214
+
215
+ let import = ImportRule {
216
+ url,
217
+ stylesheet: ImportSheet::new_pending(),
218
+ supports,
219
+ layer,
220
+ source_location: location,
221
+ };
222
+
223
+ let url = import.url.url().unwrap().clone();
224
+ let import = ServoArc::new(lock.wrap(import));
225
+ self.net_provider.fetch(
226
+ self.doc_id,
227
+ stamped_request(url.as_ref().clone(), self.abort_signal.as_ref()),
228
+ ResourceHandler::boxed(
229
+ self.tx.clone(),
230
+ self.doc_id,
231
+ None, // node_id
232
+ self.shell_provider.clone(),
233
+ NestedStylesheetHandler {
234
+ url: url.clone(),
235
+ loader: StylesheetLoader {
236
+ import_depth: self.import_depth + 1,
237
+ ..self.clone()
238
+ },
239
+ lock: lock.clone(),
240
+ media,
241
+ import_rule: import.clone(),
242
+ net_provider: self.net_provider.clone(),
243
+ },
244
+ ),
245
+ );
246
+
247
+ import
248
+ }
249
+ }
250
+
251
+ struct NestedStylesheetHandler {
252
+ loader: StylesheetLoader,
253
+ lock: SharedRwLock,
254
+ url: ServoArc<Url>,
255
+ media: ServoArc<Locked<MediaList>>,
256
+ import_rule: ServoArc<Locked<ImportRule>>,
257
+ net_provider: Arc<dyn NetProvider>,
258
+ }
259
+
260
+ impl NetHandler for ResourceHandler<NestedStylesheetHandler> {
261
+ fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
262
+ let Ok(css) = std::str::from_utf8(&bytes) else {
263
+ return self.respond(resolved_url, Err(String::from("Invalid UTF8")));
264
+ };
265
+
266
+ // NOTE(Nico): I don't *think* external stylesheets should have HTML entities escaped
267
+ // let escaped_css = html_escape::decode_html_entities(css);
268
+
269
+ let sheet = ServoArc::new(Stylesheet::from_str(
270
+ css,
271
+ UrlExtraData(self.data.url.clone()),
272
+ Origin::Author,
273
+ self.data.media.clone(),
274
+ self.data.lock.clone(),
275
+ Some(&self.data.loader),
276
+ None, // error_reporter
277
+ QuirksMode::NoQuirks,
278
+ AllowImportRules::Yes,
279
+ ));
280
+
281
+ // Fetch @font-face fonts
282
+ fetch_font_face(
283
+ self.tx.clone(),
284
+ self.doc_id,
285
+ self.node_id,
286
+ &sheet,
287
+ &self.data.net_provider,
288
+ &self.shell_provider,
289
+ &self.data.lock.read(),
290
+ self.data.loader.abort_signal.as_ref(),
291
+ );
292
+
293
+ let import_rule = self.data.import_rule.clone();
294
+ self.respond(resolved_url, Ok(Resource::ImportedCss(import_rule, sheet)))
295
+ }
296
+ }
297
+
298
+ struct FontFaceHandler {
299
+ format: FontFaceSourceFormatKeyword,
300
+ overrides: FontFaceOverrides,
301
+ }
302
+ impl NetHandler for ResourceHandler<FontFaceHandler> {
303
+ fn bytes(mut self: Box<Self>, resolved_url: String, bytes: Bytes) {
304
+ let result = self.data.parse(bytes);
305
+ self.respond(resolved_url, result)
306
+ }
307
+ }
308
+ impl FontFaceHandler {
309
+ fn parse(&mut self, bytes: Bytes) -> Result<Resource, String> {
310
+ if self.format == FontFaceSourceFormatKeyword::None && bytes.len() >= 4 {
311
+ self.format = match &bytes.as_ref()[0..4] {
312
+ // WOFF (v1) files begin with 0x774F4646 ('wOFF' in ascii)
313
+ // See: <https://w3c.github.io/woff/woff1/spec/Overview.html#WOFFHeader>
314
+ #[cfg(feature = "woff")]
315
+ b"wOFF" => FontFaceSourceFormatKeyword::Woff,
316
+ // WOFF2 files begin with 0x774F4632 ('wOF2' in ascii)
317
+ // See: <https://w3c.github.io/woff/woff2/#woff20Header>
318
+ #[cfg(feature = "woff")]
319
+ b"wOF2" => FontFaceSourceFormatKeyword::Woff2,
320
+ // Opentype fonts with CFF data begin with 0x4F54544F ('OTTO' in ascii)
321
+ // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font>
322
+ b"OTTO" => FontFaceSourceFormatKeyword::Opentype,
323
+ // Opentype fonts truetype outlines begin with 0x00010000
324
+ // See: <https://learn.microsoft.com/en-us/typography/opentype/spec/otff#organization-of-an-opentype-font>
325
+ &[0x00, 0x01, 0x00, 0x00] => FontFaceSourceFormatKeyword::Truetype,
326
+ // Truetype fonts begin with 0x74727565 ('true' in ascii)
327
+ // See: <https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6.html#ScalerTypeNote>
328
+ b"true" => FontFaceSourceFormatKeyword::Truetype,
329
+ _ => FontFaceSourceFormatKeyword::None,
330
+ }
331
+ }
332
+
333
+ // Satisfy rustc's mutability linting with woff feature both enabled/disabled
334
+ #[cfg(feature = "woff")]
335
+ let mut bytes = bytes;
336
+
337
+ match self.format {
338
+ #[cfg(feature = "woff")]
339
+ FontFaceSourceFormatKeyword::Woff => {
340
+ #[cfg(feature = "tracing")]
341
+ tracing::info!("Decompressing woff1 font");
342
+
343
+ // Use wuff crate to decompress font
344
+ let decompressed = wuff::decompress_woff1(&bytes).ok();
345
+
346
+ if let Some(decompressed) = decompressed {
347
+ bytes = Bytes::from(decompressed);
348
+ } else {
349
+ #[cfg(feature = "tracing")]
350
+ tracing::warn!("Failed to decompress woff1 font");
351
+ }
352
+ }
353
+ #[cfg(feature = "woff")]
354
+ FontFaceSourceFormatKeyword::Woff2 => {
355
+ #[cfg(feature = "tracing")]
356
+ tracing::info!("Decompressing woff2 font");
357
+
358
+ // Use wuff crate to decompress font
359
+ let decompressed = wuff::decompress_woff2(&bytes).ok();
360
+
361
+ if let Some(decompressed) = decompressed {
362
+ bytes = Bytes::from(decompressed);
363
+ } else {
364
+ #[cfg(feature = "tracing")]
365
+ tracing::warn!("Failed to decompress woff2 font");
366
+ }
367
+ }
368
+ FontFaceSourceFormatKeyword::None => {
369
+ // Should this be an error?
370
+ return Ok(Resource::None);
371
+ }
372
+ _ => {}
373
+ }
374
+
375
+ Ok(Resource::Font(bytes, std::mem::take(&mut self.overrides)))
376
+ }
377
+ }
378
+
379
+ /// Fetch the fonts of all `@font-face` rules in `sheet`
380
+ #[allow(clippy::too_many_arguments)]
381
+ pub(crate) fn fetch_font_face(
382
+ tx: Sender<DocumentEvent>,
383
+ doc_id: usize,
384
+ node_id: Option<NodeId>,
385
+ sheet: &Stylesheet,
386
+ network_provider: &Arc<dyn NetProvider>,
387
+ shell_provider: &Arc<dyn ShellProvider>,
388
+ read_guard: &SharedRwLockReadGuard,
389
+ abort_signal: Option<&AbortSignal>,
390
+ ) {
391
+ fetch_font_face_rules(
392
+ sheet.contents(read_guard).rules(read_guard).iter(),
393
+ tx,
394
+ doc_id,
395
+ node_id,
396
+ network_provider,
397
+ shell_provider,
398
+ read_guard,
399
+ abort_signal,
400
+ )
401
+ }
402
+
403
+ /// Fetch the fonts of the `@font-face` rules among `rules`
404
+ #[allow(clippy::too_many_arguments)]
405
+ pub(crate) fn fetch_font_face_rules<'a>(
406
+ rules: impl Iterator<Item = &'a CssRule>,
407
+ tx: Sender<DocumentEvent>,
408
+ doc_id: usize,
409
+ node_id: Option<NodeId>,
410
+ network_provider: &Arc<dyn NetProvider>,
411
+ shell_provider: &Arc<dyn ShellProvider>,
412
+ read_guard: &SharedRwLockReadGuard,
413
+ abort_signal: Option<&AbortSignal>,
414
+ ) {
415
+ rules
416
+ .filter_map(|rule| match rule {
417
+ CssRule::FontFace(font_face) => {
418
+ let descriptor = &font_face.read_with(read_guard).descriptors;
419
+ let family = descriptor.font_family.as_ref()?;
420
+ let src = descriptor.src.as_ref()?;
421
+ // Capture the @font-face descriptors so parley can register
422
+ // the font under the CSS-declared family name (and weight /
423
+ // style) rather than whatever metadata the TTF reports.
424
+ let overrides = FontFaceOverrides {
425
+ family_name: Some(family.name.to_string()),
426
+ weight: descriptor
427
+ .font_weight
428
+ .as_ref()
429
+ .and_then(|range| range.0.compute().map(|w| w.value())),
430
+ style: descriptor.font_style.as_ref().map(stylo_to_fontique_style),
431
+ };
432
+ Some((src, overrides))
433
+ }
434
+ _ => None,
435
+ })
436
+ .for_each(|(source_list, overrides)| {
437
+ // Find the first font source in the source list that specifies a font of a type
438
+ // that we support.
439
+ let preferred_source = source_list
440
+ .0
441
+ .iter()
442
+ .filter_map(|source| match source {
443
+ Source::Url(url_source) => Some(url_source),
444
+ // TODO: support local fonts in @font-face
445
+ Source::Local(_) => None,
446
+ })
447
+ .find_map(|url_source| {
448
+ let mut format = match &url_source.format_hint {
449
+ Some(FontFaceSourceFormat::Keyword(fmt)) => *fmt,
450
+ Some(FontFaceSourceFormat::String(str)) => match str.as_str() {
451
+ "woff2" => FontFaceSourceFormatKeyword::Woff2,
452
+ "ttf" => FontFaceSourceFormatKeyword::Truetype,
453
+ "otf" => FontFaceSourceFormatKeyword::Opentype,
454
+ _ => FontFaceSourceFormatKeyword::None,
455
+ },
456
+ _ => FontFaceSourceFormatKeyword::None,
457
+ };
458
+ // A url with no extension, such as a `data:` url, is
459
+ // sniffed from its bytes once fetched.
460
+ if format == FontFaceSourceFormatKeyword::None
461
+ && let Some((_, end)) = url_source.url.as_str().rsplit_once('.')
462
+ {
463
+ format = match end {
464
+ "woff2" => FontFaceSourceFormatKeyword::Woff2,
465
+ "woff" => FontFaceSourceFormatKeyword::Woff,
466
+ "ttf" => FontFaceSourceFormatKeyword::Truetype,
467
+ "otf" => FontFaceSourceFormatKeyword::Opentype,
468
+ "svg" => FontFaceSourceFormatKeyword::Svg,
469
+ "eot" => FontFaceSourceFormatKeyword::EmbeddedOpentype,
470
+ _ => FontFaceSourceFormatKeyword::None,
471
+ }
472
+ }
473
+
474
+ if matches!(
475
+ format,
476
+ FontFaceSourceFormatKeyword::Svg
477
+ | FontFaceSourceFormatKeyword::EmbeddedOpentype
478
+ ) {
479
+ #[cfg(feature = "tracing")]
480
+ tracing::warn!("Skipping unsupported font of type {:?}", format);
481
+ return None;
482
+ }
483
+
484
+ #[cfg(not(feature = "woff"))]
485
+ if matches!(
486
+ format,
487
+ FontFaceSourceFormatKeyword::Woff | FontFaceSourceFormatKeyword::Woff2
488
+ ) {
489
+ #[cfg(feature = "tracing")]
490
+ tracing::warn!("Skipping unsupported font of type {:?}", format);
491
+ return None;
492
+ }
493
+
494
+ // A relative url with no base url to resolve against
495
+ // yields None; skip the source instead of panicking
496
+ let Some(url) = url_source.url.url() else {
497
+ #[cfg(feature = "tracing")]
498
+ tracing::warn!("Skipping @font-face source with unresolvable url");
499
+ return None;
500
+ };
501
+ let url = url.as_ref().clone();
502
+ Some((url, format))
503
+ });
504
+
505
+ if let Some((url, format)) = preferred_source {
506
+ network_provider.fetch(
507
+ doc_id,
508
+ stamped_request(url, abort_signal),
509
+ ResourceHandler::boxed(
510
+ tx.clone(),
511
+ doc_id,
512
+ node_id,
513
+ shell_provider.clone(),
514
+ FontFaceHandler { format, overrides },
515
+ ),
516
+ );
517
+ }
518
+ })
519
+ }
520
+
521
+ /// Translate stylo's `@font-face` `font-style` descriptor into the fontique
522
+ /// `FontStyle` enum used by parley. Stylo encodes Italic and Oblique-with-
523
+ /// angle distinctly; CSS's bare `normal` is parsed as `Oblique(0deg, 0deg)`
524
+ /// by stylo (see the `FontStyle::parse` impl in stylo's `font_face.rs`), so
525
+ /// that pattern is treated as `Normal` here.
526
+ fn stylo_to_fontique_style(style: &FontStyleRange) -> parley::fontique::FontStyle {
527
+ use parley::fontique::FontStyle as Fq;
528
+ match style {
529
+ FontStyleRange::Italic => Fq::Italic,
530
+ FontStyleRange::Oblique(min, max) => {
531
+ let angle = min.degrees();
532
+ // Stylo emits `Oblique(0deg, 0deg)` for the literal CSS `normal`
533
+ // keyword. Map that back to `Normal` so parley's font matching
534
+ // doesn't misclassify upright fonts.
535
+ if angle.is_none_or(|a| a == 0.0) && max.degrees().is_none_or(|a| a == 0.0) {
536
+ Fq::Normal
537
+ } else {
538
+ Fq::Oblique(angle)
539
+ }
540
+ }
541
+ }
542
+ }
543
+
544
+ /// Handles HTML fetched for an `<iframe>` element's `src`
545
+ pub(crate) struct DocumentSrcHandler;
546
+
547
+ impl NetHandler for ResourceHandler<DocumentSrcHandler> {
548
+ fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
549
+ let html = String::from_utf8_lossy(&bytes).into_owned();
550
+ self.respond(resolved_url, Ok(Resource::DocumentSrc(html)));
551
+ }
552
+ }
553
+
554
+ pub struct ImageHandler {
555
+ kind: ImageType,
556
+ }
557
+ impl ImageHandler {
558
+ pub fn new(kind: ImageType) -> Self {
559
+ Self { kind }
560
+ }
561
+ }
562
+
563
+ impl NetHandler for ResourceHandler<ImageHandler> {
564
+ fn bytes(self: Box<Self>, resolved_url: String, bytes: Bytes) {
565
+ let result = self.data.parse(bytes);
566
+ self.respond(resolved_url, result)
567
+ }
568
+ }
569
+
570
+ impl ImageHandler {
571
+ fn parse(&self, bytes: Bytes) -> Result<Resource, String> {
572
+ let image_err = match image::ImageReader::new(Cursor::new(&bytes))
573
+ .with_guessed_format()
574
+ .expect("IO errors impossible with Cursor")
575
+ .decode()
576
+ {
577
+ Ok(image) => {
578
+ let width = image.width();
579
+ let height = image.height();
580
+ let raw_rgba8_data = image.into_rgba8().into_raw();
581
+ return Ok(Resource::Image(
582
+ self.kind,
583
+ width,
584
+ height,
585
+ Arc::new(raw_rgba8_data),
586
+ ));
587
+ }
588
+ Err(e) => e.to_string(),
589
+ };
590
+
591
+ #[cfg(feature = "svg")]
592
+ let svg_err = {
593
+ use crate::util::parse_svg_image;
594
+ match parse_svg_image(&bytes) {
595
+ Ok(svg) => return Ok(Resource::Svg(self.kind, svg)),
596
+ Err(e) => e.to_string(),
597
+ }
598
+ };
599
+ #[cfg(not(feature = "svg"))]
600
+ let svg_err = "svg feature disabled";
601
+
602
+ Err(format!(
603
+ "Could not parse image ({} bytes): image-crate error: {image_err}; svg fallback error: {svg_err}",
604
+ bytes.len()
605
+ ))
606
+ }
607
+ }
608
+
609
+ #[cfg(test)]
610
+ mod tests {
611
+ use super::*;
612
+ use parley::fontique::FontStyle as Fq;
613
+ use style::values::specified::Angle;
614
+
615
+ fn oblique(min_deg: f32, max_deg: f32) -> FontStyleRange {
616
+ FontStyleRange::Oblique(Angle::from_degrees(min_deg), Angle::from_degrees(max_deg))
617
+ }
618
+
619
+ #[test]
620
+ fn italic_maps_to_italic() {
621
+ assert_eq!(stylo_to_fontique_style(&FontStyleRange::Italic), Fq::Italic,);
622
+ }
623
+
624
+ #[test]
625
+ fn oblique_zero_zero_maps_to_normal() {
626
+ // Stylo parses bare CSS `normal` as `Oblique(0deg, 0deg)`; the
627
+ // helper must round-trip that back to `FontStyle::Normal` so
628
+ // parley's matching doesn't misclassify upright fonts.
629
+ assert_eq!(stylo_to_fontique_style(&oblique(0.0, 0.0)), Fq::Normal);
630
+ }
631
+
632
+ #[test]
633
+ fn oblique_single_angle_maps_to_oblique_with_min() {
634
+ assert_eq!(
635
+ stylo_to_fontique_style(&oblique(14.0, 14.0)),
636
+ Fq::Oblique(Some(14.0)),
637
+ );
638
+ }
639
+
640
+ #[test]
641
+ fn oblique_range_uses_min_angle() {
642
+ // For a range, fontique's single-angle representation takes the
643
+ // lower bound — confirm `min` (not `max`) is what gets through.
644
+ assert_eq!(
645
+ stylo_to_fontique_style(&oblique(10.0, 20.0)),
646
+ Fq::Oblique(Some(10.0)),
647
+ );
648
+ }
649
+ }