melvin_ob/imaging/
map_image.rs

1use super::{file_based_buffer::FileBackedBuffer, sub_buffer::SubBuffer};
2use crate::util::{MapSize, Vec2D};
3use image::{
4    DynamicImage, EncodableLayout, GenericImage, GenericImageView, ImageBuffer, Pixel,
5    PixelWithColorType, Rgb, RgbImage,
6    codecs::png::{CompressionType, FilterType, PngDecoder, PngEncoder},
7    imageops,
8};
9use std::{
10    io::{BufReader, Cursor},
11    ops::{Deref, DerefMut},
12    path::Path,
13};
14use tokio::{fs::File, io::AsyncReadExt};
15
16/// Represents an extracted and encoded image with metadata.
17///
18/// This struct contains information about the region of the image
19/// that was extracted, its dimensions, and the encoded image data.
20pub(crate) struct EncodedImageExtract {
21    /// The top-left corner of the extracted image region in the original image.
22    pub(crate) offset: Vec2D<u32>,
23    /// The dimensions (width and height) of the extracted region.
24    pub(crate) size: Vec2D<u32>,
25    /// The encoded image data as a vector of bytes.
26    pub(crate) data: Vec<u8>,
27}
28
29/// Trait representing operations for working with map images.
30///
31/// This generic trait allows manipulating and extracting data from images
32/// stored as 2D pixel buffers. It provides methods to work with sub-regions of
33/// the image, export images in PNG format, update specific areas, and more.
34///
35/// # Type Parameters
36/// * `Pixel` - The pixel type used by the image, which implements `PixelWithColorType`.
37/// * `Container` - The container storing pixel subcomponents, implementing `Deref` and `DerefMut`.
38/// * `ViewSubBuffer` - A view into a sub-region of the image, implementing `GenericImageView`.
39pub(crate) trait MapImage {
40    /// The type of the pixels in the image.
41    type Pixel: PixelWithColorType;
42
43    /// The container for the pixel data.
44    type Container: Deref<Target = [<Self::Pixel as Pixel>::Subpixel]> + DerefMut;
45
46    /// A view of a sub-region of the image.
47    type ViewSubBuffer: GenericImageView<Pixel: PixelWithColorType>;
48
49    /// Provides a mutable view of the image at the specified offset.
50    ///
51    /// # Arguments
52    /// * `offset` - The top-left corner of the requested region.
53    ///
54    /// # Returns
55    /// A `SubBuffer` representing the specified region of the image.
56    fn mut_vec_view(
57        &mut self,
58        offset: Vec2D<u32>,
59    ) -> SubBuffer<&mut ImageBuffer<Self::Pixel, Self::Container>>;
60
61    /// Provides a view of a sub-region of the image.
62    ///
63    /// # Arguments
64    /// * `offset` - The top-left corner of the requested region.
65    /// * `size` - The dimensions of the requested region.
66    ///
67    /// # Returns
68    /// A `SubBuffer` representing the specified region of the image.
69    fn vec_view(&self, offset: Vec2D<u32>, size: Vec2D<u32>) -> SubBuffer<&Self::ViewSubBuffer>;
70
71    /// Returns a reference to the entire image buffer.
72    ///
73    /// # Returns
74    /// A reference to the image buffer.
75    fn buffer(&self) -> &ImageBuffer<Self::Pixel, Self::Container>;
76
77    /// Exports the entire image buffer as a PNG.
78    ///
79    /// This method encodes the image as a PNG and returns the encoded byte array along
80    /// with metadata about the image. The encoded data is stored in an `EncodedImageExtract`
81    /// struct that contains the image's offset, size, and encoded data.
82    ///
83    /// # Returns
84    /// An `EncodedImageExtract` containing the offset, size, and encoded image data.
85    ///
86    /// # Errors
87    /// Returns an error if the PNG encoding process fails.
88    fn export_as_png(&self) -> Result<EncodedImageExtract, Box<dyn std::error::Error>>
89    where [<Self::Pixel as Pixel>::Subpixel]: EncodableLayout {
90        let mut writer = Cursor::new(Vec::<u8>::new());
91        let buffer = self.buffer();
92        buffer.write_with_encoder(PngEncoder::new(&mut writer))?;
93        Ok(EncodedImageExtract {
94            offset: Vec2D::new(0, 0),
95            size: Vec2D::new(buffer.width(), buffer.height()),
96            data: writer.into_inner(),
97        })
98    }
99
100    /// Exports a specific sub-region of the image as a PNG.
101    ///
102    /// This method extracts the specified sub-region of the image, encodes it as a PNG,
103    /// and returns it as an `EncodedImageExtract`. The sub-region to be extracted is defined
104    /// by the provided `offset` and `size`.
105    ///
106    /// # Arguments
107    /// * `offset` - The top-left corner of the region to export.
108    /// * `size` - The dimensions of the region to export, specified as a width and height.
109    ///
110    /// # Returns
111    /// An `EncodedImageExtract` containing the offset, size, and encoded image data of the sub-region.
112    ///
113    /// # Errors
114    /// Returns an error if the PNG encoding process fails.
115    #[allow(clippy::cast_sign_loss)]
116    fn export_area_as_png(
117        &self,
118        offset: Vec2D<u32>,
119        size: Vec2D<u32>,
120    ) -> Result<EncodedImageExtract, Box<dyn std::error::Error>>
121    where
122        [<<Self::ViewSubBuffer as GenericImageView>::Pixel as Pixel>::Subpixel]: EncodableLayout,
123    {
124        let area_view = self.vec_view(offset, size);
125
126        let mut area_image = ImageBuffer::<
127            <Self::ViewSubBuffer as GenericImageView>::Pixel,
128            Vec<<<Self::ViewSubBuffer as GenericImageView>::Pixel as Pixel>::Subpixel>,
129        >::new(size.x(), size.y());
130        area_image.copy_from(&area_view, 0, 0).unwrap();
131        let mut writer = Cursor::new(Vec::<u8>::new());
132        area_image.write_with_encoder(PngEncoder::new(&mut writer))?;
133        Ok(EncodedImageExtract { offset, size, data: writer.into_inner() })
134    }
135
136    /// Saves the current image buffer as a snapshot in PNG format.
137    ///
138    /// This method writes the image's content to the file at the specified path in PNG format.
139    ///
140    /// # Arguments
141    /// * `path` - The file path where the snapshot should be saved.
142    ///
143    /// # Returns
144    /// Returns `Ok(())` if the save operation is successful.
145    /// Returns an error if the save process fails.
146    fn create_snapshot<P: AsRef<Path>>(&self, path: P) -> Result<(), Box<dyn std::error::Error>>
147    where [<Self::Pixel as Pixel>::Subpixel]: EncodableLayout {
148        self.buffer().save(path)?;
149        Ok(())
150    }
151
152    /// Updates a specific sub-region of the image with the given data.
153    ///
154    /// This method copies the content of `image` into the corresponding sub-region of the current
155    /// image buffer, starting from the specified `offset`.
156    ///
157    /// # Arguments
158    /// * `offset` - The top-left corner of the target sub-region to update.
159    /// * `image` - The new image data to copy into the target sub-region.
160    fn update_area<I: GenericImageView<Pixel = Self::Pixel>>(
161        &mut self,
162        offset: Vec2D<u32>,
163        image: &I,
164    ) {
165        self.mut_vec_view(offset).copy_from(image, 0, 0).unwrap();
166    }
167}
168
169/// A struct representing a full-sized map image.
170///
171/// This struct manages the full-sized map image which includes
172/// a coverage bitmap and an image buffer backed by a memory-mapped file.
173/// It provides functionality to open and handle the image buffer efficiently.
174pub(crate) struct FullsizeMapImage {
175    /// The image buffer containing the pixel data, backed by a file.
176    image_buffer: ImageBuffer<Rgb<u8>, FileBackedBuffer>,
177}
178
179pub(crate) struct OffsetZonedObjectiveImage {
180    offset: Vec2D<u32>,
181    image_buffer: ImageBuffer<Rgb<u8>, Vec<u8>>,
182}
183
184impl OffsetZonedObjectiveImage {
185    pub fn new(offset: Vec2D<u32>, dimensions: Vec2D<u32>) -> Self {
186        Self { offset, image_buffer: ImageBuffer::new(dimensions.x(), dimensions.y()) }
187    }
188
189    #[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)]
190    pub fn update_area<I: GenericImageView<Pixel = Rgb<u8>>>(
191        &mut self,
192        offset: Vec2D<u32>,
193        image: &I,
194    ) {
195        for x in 0..image.width() {
196            let offset_x = (offset.x() + x) as i32;
197            let relative_offset_x =
198                Vec2D::wrap_coordinate(offset_x - self.offset.x() as i32, Vec2D::map_size().x())
199                    as u32;
200
201            if relative_offset_x >= self.image_buffer.width() {
202                continue;
203            }
204            for y in 0..image.height() {
205                let offset_y = (offset.y() + y) as i32;
206                let relative_offset_y = Vec2D::wrap_coordinate(
207                    offset_y - self.offset.y() as i32,
208                    Vec2D::map_size().y(),
209                ) as u32;
210
211                if relative_offset_y >= self.image_buffer.height() {
212                    continue;
213                }
214                *self.image_buffer.get_pixel_mut(relative_offset_x, relative_offset_y) =
215                    image.get_pixel(x, y);
216            }
217        }
218    }
219
220    fn export_as_png(&self) -> Result<EncodedImageExtract, Box<dyn std::error::Error>> {
221        let mut writer = Cursor::new(Vec::<u8>::new());
222        self.image_buffer.write_with_encoder(PngEncoder::new(&mut writer))?;
223        Ok(EncodedImageExtract {
224            offset: self.offset,
225            size: Vec2D::new(self.image_buffer.width(), self.image_buffer.height()),
226            data: writer.into_inner(),
227        })
228    }
229}
230
231impl GenericImageView for OffsetZonedObjectiveImage {
232    type Pixel = Rgb<u8>;
233
234    fn dimensions(&self) -> (u32, u32) { self.image_buffer.dimensions() }
235
236    fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel { *self.image_buffer.get_pixel(x, y) }
237}
238
239impl MapImage for OffsetZonedObjectiveImage {
240    type Pixel = Rgb<u8>;
241    type Container = Vec<u8>;
242    type ViewSubBuffer = OffsetZonedObjectiveImage;
243
244    fn mut_vec_view(
245        &mut self,
246        offset: Vec2D<u32>,
247    ) -> SubBuffer<&mut ImageBuffer<Self::Pixel, Self::Container>> {
248        SubBuffer {
249            buffer: &mut self.image_buffer,
250            buffer_size: u32::map_size(),
251            offset,
252            size: u32::map_size(),
253        }
254    }
255
256    fn vec_view(&self, offset: Vec2D<u32>, size: Vec2D<u32>) -> SubBuffer<&Self::ViewSubBuffer> {
257        SubBuffer { buffer: self, buffer_size: u32::map_size(), offset, size }
258    }
259
260    fn buffer(&self) -> &ImageBuffer<Self::Pixel, Self::Container> { &self.image_buffer }
261}
262
263impl FullsizeMapImage {
264    /// Opens a full-sized map image from a file.
265    ///
266    /// This function initializes a `FileBackedBuffer` for efficient memory-mapped file access
267    /// and creates an `ImageBuffer` using the data in the mapped file.
268    ///
269    /// # Arguments
270    /// * `path` - The file path of the image to open.
271    ///
272    /// # Returns
273    /// An instance of `FullsizeMapImage` with the coverage bitmap initialized
274    /// and the image buffer mapped to the file.
275    ///
276    /// # Panics
277    /// This function will panic if:
278    /// * The `FileBackedBuffer` cannot be created.
279    /// * The `ImageBuffer` cannot be created from the `FileBackedBuffer`.
280    pub(crate) fn open<P: AsRef<Path>>(path: P) -> Self {
281        let fullsize_buffer_size: usize =
282            (u32::map_size().x() as usize) * (u32::map_size().y() as usize) * 3;
283        let file_based_buffer = FileBackedBuffer::open(path, fullsize_buffer_size).unwrap();
284        Self {
285            image_buffer: ImageBuffer::from_raw(
286                u32::map_size().x(),
287                u32::map_size().y(),
288                file_based_buffer,
289            )
290            .unwrap(),
291        }
292    }
293}
294
295impl GenericImageView for FullsizeMapImage {
296    /// The pixel type used by the image buffer, in this case, `Rgba<u8>`.
297    type Pixel = Rgb<u8>;
298
299    /// Returns the dimensions of the image buffer as a tuple `(width, height)`.
300    ///
301    /// # Returns
302    /// A tuple containing the width and height of the image buffer.
303    fn dimensions(&self) -> (u32, u32) { self.image_buffer.dimensions() }
304
305    /// Retrieves the pixel at the given `(x, y)` coordinates.
306    ///
307    /// If the pixel is covered (as checked by the coverage bitmap), the corresponding
308    /// pixel data from the image buffer will be returned with an alpha value of `0xFF`
309    /// (fully opaque). Otherwise, a transparent black pixel `[0, 0, 0, 0]` is returned.
310    ///
311    /// # Arguments
312    /// * `x` - The horizontal coordinate of the pixel.
313    /// * `y` - The vertical coordinate of the pixel.
314    ///
315    /// # Returns
316    /// An `Rgba<u8>` pixel that is either from the image buffer (if covered) or
317    /// a transparent black pixel (if not covered).
318    fn get_pixel(&self, x: u32, y: u32) -> Self::Pixel { *self.image_buffer.get_pixel(x, y) }
319}
320
321impl MapImage for FullsizeMapImage {
322    /// The pixel type for the image, in this case `Rgb<u8>`.
323    type Pixel = Rgb<u8>;
324    /// The container type for the pixel data, in this case `FileBackedBuffer` used for memory-mapped file access.
325    type Container = FileBackedBuffer;
326    /// The view type for a sub-region of the image, implemented as `FullsizeMapImage`.
327    type ViewSubBuffer = FullsizeMapImage;
328
329    /// Provides a mutable view of the image buffer at the specified offset.
330    ///
331    /// # Arguments
332    /// * `offset` - The top-left corner of the region to view.
333    ///
334    /// # Returns
335    /// A `SubBuffer` containing a mutable reference to the image buffer
336    /// starting from the specified offset.
337    fn mut_vec_view(
338        &mut self,
339        offset: Vec2D<u32>,
340    ) -> SubBuffer<&mut ImageBuffer<Rgb<u8>, FileBackedBuffer>> {
341        SubBuffer {
342            buffer: &mut self.image_buffer,
343            buffer_size: u32::map_size(),
344            offset,
345            size: u32::map_size(),
346        }
347    }
348
349    /// Provides a view of a sub-region of the image buffer.
350    ///
351    /// # Arguments
352    /// * `offset` - The top-left corner of the region to view.
353    /// * `size` - The dimensions of the region to view.
354    ///
355    /// # Returns
356    /// A `SubBuffer` containing a reference to the `FullsizeMapImage` starting
357    /// from the specified offset and region size.
358    fn vec_view(&self, offset: Vec2D<u32>, size: Vec2D<u32>) -> SubBuffer<&FullsizeMapImage> {
359        SubBuffer { buffer: self, buffer_size: u32::map_size(), offset, size }
360    }
361
362    /// Returns a reference to the entire image buffer.
363    ///
364    /// # Returns
365    /// A reference to the `ImageBuffer` containing the RGB pixel data.
366    fn buffer(&self) -> &ImageBuffer<Self::Pixel, Self::Container> { &self.image_buffer }
367}
368
369/// Represents a thumbnail image generated from a full-size map image.
370///
371/// This struct is designed to manage scaled-down versions of map images,
372/// which are useful for generating previews or comparing snapshots.
373pub(crate) struct ThumbnailMapImage {
374    /// The underlying image buffer storing the pixel data of the thumbnail.
375    image_buffer: RgbImage,
376}
377
378impl MapImage for ThumbnailMapImage {
379    /// The pixel type used, which is RGBA with 8-bit sub-pixels.
380    type Pixel = Rgb<u8>;
381    /// The container type for the pixel data, represented as a vector of bytes.
382    type Container = Vec<u8>;
383    /// The view type for sub-regions of the thumbnail, implemented as an `ImageBuffer`.
384    type ViewSubBuffer = ImageBuffer<Rgb<u8>, Vec<u8>>;
385
386    /// Provides a mutable view of the thumbnail at the specified offset.
387    ///
388    /// # Arguments
389    /// * `offset` - The top-left corner of the requested sub-region.
390    ///
391    /// # Returns
392    /// A `SubBuffer` representing the specified sub-region of the thumbnail.
393    fn mut_vec_view(
394        &mut self,
395        offset: Vec2D<u32>,
396    ) -> SubBuffer<&mut ImageBuffer<Rgb<u8>, Vec<u8>>> {
397        SubBuffer {
398            buffer: &mut self.image_buffer,
399            buffer_size: Self::thumbnail_size(),
400            offset,
401            size: Self::thumbnail_size(),
402        }
403    }
404
405    /// Provides a view of a sub-region of the thumbnail.
406    ///
407    /// # Arguments
408    /// * `offset` - The top-left corner of the requested sub-region.
409    /// * `size` - The dimensions of the requested sub-region.
410    ///
411    /// # Returns
412    /// A `SubBuffer` representing the specified sub-region of the thumbnail.
413    fn vec_view(
414        &self,
415        offset: Vec2D<u32>,
416        size: Vec2D<u32>,
417    ) -> SubBuffer<&ImageBuffer<Rgb<u8>, Vec<u8>>> {
418        SubBuffer { buffer: &self.image_buffer, buffer_size: Self::thumbnail_size(), offset, size }
419    }
420
421    /// Returns a reference to the entire image buffer of the thumbnail.
422    ///
423    /// # Returns
424    /// A reference to the image buffer storing the thumbnail's pixel data.
425    fn buffer(&self) -> &ImageBuffer<Self::Pixel, Self::Container> { &self.image_buffer }
426}
427
428impl ThumbnailMapImage {
429    /// Defines the scale factor for generating a thumbnail from a full-size map image.
430    ///
431    /// The dimensions of the thumbnail are calculated by dividing the full-sized map
432    /// dimensions by this constant.
433    pub(crate) const THUMBNAIL_SCALE_FACTOR: u32 = 25;
434
435    /// Calculates the size of the thumbnail based on the full-size map dimensions.
436    ///
437    /// This method uses the `THUMBNAIL_SCALE_FACTOR` to scale down the map size.
438    ///
439    /// # Returns
440    /// A `Vec2D<u32>` representing the dimensions of the thumbnail.
441    pub(crate) fn thumbnail_size() -> Vec2D<u32> { u32::map_size() / Self::THUMBNAIL_SCALE_FACTOR }
442
443    /// Generates a thumbnail from a given full-sized map image.
444    ///
445    /// This method scales down the provided `FullsizeMapImage` to create a thumbnail
446    /// using the pre-defined `thumbnail_size`.
447    ///
448    /// # Arguments
449    /// * `fullsize_map_image` - A reference to the `FullsizeMapImage` to be converted.
450    ///
451    /// # Returns
452    /// A `ThumbnailMapImage` containing the scaled-down image.
453    pub(crate) fn from_fullsize(fullsize_map_image: &FullsizeMapImage) -> Self {
454        Self {
455            image_buffer: imageops::thumbnail(
456                fullsize_map_image,
457                Self::thumbnail_size().x(),
458                Self::thumbnail_size().y(),
459            ),
460        }
461    }
462
463    /// Generates a thumbnail from a previously saved snapshot.
464    ///
465    /// If the snapshot file exists, it is loaded and converted into a thumbnail.
466    /// If it does not exist, a blank image with the dimensions of the thumbnail is created.
467    ///
468    /// # Arguments
469    /// * `snapshot_path` - The file path to the snapshot PNG.
470    ///
471    /// # Returns
472    /// A `ThumbnailMapImage` containing either the loaded thumbnail image or a blank thumbnail.
473    pub(crate) fn from_snapshot<P: AsRef<Path>>(snapshot_path: P) -> Self {
474        let image_buffer = if let Ok(file) = std::fs::File::open(snapshot_path) {
475            DynamicImage::from_decoder(PngDecoder::new(&mut BufReader::new(file)).unwrap())
476                .unwrap()
477                .to_rgb8()
478        } else {
479            ImageBuffer::new(Self::thumbnail_size().x(), Self::thumbnail_size().y())
480        };
481        Self { image_buffer }
482    }
483
484    /// Computes the difference between the current thumbnail and a snapshot.
485    ///
486    /// This method compares the pixel data of the current thumbnail against a previously
487    /// saved snapshot and creates a new image showing the differences. Pixels that
488    /// are identical are marked as transparent, and differing pixels retain their values.
489    ///
490    /// If the snapshot file does not exist, the current thumbnail is exported as a PNG.
491    ///
492    /// # Arguments
493    /// * `base_snapshot_path` - The file path to the base snapshot PNG.
494    ///
495    /// # Returns
496    /// An `EncodedImageExtract` containing the diff image as a PNG.
497    ///
498    /// # Errors
499    /// Returns an error if the snapshot file cannot be read or the PNG encoding fails.
500    pub(crate) async fn diff_with_snapshot<P: AsRef<Path>>(
501        &self,
502        base_snapshot_path: P,
503    ) -> Result<EncodedImageExtract, Box<dyn std::error::Error>> {
504        if let Ok(mut file) = File::open(base_snapshot_path).await {
505            let mut old_snapshot_encoded = Vec::<u8>::new();
506            file.read_to_end(&mut old_snapshot_encoded).await?;
507            let old_snapshot = DynamicImage::from_decoder(PngDecoder::new(&mut Cursor::new(
508                old_snapshot_encoded,
509            ))?)?
510            .to_rgb8();
511            let mut current_snapshot = self.image_buffer.clone();
512
513            for (current_pixel, new_pixel) in
514                old_snapshot.pixels().zip(current_snapshot.pixels_mut())
515            {
516                if *current_pixel == *new_pixel {
517                    *new_pixel = Rgb([0, 0, 0]);
518                }
519            }
520            let mut writer = Cursor::new(Vec::<u8>::new());
521            current_snapshot.write_with_encoder(PngEncoder::new_with_quality(
522                &mut writer,
523                CompressionType::Best,
524                FilterType::Adaptive,
525            ))?;
526            let diff_encoded = writer.into_inner();
527            Ok(EncodedImageExtract {
528                offset: Vec2D::new(0, 0),
529                size: u32::map_size() / ThumbnailMapImage::THUMBNAIL_SCALE_FACTOR,
530                data: diff_encoded,
531            })
532        } else {
533            self.export_as_png()
534        }
535    }
536}
537
538#[cfg(test)]
539mod tests {
540    use super::*;
541    use crate::imaging::CameraAngle;
542
543    #[test]
544    fn test_overflow() {
545        let mut fullsize_image = FullsizeMapImage::open("tmp.bin");
546
547        let angle = CameraAngle::Normal;
548        let area_size = u32::from(angle.get_square_side_length());
549        let offset = Vec2D::new(
550            Vec2D::<u32>::map_size().x() - area_size / 2,
551            Vec2D::<u32>::map_size().y() - area_size / 2,
552        );
553
554        let mut area_image: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(area_size, area_size);
555        for x in 0..area_size {
556            for y in 0..area_size {
557                *area_image.get_pixel_mut(x, y) = Rgb([
558                    (x % 0xFF) as u8,
559                    (y % 0xFF) as u8,
560                    ((x + 7 + y * 3) % 130) as u8,
561                ]);
562            }
563        }
564        fullsize_image.update_area(offset, &area_image);
565        let assert_area_edge = |fs_offset: Vec2D<u32>, area_offset: Vec2D<u32>, size: u32| {
566            let fs_view = fullsize_image.vec_view(fs_offset, Vec2D::new(size, size));
567            let mut fs_image: ImageBuffer<Rgb<u8>, Vec<u8>> = ImageBuffer::new(size, size);
568            fs_image.copy_from(&fs_view, 0, 0).unwrap();
569            let area_view = area_image.view(area_offset.x(), area_offset.y(), size, size);
570            assert_eq!(fs_image.as_raw(), area_view.to_image().as_raw());
571        };
572        assert_area_edge(
573            Vec2D::new(0, 0),
574            Vec2D::new(area_size / 2, area_size / 2),
575            area_size / 2,
576        );
577        assert_area_edge(
578            Vec2D::new(
579                Vec2D::<u32>::map_size().x() - area_size / 2,
580                Vec2D::<u32>::map_size().y() - area_size / 2,
581            ),
582            Vec2D::new(0, 0),
583            area_size / 2,
584        );
585        assert_area_edge(offset, Vec2D::new(0, 0), area_size);
586    }
587}