1use kurbo::{Affine, BezPath, PathEl, Rect};
4use log::warn;
5use pdf_syntax::page::{Page, Rotation};
6use siphasher::sip128::{Hasher128, SipHasher13};
7use std::hash::Hash;
8use std::ops::Sub;
9
10pub(crate) trait OptionLog {
11 fn warn_none(self, f: &str) -> Self;
12}
13
14impl<T> OptionLog for Option<T> {
15 #[inline]
16 fn warn_none(self, f: &str) -> Self {
17 self.or_else(|| {
18 warn!("{f}");
19
20 None
21 })
22 }
23}
24
25const SCALAR_NEARLY_ZERO: f32 = 1.0 / (1 << 8) as f32;
26
27pub trait Float32Ext: Sized + Sub<f32, Output = f32> + Copy + PartialOrd<f32> {
29 fn is_nearly_zero(&self) -> bool {
31 self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO)
32 }
33
34 fn is_nearly_equal(&self, other: f32) -> bool {
36 (*self - other).is_nearly_zero()
37 }
38
39 fn is_nearly_less_or_equal(&self, other: f32) -> bool {
41 (*self - other).is_nearly_zero() || *self < other
42 }
43
44 fn is_nearly_greater_or_equal(&self, other: f32) -> bool {
46 (*self - other).is_nearly_zero() || *self > other
47 }
48
49 fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool;
51}
52
53impl Float32Ext for f32 {
54 fn is_nearly_zero_within_tolerance(&self, tolerance: f32) -> bool {
55 debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
56
57 self.abs() <= tolerance
58 }
59}
60
61pub trait Float64Ext: Sized + Sub<f64, Output = f64> + Copy + PartialOrd<f64> {
63 fn is_nearly_zero(&self) -> bool {
65 self.is_nearly_zero_within_tolerance(SCALAR_NEARLY_ZERO as f64)
66 }
67
68 fn is_nearly_equal(&self, other: f64) -> bool {
70 (*self - other).is_nearly_zero()
71 }
72
73 fn is_nearly_less_or_equal(&self, other: f64) -> bool {
75 (*self - other).is_nearly_zero() || *self < other
76 }
77
78 fn is_nearly_greater_or_equal(&self, other: f64) -> bool {
80 (*self - other).is_nearly_zero() || *self > other
81 }
82
83 fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool;
85}
86
87impl Float64Ext for f64 {
88 fn is_nearly_zero_within_tolerance(&self, tolerance: f64) -> bool {
89 debug_assert!(tolerance >= 0.0, "tolerance must be non-negative");
90
91 self.abs() <= tolerance
92 }
93}
94
95pub(crate) trait PointExt: Sized {
96 fn x(&self) -> f32;
97 fn y(&self) -> f32;
98
99 fn nearly_same(&self, other: Self) -> bool {
100 self.x().is_nearly_equal(other.x()) && self.y().is_nearly_equal(other.y())
101 }
102}
103
104impl PointExt for kurbo::Point {
105 fn x(&self) -> f32 {
106 self.x as f32
107 }
108
109 fn y(&self) -> f32 {
110 self.y as f32
111 }
112}
113
114pub(crate) fn hash128<T: Hash + ?Sized>(value: &T) -> u128 {
116 let mut state = SipHasher13::new();
117 value.hash(&mut state);
118 state.finish128().as_u128()
119}
120
121pub(crate) trait BezPathExt {
122 fn fast_bounding_box(&self) -> Rect;
123}
124
125impl BezPathExt for BezPath {
126 fn fast_bounding_box(&self) -> Rect {
127 let mut min_x = f64::INFINITY;
128 let mut min_y = f64::INFINITY;
129 let mut max_x = f64::NEG_INFINITY;
130 let mut max_y = f64::NEG_INFINITY;
131
132 let mut include = |x: f64, y: f64| {
133 min_x = min_x.min(x);
134 min_y = min_y.min(y);
135 max_x = max_x.max(x);
136 max_y = max_y.max(y);
137 };
138
139 for el in self.elements() {
140 match *el {
141 PathEl::MoveTo(p) | PathEl::LineTo(p) => include(p.x, p.y),
142 PathEl::QuadTo(p1, p2) => {
143 include(p1.x, p1.y);
144 include(p2.x, p2.y);
145 }
146 PathEl::CurveTo(p1, p2, p3) => {
147 include(p1.x, p1.y);
148 include(p2.x, p2.y);
149 include(p3.x, p3.y);
150 }
151 PathEl::ClosePath => {}
152 }
153 }
154
155 if min_x > max_x {
156 Rect::ZERO
157 } else {
158 Rect::new(min_x, min_y, max_x, max_y)
159 }
160 }
161}
162
163pub trait RectExt {
165 fn to_kurbo(&self) -> Rect;
167}
168
169impl RectExt for pdf_syntax::object::Rect {
170 fn to_kurbo(&self) -> Rect {
171 Rect::new(self.x0, self.y0, self.x1, self.y1)
172 }
173}
174
175pub trait PageExt {
178 fn initial_transform(&self, invert_y: bool) -> Affine;
182}
183
184impl PageExt for Page<'_> {
185 fn initial_transform(&self, invert_y: bool) -> Affine {
186 let crop_box = self.crop_box();
194 let (_, base_height) = self.base_dimensions();
195 let (width, height) = self.render_dimensions();
196
197 let horizontal_t =
198 Affine::rotate(90.0_f64.to_radians()) * Affine::translate((0.0, -width as f64));
199 let flipped_horizontal_t =
200 Affine::translate((0.0, height as f64)) * Affine::rotate(-90.0_f64.to_radians());
201
202 let rotation_transform = match self.rotation() {
203 Rotation::None => Affine::IDENTITY,
204 Rotation::Horizontal => {
205 if invert_y {
206 horizontal_t
207 } else {
208 flipped_horizontal_t
209 }
210 }
211 Rotation::Flipped => {
212 Affine::scale(-1.0) * Affine::translate((-width as f64, -height as f64))
213 }
214 Rotation::FlippedHorizontal => {
215 if invert_y {
216 flipped_horizontal_t
217 } else {
218 horizontal_t
219 }
220 }
221 };
222
223 let inversion_transform = if invert_y {
224 Affine::new([1.0, 0.0, 0.0, -1.0, 0.0, base_height as f64])
225 } else {
226 Affine::IDENTITY
227 };
228
229 rotation_transform * inversion_transform * Affine::translate((-crop_box.x0, -crop_box.y0))
230 }
231}