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.
- package/README.md +6 -6
- package/THIRD_PARTY_NOTICES.md +5 -2
- package/docs/layout-regressions.md +65 -0
- package/docs/pagination-regressions.md +7 -6
- package/package.json +1 -1
- package/vendor/README.md +35 -0
- package/vendor/blitz-dom/Cargo.toml +113 -0
- package/vendor/blitz-dom/LICENSE-APACHE +176 -0
- package/vendor/blitz-dom/LICENSE-MIT +23 -0
- package/vendor/blitz-dom/assets/default.css +1142 -0
- package/vendor/blitz-dom/assets/moz-bullet-font.otf +0 -0
- package/vendor/blitz-dom/src/accessibility.rs +255 -0
- package/vendor/blitz-dom/src/config.rs +97 -0
- package/vendor/blitz-dom/src/cssom.rs +971 -0
- package/vendor/blitz-dom/src/debug.rs +206 -0
- package/vendor/blitz-dom/src/document.rs +3395 -0
- package/vendor/blitz-dom/src/events/driver.rs +427 -0
- package/vendor/blitz-dom/src/events/focus.rs +40 -0
- package/vendor/blitz-dom/src/events/ime.rs +31 -0
- package/vendor/blitz-dom/src/events/keyboard.rs +174 -0
- package/vendor/blitz-dom/src/events/mod.rs +315 -0
- package/vendor/blitz-dom/src/events/pointer.rs +849 -0
- package/vendor/blitz-dom/src/font_metrics.rs +177 -0
- package/vendor/blitz-dom/src/form.rs +463 -0
- package/vendor/blitz-dom/src/html.rs +38 -0
- package/vendor/blitz-dom/src/iframe.rs +186 -0
- package/vendor/blitz-dom/src/layout/construct.rs +1328 -0
- package/vendor/blitz-dom/src/layout/damage.rs +642 -0
- package/vendor/blitz-dom/src/layout/inline.rs +1160 -0
- package/vendor/blitz-dom/src/layout/list.rs +237 -0
- package/vendor/blitz-dom/src/layout/mod.rs +969 -0
- package/vendor/blitz-dom/src/layout/paint_tree.rs +462 -0
- package/vendor/blitz-dom/src/layout/replaced.rs +439 -0
- package/vendor/blitz-dom/src/layout/table.rs +967 -0
- package/vendor/blitz-dom/src/layout/text_transform.rs +979 -0
- package/vendor/blitz-dom/src/layout/writing_mode.rs +437 -0
- package/vendor/blitz-dom/src/lib.rs +160 -0
- package/vendor/blitz-dom/src/mutator.rs +1818 -0
- package/vendor/blitz-dom/src/net.rs +649 -0
- package/vendor/blitz-dom/src/node/attributes.rs +60 -0
- package/vendor/blitz-dom/src/node/custom_widget.rs +178 -0
- package/vendor/blitz-dom/src/node/element.rs +1106 -0
- package/vendor/blitz-dom/src/node/mod.rs +30 -0
- package/vendor/blitz-dom/src/node/node.rs +2036 -0
- package/vendor/blitz-dom/src/node/scrollbar.rs +237 -0
- package/vendor/blitz-dom/src/node/serialize.rs +225 -0
- package/vendor/blitz-dom/src/node/stylo_data.rs +142 -0
- package/vendor/blitz-dom/src/node/svg.rs +265 -0
- package/vendor/blitz-dom/src/node/text.rs +804 -0
- package/vendor/blitz-dom/src/query_selector.rs +275 -0
- package/vendor/blitz-dom/src/resolve.rs +474 -0
- package/vendor/blitz-dom/src/resolved_style.rs +797 -0
- package/vendor/blitz-dom/src/scrolling.rs +780 -0
- package/vendor/blitz-dom/src/selection.rs +122 -0
- package/vendor/blitz-dom/src/stylo.rs +1518 -0
- package/vendor/blitz-dom/src/stylo_device.rs +88 -0
- package/vendor/blitz-dom/src/stylo_to_cursor_icon.rs +49 -0
- package/vendor/blitz-dom/src/stylo_to_kurbo.rs +129 -0
- package/vendor/blitz-dom/src/stylo_to_parley.rs +655 -0
- package/vendor/blitz-dom/src/traversal.rs +600 -0
- package/vendor/blitz-dom/src/tree.rs +136 -0
- package/vendor/blitz-dom/src/url.rs +63 -0
- package/vendor/blitz-dom/src/util.rs +256 -0
- 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
|
+
}
|