logo
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
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
use crate::attr::Attributes;
use crate::blur::{liq_blur, liq_max3, liq_min3};
use crate::error::*;
use crate::pal::{PalIndex, MAX_COLORS, MIN_OPAQUE_A, PalF, RGBA, f_pixel, gamma_lut};
use crate::remap::DitherMapMode;
use crate::rows::{DynamicRows, PixelsSource};
use crate::seacow::RowBitmap;
use crate::seacow::SeaCow;
use crate::seacow::Pointer;
use crate::LIQ_HIGH_MEMORY_LIMIT;
use rgb::ComponentMap;
use std::mem::MaybeUninit;

/// Describes image dimensions and pixels for the library
///
/// Create one using [`Attributes::new_image()`].
///
/// All images are internally in the RGBA format.
pub struct Image<'pixels> {
    pub(crate) px: DynamicRows<'pixels, 'pixels>,
    pub(crate) importance_map: Option<Box<[u8]>>,
    pub(crate) edges: Option<Box<[u8]>>,
    pub(crate) dither_map: Option<Box<[u8]>>,
    pub(crate) background: Option<Box<Image<'pixels>>>,
    pub(crate) fixed_colors: Vec<f_pixel>,
}

impl<'pixels> Image<'pixels> {

    /// Makes an image from RGBA pixels.
    ///
    /// See the [`rgb`] and [`bytemuck`](//lib.rs/bytemuck) crates for making `[RGBA]` slices from `[u8]` slices.
    ///
    /// The `pixels` argument can be `Vec<RGBA>`, or `Box<[RGBA]>` or `&[RGBA]`.
    ///
    /// If you want to supply RGB or ARGB pixels, convert them to RGBA first, or use [`Image::new_fn`] to supply your own pixel-swapping function.
    ///
    /// Use `0.` for gamma if the image is sRGB (most images are).
    #[inline(always)]
    pub fn new<VecRGBA>(attr: &Attributes, pixels: VecRGBA, width: usize, height: usize, gamma: f64) -> Result<Self, Error> where VecRGBA: Into<Box<[RGBA]>> {
        Self::new_stride(attr, pixels, width, height, width, gamma)
    }

    /// Describe dimensions of a slice of RGBA pixels.
    ///
    /// Same as [`Image::new`], except it doesn't copy the pixels, but holds a temporary reference instead.
    ///
    /// If you want to supply RGB or ARGB pixels, use [`Image::new_fn`] to supply your own pixel-swapping function.
    ///
    /// See the [`rgb`] and [`bytemuck`](//lib.rs/bytemuck) crates for making `[RGBA]` slices from `[u8]` slices.
    ///
    /// Use `0.` for gamma if the image is sRGB (most images are).
    #[inline(always)]
    pub fn new_borrowed(attr: &Attributes, pixels: &'pixels [RGBA], width: usize, height: usize, gamma: f64) -> Result<Self, Error> {
        Self::new_stride_borrowed(attr, pixels, width, height, width, gamma)
    }

    /// Generate rows on demand using a callback function.
    ///
    /// The callback function should be cheap (e.g. just byte-swap pixels). The parameters are: line of RGBA pixels (slice's len is equal to image width), and row number (0-indexed).
    /// The callback will be called multiple times per row. May be called from multiple threads at once.
    ///
    /// Use `0.` for gamma if the image is sRGB (most images are).
    ///
    /// ## Safety
    ///
    /// This function is marked as unsafe, because the callback function MUST initialize the entire row (call `write` on every `MaybeUninit` pixel).
    ///
    pub unsafe fn new_fn<F: 'pixels + Fn(&mut [MaybeUninit<RGBA>], usize) + Send + Sync>(attr: &Attributes, convert_row_fn: F, width: usize, height: usize, gamma: f64) -> Result<Self, Error> {
        Image::new_internal(attr, PixelsSource::Callback(Box::new(convert_row_fn)), width as u32, height as u32, gamma)
    }

    pub(crate) fn free_histogram_inputs(&mut self) {
        self.importance_map = None;
        self.px.free_histogram_inputs();
    }

    pub(crate) fn new_internal(
        attr: &Attributes,
        pixels: PixelsSource<'pixels, 'pixels>,
        width: u32,
        height: u32,
        gamma: f64,
    ) -> Result<Self, Error> {
        if !Self::check_image_size(width, height) {
            return Err(ValueOutOfRange);
        }

        if !(0. ..=1.).contains(&gamma) {
            attr.verbose_print("  error: gamma must be >= 0 and <= 1 (try 1/gamma instead)");
            return Err(ValueOutOfRange);
        }
        let img = Image {
            px: DynamicRows::new(
                width,
                height,
                pixels,
                if gamma > 0. { gamma } else { 0.45455 },
            ),
            importance_map: None,
            edges: None,
            dither_map: None,
            background: None,
            fixed_colors: Vec::new(),
        };
        // if image is huge or converted pixels are not likely to be reused then don't cache converted pixels
        let low_memory_hint = !attr.use_contrast_maps && attr.use_dither_map == DitherMapMode::None;
        let limit = if low_memory_hint { LIQ_HIGH_MEMORY_LIMIT / 8 } else { LIQ_HIGH_MEMORY_LIMIT } / std::mem::size_of::<f_pixel>();
        if (img.width()) * (img.height()) > limit {
            attr.verbose_print("  conserving memory"); // for simplicity of this API there's no explicit pixels argument,
        }
        Ok(img)
    }

    fn check_image_size(width: u32, height: u32) -> bool {
        if width == 0 || height == 0 {
            return false;
        }
        if width.max(height) as usize > i32::MAX as usize ||
            width as usize > isize::MAX as usize / std::mem::size_of::<f_pixel>() / height as usize {
            return false;
        }
        true
    }

    pub(crate) fn update_dither_map(&mut self, remapped_image: &RowBitmap<'_, PalIndex>, palette: &PalF) {
        let width = self.width();
        let mut edges = match self.edges.take() {
            Some(e) => e,
            None => return,
        };
        let colors = palette.as_slice();

        let mut prev_row: Option<&[_]> = None;
        let mut rows = remapped_image.rows().zip(edges.chunks_exact_mut(width)).peekable();
        while let Some((this_row, edges)) = rows.next() {
            let mut lastpixel = this_row[0];
            let mut lastcol = 0;
            for (col, px) in this_row.iter().copied().enumerate().skip(1) {
                if self.background.is_some() && (colors[px as usize]).a < MIN_OPAQUE_A {
                    // Transparency may or may not create an edge. When there's an explicit background set, assume no edge.
                    continue;
                }
                if px != lastpixel || col == width - 1 {
                    let mut neighbor_count = 10 * (col - lastcol);
                    let mut i = lastcol;
                    while i < col {
                        if let Some(prev_row) = prev_row {
                            let pixelabove = prev_row[i];
                            if pixelabove == lastpixel { neighbor_count += 15; };
                        }
                        if let Some((next_row, _)) = rows.peek() {
                            let pixelbelow = next_row[i];
                            if pixelbelow == lastpixel { neighbor_count += 15; };
                        }
                        i += 1;
                    }
                    while lastcol <= col {
                        edges[lastcol] = ((edges[lastcol] as u16 + 128) as f32
                            * (255. / (255 + 128) as f32)
                            * (1. - 20. / (20 + neighbor_count) as f32))
                            as u8;
                        lastcol += 1;
                    }
                    lastpixel = px;
                }
            }
            prev_row = Some(this_row);
        }
        self.dither_map = Some(edges);
    }

    /// Set which pixels are more important (and more likely to get a palette entry)
    ///
    /// The map must be `width`×`height` pixels large. Higher numbers = more important.
    pub fn set_importance_map(&mut self, map: impl Into<Box<[u8]>>) -> Result<(), Error> {
        self.importance_map = Some(map.into());
        Ok(())
    }

    /// Remap pixels assuming they will be displayed on this background. This is designed for GIF's "keep" mode.
    ///
    /// Pixels that match the background color will be made transparent if there's a fully transparent color available in the palette.
    ///
    /// The background image's pixels must outlive this image.
    pub fn set_background(&mut self, background: Image<'pixels>) -> Result<(), Error> {
        if background.background.is_some() {
            return Err(Unsupported);
        }
        if self.px.width != background.px.width || self.px.height != background.px.height {
            return Err(BufferTooSmall);
        }
        self.background = Some(Box::new(background));
        self.dither_map = None;
        Ok(())
    }

    /// Reserves a color in the output palette created from this image. It behaves as if the given color was used in the image and was very important.
    ///
    /// The RGB values are assumed to have the same gamma as the image.
    ///
    /// It must be called before the image is quantized.
    ///
    /// Returns error if more than 256 colors are added. If image is quantized to fewer colors than the number of fixed colors added, then excess fixed colors will be ignored.
    pub fn add_fixed_color(&mut self, color: RGBA) -> Result<(), Error> {
        if self.fixed_colors.len() >= MAX_COLORS { return Err(Unsupported); }
        let lut = gamma_lut(self.px.gamma);
        self.fixed_colors.try_reserve(1)?;
        self.fixed_colors.push(f_pixel::from_rgba(&lut, RGBA {r: color.r, g: color.g, b: color.b, a: color.a}));
        Ok(())
    }

    /// Width of the image in pixels
    #[must_use]
    #[inline(always)]
    pub fn width(&self) -> usize {
        self.px.width as _
    }

    /// Height of the image in pixels
    #[must_use]
    #[inline(always)]
    pub fn height(&self) -> usize {
        self.px.height as _
    }

    #[inline(always)]
    pub(crate) fn gamma(&self) -> f64 {
        self.px.gamma
    }

    /// Builds two maps:
    ///    importance_map - approximation of areas with high-frequency noise, except straight edges. 1=flat, 0=noisy.
    ///    edges - noise map including all edges
    pub(crate) fn contrast_maps(&mut self) -> Result<(), Error> {
        let width = self.width();
        let height = self.height();
        if width < 4 || height < 4 || (3 * width * height) > LIQ_HIGH_MEMORY_LIMIT {
            return Ok(()); // shrug
        }

        let noise = match self.importance_map.as_deref_mut() {
            Some(n) => n,
            None => {
                let vec = try_zero_vec(width * height)?;
                self.importance_map.get_or_insert_with(move || vec.into_boxed_slice())
            },
        };
        let edges = match self.edges.as_mut() {
            Some(e) => e,
            None => {
                let vec = try_zero_vec(width * height)?;
                self.edges.get_or_insert_with(move || vec.into_boxed_slice())
            },
        };

        let mut rows_iter = self.px.all_rows_f()?.chunks_exact(width);

        let mut next_row = rows_iter.next().unwrap();
        let mut curr_row = next_row;
        let mut prev_row;

        for (noise_row, edges_row) in noise[..width * height].chunks_exact_mut(width).zip(edges[..width * height].chunks_exact_mut(width)) {
            prev_row = curr_row;
            curr_row = next_row;
            next_row = rows_iter.next().unwrap_or(next_row);
            let mut prev;
            let mut curr = curr_row[0].0;
            let mut next = curr;
            for i in 0..width {
                prev = curr;
                curr = next;
                next = curr_row[(i + 1).min(width - 1)].0;
                // contrast is difference between pixels neighbouring horizontally and vertically
                let horiz = (prev + next - curr * 2.).map(|c| c.abs()); // noise is amplified
                let prevl = prev_row[i].0;
                let nextl = next_row[i].0;
                let vert = (prevl + nextl - curr * 2.).map(|c| c.abs());
                let horiz = horiz.a.max(horiz.r).max(horiz.g.max(horiz.b));
                let vert = vert.a.max(vert.r).max(vert.g.max(vert.b));
                let edge = horiz.max(vert);
                let mut z = edge - (horiz - vert).abs() * 0.5;
                z = 1. - z.max(horiz.min(vert));
                z *= z;
                z *= z;
                // 85 is about 1/3rd of weight (not 0, because noisy pixels still need to be included, just not as precisely).
                noise_row[i] = (80. + z * 176.) as u8;
                edges_row[i] = ((1. - edge) * 256.) as u8;
            }
        }
        // noise areas are shrunk and then expanded to remove thin edges from the map
        let mut tmp = try_zero_vec(width * height)?;
        liq_max3(noise, &mut tmp, width, height);
        liq_max3(&tmp, noise, width, height);
        liq_blur(noise, &mut tmp, width, height, 3);
        liq_max3(noise, &mut tmp, width, height);
        liq_min3(&tmp, noise, width, height);
        liq_min3(noise, &mut tmp, width, height);
        liq_min3(&tmp, noise, width, height);
        liq_min3(edges, &mut tmp, width, height);
        liq_max3(&tmp, edges, width, height);
        for (edges, noise) in edges.iter_mut().zip(noise) {
            *edges = (*noise).min(*edges);
        }
        Ok(())
    }

    /// Stride is in pixels. Allows defining regions of larger images or images with padding without copying. The stride is in pixels.
    ///
    /// Otherwise the same as [`Image::new_borrowed`].
    #[inline(always)]
    pub fn new_stride_borrowed(attr: &Attributes, pixels: &'pixels [RGBA], width: usize, height: usize, stride: usize, gamma: f64) -> Result<Self, Error> {
        Self::new_stride_internal(attr, SeaCow::borrowed(pixels), width, height, stride, gamma)
    }

    /// Create new image by copying `pixels` to an internal buffer, so that it makes a self-contained type.
    ///
    /// The `pixels` argument can be `Vec<RGBA>`, or `Box<[RGBA]>` or `&[RGBA]`.
    ///
    /// Otherwise the same as [`Image::new_stride_borrowed`].
    #[inline]
    pub fn new_stride<VecRGBA>(attr: &Attributes, pixels: VecRGBA, width: usize, height: usize, stride: usize, gamma: f64) -> Result<Image<'static>, Error> where VecRGBA: Into<Box<[RGBA]>> {
        Self::new_stride_internal(attr, SeaCow::boxed(pixels.into()), width, height, stride, gamma)
    }

    fn new_stride_internal<'a>(attr: &Attributes, pixels: SeaCow<'a, RGBA>, width: usize, height: usize, stride: usize, gamma: f64) -> Result<Image<'a>, Error> {
        let slice = pixels.as_slice();
        if slice.len() < (stride * height + width - stride) {
            attr.verbose_print(format!("Buffer length is {} bytes, which is not enough for {}×{}×4 RGBA bytes", slice.len()*4, stride, height));
            return Err(BufferTooSmall);
        }

        let rows = SeaCow::boxed(slice.chunks(stride).map(|row| Pointer(row.as_ptr())).take(height).collect());
        Image::new_internal(attr, PixelsSource::Pixels { rows, pixels: Some(pixels) }, width as u32, height as u32, gamma)
    }
}

fn try_zero_vec(len: usize) -> Result<Vec<u8>, Error> {
    let mut vec = Vec::new();
    vec.try_reserve_exact(len)?;
    vec.resize(len, 0);
    Ok(vec)
}