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;
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(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 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 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 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 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 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;