1use std::cmp::Ordering;
4
5use tiny_skia::BlendMode;
6use tiny_skia::FilterQuality;
7use tiny_skia::Pixmap;
8use tiny_skia::PixmapPaint;
9use tiny_skia::Transform;
10
11#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
14pub enum Origin {
15 #[default]
18 TopLeft,
19
20 TopRight,
23
24 BottomLeft,
27
28 BottomRight,
31}
32
33impl Origin {
34 pub fn is_left(&self) -> bool {
36 matches!(self, Self::TopLeft | Self::BottomLeft)
37 }
38
39 pub fn is_right(&self) -> bool {
41 matches!(self, Self::TopRight | Self::BottomRight)
42 }
43
44 pub fn is_top(&self) -> bool {
46 matches!(self, Self::TopLeft | Self::TopRight)
47 }
48
49 pub fn is_bottom(&self) -> bool {
51 matches!(self, Self::BottomLeft | Self::BottomRight)
52 }
53}
54
55pub const PPP_TO_PPI_FACTOR: f32 = 72.0;
57
58pub const DEFAULT_PIXEL_PER_PT: f32 = 144.0 / PPP_TO_PPI_FACTOR;
64
65pub fn ppp_to_ppi(pixel_per_pt: f32) -> f32 {
67 pixel_per_pt * PPP_TO_PPI_FACTOR
68}
69
70pub fn ppi_to_ppp(pixel_per_inch: f32) -> f32 {
72 pixel_per_inch / PPP_TO_PPI_FACTOR
73}
74
75pub fn page_diff(base: &Pixmap, change: &Pixmap, origin: Origin) -> Pixmap {
82 fn aligned_offset((a, b): (u32, u32), end: bool) -> (i32, i32) {
83 match Ord::cmp(&a, &b) {
84 Ordering::Less if end => (u32::abs_diff(a, b) as i32, 0),
85 Ordering::Greater if end => (0, u32::abs_diff(a, b) as i32),
86 _ => (0, 0),
87 }
88 }
89
90 let mut diff = Pixmap::new(
91 Ord::max(base.width(), change.width()),
92 Ord::max(base.height(), change.height()),
93 )
94 .expect("must be larger than zero");
95
96 let (base_x, change_x) = aligned_offset((base.width(), change.width()), origin.is_right());
97 let (base_y, change_y) = aligned_offset((base.height(), change.height()), origin.is_right());
98
99 diff.draw_pixmap(
100 base_x,
101 base_y,
102 base.as_ref(),
103 &PixmapPaint {
104 opacity: 1.0,
105 blend_mode: BlendMode::Source,
106 quality: FilterQuality::Nearest,
107 },
108 Transform::identity(),
109 None,
110 );
111
112 diff.draw_pixmap(
113 change_x,
114 change_y,
115 change.as_ref(),
116 &PixmapPaint {
117 opacity: 1.0,
118 blend_mode: BlendMode::Difference,
119 quality: FilterQuality::Nearest,
120 },
121 Transform::identity(),
122 None,
123 );
124
125 diff
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn test_page_diff_top_left() {
134 let mut base = Pixmap::new(10, 10).unwrap();
135 let mut change = Pixmap::new(15, 5).unwrap();
136 let mut diff = Pixmap::new(15, 10).unwrap();
137
138 base.fill(tiny_skia::Color::from_rgba8(255, 255, 255, 255));
139 change.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 255));
140
141 let is_in = |x, y, pixmap: &Pixmap| x < pixmap.width() && y < pixmap.height();
142
143 for y in 0..10 {
144 for x in 0..15 {
145 let idx = diff.width().checked_mul(y).unwrap().checked_add(x).unwrap();
146 let px = diff.pixels_mut().get_mut(idx as usize).unwrap();
147
148 *px = bytemuck::cast(match (is_in(x, y, &base), is_in(x, y, &change)) {
155 (true, true) => [0u8, 255, 255, 255],
157 (true, false) => [255, 255, 255, 255],
159 (false, true) => [255, 0, 0, 255],
161 (false, false) => [0, 0, 0, 0],
163 });
164 }
165 }
166
167 assert_eq!(
168 page_diff(&base, &change, Origin::TopLeft).data(),
169 diff.data()
170 );
171 }
172
173 #[test]
174 fn test_page_diff_bottom_right() {
175 let mut base = Pixmap::new(10, 10).unwrap();
176 let mut change = Pixmap::new(15, 5).unwrap();
177 let mut diff = Pixmap::new(15, 10).unwrap();
178
179 base.fill(tiny_skia::Color::from_rgba8(255, 255, 255, 255));
180 change.fill(tiny_skia::Color::from_rgba8(255, 0, 0, 255));
181
182 let is_in =
184 |x, y, pixmap: &Pixmap| (15 - x) <= pixmap.width() && (10 - y) <= pixmap.height();
185
186 for y in 0..10 {
187 for x in 0..15 {
188 let idx = diff.width().checked_mul(y).unwrap().checked_add(x).unwrap();
189 let px = diff.pixels_mut().get_mut(idx as usize).unwrap();
190
191 *px = bytemuck::cast(match (is_in(x, y, &base), is_in(x, y, &change)) {
198 (true, true) => [0u8, 255, 255, 255],
200 (true, false) => [255, 255, 255, 255],
202 (false, true) => [255, 0, 0, 255],
204 (false, false) => [0, 0, 0, 0],
206 });
207 }
208 }
209
210 assert_eq!(
211 page_diff(&base, &change, Origin::BottomRight).data(),
212 diff.data()
213 );
214 }
215}