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```toml
12[dependencies]
13pixelmosh = { version = "3.1", default-features = false }
14```
15
16# Example
17```rust
18use libmosh::{
19    err::MoshError,
20    ops::{read_file, write_file},
21    MoshCore,
22};
23
24let input = read_file("src/util/test-rgb.png")?;
25let output = "test.png";
26let mut core = MoshCore::new();
27
28core.read_image(&input)?;
29core.mosh()?;
30write_file(
31    output,
32    &core.data.buf,
33    core.data.width,
34    core.data.height,
35    core.data.color_type,
36    core.data.bit_depth,
37)?;
38# Ok::<(), MoshError>(())
39```
40*/
41#![allow(deprecated)]
42
43use fast_image_resize as fr;
44
45use png::{BitDepth, ColorType, Decoder};
46use rand::{
47    distributions::{Distribution, Uniform},
48    RngCore, SeedableRng,
49};
50
51use std::cmp;
52
53use crate::{
54    err::MoshError,
55    fx::{Mosh, MoshChunk, MoshLine},
56};
57
58pub mod err;
59pub mod fx;
60pub mod ops;
61
62/// Image data.
63///
64/// It holds the original image, buffer and parameters.
65#[derive(Clone)]
66pub struct MoshData {
67    /// Buffer.
68    pub buf: Vec<u8>,
69    /// Original image.
70    pub image: Vec<u8>,
71    /// Width.
72    pub width: u32,
73    /// Height.
74    pub height: u32,
75    /// Color type.
76    pub color_type: ColorType,
77    /// Bit depth.
78    pub bit_depth: BitDepth,
79    /// Line size.
80    pub line_size: usize,
81}
82
83/// Processing options.
84///
85/// Minimal `pixelation` value is `1` (OFF).
86#[derive(Clone, Debug)]
87pub struct MoshOptions {
88    /// Minimal amount of chunks to process.
89    pub min_rate: u16,
90    /// Maximal amount of chunks to process.
91    pub max_rate: u16,
92    /// Pixelation's intensity.
93    pub pixelation: u8,
94    /// Chance of line shift.
95    pub line_shift: f64,
96    /// Chance of reverse.
97    pub reverse: f64,
98    /// Chance of flip.
99    pub flip: f64,
100    /// Chance of channel swap.
101    pub channel_swap: f64,
102    /// Chance of channel shift.
103    pub channel_shift: f64,
104    /// Random seed.
105    pub seed: u64,
106}
107
108/// Core container.
109///
110/// Holds image data and processing options.
111#[derive(Clone, Default)]
112pub struct MoshCore {
113    pub data: MoshData,
114    pub options: MoshOptions,
115}
116
117impl MoshCore {
118    /// Creates a new, empty instance of [`MoshCore`] with a random [seed].
119    ///
120    /// [seed]: MoshOptions::seed
121    pub fn new() -> Self {
122        Self {
123            data: MoshData::default(),
124            options: MoshOptions::default(),
125        }
126    }
127
128    /// Reads provided image for future processing.
129    ///
130    /// # Errors
131    ///
132    /// It may fail if an image is not a valid PNG file.
133    pub fn read_image(&mut self, input: &[u8]) -> Result<(), MoshError> {
134        let decoder = Decoder::new(input);
135        let mut reader = decoder.read_info()?;
136        let mut buf = vec![0_u8; reader.output_buffer_size()];
137        let info = reader.next_frame(&mut buf)?;
138
139        self.data.buf.clone_from(&buf);
140        self.data.image = buf;
141        self.data.width = info.width;
142        self.data.height = info.height;
143        self.data.color_type = info.color_type;
144        self.data.bit_depth = info.bit_depth;
145        self.data.line_size = info.line_size;
146
147        Ok(())
148    }
149
150    /**
151    Processes an image with current [settings], storing the result in a [buffer].
152
153    [buffer]: MoshData::buf
154    [settings]: MoshOptions
155
156    # Errors
157
158    * [`UnsupportedColorType`]: [`Indexed`] is not supported.
159
160    [`Indexed`]: ColorType::Indexed
161
162    # Example
163    ```rust
164    use libmosh::{
165        err::MoshError,
166        ops::{read_file, write_file},
167        MoshCore,
168    };
169
170    let input = read_file("src/util/test-rgb.png")?;
171    let output = "test.png";
172    let mut image = MoshCore::new();
173
174    image.options.min_rate = 5;
175    image.options.max_rate = 7;
176    image.options.pixelation = 10;
177    image.options.line_shift = 0.7;
178    image.options.reverse = 0.4;
179    image.options.flip = 0.3;
180    image.options.channel_swap = 0.5;
181    image.options.channel_shift = 0.5;
182    image.options.seed = 42;
183
184    image.read_image(&input)?;
185    image.mosh()?;
186    write_file(
187        output,
188        &image.data.buf,
189        image.data.width,
190        image.data.height,
191        image.data.color_type,
192        image.data.bit_depth,
193    )?;
194    # Ok::<(), MoshError>(())
195    ```
196
197    [`UnsupportedColorType`]: crate::err::MoshError::UnsupportedColorType
198    */
199    pub fn mosh(&mut self) -> Result<(), MoshError> {
200        self.data.mosh(&self.options)?;
201
202        Ok(())
203    }
204}
205
206impl MoshOptions {
207    fn generate_seed() -> u64 {
208        if cfg!(test) {
209            TEST_SEED
210        } else {
211            rand::thread_rng().next_u64()
212        }
213    }
214
215    /// Generates a new random seed.
216    pub fn new_seed(&mut self) {
217        self.seed = Self::generate_seed();
218    }
219}
220
221impl MoshData {
222    #[deprecated(since = "3.1.0", note = "Users should use MoshCore instead")]
223    pub fn new(input: &[u8]) -> Result<Self, MoshError> {
224        let decoder = Decoder::new(input);
225        let mut reader = decoder.read_info()?;
226        let mut buf = vec![0_u8; reader.output_buffer_size()];
227        let info = reader.next_frame(&mut buf)?;
228
229        Ok(Self {
230            buf: vec![0_u8],
231            image: buf,
232            width: info.width,
233            height: info.height,
234            color_type: info.color_type,
235            bit_depth: info.bit_depth,
236            line_size: info.line_size,
237        })
238    }
239
240    #[deprecated(since = "3.1.0")]
241    pub fn mosh(&mut self, options: &MoshOptions) -> Result<(), MoshError> {
242        self.buf.clone_from(&self.image);
243
244        let min_rate = options.min_rate;
245        let max_rate = cmp::max(options.min_rate, options.max_rate);
246        let mut rng = rand_chacha::ChaCha8Rng::seed_from_u64(options.seed);
247        let chunk_count_distrib = Uniform::from(min_rate..=max_rate);
248        let mosh_rate = chunk_count_distrib.sample(&mut rng);
249
250        for _ in 0..mosh_rate {
251            Self::chunkmosh(self, &mut rng, options);
252        }
253
254        match self.color_type {
255            ColorType::Indexed => {
256                return Err(MoshError::UnsupportedColorType);
257            }
258            ColorType::Grayscale => {
259                Self::pixelation(self, options, fr::PixelType::U8);
260            }
261            ColorType::GrayscaleAlpha => {
262                Self::pixelation(self, options, fr::PixelType::U8x2);
263            }
264            ColorType::Rgb => {
265                Self::pixelation(self, options, fr::PixelType::U8x3);
266            }
267            ColorType::Rgba => {
268                Self::pixelation(self, options, fr::PixelType::U8x4);
269            }
270        }
271
272        Ok(())
273    }
274
275    fn pixelation(&mut self, options: &MoshOptions, pixel_type: fr::PixelType) {
276        if options.pixelation > 1 {
277            let width = self.width;
278            let height = self.height;
279            let src_image =
280                fr::images::Image::from_vec_u8(width, height, self.buf.clone(), pixel_type)
281                    .unwrap();
282
283            let dest_width = self.width / u32::from(options.pixelation);
284            let dest_height = self.height / u32::from(options.pixelation);
285            let orig_width = self.width;
286            let orig_height = self.height;
287
288            let mut dest_image =
289                fr::images::Image::new(dest_width, dest_height, src_image.pixel_type());
290            let mut orig_image =
291                fr::images::Image::new(orig_width, orig_height, src_image.pixel_type());
292            let mut resizer = fr::Resizer::new();
293
294            resizer
295                .resize(
296                    &src_image,
297                    &mut dest_image,
298                    &fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
299                )
300                .unwrap();
301            resizer
302                .resize(
303                    &dest_image,
304                    &mut orig_image,
305                    &fr::ResizeOptions::new().resize_alg(fr::ResizeAlg::Nearest),
306                )
307                .unwrap();
308
309            self.buf = orig_image.into_vec();
310        }
311    }
312
313    // Use pnglitch approach
314    //
315    // TODO
316    // Add more `rng` to `chunk_size`?
317    fn chunkmosh(&mut self, rng: &mut impl rand::Rng, options: &MoshOptions) {
318        let line_count = self.buf.len() / self.line_size;
319        let channel_count = match self.color_type {
320            ColorType::Grayscale | ColorType::Indexed => 1,
321            ColorType::GrayscaleAlpha => 2,
322            ColorType::Rgb => 3,
323            ColorType::Rgba => 4,
324        };
325
326        let line_shift_distrib = Uniform::from(0..self.line_size);
327        let line_number_distrib = Uniform::from(0..line_count);
328        let channel_count_distrib = Uniform::from(0..channel_count);
329
330        let first_line = line_number_distrib.sample(rng);
331        let chunk_size = line_number_distrib.sample(rng) / 2;
332        let last_line = if (first_line + chunk_size) > line_count {
333            line_count
334        } else {
335            first_line + chunk_size
336        };
337
338        let reverse = rng.gen_bool(options.reverse);
339        let flip = rng.gen_bool(options.flip);
340
341        let line_shift = rng.gen_bool(options.line_shift).then(|| {
342            let line_shift_amount = line_shift_distrib.sample(rng);
343            MoshLine::Shift(line_shift_amount)
344        });
345
346        let channel_shift = rng.gen_bool(options.channel_shift).then(|| {
347            let amount = line_shift_distrib.sample(rng) / channel_count;
348            let channel = channel_count_distrib.sample(rng);
349            MoshLine::ChannelShift(amount, channel, channel_count)
350        });
351
352        let channel_swap = rng.gen_bool(options.channel_swap).then(|| {
353            let channel_1 = channel_count_distrib.sample(rng);
354            let channel_2 = channel_count_distrib.sample(rng);
355            MoshChunk::ChannelSwap(channel_1, channel_2, channel_count)
356        });
357
358        for line_number in first_line..last_line {
359            let line_start = line_number * self.line_size;
360            let line_end = line_start + self.line_size;
361            let line = &mut self.buf[line_start..line_end];
362
363            if let Some(do_channel_shift) = &channel_shift {
364                do_channel_shift.glitch(line);
365            }
366
367            if let Some(do_line_shift) = &line_shift {
368                do_line_shift.glitch(line);
369            }
370            if reverse {
371                MoshLine::Reverse.glitch(line);
372            }
373        }
374
375        let chunk_start = first_line * self.line_size;
376        let chunk_end = last_line * self.line_size;
377        let chunk = &mut self.buf[chunk_start..chunk_end];
378
379        if let Some(do_channel_swap) = channel_swap {
380            do_channel_swap.glitch(chunk);
381        };
382
383        if flip {
384            MoshChunk::Flip.glitch(chunk);
385        };
386    }
387}
388
389impl Default for MoshData {
390    fn default() -> Self {
391        Self {
392            buf: vec![0_u8],
393            image: vec![0_u8],
394            width: 1,
395            height: 1,
396            color_type: ColorType::Rgba,
397            bit_depth: BitDepth::Eight,
398            line_size: 1,
399        }
400    }
401}
402
403impl Default for MoshOptions {
404    fn default() -> Self {
405        Self {
406            min_rate: 1,
407            max_rate: 7,
408            pixelation: 10,
409            line_shift: 0.3,
410            reverse: 0.3,
411            flip: 0.3,
412            channel_swap: 0.3,
413            channel_shift: 0.3,
414            seed: Self::generate_seed(),
415        }
416    }
417}
418
419const TEST_SEED: u64 = 901_042_006;
420
421#[cfg(test)]
422mod util;