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, io::Cursor};
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(Cursor::new(input));
157        let mut reader = decoder.read_info()?;
158        let mut buf = vec![
159            0_u8;
160            reader
161                .output_buffer_size()
162                .expect("Failed to read from buffer")
163        ];
164
165        let info = reader.next_frame(&mut buf)?;
166
167        if let Some(palette) = &reader.info().palette {
168            self.data.palette = Some(palette.to_vec());
169        }
170
171        self.data.buf.clone_from(&buf);
172        self.data.image = buf;
173        self.data.width = info.width;
174        self.data.height = info.height;
175        self.data.color_type = info.color_type;
176        self.data.bit_depth = info.bit_depth;
177        self.data.line_size = info.line_size;
178
179        Ok(())
180    }
181
182    /**
183    Processes an image with current [settings], storing the result in a [buffer].
184
185    [buffer]: MoshData::buf
186    [settings]: MoshOptions
187
188    # Errors
189
190    * [`UnsupportedColorType`]: [`Indexed`] is not supported.
191
192    [`Indexed`]: ColorType::Indexed
193
194    # Example
195    ```rust
196    use libmosh::{
197        err::MoshError,
198        ops::{read_file, write_file},
199        MoshCore,
200    };
201
202    let input = read_file("tests/assets/test-rgb.png")?;
203    let output = "test.png";
204    let mut image = MoshCore::new();
205
206    image.options.min_rate = 5;
207    image.options.max_rate = 7;
208    image.options.pixelation = 10;
209    image.options.line_shift = 0.7;
210    image.options.reverse = 0.4;
211    image.options.flip = 0.3;
212    image.options.channel_swap = 0.5;
213    image.options.channel_shift = 0.5;
214    image.options.seed = 42;
215
216    image.read_image(&input)?;
217    image.mosh()?;
218    write_file(
219        output,
220        &image.data,
221        &image.options,
222    )?;
223    # Ok::<(), MoshError>(())
224    ```
225
226    [`UnsupportedColorType`]: crate::err::MoshError::UnsupportedColorType
227    */
228    pub fn mosh(&mut self) -> Result<(), MoshError> {
229        self.data.mosh(&self.options)?;
230
231        Ok(())
232    }
233}
234
235impl MoshOptions {
236    fn generate_seed() -> u64 {
237        if cfg!(test) {
238            TEST_SEED
239        } else {
240            rand::rng().next_u64()
241        }
242    }
243
244    /// Generates a new random seed.
245    pub fn new_seed(&mut self) {
246        self.seed = Self::generate_seed();
247    }
248}
249
250impl MoshData {
251    fn mosh(&mut self, options: &MoshOptions) -> Result<(), MoshError> {
252        self.buf.clone_from(&self.image);
253
254        let min_rate = options.min_rate;
255        let max_rate = cmp::max(options.min_rate, options.max_rate);
256        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(options.seed);
257        let chunk_count_distrib = Uniform::new(min_rate, max_rate)?;
258        let mosh_rate = chunk_count_distrib.sample(&mut rng);
259
260        for _ in 0..mosh_rate {
261            Self::chunkmosh(self, &mut rng, options)?;
262        }
263
264        match self.color_type {
265            ColorType::Grayscale | ColorType::Indexed => {
266                self.pixelation(options, fr::PixelType::U8);
267            }
268            ColorType::GrayscaleAlpha => {
269                self.pixelation(options, fr::PixelType::U8x2);
270            }
271            ColorType::Rgb => {
272                self.pixelation(options, fr::PixelType::U8x3);
273            }
274            ColorType::Rgba => {
275                self.pixelation(options, fr::PixelType::U8x4);
276            }
277        }
278
279        if options.ansi {
280            self.generate_ansi_data()?;
281        }
282
283        Ok(())
284    }
285
286    fn pixelation(&mut self, options: &MoshOptions, pixel_type: fr::PixelType) {
287        if options.pixelation > 1 {
288            let width = self.width;
289            let height = self.height;
290            let src_image =
291                fr::images::Image::from_vec_u8(width, height, self.buf.clone(), pixel_type)
292                    .unwrap();
293
294            let dest_width = self.width / u32::from(options.pixelation);
295            let dest_height = self.height / u32::from(options.pixelation);
296            let orig_width = self.width;
297            let orig_height = self.height;
298
299            let mut dest_image =
300                fr::images::Image::new(dest_width, dest_height, src_image.pixel_type());
301            let mut orig_image =
302                fr::images::Image::new(orig_width, orig_height, src_image.pixel_type());
303            let mut resizer = fr::Resizer::new();
304
305            resizer
306                .resize(
307                    &src_image,
308                    &mut dest_image,
309                    &fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
310                )
311                .unwrap();
312            resizer
313                .resize(
314                    &dest_image,
315                    &mut orig_image,
316                    &fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
317                )
318                .unwrap();
319
320            self.buf = orig_image.into_vec();
321        }
322    }
323
324    fn get_palette_color(&self, idx: usize) -> Result<(u8, u8, u8), MoshError> {
325        match &self.palette {
326            Some(palette) => {
327                let r = palette[idx * 3];
328                let g = palette[idx * 3 + 1];
329                let b = palette[idx * 3 + 2];
330                Ok((r, g, b))
331            }
332            None => Err(MoshError::InvalidPalette),
333        }
334    }
335
336    /// Converts an image buffer using the ANSI color set.
337    ///
338    /// # Errors
339    ///
340    /// It may fail if the image data has the wrong format.
341    pub fn generate_ansi_data(&mut self) -> Result<(), MoshError> {
342        let mut ansi_data: Vec<u8> = Vec::new();
343        for y in 0..self.height {
344            for x in 0..self.width {
345                let idx = (y * self.width + x) as usize
346                    * match self.color_type {
347                        ColorType::Grayscale | ColorType::Indexed => 1,
348                        ColorType::GrayscaleAlpha => 2,
349                        ColorType::Rgb => 3,
350                        ColorType::Rgba => 4,
351                    };
352
353                let r = match self.color_type {
354                    ColorType::Indexed => {
355                        let palette_idx = self.buf[idx] as usize;
356                        let (r, _, _) = self.get_palette_color(palette_idx)?;
357                        r
358                    }
359                    _ => self.buf[idx],
360                };
361
362                let g = match self.color_type {
363                    ColorType::Rgb | ColorType::Rgba => self.buf[idx + 1],
364                    ColorType::Indexed => {
365                        let palette_idx = self.buf[idx] as usize;
366                        let (_, g, _) = self.get_palette_color(palette_idx)?;
367                        g
368                    }
369                    _ => self.buf[idx],
370                };
371
372                let b = match self.color_type {
373                    ColorType::Rgb | ColorType::Rgba => self.buf[idx + 2],
374                    ColorType::Indexed => {
375                        let palette_idx = self.buf[idx] as usize;
376                        let (_, _, b) = self.get_palette_color(palette_idx)?;
377                        b
378                    }
379                    _ => self.buf[idx],
380                };
381
382                let ansi_color = get_ansi_color(r, g, b)?;
383                ansi_data.push(ansi_color);
384            }
385        }
386
387        self.buf = ansi_data;
388
389        Ok(())
390    }
391
392    // Use pnglitch approach
393    //
394    // TODO
395    // Add more `rng` to `chunk_size`?
396    fn chunkmosh(
397        &mut self,
398        rng: &mut impl rand::Rng,
399        options: &MoshOptions,
400    ) -> Result<(), MoshError> {
401        let line_count = self.buf.len() / self.line_size;
402        let channel_count = match self.color_type {
403            ColorType::Grayscale | ColorType::Indexed => 1,
404            ColorType::GrayscaleAlpha => 2,
405            ColorType::Rgb => 3,
406            ColorType::Rgba => 4,
407        };
408
409        let line_shift_distrib = Uniform::new(0, self.line_size)?;
410        let line_number_distrib = Uniform::new(0, line_count)?;
411        let channel_count_distrib = Uniform::new(0, channel_count)?;
412
413        let first_line = line_number_distrib.sample(rng);
414        let chunk_size = line_number_distrib.sample(rng) / 2;
415        let last_line = if (first_line + chunk_size) > line_count {
416            line_count
417        } else {
418            first_line + chunk_size
419        };
420
421        let reverse = rng.random_bool(options.reverse);
422        let flip = rng.random_bool(options.flip);
423
424        let line_shift = rng.random_bool(options.line_shift).then(|| {
425            let line_shift_amount = line_shift_distrib.sample(rng);
426            MoshLine::Shift(line_shift_amount)
427        });
428
429        let channel_shift = rng.random_bool(options.channel_shift).then(|| {
430            let amount = line_shift_distrib.sample(rng) / channel_count;
431            let channel = channel_count_distrib.sample(rng);
432            MoshLine::ChannelShift(amount, channel, channel_count)
433        });
434
435        let channel_swap = rng.random_bool(options.channel_swap).then(|| {
436            let channel_1 = channel_count_distrib.sample(rng);
437            let channel_2 = channel_count_distrib.sample(rng);
438            MoshChunk::ChannelSwap(channel_1, channel_2, channel_count)
439        });
440
441        for line_number in first_line..last_line {
442            let line_start = line_number * self.line_size;
443            let line_end = line_start + self.line_size;
444            let line = &mut self.buf[line_start..line_end];
445
446            if let Some(do_channel_shift) = &channel_shift {
447                do_channel_shift.glitch(line);
448            }
449
450            if let Some(do_line_shift) = &line_shift {
451                do_line_shift.glitch(line);
452            }
453            if reverse {
454                MoshLine::Reverse.glitch(line);
455            }
456        }
457
458        let chunk_start = first_line * self.line_size;
459        let chunk_end = last_line * self.line_size;
460        let chunk = &mut self.buf[chunk_start..chunk_end];
461
462        if let Some(do_channel_swap) = channel_swap {
463            do_channel_swap.glitch(chunk);
464        }
465
466        if flip {
467            MoshChunk::Flip.glitch(chunk);
468        }
469
470        Ok(())
471    }
472}
473
474impl Default for MoshData {
475    fn default() -> Self {
476        Self {
477            buf: vec![0_u8],
478            image: vec![0_u8],
479            width: 1,
480            height: 1,
481            color_type: ColorType::Rgba,
482            bit_depth: BitDepth::Eight,
483            palette: None,
484            line_size: 1,
485        }
486    }
487}
488
489impl Default for MoshOptions {
490    fn default() -> Self {
491        Self {
492            min_rate: 1,
493            max_rate: 7,
494            pixelation: 10,
495            line_shift: 0.3,
496            reverse: 0.3,
497            flip: 0.3,
498            channel_swap: 0.3,
499            channel_shift: 0.3,
500            ansi: false,
501            seed: Self::generate_seed(),
502        }
503    }
504}
505
506fn get_ansi_color(r: u8, g: u8, b: u8) -> Result<u8, MoshError> {
507    let mut closest_index = 0;
508    let mut min_distance: i32 = i32::MAX;
509
510    for (index, &color) in ANSI_COLORS.iter().enumerate() {
511        // Calculate squared Euclidean distance between RGB colors
512        let distance = (i32::from(r) - i32::from(color.0)).pow(2)
513            + (i32::from(g) - i32::from(color.1)).pow(2)
514            + (i32::from(b) - i32::from(color.2)).pow(2);
515
516        if distance < min_distance {
517            min_distance = distance;
518            closest_index = index;
519        }
520    }
521
522    let color = u8::try_from(closest_index)?;
523    Ok(color)
524}
525
526#[must_use]
527pub fn generate_palette() -> Vec<u8> {
528    let mut palette = Vec::with_capacity(ANSI_COLORS.len() * 3);
529    for &(r, g, b) in &ANSI_COLORS {
530        palette.push(r);
531        palette.push(g);
532        palette.push(b);
533    }
534
535    palette
536}
537
538const TEST_SEED: u64 = 901_042_006;
539
540#[cfg(test)]
541mod tests;