1use 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), (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 #[must_use]
144 pub fn new() -> Self {
145 Self {
146 data: MoshData::default(),
147 options: MoshOptions::default(),
148 }
149 }
150
151 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 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 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 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 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 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;