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,536 @@
1
+ use crate::color::{Color, ToColorColor};
2
+ use color::DynamicColor;
3
+ use kurbo::{self, Affine, Point, Rect, Vec2};
4
+ use peniko::{self, ColorStop, Gradient, LinearGradientPosition, SweepGradientPosition};
5
+ use style::color::AbsoluteColor;
6
+ use style::{
7
+ OwnedSlice,
8
+ values::{
9
+ computed::{
10
+ Angle, AngleOrPercentage, CSSPixelLength, Gradient as StyloGradient, LengthPercentage,
11
+ LineDirection, Percentage,
12
+ },
13
+ generics::{
14
+ NonNegative,
15
+ color::GenericColor,
16
+ image::{
17
+ EndingShape, GenericCircle, GenericEllipse, GenericEndingShape, GenericGradient,
18
+ GenericGradientItem, GradientFlags, ShapeExtent,
19
+ },
20
+ position::GenericPosition,
21
+ },
22
+ specified::{
23
+ percentage::ToPercentage,
24
+ position::{HorizontalPositionKeyword, VerticalPositionKeyword},
25
+ },
26
+ },
27
+ };
28
+
29
+ type GradientItem<T> = GenericGradientItem<GenericColor<Percentage>, T>;
30
+ type LinearGradient<'a> = (
31
+ &'a LineDirection,
32
+ &'a [GradientItem<LengthPercentage>],
33
+ GradientFlags,
34
+ );
35
+ type RadialGradient<'a> = (
36
+ &'a EndingShape<NonNegative<CSSPixelLength>, NonNegative<LengthPercentage>>,
37
+ &'a GenericPosition<LengthPercentage, LengthPercentage>,
38
+ &'a OwnedSlice<GenericGradientItem<GenericColor<Percentage>, LengthPercentage>>,
39
+ GradientFlags,
40
+ );
41
+ type ConicGradient<'a> = (
42
+ &'a Angle,
43
+ &'a GenericPosition<LengthPercentage, LengthPercentage>,
44
+ &'a OwnedSlice<GenericGradientItem<GenericColor<Percentage>, AngleOrPercentage>>,
45
+ GradientFlags,
46
+ );
47
+
48
+ pub(crate) fn to_peniko_gradient(
49
+ gradient: &StyloGradient,
50
+ origin_rect: Rect,
51
+ bounding_box: Rect,
52
+ scale: f64,
53
+ current_color: &AbsoluteColor,
54
+ ) -> (peniko::Gradient, Option<Affine>) {
55
+ match gradient {
56
+ // https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient
57
+ GenericGradient::Linear {
58
+ direction,
59
+ items,
60
+ flags,
61
+ // compat_mode,
62
+ ..
63
+ } => linear_gradient(
64
+ (direction, items, *flags),
65
+ origin_rect,
66
+ bounding_box,
67
+ scale,
68
+ current_color,
69
+ ),
70
+ GenericGradient::Radial {
71
+ shape,
72
+ position,
73
+ items,
74
+ flags,
75
+ // compat_mode,
76
+ ..
77
+ } => radial_gradient((shape, position, items, *flags), origin_rect, current_color),
78
+ GenericGradient::Conic {
79
+ angle,
80
+ position,
81
+ items,
82
+ flags,
83
+ ..
84
+ } => conic_gradient((angle, position, items, *flags), origin_rect, current_color),
85
+ }
86
+ }
87
+
88
+ fn linear_gradient(
89
+ gradient: LinearGradient,
90
+ rect: Rect,
91
+ bounding_box: Rect,
92
+ scale: f64,
93
+ current_color: &AbsoluteColor,
94
+ ) -> (peniko::Gradient, Option<Affine>) {
95
+ let (direction, items, flags) = gradient;
96
+
97
+ let center = bounding_box.center();
98
+ let (start, end) = match direction {
99
+ LineDirection::Angle(angle) => {
100
+ let angle = -angle.radians64() + std::f64::consts::PI;
101
+ let offset_length =
102
+ rect.width() / 2.0 * angle.sin().abs() + rect.height() / 2.0 * angle.cos().abs();
103
+ let offset_vec = Vec2::new(angle.sin(), angle.cos()) * offset_length;
104
+ (center - offset_vec, center + offset_vec)
105
+ }
106
+ LineDirection::Horizontal(horizontal) => {
107
+ let start = Point::new(rect.x0, rect.y0 + rect.height() / 2.0);
108
+ let end = Point::new(rect.x1, rect.y0 + rect.height() / 2.0);
109
+ match horizontal {
110
+ HorizontalPositionKeyword::Right => (start, end),
111
+ HorizontalPositionKeyword::Left => (end, start),
112
+ }
113
+ }
114
+ LineDirection::Vertical(vertical) => {
115
+ let start = Point::new(rect.x0 + rect.width() / 2.0, rect.y0);
116
+ let end = Point::new(rect.x0 + rect.width() / 2.0, rect.y1);
117
+ match vertical {
118
+ VerticalPositionKeyword::Top => (end, start),
119
+ VerticalPositionKeyword::Bottom => (start, end),
120
+ }
121
+ }
122
+ LineDirection::Corner(horizontal, vertical) => {
123
+ let (start_x, end_x) = match horizontal {
124
+ HorizontalPositionKeyword::Right => (rect.x0, rect.x1),
125
+ HorizontalPositionKeyword::Left => (rect.x1, rect.x0),
126
+ };
127
+ let (start_y, end_y) = match vertical {
128
+ VerticalPositionKeyword::Top => (rect.y1, rect.y0),
129
+ VerticalPositionKeyword::Bottom => (rect.y0, rect.y1),
130
+ };
131
+ (Point::new(start_x, start_y), Point::new(end_x, end_y))
132
+ }
133
+ };
134
+
135
+ let gradient_length = CSSPixelLength::new((start.distance(end) / scale) as f32);
136
+ let repeating = flags.contains(GradientFlags::REPEATING);
137
+
138
+ let mut gradient = peniko::Gradient::new_linear(start, end).with_extend(if repeating {
139
+ peniko::Extend::Repeat
140
+ } else {
141
+ peniko::Extend::Pad
142
+ });
143
+
144
+ let (first_offset, last_offset) = resolve_length_color_stops(
145
+ current_color,
146
+ items,
147
+ gradient_length,
148
+ &mut gradient,
149
+ repeating,
150
+ );
151
+ if repeating && gradient.stops.len() > 1 {
152
+ gradient.kind = peniko::GradientKind::Linear(LinearGradientPosition {
153
+ start: start + (end - start) * first_offset as f64,
154
+ end: end + (start - end) * (1.0 - last_offset) as f64,
155
+ });
156
+ }
157
+
158
+ (gradient, None)
159
+ }
160
+
161
+ fn radial_gradient(
162
+ gradient: RadialGradient,
163
+ rect: Rect,
164
+ current_color: &AbsoluteColor,
165
+ ) -> (peniko::Gradient, Option<Affine>) {
166
+ let (shape, position, items, flags) = gradient;
167
+ let repeating = flags.contains(GradientFlags::REPEATING);
168
+
169
+ let mut gradient = peniko::Gradient::new_radial((0.0, 0.0), 1.0).with_extend(if repeating {
170
+ peniko::Extend::Repeat
171
+ } else {
172
+ peniko::Extend::Pad
173
+ });
174
+
175
+ let (width_px, height_px) = (
176
+ resolve_gradient_length(&position.horizontal, rect.width()),
177
+ resolve_gradient_length(&position.vertical, rect.height()),
178
+ );
179
+
180
+ let gradient_scale: Option<Vec2> = match shape {
181
+ GenericEndingShape::Circle(circle) => {
182
+ let scale = match circle {
183
+ GenericCircle::Extent(extent) => match extent {
184
+ ShapeExtent::FarthestSide => width_px
185
+ .max(rect.width() - width_px)
186
+ .max(height_px.max(rect.height() - height_px)),
187
+ ShapeExtent::ClosestSide => width_px
188
+ .min(rect.width() - width_px)
189
+ .min(height_px.min(rect.height() - height_px)),
190
+ ShapeExtent::FarthestCorner => {
191
+ (width_px.max(rect.width() - width_px)
192
+ + height_px.max(rect.height() - height_px))
193
+ * 0.5_f64.sqrt()
194
+ }
195
+ ShapeExtent::ClosestCorner => {
196
+ (width_px.min(rect.width() - width_px)
197
+ + height_px.min(rect.height() - height_px))
198
+ * 0.5_f64.sqrt()
199
+ }
200
+ _ => 0.0,
201
+ },
202
+ GenericCircle::Radius(radius) => radius.0.px() as f64,
203
+ };
204
+ Some(Vec2::new(scale, scale))
205
+ }
206
+ GenericEndingShape::Ellipse(ellipse) => match ellipse {
207
+ GenericEllipse::Extent(extent) => match extent {
208
+ ShapeExtent::FarthestCorner | ShapeExtent::FarthestSide => {
209
+ let mut scale = Vec2::new(
210
+ width_px.max(rect.width() - width_px),
211
+ height_px.max(rect.height() - height_px),
212
+ );
213
+ if *extent == ShapeExtent::FarthestCorner {
214
+ scale *= 2.0_f64.sqrt();
215
+ }
216
+ Some(scale)
217
+ }
218
+ ShapeExtent::ClosestCorner | ShapeExtent::ClosestSide => {
219
+ let mut scale = Vec2::new(
220
+ width_px.min(rect.width() - width_px),
221
+ height_px.min(rect.height() - height_px),
222
+ );
223
+ if *extent == ShapeExtent::ClosestCorner {
224
+ scale *= 2.0_f64.sqrt();
225
+ }
226
+ Some(scale)
227
+ }
228
+ _ => None,
229
+ },
230
+ GenericEllipse::Radii(x, y) => Some(Vec2::new(
231
+ resolve_gradient_length(&x.0, rect.width()),
232
+ resolve_gradient_length(&y.0, rect.height()),
233
+ )),
234
+ },
235
+ };
236
+
237
+ let gradient_transform = {
238
+ // If the gradient has no valid scale, we don't need to calculate the color stops
239
+ if let Some(gradient_scale) = gradient_scale {
240
+ let (first_offset, last_offset) = resolve_length_color_stops(
241
+ current_color,
242
+ items,
243
+ CSSPixelLength::new(gradient_scale.x as f32),
244
+ &mut gradient,
245
+ repeating,
246
+ );
247
+ let scale = if repeating && gradient.stops.len() >= 2 {
248
+ (last_offset - first_offset) as f64
249
+ } else {
250
+ 1.0
251
+ };
252
+ Some(
253
+ Affine::scale_non_uniform(gradient_scale.x * scale, gradient_scale.y * scale)
254
+ .then_translate(get_translation(position, rect)),
255
+ )
256
+ } else {
257
+ None
258
+ }
259
+ };
260
+
261
+ (gradient, gradient_transform)
262
+ }
263
+
264
+ fn conic_gradient(
265
+ gradient: ConicGradient,
266
+ rect: Rect,
267
+ current_color: &AbsoluteColor,
268
+ ) -> (peniko::Gradient, Option<Affine>) {
269
+ let (angle, position, items, flags) = gradient;
270
+
271
+ let repeating = flags.contains(GradientFlags::REPEATING);
272
+ let mut gradient = peniko::Gradient::new_sweep((0.0, 0.0), 0.0, std::f32::consts::PI * 2.0)
273
+ .with_extend(if repeating {
274
+ peniko::Extend::Repeat
275
+ } else {
276
+ peniko::Extend::Pad
277
+ });
278
+
279
+ let (first_offset, last_offset) = resolve_angle_color_stops(
280
+ current_color,
281
+ items,
282
+ CSSPixelLength::new(1.0),
283
+ &mut gradient,
284
+ repeating,
285
+ );
286
+ if repeating && gradient.stops.len() >= 2 {
287
+ gradient.kind = peniko::GradientKind::Sweep(SweepGradientPosition {
288
+ center: Point::new(0.0, 0.0),
289
+ start_angle: std::f32::consts::PI * 2.0 * first_offset,
290
+ end_angle: std::f32::consts::PI * 2.0 * last_offset,
291
+ });
292
+ }
293
+
294
+ let gradient_transform = Some(
295
+ Affine::rotate(angle.radians() as f64 - std::f64::consts::PI / 2.0)
296
+ .then_translate(get_translation(position, rect)),
297
+ );
298
+
299
+ (gradient, gradient_transform)
300
+ }
301
+
302
+ #[inline]
303
+ fn resolve_length_color_stops(
304
+ current_color: &AbsoluteColor,
305
+ items: &[GradientItem<LengthPercentage>],
306
+ gradient_length: CSSPixelLength,
307
+ gradient: &mut Gradient,
308
+ repeating: bool,
309
+ ) -> (f32, f32) {
310
+ resolve_color_stops(
311
+ current_color,
312
+ items,
313
+ gradient_length,
314
+ gradient,
315
+ repeating,
316
+ |gradient_length: CSSPixelLength, position: &LengthPercentage| -> Option<f32> {
317
+ position
318
+ .to_percentage_of(gradient_length)
319
+ .map(|percentage| percentage.to_percentage().unwrap_or(0.0))
320
+ },
321
+ )
322
+ }
323
+
324
+ #[inline]
325
+ fn resolve_color_stops<T>(
326
+ current_color: &AbsoluteColor,
327
+ items: &[GradientItem<T>],
328
+ gradient_length: CSSPixelLength,
329
+ gradient: &mut Gradient,
330
+ repeating: bool,
331
+ item_resolver: impl Fn(CSSPixelLength, &T) -> Option<f32>,
332
+ ) -> (f32, f32) {
333
+ let mut hint: Option<f32> = None;
334
+
335
+ for (idx, item) in items.iter().enumerate() {
336
+ let (color, offset) = match item {
337
+ GenericGradientItem::SimpleColorStop(color) => {
338
+ let step = 1.0 / (items.len() as f32 - 1.0);
339
+ (
340
+ color.resolve_to_absolute(current_color).as_dynamic_color(),
341
+ step * idx as f32,
342
+ )
343
+ }
344
+ GenericGradientItem::ComplexColorStop { color, position } => {
345
+ let offset = item_resolver(gradient_length, position);
346
+ if let Some(offset) = offset {
347
+ (
348
+ color.resolve_to_absolute(current_color).as_dynamic_color(),
349
+ offset,
350
+ )
351
+ } else {
352
+ continue;
353
+ }
354
+ }
355
+ GenericGradientItem::InterpolationHint(position) => {
356
+ hint = item_resolver(gradient_length, position);
357
+ continue;
358
+ }
359
+ };
360
+
361
+ if idx == 0 && !repeating && offset != 0.0 {
362
+ gradient
363
+ .stops
364
+ .push(peniko::ColorStop { color, offset: 0.0 });
365
+ }
366
+
367
+ match hint {
368
+ None => gradient.stops.push(peniko::ColorStop { color, offset }),
369
+ Some(hint) => {
370
+ let &last_stop = gradient.stops.last().unwrap();
371
+
372
+ if hint <= last_stop.offset {
373
+ // Upstream code has a bug here, so we're going to do something different
374
+ match gradient.stops.len() {
375
+ 0 => (),
376
+ 1 => {
377
+ gradient.stops.pop();
378
+ }
379
+ _ => {
380
+ let prev_stop = gradient.stops[gradient.stops.len() - 2];
381
+ if prev_stop.offset == hint {
382
+ gradient.stops.pop();
383
+ }
384
+ }
385
+ }
386
+ gradient.stops.push(peniko::ColorStop {
387
+ color,
388
+ offset: hint,
389
+ });
390
+ } else if hint >= offset {
391
+ gradient.stops.push(peniko::ColorStop {
392
+ color: last_stop.color,
393
+ offset: hint,
394
+ });
395
+ gradient.stops.push(peniko::ColorStop {
396
+ color,
397
+ offset: last_stop.offset,
398
+ });
399
+ } else if hint == (last_stop.offset + offset) / 2.0 {
400
+ gradient.stops.push(peniko::ColorStop { color, offset });
401
+ } else {
402
+ let mid_point = (hint - last_stop.offset) / (offset - last_stop.offset);
403
+ let mut interpolate_stop = |cur_offset: f32| {
404
+ let relative_offset =
405
+ (cur_offset - last_stop.offset) / (offset - last_stop.offset);
406
+ let multiplier = relative_offset.powf(0.5f32.log(mid_point));
407
+ let [last_r, last_g, last_b, last_a] = last_stop.color.components;
408
+ let [r, g, b, a] = color.components;
409
+
410
+ let color = Color::new([
411
+ (last_r + multiplier * (r - last_r)),
412
+ (last_g + multiplier * (g - last_g)),
413
+ (last_b + multiplier * (b - last_b)),
414
+ (last_a + multiplier * (a - last_a)),
415
+ ]);
416
+ gradient.stops.push(peniko::ColorStop {
417
+ color: DynamicColor::from_alpha_color(color),
418
+ offset: cur_offset,
419
+ });
420
+ };
421
+ if mid_point > 0.5 {
422
+ for i in 0..7 {
423
+ interpolate_stop(
424
+ last_stop.offset
425
+ + (hint - last_stop.offset) * (7.0 + i as f32) / 13.0,
426
+ );
427
+ }
428
+ interpolate_stop(hint + (offset - hint) / 3.0);
429
+ interpolate_stop(hint + (offset - hint) * 2.0 / 3.0);
430
+ } else {
431
+ interpolate_stop(last_stop.offset + (hint - last_stop.offset) / 3.0);
432
+ interpolate_stop(last_stop.offset + (hint - last_stop.offset) * 2.0 / 3.0);
433
+ for i in 0..7 {
434
+ interpolate_stop(hint + (offset - hint) * (i as f32) / 13.0);
435
+ }
436
+ }
437
+ gradient.stops.push(peniko::ColorStop { color, offset });
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ // Post-process the stops for repeating gradients
444
+ if repeating && gradient.stops.len() > 1 {
445
+ let first_offset = gradient.stops.first().unwrap().offset;
446
+ let last_offset = gradient.stops.last().unwrap().offset;
447
+ if first_offset != 0.0 || last_offset != 1.0 {
448
+ let scale_inv = 1e-7_f32.max(1.0 / (last_offset - first_offset));
449
+ for stop in &mut *gradient.stops {
450
+ stop.offset = (stop.offset - first_offset) * scale_inv;
451
+ }
452
+ }
453
+ (first_offset, last_offset)
454
+ } else {
455
+ // Ensure that the gradient ends at offset 1.0
456
+ if gradient.stops.len() > 1 {
457
+ let last_stop = &gradient.stops.last().unwrap();
458
+ if last_stop.offset < 1.0 {
459
+ let last_stop = ColorStop {
460
+ offset: 1.0,
461
+ ..(**last_stop)
462
+ };
463
+ gradient.stops.push(last_stop);
464
+ }
465
+ }
466
+ (0.0, 1.0)
467
+ }
468
+ }
469
+
470
+ #[inline]
471
+ fn resolve_angle_color_stops(
472
+ current_color: &AbsoluteColor,
473
+ items: &[GradientItem<AngleOrPercentage>],
474
+ gradient_length: CSSPixelLength,
475
+ gradient: &mut Gradient,
476
+ repeating: bool,
477
+ ) -> (f32, f32) {
478
+ resolve_color_stops(
479
+ current_color,
480
+ items,
481
+ gradient_length,
482
+ gradient,
483
+ repeating,
484
+ |_gradient_length: CSSPixelLength, position: &AngleOrPercentage| -> Option<f32> {
485
+ match position {
486
+ AngleOrPercentage::Angle(angle) => {
487
+ Some(angle.radians() / (std::f64::consts::PI * 2.0) as f32)
488
+ }
489
+ AngleOrPercentage::Percentage(percentage) => {
490
+ Some(percentage.to_percentage().unwrap_or(0.0))
491
+ }
492
+ }
493
+ },
494
+ )
495
+ }
496
+
497
+ #[inline]
498
+ fn resolve_gradient_length(value: &LengthPercentage, basis: f64) -> f64 {
499
+ // Percentage resolution can overflow even though computed lengths are finite.
500
+ value
501
+ .resolve(CSSPixelLength::new(basis as f32))
502
+ .px()
503
+ .clamp(f32::MIN, f32::MAX) as f64
504
+ }
505
+
506
+ #[inline]
507
+ fn get_translation(
508
+ position: &GenericPosition<LengthPercentage, LengthPercentage>,
509
+ rect: Rect,
510
+ ) -> Vec2 {
511
+ Vec2::new(
512
+ rect.x0 + resolve_gradient_length(&position.horizontal, rect.width()),
513
+ rect.y0 + resolve_gradient_length(&position.vertical, rect.height()),
514
+ )
515
+ }
516
+
517
+ #[cfg(test)]
518
+ mod tests {
519
+ use super::*;
520
+
521
+ #[test]
522
+ fn gradient_percentage_overflow_is_clamped() {
523
+ for percentage in [f32::MAX, f32::MIN] {
524
+ let value = LengthPercentage::new_percent(Percentage(percentage));
525
+ assert_eq!(resolve_gradient_length(&value, 300.0), percentage as f64);
526
+ }
527
+ }
528
+
529
+ #[test]
530
+ fn finite_gradient_lengths_are_preserved() {
531
+ let length = LengthPercentage::new_length(CSSPixelLength::new(-12.0));
532
+ let percentage = LengthPercentage::new_percent(Percentage(-0.5));
533
+ assert_eq!(resolve_gradient_length(&length, 300.0), -12.0);
534
+ assert_eq!(resolve_gradient_length(&percentage, 300.0), -150.0);
535
+ }
536
+ }