1use fast_image_resize as fr;
40
41use png::{BitDepth, ColorType, Decoder};
42use rand::{
43 RngCore, SeedableRng,
44 distr::{Distribution, Uniform},
45};
46
47use std::cmp;
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), (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), ];
76
77#[non_exhaustive]
81#[derive(Clone)]
82pub struct MoshData {
83 pub buf: Vec<u8>,
85 pub image: Vec<u8>,
87 pub width: u32,
89 pub height: u32,
91 pub color_type: ColorType,
93 pub bit_depth: BitDepth,
95 pub palette: Option<Vec<u8>>,
97 pub line_size: usize,
99}
100
101#[non_exhaustive]
105#[derive(Clone, Debug)]
106pub struct MoshOptions {
107 pub min_rate: u16,
109 pub max_rate: u16,
111 pub pixelation: u8,
113 pub line_shift: f64,
115 pub reverse: f64,
117 pub flip: f64,
119 pub channel_swap: f64,
121 pub channel_shift: f64,
123 pub ansi: bool,
125 pub seed: u64,
127}
128
129#[non_exhaustive]
133#[derive(Clone, Default)]
134pub struct MoshCore {
135 pub data: MoshData,
136 pub options: MoshOptions,
137}
138
139impl MoshCore {
140 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> {
331 let mut ansi_data: Vec<u8> = Vec::new();
332 for y in 0..self.height {
333 for x in 0..self.width {
334 let idx = (y * self.width + x) as usize
335 * match self.color_type {
336 ColorType::Grayscale | ColorType::Indexed => 1,
337 ColorType::GrayscaleAlpha => 2,
338 ColorType::Rgb => 3,
339 ColorType::Rgba => 4,
340 };
341
342 let r = match self.color_type {
343 ColorType::Indexed => {
344 let palette_idx = self.buf[idx] as usize;
345 let (r, _, _) = self.get_palette_color(palette_idx)?;
346 r
347 }
348 _ => self.buf[idx],
349 };
350
351 let g = match self.color_type {
352 ColorType::Rgb | ColorType::Rgba => self.buf[idx + 1],
353 ColorType::Indexed => {
354 let palette_idx = self.buf[idx] as usize;
355 let (_, g, _) = self.get_palette_color(palette_idx)?;
356 g
357 }
358 _ => self.buf[idx],
359 };
360
361 let b = match self.color_type {
362 ColorType::Rgb | ColorType::Rgba => self.buf[idx + 2],
363 ColorType::Indexed => {
364 let palette_idx = self.buf[idx] as usize;
365 let (_, _, b) = self.get_palette_color(palette_idx)?;
366 b
367 }
368 _ => self.buf[idx],
369 };
370
371 let ansi_color = get_ansi_color(r, g, b)?;
372 ansi_data.push(ansi_color);
373 }
374 }
375
376 self.buf = ansi_data;
377
378 Ok(())
379 }
380
381 fn chunkmosh(
386 &mut self,
387 rng: &mut impl rand::Rng,
388 options: &MoshOptions,
389 ) -> Result<(), MoshError> {
390 let line_count = self.buf.len() / self.line_size;
391 let channel_count = match self.color_type {
392 ColorType::Grayscale | ColorType::Indexed => 1,
393 ColorType::GrayscaleAlpha => 2,
394 ColorType::Rgb => 3,
395 ColorType::Rgba => 4,
396 };
397
398 let line_shift_distrib = Uniform::new(0, self.line_size)?;
399 let line_number_distrib = Uniform::new(0, line_count)?;
400 let channel_count_distrib = Uniform::new(0, channel_count)?;
401
402 let first_line = line_number_distrib.sample(rng);
403 let chunk_size = line_number_distrib.sample(rng) / 2;
404 let last_line = if (first_line + chunk_size) > line_count {
405 line_count
406 } else {
407 first_line + chunk_size
408 };
409
410 let reverse = rng.random_bool(options.reverse);
411 let flip = rng.random_bool(options.flip);
412
413 let line_shift = rng.random_bool(options.line_shift).then(|| {
414 let line_shift_amount = line_shift_distrib.sample(rng);
415 MoshLine::Shift(line_shift_amount)
416 });
417
418 let channel_shift = rng.random_bool(options.channel_shift).then(|| {
419 let amount = line_shift_distrib.sample(rng) / channel_count;
420 let channel = channel_count_distrib.sample(rng);
421 MoshLine::ChannelShift(amount, channel, channel_count)
422 });
423
424 let channel_swap = rng.random_bool(options.channel_swap).then(|| {
425 let channel_1 = channel_count_distrib.sample(rng);
426 let channel_2 = channel_count_distrib.sample(rng);
427 MoshChunk::ChannelSwap(channel_1, channel_2, channel_count)
428 });
429
430 for line_number in first_line..last_line {
431 let line_start = line_number * self.line_size;
432 let line_end = line_start + self.line_size;
433 let line = &mut self.buf[line_start..line_end];
434
435 if let Some(do_channel_shift) = &channel_shift {
436 do_channel_shift.glitch(line);
437 }
438
439 if let Some(do_line_shift) = &line_shift {
440 do_line_shift.glitch(line);
441 }
442 if reverse {
443 MoshLine::Reverse.glitch(line);
444 }
445 }
446
447 let chunk_start = first_line * self.line_size;
448 let chunk_end = last_line * self.line_size;
449 let chunk = &mut self.buf[chunk_start..chunk_end];
450
451 if let Some(do_channel_swap) = channel_swap {
452 do_channel_swap.glitch(chunk);
453 };
454
455 if flip {
456 MoshChunk::Flip.glitch(chunk);
457 };
458
459 Ok(())
460 }
461}
462
463impl Default for MoshData {
464 fn default() -> Self {
465 Self {
466 buf: vec![0_u8],
467 image: vec![0_u8],
468 width: 1,
469 height: 1,
470 color_type: ColorType::Rgba,
471 bit_depth: BitDepth::Eight,
472 palette: None,
473 line_size: 1,
474 }
475 }
476}
477
478impl Default for MoshOptions {
479 fn default() -> Self {
480 Self {
481 min_rate: 1,
482 max_rate: 7,
483 pixelation: 10,
484 line_shift: 0.3,
485 reverse: 0.3,
486 flip: 0.3,
487 channel_swap: 0.3,
488 channel_shift: 0.3,
489 ansi: false,
490 seed: Self::generate_seed(),
491 }
492 }
493}
494
495fn get_ansi_color(r: u8, g: u8, b: u8) -> Result<u8, MoshError> {
496 let mut closest_index = 0;
497 let mut min_distance: i32 = i32::MAX;
498
499 for (index, &color) in ANSI_COLORS.iter().enumerate() {
500 let distance = (i32::from(r) - i32::from(color.0)).pow(2)
502 + (i32::from(g) - i32::from(color.1)).pow(2)
503 + (i32::from(b) - i32::from(color.2)).pow(2);
504
505 if distance < min_distance {
506 min_distance = distance;
507 closest_index = index;
508 }
509 }
510
511 let color = u8::try_from(closest_index)?;
512 Ok(color)
513}
514
515pub fn generate_palette() -> Vec<u8> {
516 let mut palette = Vec::with_capacity(ANSI_COLORS.len() * 3);
517 for &(r, g, b) in &ANSI_COLORS {
518 palette.push(r);
519 palette.push(g);
520 palette.push(b);
521 }
522
523 palette
524}
525
526const TEST_SEED: u64 = 901_042_006;
527
528#[cfg(test)]
529mod tests;