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