blittle/
lib.rs

1#![doc = include_str!("../README.md")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3
4#[cfg(feature = "rayon")]
5mod multi_threaded;
6mod position;
7mod size;
8pub mod stride;
9#[cfg(feature = "rayon")]
10pub use multi_threaded::*;
11
12pub use position::*;
13pub use size::Size;
14
15/// Blit `src` onto `dst`.
16///
17/// - `src` and `dst` are flat byte slices of images. There are many ways to cast your pixel map to `[u8]`, such as with the `bytemuck` crate.
18/// - `dst_position` is the top-left position of the region that `src` will blit onto.
19/// - `dst_size` and `src_size` are the [`Size`]'s of the destination and source images, respectively.
20/// - `stride` is the per-pixel stride length. See `crate::stride` for some common stride values.
21pub fn blit(
22    src: &[u8],
23    src_size: &Size,
24    dst: &mut [u8],
25    dst_position: &PositionU,
26    dst_size: &Size,
27    stride: usize,
28) {
29    if src_size.w > 0 && src_size.h > 0 {
30        let src_w_stride = src_size.w * stride;
31        (0..src_size.h).for_each(|src_y| {
32            let src_index = get_index(0, src_y, src_size.w, stride);
33            let dst_index = get_index(dst_position.x, dst_position.y + src_y, dst_size.w, stride);
34            dst[dst_index..dst_index + src_w_stride]
35                .copy_from_slice(&src[src_index..src_index + src_w_stride]);
36        });
37    }
38}
39
40/// Clip `src_size` such that it fits within the rectangle defined by `dst_position` and `dst_size`.
41/// Returns `dst_position` as a clipped `PositionU` that can be used in [`blit`].
42pub fn clip(dst_position: &PositionI, dst_size: &Size, src_size: &mut Size) -> PositionU {
43    // Check if the source image is totally out of bounds.
44    if dst_position.x + (src_size.w.cast_signed()) < 0 || dst_position.y + (src_size.h.cast_signed()) < 0 {
45        src_size.w = 0;
46        src_size.h = 0;
47        PositionU::default()
48    } else {
49        let mut x = 0;
50        if dst_position.x < 0 {
51            src_size.w = src_size.w.saturating_sub(dst_position.x.unsigned_abs());
52        } else {
53            x = dst_position.x.unsigned_abs();
54        }
55        let mut y = 0;
56        if dst_position.y < 0 {
57            src_size.h = src_size.h.saturating_sub(dst_position.y.unsigned_abs());
58        } else {
59            y = dst_position.y.unsigned_abs();
60        }
61        let dst_position = PositionU { x, y };
62        // This allows us to do unchecked subtraction.
63        // The `blit` methods will also check `is_inside`.
64        if dst_position.x < dst_size.w && dst_position.y < dst_size.h {
65            src_size.w = src_size.w.min(dst_size.w - dst_position.x);
66            src_size.h = src_size.h.min(dst_size.h - dst_position.y);
67            dst_position
68        } else {
69            *src_size = Size::default();
70            PositionU::default()
71        }
72    }
73}
74
75/// Converts a position, width, and stride to an index in a 1D byte slice.
76pub const fn get_index(x: usize, y: usize, w: usize, stride: usize) -> usize {
77    (x + y * w) * stride
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::stride::RGB;
84    use std::{fs::File, io::BufWriter, path::Path};
85
86    const SRC_W: usize = 32;
87    const SRC_H: usize = 17;
88    const DST_W: usize = 64;
89    const DST_H: usize = 64;
90
91    #[test]
92    fn test_blit() {
93        let src = [255u8; SRC_W * SRC_H * RGB];
94        let mut dst = [0u8; DST_W * DST_H * RGB];
95
96        let dst_position = PositionU { x: 2, y: 12 };
97        let dst_size = Size { w: DST_W, h: DST_H };
98        let src_size = Size { w: SRC_W, h: SRC_H };
99
100        blit(&src, &src_size, &mut dst, &dst_position, &dst_size, RGB);
101
102        save_png("blit.png", &dst, DST_W as u32, DST_H as u32);
103    }
104
105    #[test]
106    fn test_clip() {
107        blit_clipped("clip_positive.png", 42, 16);
108        blit_clipped("clip_negative.png", -8, -8);
109    }
110
111    fn blit_clipped(name: &str, x: isize, y: isize) {
112        let src = [255u8; SRC_W * SRC_H * RGB];
113        let mut dst = [0u8; DST_W * DST_H * RGB];
114
115        let dst_position = PositionI { x, y };
116        let dst_size = Size { w: DST_W, h: DST_H };
117        let mut src_size = Size { w: SRC_W, h: SRC_H };
118        let dst_position = clip(&dst_position, &dst_size, &mut src_size);
119
120        blit(&src, &src_size, &mut dst, &dst_position, &dst_size, RGB);
121        save_png(name, &dst, DST_W as u32, DST_H as u32);
122    }
123
124    fn save_png(path: &str, dst: &[u8], dst_w: u32, dst_h: u32) {
125        let path = Path::new(path);
126        let file = File::create(path).unwrap();
127        let w = BufWriter::new(file);
128        let mut encoder = png::Encoder::new(w, dst_w, dst_h);
129        encoder.set_color(png::ColorType::Rgb);
130        encoder.set_depth(png::BitDepth::Eight);
131        let mut writer = encoder.write_header().unwrap();
132        writer.write_image_data(dst).unwrap();
133    }
134}