blitz-pdf 0.1.5 → 0.1.7

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 (60) hide show
  1. package/README.md +61 -9
  2. package/THIRD_PARTY_NOTICES.md +20 -0
  3. package/dist/types.d.ts +2 -1
  4. package/dist/types.d.ts.map +1 -1
  5. package/licenses/Noto-OFL.txt +94 -0
  6. package/licenses/NotoCJK-OFL.txt +92 -0
  7. package/licenses/NotoEmoji-OFL.txt +93 -0
  8. package/licenses/Unifont-OFL.txt +90 -0
  9. package/licenses/font-sources.json +106 -0
  10. package/package.json +1 -1
  11. package/src/types.ts +2 -1
  12. package/vendor/README.md +43 -0
  13. package/vendor/blitz-paint/Cargo.toml +50 -0
  14. package/vendor/blitz-paint/LICENSE-APACHE +176 -0
  15. package/vendor/blitz-paint/LICENSE-MIT +23 -0
  16. package/vendor/blitz-paint/src/color.rs +66 -0
  17. package/vendor/blitz-paint/src/debug_overlay.rs +168 -0
  18. package/vendor/blitz-paint/src/filters.rs +40 -0
  19. package/vendor/blitz-paint/src/gradient.rs +536 -0
  20. package/vendor/blitz-paint/src/kurbo_css/css_box.rs +847 -0
  21. package/vendor/blitz-paint/src/kurbo_css/mod.rs +77 -0
  22. package/vendor/blitz-paint/src/kurbo_css/non_uniform_radii.rs +60 -0
  23. package/vendor/blitz-paint/src/layers.rs +95 -0
  24. package/vendor/blitz-paint/src/lib.rs +190 -0
  25. package/vendor/blitz-paint/src/render/background.rs +968 -0
  26. package/vendor/blitz-paint/src/render/border.rs +631 -0
  27. package/vendor/blitz-paint/src/render/box_shadow.rs +157 -0
  28. package/vendor/blitz-paint/src/render/clip_path.rs +492 -0
  29. package/vendor/blitz-paint/src/render/form_controls.rs +103 -0
  30. package/vendor/blitz-paint/src/render/mask.rs +144 -0
  31. package/vendor/blitz-paint/src/render.rs +1351 -0
  32. package/vendor/blitz-paint/src/sizing.rs +63 -0
  33. package/vendor/blitz-paint/src/text.rs +747 -0
  34. package/vendor/fontique/Cargo.toml +194 -0
  35. package/vendor/fontique/LICENSE-APACHE +201 -0
  36. package/vendor/fontique/LICENSE-MIT +25 -0
  37. package/vendor/fontique/README.md +54 -0
  38. package/vendor/fontique/examples/generic_families.rs +37 -0
  39. package/vendor/fontique/src/attributes.rs +42 -0
  40. package/vendor/fontique/src/backend/android.rs +218 -0
  41. package/vendor/fontique/src/backend/coretext.rs +206 -0
  42. package/vendor/fontique/src/backend/dwrite.rs +395 -0
  43. package/vendor/fontique/src/backend/fontconfig.rs +844 -0
  44. package/vendor/fontique/src/backend/mod.rs +75 -0
  45. package/vendor/fontique/src/charmap.rs +127 -0
  46. package/vendor/fontique/src/collection/mod.rs +1016 -0
  47. package/vendor/fontique/src/collection/query.rs +392 -0
  48. package/vendor/fontique/src/fallback.rs +293 -0
  49. package/vendor/fontique/src/family.rs +111 -0
  50. package/vendor/fontique/src/family_name.rs +153 -0
  51. package/vendor/fontique/src/font.rs +562 -0
  52. package/vendor/fontique/src/generic.rs +36 -0
  53. package/vendor/fontique/src/impl_fontconfig.rs +159 -0
  54. package/vendor/fontique/src/lib.rs +87 -0
  55. package/vendor/fontique/src/matching.rs +483 -0
  56. package/vendor/fontique/src/scan.rs +312 -0
  57. package/vendor/fontique/src/script.rs +271 -0
  58. package/vendor/fontique/src/source.rs +108 -0
  59. package/vendor/fontique/src/source_cache.rs +254 -0
  60. package/wasm/blitz_pdf_bg.wasm +0 -0
@@ -0,0 +1,968 @@
1
+ use super::{ElementCx, to_image_quality, to_peniko_image};
2
+ use crate::color::{Color, ToColorColor};
3
+ use crate::gradient::to_peniko_gradient;
4
+ use anyrender::PaintScene;
5
+ use blitz_dom::NodeId;
6
+ use blitz_dom::node::{ImageData, ImageResourceData, SpecialElementData};
7
+ use kurbo::{self, Affine, BezPath, Point, Rect, Shape, Size, Vec2};
8
+ use peniko::{self, Fill};
9
+ use style::{
10
+ computed_values::border_collapse::T as BorderCollapse,
11
+ properties::{
12
+ generated::longhands::{
13
+ background_attachment::single_value::computed_value::T as StyloBackgroundAttachment,
14
+ background_clip::single_value::computed_value::T as StyloBackgroundClip,
15
+ background_origin::single_value::computed_value::T as StyloBackgroundOrigin,
16
+ mask_origin::single_value::computed_value::T as StyloMaskOrigin,
17
+ },
18
+ style_structs::{Background, SVG},
19
+ },
20
+ values::{
21
+ computed::{
22
+ BackgroundRepeat, Gradient as StyloGradient, Image as ComputedImage, LengthPercentage,
23
+ background::BackgroundSize,
24
+ },
25
+ generics::image::GenericImage,
26
+ specified::background::BackgroundRepeatKeyword,
27
+ },
28
+ };
29
+
30
+ #[cfg(feature = "tracing")]
31
+ use tracing::warn;
32
+
33
+ /// A box from the CSS box model. Abstracts over the (structurally identical)
34
+ /// computed value types of the `background-clip`/`background-origin` and
35
+ /// `mask-clip`/`mask-origin` properties.
36
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
37
+ #[allow(clippy::enum_variant_names)] // The variants are named after the CSS keywords
38
+ pub(super) enum BoxModelBox {
39
+ BorderBox,
40
+ PaddingBox,
41
+ ContentBox,
42
+ }
43
+
44
+ // Also covers MaskClip as the type is the same
45
+ impl From<StyloBackgroundClip> for BoxModelBox {
46
+ fn from(value: StyloBackgroundClip) -> Self {
47
+ match value {
48
+ StyloBackgroundClip::BorderBox => Self::BorderBox,
49
+ StyloBackgroundClip::PaddingBox => Self::PaddingBox,
50
+ StyloBackgroundClip::ContentBox => Self::ContentBox,
51
+
52
+ // TODO: support BorderArea
53
+ StyloBackgroundClip::BorderArea => Self::BorderBox,
54
+ }
55
+ }
56
+ }
57
+
58
+ impl From<StyloBackgroundOrigin> for BoxModelBox {
59
+ fn from(value: StyloBackgroundOrigin) -> Self {
60
+ match value {
61
+ StyloBackgroundOrigin::BorderBox => Self::BorderBox,
62
+ StyloBackgroundOrigin::PaddingBox => Self::PaddingBox,
63
+ StyloBackgroundOrigin::ContentBox => Self::ContentBox,
64
+ }
65
+ }
66
+ }
67
+
68
+ impl From<StyloMaskOrigin> for BoxModelBox {
69
+ fn from(value: StyloMaskOrigin) -> Self {
70
+ match value {
71
+ StyloMaskOrigin::BorderBox => Self::BorderBox,
72
+ StyloMaskOrigin::PaddingBox => Self::PaddingBox,
73
+ StyloMaskOrigin::ContentBox => Self::ContentBox,
74
+ }
75
+ }
76
+ }
77
+
78
+ /// The styles and image data for a single layer of a CSS image layer list
79
+ /// (`background-image` or `mask-image`). The `background-*` and `mask-*`
80
+ /// properties share computed value types, which allows the layer painting code
81
+ /// to be shared.
82
+ pub(super) struct ImageLayerStyles<'a> {
83
+ /// The computed value of the `background-image`/`mask-image` layer
84
+ pub stylo_image: &'a ComputedImage,
85
+ /// The loaded image resource if `stylo_image` is a `url()` image
86
+ pub image_data: Option<&'a ImageResourceData>,
87
+ pub position_x: &'a LengthPercentage,
88
+ pub position_y: &'a LengthPercentage,
89
+ pub repeat: &'a BackgroundRepeat,
90
+ pub size: &'a BackgroundSize,
91
+ pub clip: BoxModelBox,
92
+ pub origin: BoxModelBox,
93
+ pub attachment: StyloBackgroundAttachment,
94
+ }
95
+
96
+ impl<'a> ImageLayerStyles<'a> {
97
+ pub(super) fn from_background(
98
+ bg_styles: &'a Background,
99
+ image_data: &'a [Option<ImageResourceData>],
100
+ idx: usize,
101
+ ) -> Self {
102
+ Self {
103
+ stylo_image: &bg_styles.background_image.0[idx],
104
+ image_data: image_data.get(idx).and_then(Option::as_ref),
105
+ position_x: get_cyclic(&bg_styles.background_position_x.0, idx),
106
+ position_y: get_cyclic(&bg_styles.background_position_y.0, idx),
107
+ repeat: get_cyclic(&bg_styles.background_repeat.0, idx),
108
+ size: get_cyclic(&bg_styles.background_size.0, idx),
109
+ clip: (*get_cyclic(&bg_styles.background_clip.0, idx)).into(),
110
+ origin: (*get_cyclic(&bg_styles.background_origin.0, idx)).into(),
111
+ attachment: *get_cyclic(&bg_styles.background_attachment.0, idx),
112
+ }
113
+ }
114
+
115
+ pub(super) fn from_svg(
116
+ svg_styles: &'a SVG,
117
+ image_data: &'a [Option<ImageResourceData>],
118
+ idx: usize,
119
+ ) -> Self {
120
+ Self {
121
+ stylo_image: &svg_styles.mask_image.0[idx],
122
+ image_data: image_data.get(idx).and_then(Option::as_ref),
123
+ position_x: get_cyclic(&svg_styles.mask_position_x.0, idx),
124
+ position_y: get_cyclic(&svg_styles.mask_position_y.0, idx),
125
+ repeat: get_cyclic(&svg_styles.mask_repeat.0, idx),
126
+ size: get_cyclic(&svg_styles.mask_size.0, idx),
127
+ clip: (*get_cyclic(&svg_styles.mask_clip.0, idx)).into(),
128
+ origin: (*get_cyclic(&svg_styles.mask_origin.0, idx)).into(),
129
+ // There is no `mask-attachment` property
130
+ attachment: StyloBackgroundAttachment::Scroll,
131
+ }
132
+ }
133
+ }
134
+
135
+ impl ElementCx<'_, '_> {
136
+ pub(super) fn draw_background(&self, scene: &mut impl PaintScene) {
137
+ let bg_styles = &self.style.get_background();
138
+ let image_data = &self.element.background_images;
139
+ let layer_count = bg_styles.background_image.0.len();
140
+
141
+ // The background color is clipped by the clip of the last layer in the list
142
+ let background_clip: BoxModelBox =
143
+ (*get_cyclic(&bg_styles.background_clip.0, layer_count - 1)).into();
144
+ let background_clip_path = self.box_path(background_clip);
145
+
146
+ // Draw background color (if any)
147
+ self.draw_solid_bg(scene, &background_clip_path);
148
+
149
+ for idx in (0..layer_count).rev() {
150
+ let layer = ImageLayerStyles::from_background(bg_styles, image_data, idx);
151
+ if layer_paints_nothing(&layer) {
152
+ continue;
153
+ }
154
+ let background_clip_path = self.box_path(layer.clip);
155
+
156
+ self.context.layer_manager.maybe_with_layer(
157
+ scene,
158
+ true,
159
+ 1.0,
160
+ self.transform,
161
+ &background_clip_path,
162
+ None,
163
+ None,
164
+ |scene| {
165
+ self.draw_image_layer(scene, &layer);
166
+ },
167
+ );
168
+ }
169
+ }
170
+
171
+ /// The path of the given CSS box model box for this element
172
+ pub(super) fn box_path(&self, css_box: BoxModelBox) -> BezPath {
173
+ match css_box {
174
+ BoxModelBox::BorderBox => self.frame.border_box_path(),
175
+ BoxModelBox::PaddingBox => self.frame.padding_box_path(),
176
+ BoxModelBox::ContentBox => self.frame.content_box_path(),
177
+ }
178
+ }
179
+
180
+ /// The rect of the given CSS box model box for this element
181
+ fn box_rect(&self, css_box: BoxModelBox) -> Rect {
182
+ match css_box {
183
+ BoxModelBox::BorderBox => self.frame.border_box,
184
+ BoxModelBox::PaddingBox => self.frame.padding_box,
185
+ BoxModelBox::ContentBox => self.frame.content_box,
186
+ }
187
+ }
188
+
189
+ /// Draw a single layer of a CSS image layer list (`background-image` or `mask-image`)
190
+ pub(super) fn draw_image_layer(&self, scene: &mut impl PaintScene, layer: &ImageLayerStyles) {
191
+ match layer.stylo_image {
192
+ GenericImage::None => {
193
+ // Do nothing
194
+ }
195
+ GenericImage::Gradient(gradient) => self.draw_gradient_layer(scene, gradient, layer),
196
+ GenericImage::Url(_) => {
197
+ self.draw_raster_image_layer(scene, layer);
198
+ #[cfg(feature = "svg")]
199
+ self.draw_svg_image_layer(scene, layer);
200
+ }
201
+ GenericImage::LightDark(_) => {
202
+ #[cfg(feature = "tracing")]
203
+ warn!("Implement image layer drawing for ImageLightDark")
204
+ }
205
+ GenericImage::PaintWorklet(_) => {
206
+ #[cfg(feature = "tracing")]
207
+ warn!("Implement image layer drawing for Image::PaintWorklet")
208
+ }
209
+ GenericImage::CrossFade(_) => {
210
+ #[cfg(feature = "tracing")]
211
+ warn!("Implement image layer drawing for Image::CrossFade")
212
+ }
213
+ GenericImage::Image(_) => {
214
+ #[cfg(feature = "tracing")]
215
+ warn!("Implement image layer drawing for Image::Image")
216
+ }
217
+ GenericImage::ImageSet(_) => {
218
+ #[cfg(feature = "tracing")]
219
+ warn!("Implement image layer drawing for Image::ImageSet")
220
+ }
221
+ }
222
+ }
223
+
224
+ /// Paint the backgrounds of the table's column groups, columns, row groups and rows
225
+ /// (in that order, bottom to top: CSS 2.1 §17.5.1).
226
+ ///
227
+ /// These backgrounds are only painted behind the table's cells (not in the
228
+ /// `border-spacing` between them, nor where a row has no cell), so they are
229
+ /// painted cell by cell.
230
+ pub(super) fn draw_table_row_backgrounds(&self, scene: &mut impl PaintScene) {
231
+ let SpecialElementData::TableRoot(table) = &self.element.special_data else {
232
+ return;
233
+ };
234
+
235
+ let background_color = |node_id: NodeId| -> Option<Color> {
236
+ let node = self.context.dom.get_node(node_id)?;
237
+ let style = node.primary_styles()?;
238
+ let current_color = style.clone_color();
239
+ let color = style
240
+ .get_background()
241
+ .background_color
242
+ .resolve_to_absolute(&current_color)
243
+ .as_srgb_color();
244
+ (color != Color::TRANSPARENT).then_some(color)
245
+ };
246
+
247
+ // Resolve the (group, track) background colors once per track rather than once per cell
248
+ let column_colors: Vec<[Option<Color>; 2]> = table
249
+ .columns
250
+ .iter()
251
+ .map(|col| {
252
+ [
253
+ col.group.and_then(background_color),
254
+ background_color(col.node_id),
255
+ ]
256
+ })
257
+ .collect();
258
+ let row_colors: Vec<[Option<Color>; 2]> = table
259
+ .rows
260
+ .iter()
261
+ .map(|row| {
262
+ [
263
+ row.group.and_then(background_color),
264
+ background_color(row.node_id),
265
+ ]
266
+ })
267
+ .collect();
268
+ let has_color =
269
+ |colors: &[[Option<Color>; 2]]| colors.iter().flatten().any(Option::is_some);
270
+ if !has_color(&column_colors) && !has_color(&row_colors) {
271
+ return;
272
+ }
273
+
274
+ let cell_rect = |cell_id: NodeId| -> Option<Rect> {
275
+ let layout = self.context.dom.get_node(cell_id)?.final_layout();
276
+ Some(Rect::new(
277
+ layout.location.x as f64,
278
+ layout.location.y as f64,
279
+ (layout.location.x + layout.size.width) as f64,
280
+ (layout.location.y + layout.size.height) as f64,
281
+ ))
282
+ };
283
+
284
+ // In the collapsed borders model the borders are laid out as gutters between the
285
+ // cells, but a cell's border box (and so its background) extends to the middle of
286
+ // the borders around it. The gutter is split on a whole pixel so that the
287
+ // backgrounds of adjacent cells tile exactly.
288
+ let collapsed = table.border_collapse == BorderCollapse::Collapse;
289
+ let (gutter, bounds) = if collapsed {
290
+ let gap = table.style.gap.map(|gap| gap.into_raw().value() as f64);
291
+ let bounds = table
292
+ .cells
293
+ .iter()
294
+ .filter_map(|cell| cell_rect(cell.node_id))
295
+ .reduce(|a, b| a.union(b));
296
+ (Size::new(gap.width, gap.height), bounds)
297
+ } else {
298
+ (Size::ZERO, None)
299
+ };
300
+ let before = Size::new((gutter.width / 2.0).ceil(), (gutter.height / 2.0).ceil());
301
+ let after = gutter - before;
302
+
303
+ for cell in &table.cells {
304
+ let no_colors = [None, None];
305
+ let column = column_colors
306
+ .get(cell.column as usize)
307
+ .unwrap_or(&no_colors);
308
+ let row = row_colors.get(cell.row as usize).unwrap_or(&no_colors);
309
+ let mut colors = column.iter().chain(row).flatten().peekable();
310
+ if colors.peek().is_none() {
311
+ continue;
312
+ }
313
+ let Some(mut rect) = cell_rect(cell.node_id) else {
314
+ continue;
315
+ };
316
+ if let Some(bounds) = bounds {
317
+ rect = Rect::new(
318
+ rect.x0 - before.width,
319
+ rect.y0 - before.height,
320
+ rect.x1 + after.width,
321
+ rect.y1 + after.height,
322
+ )
323
+ .intersect(bounds);
324
+ }
325
+
326
+ let shape = rect.scale_from_origin(self.scale);
327
+ for color in colors {
328
+ scene.fill(Fill::NonZero, self.transform, *color, None, &shape);
329
+ }
330
+ }
331
+ }
332
+
333
+ fn draw_solid_bg(&self, scene: &mut impl PaintScene, shape: &BezPath) {
334
+ let current_color = self.style.clone_color();
335
+ let background_color = &self.style.get_background().background_color;
336
+ let bg_color = background_color
337
+ .resolve_to_absolute(&current_color)
338
+ .as_srgb_color();
339
+
340
+ if bg_color != Color::TRANSPARENT {
341
+ // Fill the color
342
+ scene.fill(Fill::NonZero, self.transform, bg_color, None, shape);
343
+ }
344
+ }
345
+
346
+ /// Whether the layer is positioned against the viewport
347
+ /// (`background-attachment: fixed`) rather than the element's origin box.
348
+ /// `fixed` behaves as `scroll` on elements affected by a CSS transform
349
+ /// (the transformed element acts as the layer's containing block).
350
+ fn layer_is_fixed(&self, layer: &ImageLayerStyles) -> bool {
351
+ layer.attachment == StyloBackgroundAttachment::Fixed && !self.is_transformed()
352
+ }
353
+
354
+ /// The background positioning area and the transform from its coordinate
355
+ /// space to the scene for a fixed layer: the viewport, unaffected by any
356
+ /// scrolling.
357
+ fn fixed_positioning_area(&self) -> (Rect, Affine) {
358
+ let viewport_rect = Rect::new(
359
+ 0.0,
360
+ 0.0,
361
+ self.context.width as f64,
362
+ self.context.height as f64,
363
+ );
364
+ let transform = Affine::translate((self.context.initial_x, self.context.initial_y));
365
+ (viewport_rect, transform)
366
+ }
367
+
368
+ /// Whether this element or any of its ancestors has a CSS transform
369
+ ///
370
+ /// TODO: this misses transformed elements whose resolved transform is the
371
+ /// identity (e.g. `transform: translate(0)`), and elements with
372
+ /// `will-change: transform`, both of which should also degrade `fixed`
373
+ /// to `scroll` (see WPT css/css-transforms/transform-fixed-bg-005/008)
374
+ fn is_transformed(&self) -> bool {
375
+ let mut current = Some(self.node.id);
376
+ while let Some(node) = current.and_then(|id| self.context.dom.get_node(id)) {
377
+ if node.transform().is_some() {
378
+ return true;
379
+ }
380
+ current = node.parent;
381
+ }
382
+ false
383
+ }
384
+
385
+ #[cfg(feature = "svg")]
386
+ fn draw_svg_image_layer(&self, scene: &mut impl PaintScene, layer: &ImageLayerStyles) {
387
+ let Some(bg_image) = layer.image_data else {
388
+ return;
389
+ };
390
+ let ImageData::Svg(svg) = &bg_image.image else {
391
+ return;
392
+ };
393
+
394
+ // A zero-sized `viewBox` disables rendering of the SVG
395
+ if svg.intrinsic_dimensions.degenerate_view_box {
396
+ return;
397
+ }
398
+
399
+ let (origin_rect, base_transform) = if self.layer_is_fixed(layer) {
400
+ self.fixed_positioning_area()
401
+ } else {
402
+ (self.box_rect(layer.origin), self.transform)
403
+ };
404
+
405
+ let frame_w = (origin_rect.width() / self.scale) as f32;
406
+ let frame_h = (origin_rect.height() / self.scale) as f32;
407
+
408
+ // Size the SVG per the CSS default sizing algorithm
409
+ // (https://drafts.csswg.org/css-images/#default-sizing). An SVG image
410
+ // may lack an intrinsic width, height, and/or aspect ratio, so each is
411
+ // passed separately (usvg's resolved `Tree::size` is only used as the
412
+ // source coordinate space of the rendered tree).
413
+ let intrinsic_width = svg.intrinsic_width().filter(|w| w.is_finite() && *w > 0.0);
414
+ let intrinsic_height = svg.intrinsic_height().filter(|h| h.is_finite() && *h > 0.0);
415
+ let aspect_ratio = match (intrinsic_width, intrinsic_height) {
416
+ (Some(w), Some(h)) => Some(w / h),
417
+ _ => svg.viewbox_aspect_ratio(),
418
+ }
419
+ .filter(|r| r.is_finite() && *r > 0.0);
420
+
421
+ let bg_size = compute_layer_size(
422
+ layer,
423
+ frame_w,
424
+ frame_h,
425
+ BackgroundSizeComputeMode::Intrinsic {
426
+ width: intrinsic_width,
427
+ height: intrinsic_height,
428
+ ratio: aspect_ratio,
429
+ },
430
+ );
431
+
432
+ if bg_size.width <= 0.0 || bg_size.height <= 0.0 {
433
+ return;
434
+ }
435
+
436
+ let viewport = super::svg_viewport_affine(
437
+ svg,
438
+ (bg_size.width * self.scale) as f32,
439
+ (bg_size.height * self.scale) as f32,
440
+ );
441
+
442
+ let bg_pos = compute_layer_position(
443
+ layer,
444
+ frame_w - bg_size.width as f32,
445
+ frame_h - bg_size.height as f32,
446
+ );
447
+
448
+ let transform = base_transform
449
+ * kurbo::Affine::translate((
450
+ origin_rect.x0 + bg_pos.x * self.scale,
451
+ origin_rect.y0 + bg_pos.y * self.scale,
452
+ ))
453
+ * viewport;
454
+
455
+ anyrender_svg::render_svg_tree(scene, &svg.tree, transform);
456
+ }
457
+
458
+ fn draw_raster_image_layer(&self, scene: &mut impl PaintScene, layer: &ImageLayerStyles) {
459
+ let Some(bg_image) = layer.image_data else {
460
+ return;
461
+ };
462
+ let ImageData::Raster(image_data) = &bg_image.image else {
463
+ return;
464
+ };
465
+
466
+ let image_rendering = self.style.clone_image_rendering();
467
+ let quality = to_image_quality(image_rendering);
468
+
469
+ let (origin_rect, base_transform) = if self.layer_is_fixed(layer) {
470
+ self.fixed_positioning_area()
471
+ } else {
472
+ (self.box_rect(layer.origin), self.transform)
473
+ };
474
+
475
+ let image_width = image_data.width as f64;
476
+ let image_height = image_data.height as f64;
477
+
478
+ let (bg_pos, bg_size) = compute_layer_position_and_size(
479
+ layer,
480
+ origin_rect.width() / self.scale,
481
+ origin_rect.height() / self.scale,
482
+ BackgroundSizeComputeMode::Size(image_width as f32, image_height as f32),
483
+ );
484
+
485
+ let bg_pos = (bg_pos.to_vec2() * self.scale).to_point();
486
+ let bg_size = bg_size * self.scale;
487
+
488
+ let x_ratio = bg_size.width / image_width;
489
+ let y_ratio = bg_size.height / image_height;
490
+
491
+ let BackgroundRepeat(repeat_x, repeat_y) = layer.repeat;
492
+
493
+ let x = raster_axis_tiling(
494
+ *repeat_x,
495
+ origin_rect.x0,
496
+ origin_rect.width(),
497
+ bg_pos.x,
498
+ bg_size.width,
499
+ image_width,
500
+ x_ratio,
501
+ );
502
+ let y = raster_axis_tiling(
503
+ *repeat_y,
504
+ origin_rect.y0,
505
+ origin_rect.height(),
506
+ bg_pos.y,
507
+ bg_size.height,
508
+ image_height,
509
+ y_ratio,
510
+ );
511
+
512
+ let transform = base_transform
513
+ .pre_scale_non_uniform(x_ratio, y_ratio)
514
+ .then_translate(Vec2 {
515
+ x: x.translate,
516
+ y: y.translate,
517
+ });
518
+ let tile_rect = Rect::new(0.0, 0.0, x.rect_len, y.rect_len);
519
+
520
+ for hc in 0..y.count {
521
+ for wc in 0..x.count {
522
+ let transform = transform.then_translate(Vec2 {
523
+ x: wc as f64 * x.stride,
524
+ y: hc as f64 * y.stride,
525
+ });
526
+
527
+ scene.fill(
528
+ peniko::Fill::NonZero,
529
+ transform,
530
+ to_peniko_image(image_data, quality).as_ref(),
531
+ None,
532
+ &tile_rect,
533
+ );
534
+ }
535
+ }
536
+ }
537
+
538
+ fn draw_gradient_layer(
539
+ &self,
540
+ scene: &mut impl PaintScene,
541
+ gradient: &StyloGradient,
542
+ layer: &ImageLayerStyles,
543
+ ) {
544
+ // For a fixed layer the positioning area (the viewport) already covers
545
+ // everything visible, so it also serves as the clip rect (no extension
546
+ // towards the clip box is needed).
547
+ let (origin_rect, base_transform, clip_rect) = if self.layer_is_fixed(layer) {
548
+ let (viewport_rect, transform) = self.fixed_positioning_area();
549
+ (viewport_rect, transform, viewport_rect)
550
+ } else {
551
+ (
552
+ self.box_rect(layer.origin),
553
+ self.transform,
554
+ self.box_rect(layer.clip),
555
+ )
556
+ };
557
+
558
+ let (bg_pos, bg_size) = compute_layer_position_and_size(
559
+ layer,
560
+ origin_rect.width() / self.scale,
561
+ origin_rect.height() / self.scale,
562
+ BackgroundSizeComputeMode::Auto,
563
+ );
564
+
565
+ let bg_pos = (bg_pos.to_vec2() * self.scale).to_point();
566
+ let bg_size = bg_size * self.scale;
567
+
568
+ let BackgroundRepeat(repeat_x, repeat_y) = layer.repeat;
569
+
570
+ let x = gradient_axis_tiling(
571
+ *repeat_x,
572
+ origin_rect.x0,
573
+ origin_rect.width(),
574
+ clip_rect.x0,
575
+ clip_rect.width(),
576
+ bg_pos.x,
577
+ bg_size.width,
578
+ );
579
+ let y = gradient_axis_tiling(
580
+ *repeat_y,
581
+ origin_rect.y0,
582
+ origin_rect.height(),
583
+ clip_rect.y0,
584
+ clip_rect.height(),
585
+ bg_pos.y,
586
+ bg_size.height,
587
+ );
588
+
589
+ // FIXME: https://wpt.live/css/css-backgrounds/background-size/background-size-near-zero-gradient.html
590
+ if x.count as u64 * y.count as u64 > 500 {
591
+ return;
592
+ }
593
+
594
+ let tile_rect = Rect::new(0.0, 0.0, x.rect_len, y.rect_len);
595
+ let bounding_box = self.frame.border_box.bounding_box();
596
+ let current_color = self.style.clone_color();
597
+
598
+ let (gradient, gradient_transform) = to_peniko_gradient(
599
+ gradient,
600
+ tile_rect,
601
+ bounding_box,
602
+ self.scale,
603
+ &current_color,
604
+ );
605
+ let brush = anyrender::Paint::Gradient(&gradient);
606
+
607
+ let transform = base_transform.then_translate(Vec2 {
608
+ x: x.translate,
609
+ y: y.translate,
610
+ });
611
+
612
+ for hc in 0..y.count {
613
+ for wc in 0..x.count {
614
+ let transform = transform.then_translate(Vec2 {
615
+ x: wc as f64 * x.stride,
616
+ y: hc as f64 * y.stride,
617
+ });
618
+
619
+ scene.fill(
620
+ peniko::Fill::NonZero,
621
+ transform,
622
+ brush.clone(),
623
+ gradient_transform,
624
+ &tile_rect,
625
+ );
626
+ }
627
+ }
628
+ }
629
+ }
630
+
631
+ fn compute_layer_position_and_size(
632
+ layer: &ImageLayerStyles,
633
+ container_w: f64,
634
+ container_h: f64,
635
+ size_mode: BackgroundSizeComputeMode,
636
+ ) -> (Point, Size) {
637
+ use BackgroundRepeatKeyword::*;
638
+
639
+ let bg_size = compute_layer_size(layer, container_w as f32, container_h as f32, size_mode);
640
+
641
+ let bg_pos = compute_layer_position(
642
+ layer,
643
+ (container_w - bg_size.width) as f32,
644
+ (container_h - bg_size.height) as f32,
645
+ );
646
+
647
+ let BackgroundRepeat(repeat_x, repeat_y) = layer.repeat;
648
+
649
+ let bg_size = if matches!(repeat_x, Round) && matches!(repeat_y, Round) {
650
+ let count = (container_w / bg_size.width).round();
651
+ let width = container_w / count;
652
+
653
+ let count = (container_h / bg_size.height).round();
654
+ let height = container_h / count;
655
+
656
+ Size::new(width, height)
657
+ } else if matches!(repeat_x, Round) {
658
+ let count = (container_w / bg_size.width).round();
659
+ let width = container_w / count;
660
+ Size::new(width, bg_size.height)
661
+ } else if matches!(repeat_y, Round) {
662
+ let count = (container_h / bg_size.height).round();
663
+ let height = container_h / count;
664
+ Size::new(bg_size.width, height)
665
+ } else {
666
+ bg_size
667
+ };
668
+
669
+ (bg_pos, bg_size)
670
+ }
671
+
672
+ #[inline]
673
+ fn compute_layer_position(layer: &ImageLayerStyles, width: f32, height: f32) -> Point {
674
+ use style::values::computed::Length;
675
+
676
+ let bg_pos_x = layer.position_x.resolve(Length::new(width)).px() as f64;
677
+ let bg_pos_y = layer.position_y.resolve(Length::new(height)).px() as f64;
678
+
679
+ Point::new(bg_pos_x, bg_pos_y)
680
+ }
681
+
682
+ fn compute_layer_size(
683
+ layer: &ImageLayerStyles,
684
+ container_w: f32,
685
+ container_h: f32,
686
+ mode: BackgroundSizeComputeMode,
687
+ ) -> kurbo::Size {
688
+ use style::values::computed::Length;
689
+ use style::values::generics::length::GenericLengthPercentageOrAuto as Lpa;
690
+
691
+ let (width, height): (f32, f32) = match layer.size {
692
+ BackgroundSize::ExplicitSize { width, height } => {
693
+ let width = width.map(|w| w.0.resolve(Length::new(container_w)));
694
+ let height = height.map(|h| h.0.resolve(Length::new(container_h)));
695
+
696
+ match (width, height) {
697
+ (Lpa::LengthPercentage(width), Lpa::LengthPercentage(height)) => {
698
+ let width = width.px();
699
+ let height = height.px();
700
+ match mode {
701
+ BackgroundSizeComputeMode::Auto => (width, height),
702
+ BackgroundSizeComputeMode::Size(_, _) => (width, height),
703
+ BackgroundSizeComputeMode::Intrinsic { .. } => (width, height),
704
+ }
705
+ }
706
+ (Lpa::LengthPercentage(width), Lpa::Auto) => {
707
+ let width = width.px();
708
+ let height = match mode {
709
+ BackgroundSizeComputeMode::Auto => container_h,
710
+ BackgroundSizeComputeMode::Size(bg_w, bg_h) => bg_h / bg_w * width,
711
+ BackgroundSizeComputeMode::Intrinsic { height, ratio, .. } => ratio
712
+ .map(|ratio| width / ratio)
713
+ .or(height)
714
+ .unwrap_or(container_h),
715
+ };
716
+ (width, height)
717
+ }
718
+ (Lpa::Auto, Lpa::LengthPercentage(height)) => {
719
+ let height = height.px();
720
+ let width = match mode {
721
+ BackgroundSizeComputeMode::Auto => container_w,
722
+ BackgroundSizeComputeMode::Size(bg_w, bg_h) => bg_w / bg_h * height,
723
+ BackgroundSizeComputeMode::Intrinsic { width, ratio, .. } => ratio
724
+ .map(|ratio| height * ratio)
725
+ .or(width)
726
+ .unwrap_or(container_w),
727
+ };
728
+ (width, height)
729
+ }
730
+ (Lpa::Auto, Lpa::Auto) => match mode {
731
+ BackgroundSizeComputeMode::Auto => (container_w, container_h),
732
+ BackgroundSizeComputeMode::Size(bg_w, bg_h) => (bg_w, bg_h),
733
+ BackgroundSizeComputeMode::Intrinsic {
734
+ width,
735
+ height,
736
+ ratio,
737
+ } => default_sizing(width, height, ratio, container_w, container_h),
738
+ },
739
+ }
740
+ }
741
+ BackgroundSize::Cover => match mode {
742
+ BackgroundSizeComputeMode::Auto => (container_w, container_h),
743
+ BackgroundSizeComputeMode::Size(bg_w, bg_h) => {
744
+ // Scale to the smallest size that covers both axes
745
+ let ratio = (container_w / bg_w).max(container_h / bg_h);
746
+ (bg_w * ratio, bg_h * ratio)
747
+ }
748
+ BackgroundSizeComputeMode::Intrinsic { ratio, .. } => match ratio {
749
+ // Scale the aspect ratio to the smallest size that covers both axes
750
+ Some(ratio) => {
751
+ let scale = (container_w / ratio).max(container_h);
752
+ (scale * ratio, scale)
753
+ }
754
+ // No intrinsic aspect ratio: fill the positioning area
755
+ None => (container_w, container_h),
756
+ },
757
+ },
758
+ BackgroundSize::Contain => match mode {
759
+ BackgroundSizeComputeMode::Auto => (container_w, container_h),
760
+ BackgroundSizeComputeMode::Size(bg_w, bg_h) => {
761
+ // Scale to the largest size contained by both axes
762
+ let ratio = (container_w / bg_w).min(container_h / bg_h);
763
+ (bg_w * ratio, bg_h * ratio)
764
+ }
765
+ BackgroundSizeComputeMode::Intrinsic { ratio, .. } => match ratio {
766
+ // Scale the aspect ratio to the largest size contained by both axes
767
+ Some(ratio) => {
768
+ let scale = (container_w / ratio).min(container_h);
769
+ (scale * ratio, scale)
770
+ }
771
+ // No intrinsic aspect ratio: fill the positioning area
772
+ None => (container_w, container_h),
773
+ },
774
+ },
775
+ };
776
+
777
+ kurbo::Size {
778
+ width: width as f64,
779
+ height: height as f64,
780
+ }
781
+ }
782
+
783
+ enum BackgroundSizeComputeMode {
784
+ Auto,
785
+ Size(f32, f32),
786
+ /// Intrinsic dimensions of an image which may lack an intrinsic width,
787
+ /// height, and/or aspect ratio (e.g. SVG), sized per the CSS default
788
+ /// sizing algorithm (https://drafts.csswg.org/css-images/#default-sizing)
789
+ Intrinsic {
790
+ width: Option<f32>,
791
+ height: Option<f32>,
792
+ ratio: Option<f32>,
793
+ },
794
+ }
795
+
796
+ /// The CSS default sizing algorithm for the unconstrained (`auto auto`) case:
797
+ /// resolve the concrete object size from whichever intrinsic dimensions exist,
798
+ /// falling back to the default object size (the background positioning area).
799
+ fn default_sizing(
800
+ width: Option<f32>,
801
+ height: Option<f32>,
802
+ ratio: Option<f32>,
803
+ container_w: f32,
804
+ container_h: f32,
805
+ ) -> (f32, f32) {
806
+ match (width, height) {
807
+ (Some(w), Some(h)) => (w, h),
808
+ (Some(w), None) => (w, ratio.map(|r| w / r).unwrap_or(container_h)),
809
+ (None, Some(h)) => (ratio.map(|r| h * r).unwrap_or(container_w), h),
810
+ (None, None) => match ratio {
811
+ // Intrinsic aspect ratio only: size as if `contain` were specified
812
+ Some(ratio) => {
813
+ let scale = (container_w / ratio).min(container_h);
814
+ (scale * ratio, scale)
815
+ }
816
+ None => (container_w, container_h),
817
+ },
818
+ }
819
+ }
820
+
821
+ /// The placement and tiling of a background layer along one axis: a
822
+ /// translation applied to the layer as a whole, the length of each filled
823
+ /// rect, and the number of explicit tiles with the stride between them.
824
+ struct AxisTiling {
825
+ /// Translation (in device pixels) positioning the first tile
826
+ translate: f64,
827
+ /// Length of each filled rect, in the coordinate space of the fill's transform
828
+ rect_len: f64,
829
+ /// Number of explicitly drawn tiles
830
+ count: u32,
831
+ /// Stride (in device pixels) between the starts of consecutive tiles
832
+ stride: f64,
833
+ }
834
+
835
+ /// Per-axis placement and tiling for a raster image layer. `Repeat`/`Round`
836
+ /// produce a single fill covering the whole positioning area (relying on the
837
+ /// image brush repeating), while `Space` produces `count` explicit tiles
838
+ /// spaced `stride` apart.
839
+ ///
840
+ /// The fill rect is in image pixel coordinates (the drawing transform is
841
+ /// pre-scaled by `ratio`), while translations are in device pixels.
842
+ fn raster_axis_tiling(
843
+ repeat: BackgroundRepeatKeyword,
844
+ origin_start: f64,
845
+ origin_len: f64,
846
+ bg_pos: f64,
847
+ tile_len: f64,
848
+ image_len: f64,
849
+ ratio: f64,
850
+ ) -> AxisTiling {
851
+ use BackgroundRepeatKeyword::*;
852
+
853
+ match repeat {
854
+ Repeat | Round => {
855
+ let extend_len = extend(bg_pos, tile_len);
856
+ AxisTiling {
857
+ translate: origin_start - extend_len,
858
+ rect_len: (origin_len + extend_len) / ratio,
859
+ count: 1,
860
+ stride: 0.0,
861
+ }
862
+ }
863
+ Space => {
864
+ let (count, stride) = compute_space_count_and_stride(origin_len, tile_len);
865
+ AxisTiling {
866
+ translate: origin_start + if count == 1 { bg_pos } else { 0.0 },
867
+ rect_len: image_len,
868
+ count,
869
+ stride,
870
+ }
871
+ }
872
+ NoRepeat => AxisTiling {
873
+ translate: origin_start + bg_pos,
874
+ rect_len: image_len,
875
+ count: 1,
876
+ stride: 0.0,
877
+ },
878
+ }
879
+ }
880
+
881
+ /// Per-axis placement and tiling for a gradient layer. Unlike raster images,
882
+ /// gradients cannot rely on brush repetition, so `Repeat`/`Round` also produce
883
+ /// explicit tiles. When the clip box extends beyond the origin box, tiling
884
+ /// starts from the clip box edge so the pattern covers the whole clipped area.
885
+ fn gradient_axis_tiling(
886
+ repeat: BackgroundRepeatKeyword,
887
+ origin_start: f64,
888
+ origin_len: f64,
889
+ clip_start: f64,
890
+ clip_len: f64,
891
+ bg_pos: f64,
892
+ tile_len: f64,
893
+ ) -> AxisTiling {
894
+ use BackgroundRepeatKeyword::*;
895
+
896
+ match repeat {
897
+ Repeat | Round => {
898
+ // The clip and origin boxes are nested, so the clip box extends
899
+ // beyond the origin box iff it does so at either end
900
+ let clip_is_outer =
901
+ clip_start < origin_start || clip_start + clip_len > origin_start + origin_len;
902
+ let (area_start, area_len) = if clip_is_outer {
903
+ (clip_start, clip_len)
904
+ } else {
905
+ (origin_start, origin_len)
906
+ };
907
+ let extend_len = extend((origin_start - area_start) + bg_pos, tile_len);
908
+ let count = ((area_len + extend_len) / tile_len).ceil() as u32;
909
+ AxisTiling {
910
+ translate: area_start - extend_len,
911
+ rect_len: tile_len,
912
+ count,
913
+ stride: tile_len,
914
+ }
915
+ }
916
+ Space => {
917
+ let (count, stride) = compute_space_count_and_stride(origin_len, tile_len);
918
+ AxisTiling {
919
+ translate: origin_start + if count == 1 { bg_pos } else { 0.0 },
920
+ rect_len: tile_len,
921
+ count,
922
+ stride,
923
+ }
924
+ }
925
+ NoRepeat => AxisTiling {
926
+ translate: origin_start + bg_pos,
927
+ rect_len: tile_len,
928
+ count: 1,
929
+ stride: 0.0,
930
+ },
931
+ }
932
+ }
933
+
934
+ fn compute_space_count_and_stride(bg_size: f64, size: f64) -> (u32, f64) {
935
+ let modulo = bg_size % size;
936
+ let count = (((bg_size - modulo) / size) as u32).max(1);
937
+ let stride = if count > 1 {
938
+ modulo / (count - 1) as f64
939
+ } else {
940
+ 0.0
941
+ } + size;
942
+
943
+ (count, stride)
944
+ }
945
+
946
+ /// Whether drawing the layer is guaranteed to paint nothing, making its clip
947
+ /// layer unnecessary
948
+ fn layer_paints_nothing(layer: &ImageLayerStyles) -> bool {
949
+ match layer.stylo_image {
950
+ GenericImage::None => true,
951
+ GenericImage::Url(_) => layer.image_data.is_none(),
952
+ _ => false,
953
+ }
954
+ }
955
+
956
+ #[inline]
957
+ pub(super) fn get_cyclic<T>(values: &[T], layer_index: usize) -> &T {
958
+ &values[layer_index % values.len()]
959
+ }
960
+
961
+ fn extend(offset: f64, length: f64) -> f64 {
962
+ let extend_length = offset % length;
963
+ if extend_length > 0.0 {
964
+ length - extend_length
965
+ } else {
966
+ -extend_length
967
+ }
968
+ }