1use 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), (205, 0, 0), (0, 205, 0), (205, 205, 0), (0, 0, 205), (205, 0, 205), (0, 205, 205), (229, 229, 229), (127, 127, 127), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 0, 255), (255, 0, 255), (0, 255, 255), (255, 255, 255), ];
75
76#[non_exhaustive]
80#[derive(Clone)]
81pub struct MoshData {
82 pub buf: Vec<u8>,
84 pub image: Vec<u8>,
86 pub width: u32,
88 pub height: u32,
90 pub color_type: ColorType,
92 pub bit_depth: BitDepth,
94 pub palette: Option<Vec<u8>>,
96 pub line_size: usize,
98}
99
100#[non_exhaustive]
104#[derive(Clone, Debug)]
105pub struct MoshOptions {
106 pub min_rate: u16,
108 pub max_rate: u16,
110 pub pixelation: u8,
112 pub line_shift: f64,
114 pub reverse: f64,
116 pub flip: f64,
118 pub channel_swap: f64,
120 pub channel_shift: f64,
122 pub ansi: bool,
124 pub seed: u64,
126}
127
128#[non_exhaustive]
132#[derive(Clone, Default)]
133pub struct MoshCore {
134 pub data: MoshData,
135 pub options: MoshOptions,
136}
137
138impl MoshCore {
139 #[must_use]
143 pub fn new() -> Self {
144 Self {
145 data: MoshData::default(),
146 options: MoshOptions::default(),
147 }
148 }
149
150 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 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 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 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 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 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;