aboutsummaryrefslogtreecommitdiff
path: root/src/layer.rs
blob: 465953d77bc3308620d5f71e6968d3d3a9fef527 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
//! Lazy tiled image.
//!
//! This supports OSM-style "tiled" images, but not all of the tiles have to be present. If a tile
//! is not present, a default pixel is returned. The tile is allocated with the first call to a
//! mutating operation.
use std::{fs::{self, File}, io::BufWriter, path::Path};

use color_eyre::eyre::{bail, Result};
use fnv::FnvHashMap;
use image::{
    codecs::png::{CompressionType, FilterType, PngEncoder},
    ColorType, ImageBuffer, ImageEncoder, Pixel, Rgba, RgbaImage,
};

pub const TILE_HEIGHT: u64 = 256;
pub const TILE_WIDTH: u64 = 256;

/// Main "lazy image buffer" struct.
#[derive(Debug, Clone)]
pub struct TileLayer<P: Pixel> {
    tiles: FnvHashMap<(u64, u64), ImageBuffer<P, Vec<P::Subpixel>>>,
    default_pixel: P,
    width: u64,
    height: u64,
}

impl<P: Pixel> TileLayer<P> {
    pub fn from_pixel(width: u64, height: u64, pixel: P) -> Self {
        TileLayer {
            tiles: Default::default(),
            default_pixel: pixel,
            width,
            height,
        }
    }

    pub fn width(&self) -> u64 {
        self.width
    }

    pub fn height(&self) -> u64 {
        self.height
    }

    fn index(&self, x: u64, y: u64) -> ((u64, u64), (u32, u32)) {
        (
            (x / TILE_WIDTH, y / TILE_HEIGHT),
            ((x % TILE_WIDTH).try_into().unwrap(), (y % TILE_HEIGHT).try_into().unwrap()),
        )
    }

    pub fn enumerate_tiles(&self) -> impl Iterator<Item = (u64, u64, &ImageBuffer<P, Vec<P::Subpixel>>)> {
        self.tiles.iter().map(|((x, y), t)| (*x, *y, t))
    }

    pub fn tile_mut(&mut self, tile_x: u64, tile_y: u64) -> &mut ImageBuffer<P, Vec<P::Subpixel>> {
        self.tiles
            .entry((tile_x, tile_y))
            .or_insert_with(|| ImageBuffer::from_pixel(TILE_WIDTH as u32, TILE_HEIGHT as u32, self.default_pixel))
    }

    pub fn tile_for_mut(&mut self, x: u64, y: u64) -> &mut ImageBuffer<P, Vec<P::Subpixel>> {
        let ((tx, ty), _) = self.index(x, y);
        self.tile_mut(tx, ty)
    }

    pub fn get_pixel_checked(&self, x: u64, y: u64) -> Option<&P> {
        if x >= self.width || y >= self.height {
            return None;
        }

        let (outer_idx, (inner_x, inner_y)) = self.index(x, y);
        self.tiles
            .get(&outer_idx)
            .map(|tile| tile.get_pixel(inner_x, inner_y))
            .or_else(|| Some(&self.default_pixel))
    }

    pub fn get_pixel(&self, x: u64, y: u64) -> &P {
        // This is kinda cheating, but we care about the API for now, not the speed. We can
        // optimize this later.
        self.get_pixel_checked(x, y).unwrap()
    }

    pub fn get_pixel_mut_checked(&mut self, x: u64, y: u64) -> Option<&mut P> {
        if x >= self.width || y >= self.height {
            return None;
        }

        let ((outer_x, outer_y), (inner_x, inner_y)) = self.index(x, y);
        Some(
            self.tile_mut(outer_x, outer_y)
                .get_pixel_mut(inner_x, inner_y),
        )
    }

    pub fn get_pixel_mut(&mut self, x: u64, y: u64) -> &mut P {
        self.get_pixel_mut_checked(x, y).unwrap()
    }

    /// Enumerate all pixels that are explicitely set in this layer.
    pub fn enumerate_pixels(&self) -> impl Iterator<Item = (u64, u64, &P)> {
        self.tiles.iter().flat_map(|((tx, ty), tile)| {
            tile.enumerate_pixels()
                .map(move |(x, y, p)| (u64::from(x) + tx * TILE_WIDTH, u64::from(y) + ty * TILE_HEIGHT, p))
        })
    }

    /// Mutably enumerate all pixels that are explicitely set in this layer.
    pub fn enumerate_pixels_mut(&mut self) -> impl Iterator<Item = (u64, u64, &mut P)> {
        self.tiles.iter_mut().flat_map(|((tx, ty), tile)| {
            tile.enumerate_pixels_mut()
                .map(move |(x, y, p)| (u64::from(x) + tx * TILE_WIDTH, u64::from(y) + ty * TILE_HEIGHT, p))
        })
    }

    pub fn pixels(&self) -> impl Iterator<Item = &P> {
        self.enumerate_pixels().map(|x| x.2)
    }

    pub fn pixels_mut(&mut self) -> impl Iterator<Item = &mut P> {
        self.enumerate_pixels_mut().map(|x| x.2)
    }
}

impl TileLayer<Rgba<u8>> {
    pub fn save_to_directory<S: AsRef<Path>>(&self, path: S) -> Result<()> {
        let path = path.as_ref();

        for ((x, y), tile) in self.tiles.iter() {
            let folder = path.join(&x.to_string());
            let metadata = folder.metadata();
            match metadata {
                Err(_) => fs::create_dir(&folder)?,
                Ok(m) if !m.is_dir() => bail!("Output path is not a directory"),
                _ => {},
            }
            let file = folder.join(&format!("{y}.png"));
            compress_png(tile, file)?;
        }

        Ok(())
    }
}

pub fn compress_png<P: AsRef<Path>>(image: &RgbaImage, path: P) -> Result<()> {
    let outstream = BufWriter::new(File::create(path)?);
    let encoder =
        PngEncoder::new_with_quality(outstream, CompressionType::Best, FilterType::Adaptive);

    encoder.write_image(&image, image.width(), image.height(), ColorType::Rgba8)?;

    Ok(())
}