Skip to main content

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