libmosh/
lib.rs

1/*! # Overview
2
3_Glitch and pixelate PNG images_
4
5Provides the [`MoshCore`] type for image processing and I/O functions,
6available in the [`ops`] module.
7
8# Usage
9Add `pixelmosh` to your dependencies in your project's `Cargo.toml`.
10
11```shell
12cargo add pixelmosh --no-default-features
13```
14
15# Example
16```rust
17use libmosh::{
18    err::MoshError,
19    ops::{read_file, write_file},
20    MoshCore,
21};
22
23let input = read_file("tests/assets/test-rgb.png")?;
24let output = "test.png";
25let mut core = MoshCore::new();
26
27core.read_image(&input)?;
28core.mosh()?;
29write_file(
30    output,
31    &core.data,
32    &core.options,
33)?;
34# Ok::<(), MoshError>(())
35```
36*/
37
38use fast_image_resize as fr;
39
40use png::{BitDepth, ColorType, Decoder};
41use rand::{
42    RngCore, SeedableRng,
43    distr::{Distribution, Uniform},
44};
45
46use std::cmp;
47
48use crate::{
49    err::MoshError,
50    fx::{Mosh, MoshChunk, MoshLine},
51};
52
53pub mod err;
54pub mod fx;
55pub mod ops;
56
57const ANSI_COLORS: [(u8, u8, u8); 16] = [
58    (0, 0, 0),       // Black
59    (205, 0, 0),     // Red
60    (0, 205, 0),     // Green
61    (205, 205, 0),   // Yellow
62    (0, 0, 205),     // Blue
63    (205, 0, 205),   // Magenta
64    (0, 205, 205),   // Cyan
65    (229, 229, 229), // White
66    (127, 127, 127), // Bright Black
67    (255, 0, 0),     // Bright Red
68    (0, 255, 0),     // Bright Green
69    (255, 255, 0),   // Bright Yellow
70    (0, 0, 255),     // Bright Blue
71    (255, 0, 255),   // Bright Magenta
72    (0, 255, 255),   // Bright Cyan
73    (255, 255, 255), // Bright White
74];
75
76/// Image data.
77///
78/// It holds the original image, buffer and parameters.
79#[non_exhaustive]
80#[derive(Clone)]
81pub struct MoshData {
82    /// Buffer.
83    pub buf: Vec<u8>,
84    /// Original image.
85    pub image: Vec<u8>,
86    /// Width.
87    pub width: u32,
88    /// Height.
89    pub height: u32,
90    /// Color type.
91    pub color_type: ColorType,
92    /// Bit depth.
93    pub bit_depth: BitDepth,
94    /// Color palette.
95    pub palette: Option<Vec<u8>>,
96    /// Line size.
97    pub line_size: usize,
98}
99
100/// Processing options.
101///
102/// Minimal `pixelation` value is `1` (OFF).
103#[non_exhaustive]
104#[derive(Clone, Debug)]
105pub struct MoshOptions {
106    /// Minimal amount of chunks to process.
107    pub min_rate: u16,
108    /// Maximal amount of chunks to process.
109    pub max_rate: u16,
110    /// Pixelation's intensity.
111    pub pixelation: u8,
112    /// Chance of line shift.
113    pub line_shift: f64,
114    /// Chance of reverse.
115    pub reverse: f64,
116    /// Chance of flip.
117    pub flip: f64,
118    /// Chance of channel swap.
119    pub channel_swap: f64,
120    /// Chance of channel shift.
121    pub channel_shift: f64,
122    /// Convert to ANSI color palette.
123    pub ansi: bool,
124    /// Random seed.
125    pub seed: u64,
126}
127
128/// Core container.
129///
130/// Holds image data and processing options.
131#[non_exhaustive]
132#[derive(Clone, Default)]
133pub struct MoshCore {
134    pub data: MoshData,
135    pub options: MoshOptions,
136}
137
138impl MoshCore {
139    /// Creates a new, empty instance of [`MoshCore`] with a random [seed].
140    ///
141    /// [seed]: MoshOptions::seed
142    #[must_use]
143    pub fn new() -> Self {
144        Self {
145            data: MoshData::default(),
146            options: MoshOptions::default(),
147        }
148    }
149
150    /// Reads provided image for future processing.
151    ///
152    /// # Errors
153    ///
154    /// It may fail if an image is not a valid PNG file.
155    pub fn read_image(&mut self, input: &[u8]) -> Result<(), MoshError> {
156        let decoder = Decoder::new(input);
157        let mut reader = decoder.read_info()?;
158        let mut buf = vec![0_u8; reader.output_buffer_size()];
159        let info = reader.next_frame(&mut buf)?;
160
161        if let Some(palette) = &reader.info().palette {
162            self.data.palette = Some(palette.to_vec());
163        }
164
165        self.data.buf.clone_from(&buf);
166        self.data.image = buf;
167        self.data.width = info.width;
168        self.data.height = info.height;
169        self.data.color_type = info.color_type;
170        self.data.bit_depth = info.bit_depth;
171        self.data.line_size = info.line_size;
172
173        Ok(())
174    }
175
176    /**
177    Processes an image with current [settings], storing the result in a [buffer].
178
179    [buffer]: MoshData::buf
180    [settings]: MoshOptions
181
182    # Errors
183
184    * [`UnsupportedColorType`]: [`Indexed`] is not supported.
185
186    [`Indexed`]: ColorType::Indexed
187
188    # Example
189    ```rust
190    use libmosh::{
191        err::MoshError,
192        ops::{read_file, write_file},
193        MoshCore,
194    };
195
196    let input = read_file("tests/assets/test-rgb.png")?;
197    let output = "test.png";
198    let mut image = MoshCore::new();
199
200    image.options.min_rate = 5;
201    image.options.max_rate = 7;
202    image.options.pixelation = 10;
203    image.options.line_shift = 0.7;
204    image.options.reverse = 0.4;
205    image.options.flip = 0.3;
206    image.options.channel_swap = 0.5;
207    image.options.channel_shift = 0.5;
208    image.options.seed = 42;
209
210    image.read_image(&input)?;
211    image.mosh()?;
212    write_file(
213        output,
214        &image.data,
215        &image.options,
216    )?;
217    # Ok::<(), MoshError>(())
218    ```
219
220    [`UnsupportedColorType`]: crate::err::MoshError::UnsupportedColorType
221    */
222    pub fn mosh(&mut self) -> Result<(), MoshError> {
223        self.data.mosh(&self.options)?;
224
225        Ok(())
226    }
227}
228
229impl MoshOptions {
230    fn generate_seed() -> u64 {
231        if cfg!(test) {
232            TEST_SEED
233        } else {
234            rand::rng().next_u64()
235        }
236    }
237
238    /// Generates a new random seed.
239    pub fn new_seed(&mut self) {
240        self.seed = Self::generate_seed();
241    }
242}
243
244impl MoshData {
245    fn mosh(&mut self, options: &MoshOptions) -> Result<(), MoshError> {
246        self.buf.clone_from(&self.image);
247
248        let min_rate = options.min_rate;
249        let max_rate = cmp::max(options.min_rate, options.max_rate);
250        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(options.seed);
251        let chunk_count_distrib = Uniform::new(min_rate, max_rate)?;
252        let mosh_rate = chunk_count_distrib.sample(&mut rng);
253
254        for _ in 0..mosh_rate {
255            Self::chunkmosh(self, &mut rng, options)?;
256        }
257
258        match self.color_type {
259            ColorType::Grayscale | ColorType::Indexed => {
260                self.pixelation(options, fr::PixelType::U8);
261            }
262            ColorType::GrayscaleAlpha => {
263                self.pixelation(options, fr::PixelType::U8x2);
264            }
265            ColorType::Rgb => {
266                self.pixelation(options, fr::PixelType::U8x3);
267            }
268            ColorType::Rgba => {
269                self.pixelation(options, fr::PixelType::U8x4);
270            }
271        }
272
273        if options.ansi {
274            self.generate_ansi_data()?;
275        }
276
277        Ok(())
278    }
279
280    fn pixelation(&mut self, options: &MoshOptions, pixel_type: fr::PixelType) {
281        if options.pixelation > 1 {
282            let width = self.width;
283            let height = self.height;
284            let src_image =
285                fr::images::Image::from_vec_u8(width, height, self.buf.clone(), pixel_type)
286                    .unwrap();
287
288            let dest_width = self.width / u32::from(options.pixelation);
289            let dest_height = self.height / u32::from(options.pixelation);
290            let orig_width = self.width;
291            let orig_height = self.height;
292
293            let mut dest_image =
294                fr::images::Image::new(dest_width, dest_height, src_image.pixel_type());
295            let mut orig_image =
296                fr::images::Image::new(orig_width, orig_height, src_image.pixel_type());
297            let mut resizer = fr::Resizer::new();
298
299            resizer
300                .resize(
301                    &src_image,
302                    &mut dest_image,
303                    &fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
304                )
305                .unwrap();
306            resizer
307                .resize(
308                    &dest_image,
309                    &mut orig_image,
310                    &fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
311                )
312                .unwrap();
313
314            self.buf = orig_image.into_vec();
315        }
316    }
317
318    fn get_palette_color(&self, idx: usize) -> Result<(u8, u8, u8), MoshError> {
319        match &self.palette {
320            Some(palette) => {
321                let r = palette[idx * 3];
322                let g = palette[idx * 3 + 1];
323                let b = palette[idx * 3 + 2];
324                Ok((r, g, b))
325            }
326            None => Err(MoshError::InvalidPalette),
327        }
328    }
329
330    /// Converts an image buffer using the ANSI color set.
331    ///
332    /// # Errors
333    ///
334    /// It may fail if the image data has the wrong format.
335    pub fn generate_ansi_data(&mut self) -> Result<(), MoshError> {
336        let mut ansi_data: Vec<u8> = Vec::new();
337        for y in 0..self.height {
338            for x in 0..self.width {
339                let idx = (y * self.width + x) as usize
340                    * match self.color_type {
341                        ColorType::Grayscale | ColorType::Indexed => 1,
342                        ColorType::GrayscaleAlpha => 2,
343                        ColorType::Rgb => 3,
344                        ColorType::Rgba => 4,
345                    };
346
347                let r = match self.color_type {
348                    ColorType::Indexed => {
349                        let palette_idx = self.buf[idx] as usize;
350                        let (r, _, _) = self.get_palette_color(palette_idx)?;
351                        r
352                    }
353                    _ => self.buf[idx],
354                };
355
356                let g = match self.color_type {
357                    ColorType::Rgb | ColorType::Rgba => self.buf[idx + 1],
358                    ColorType::Indexed => {
359                        let palette_idx = self.buf[idx] as usize;
360                        let (_, g, _) = self.get_palette_color(palette_idx)?;
361                        g
362                    }
363                    _ => self.buf[idx],
364                };
365
366                let b = match self.color_type {
367                    ColorType::Rgb | ColorType::Rgba => self.buf[idx + 2],
368                    ColorType::Indexed => {
369                        let palette_idx = self.buf[idx] as usize;
370                        let (_, _, b) = self.get_palette_color(palette_idx)?;
371                        b
372                    }
373                    _ => self.buf[idx],
374                };
375
376                let ansi_color = get_ansi_color(r, g, b)?;
377                ansi_data.push(ansi_color);
378            }
379        }
380
381        self.buf = ansi_data;
382
383        Ok(())
384    }
385
386    // Use pnglitch approach
387    //
388    // TODO
389    // Add more `rng` to `chunk_size`?
390    fn chunkmosh(
391        &mut self,
392        rng: &mut impl rand::Rng,
393        options: &MoshOptions,
394    ) -> Result<(), MoshError> {
395        let line_count = self.buf.len() / self.line_size;
396        let channel_count = match self.color_type {
397            ColorType::Grayscale | ColorType::Indexed => 1,
398            ColorType::GrayscaleAlpha => 2,
399            ColorType::Rgb => 3,
400            ColorType::Rgba => 4,
401        };
402
403        let line_shift_distrib = Uniform::new(0, self.line_size)?;
404        let line_number_distrib = Uniform::new(0, line_count)?;
405        let channel_count_distrib = Uniform::new(0, channel_count)?;
406
407        let first_line = line_number_distrib.sample(rng);
408        let chunk_size = line_number_distrib.sample(rng) / 2;
409        let last_line = if (first_line + chunk_size) > line_count {
410            line_count
411        } else {
412            first_line + chunk_size
413        };
414
415        let reverse = rng.random_bool(options.reverse);
416        let flip = rng.random_bool(options.flip);
417
418        let line_shift = rng.random_bool(options.line_shift).then(|| {
419            let line_shift_amount = line_shift_distrib.sample(rng);
420            MoshLine::Shift(line_shift_amount)
421        });
422
423        let channel_shift = rng.random_bool(options.channel_shift).then(|| {
424            let amount = line_shift_distrib.sample(rng) / channel_count;
425            let channel = channel_count_distrib.sample(rng);
426            MoshLine::ChannelShift(amount, channel, channel_count)
427        });
428
429        let channel_swap = rng.random_bool(options.channel_swap).then(|| {
430            let channel_1 = channel_count_distrib.sample(rng);
431            let channel_2 = channel_count_distrib.sample(rng);
432            MoshChunk::ChannelSwap(channel_1, channel_2, channel_count)
433        });
434
435        for line_number in first_line..last_line {
436            let line_start = line_number * self.line_size;
437            let line_end = line_start + self.line_size;
438            let line = &mut self.buf[line_start..line_end];
439
440            if let Some(do_channel_shift) = &channel_shift {
441                do_channel_shift.glitch(line);
442            }
443
444            if let Some(do_line_shift) = &line_shift {
445                do_line_shift.glitch(line);
446            }
447            if reverse {
448                MoshLine::Reverse.glitch(line);
449            }
450        }
451
452        let chunk_start = first_line * self.line_size;
453        let chunk_end = last_line * self.line_size;
454        let chunk = &mut self.buf[chunk_start..chunk_end];
455
456        if let Some(do_channel_swap) = channel_swap {
457            do_channel_swap.glitch(chunk);
458        }
459
460        if flip {
461            MoshChunk::Flip.glitch(chunk);
462        }
463
464        Ok(())
465    }
466}
467
468impl Default for MoshData {
469    fn default() -> Self {
470        Self {
471            buf: vec![0_u8],
472            image: vec![0_u8],
473            width: 1,
474            height: 1,
475            color_type: ColorType::Rgba,
476            bit_depth: BitDepth::Eight,
477            palette: None,
478            line_size: 1,
479        }
480    }
481}
482
483impl Default for MoshOptions {
484    fn default() -> Self {
485        Self {
486            min_rate: 1,
487            max_rate: 7,
488            pixelation: 10,
489            line_shift: 0.3,
490            reverse: 0.3,
491            flip: 0.3,
492            channel_swap: 0.3,
493            channel_shift: 0.3,
494            ansi: false,
495            seed: Self::generate_seed(),
496        }
497    }
498}
499
500fn get_ansi_color(r: u8, g: u8, b: u8) -> Result<u8, MoshError> {
501    let mut closest_index = 0;
502    let mut min_distance: i32 = i32::MAX;
503
504    for (index, &color) in ANSI_COLORS.iter().enumerate() {
505        // Calculate squared Euclidean distance between RGB colors
506        let distance = (i32::from(r) - i32::from(color.0)).pow(2)
507            + (i32::from(g) - i32::from(color.1)).pow(2)
508            + (i32::from(b) - i32::from(color.2)).pow(2);
509
510        if distance < min_distance {
511            min_distance = distance;
512            closest_index = index;
513        }
514    }
515
516    let color = u8::try_from(closest_index)?;
517    Ok(color)
518}
519
520#[must_use]
521pub fn generate_palette() -> Vec<u8> {
522    let mut palette = Vec::with_capacity(ANSI_COLORS.len() * 3);
523    for &(r, g, b) in &ANSI_COLORS {
524        palette.push(r);
525        palette.push(g);
526        palette.push(b);
527    }
528
529    palette
530}
531
532const TEST_SEED: u64 = 901_042_006;
533
534#[cfg(test)]
535mod tests;