blitz-pdf 0.1.5 → 0.1.6

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.
@@ -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
+ }