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}