cpclib_image/
image.rs

1#![allow(clippy::needless_range_loop)]
2
3use std::collections::HashSet;
4
5use anyhow::Context;
6use cpclib_common::camino::Utf8Path;
7use cpclib_common::itertools::Itertools;
8#[cfg(all(not(target_arch = "wasm32"), feature = "rayon"))]
9use cpclib_common::rayon::iter::{IntoParallelRefIterator, ParallelIterator};
10use {anyhow, image as im};
11
12use crate::ga::*;
13use crate::pixels;
14use crate::pixels::bytes_to_pens;
15
16/// Screen mode
17#[derive(Clone, Copy, PartialEq, Debug)]
18pub enum Mode {
19    /// Mode 0 - 16 colors
20    Zero,
21    /// Mode 1 - 4 colors
22    One,
23    /// Mode 2 - 2 colors
24    Two,
25    /// Mode 3 - 4 colors / same resolution than Mode 0
26    Three
27}
28
29impl From<u8> for Mode {
30    fn from(val: u8) -> Self {
31        match val {
32            0 => Mode::Zero,
33            1 => Mode::One,
34            2 => Mode::Two,
35            3 => Mode::Three,
36            _ => panic!("{val} is not a valid mode.")
37        }
38    }
39}
40
41#[allow(missing_docs)]
42impl Mode {
43    /// Return the maximum number of colors for the current mode (without using rasters)
44    pub fn max_colors(&self) -> usize {
45        match self {
46            Mode::Zero => 16,
47            Mode::One | Mode::Three => 4,
48            Mode::Two => 2
49        }
50    }
51
52    /// Return the number of pixels encode by one byte in the given mode
53    pub fn nb_pixels_per_byte(&self) -> usize {
54        match self {
55            Mode::Zero | Mode::Three => 2,
56            Mode::One => 4,
57            Mode::Two => 8
58        }
59    }
60
61    pub fn nb_pixels_for_bytes_width(&self, width: usize) -> usize {
62        width * self.nb_pixels_per_byte()
63    }
64
65    pub fn nb_bytes_for_pixels_width(self, width: usize) -> usize {
66        let extra = if !width.is_multiple_of(self.nb_pixels_per_byte()) {
67            1
68        }
69        else {
70            0
71        };
72        width / self.nb_pixels_per_byte() + extra
73    }
74}
75
76/// Conversion rules
77#[derive(Copy, Clone, Debug)]
78pub enum ConversionRule {
79    /// All pixels are used
80    AnyModeUseAllPixels,
81    /// One pixel out of two is skiped (used for mode0 pictures where the graphician has doubled each pixel)
82    ZeroSkipOddPixels
83}
84
85/// Browse the image and returns the list of colors
86#[allow(unused)]
87fn get_unique_colors(img: &im::ImageBuffer<im::Rgb<u8>, Vec<u8>>) -> HashSet<im::Rgb<u8>> {
88    let mut set = HashSet::new();
89    for pixel in img.pixels() {
90        set.insert(*pixel);
91    }
92    set
93}
94
95/// Browse the image and returns the palette to use
96#[allow(unused)]
97fn extract_palette(img: &im::ImageBuffer<im::Rgb<u8>, Vec<u8>>) -> Palette {
98    let colors = get_unique_colors(img);
99    let mut p = Palette::empty();
100
101    assert!(colors.len() <= 16);
102
103    for (idx, color) in colors.iter().enumerate() {
104        let color = *color;
105        p.set(Pen::from(idx as u8), Ink::from(color))
106    }
107
108    p
109}
110
111/// Encode the raw array of Pens in an array of CPC bytes encoded for the right screen mode
112fn encode(pens: &[Vec<Pen>], mode: Mode, missing_pen: Option<Pen>) -> Vec<Vec<u8>> {
113    let mut rows = Vec::new();
114    for input_row in pens.iter() {
115        let row = {
116            if let Some(replacement) = missing_pen {
117                match mode {
118                    Mode::Zero => {
119                        pixels::mode0::pens_to_vec_with_replacement(input_row, replacement)
120                    },
121                    Mode::One => {
122                        pixels::mode1::pens_to_vec_with_replacement(input_row, replacement)
123                    },
124                    Mode::Two => {
125                        pixels::mode2::pens_to_vec_with_replacement(input_row, replacement)
126                    },
127                    _ => panic!("Unimplemented yet ...")
128                }
129            }
130            else {
131                match mode {
132                    Mode::Zero => pixels::mode0::pens_to_vec_with_crop(input_row),
133                    Mode::One => pixels::mode1::pens_to_vec_with_crop(input_row),
134                    Mode::Two => pixels::mode2::pens_to_vec_with_crop(input_row),
135                    _ => panic!("Unimplemented yet ...")
136                }
137            }
138        };
139        rows.push(row);
140    }
141
142    rows
143}
144
145/// Build a new screen line that reprents line1 in mode 0 and line2 in mode3
146fn merge_mode0_mode3(line1: &[u8], line2: &[u8]) -> Vec<u8> {
147    assert_eq!(line1.len(), line2.len());
148
149    line1
150        .iter()
151        .zip(line2.iter())
152        .map(|(&u1, &u2)| {
153            let [p10, p11] = pixels::mode0::byte_to_pens(u1);
154            let [p20, p21] = pixels::mode0::byte_to_pens(u2);
155
156            let p0 = pixels::mode0::mix_mode0_mode3(p10, p20);
157            let p1 = pixels::mode0::mix_mode0_mode3(p11, p21);
158
159            eprintln!("{}/{} {:?} + {:?} = {:?}", u1, u2, &p10, &p20, &p0);
160            eprintln!("{}/{} {:?} + {:?} = {:?}", u1, u2, &p11, &p21, &p1);
161
162            pixels::mode0::pens_to_byte(p0, p1)
163        })
164        .collect::<Vec<u8>>()
165}
166
167// Convert inks to pens
168fn inks_to_pens(inks: &[Vec<Ink>], p: &Palette) -> Vec<Vec<Pen>> {
169    #[cfg(all(not(target_arch = "wasm32"), feature = "rayon"))]
170    let iter = inks.par_iter();
171    #[cfg(any(target_arch = "wasm32", not(feature = "rayon")))]
172    let iter = inks.iter();
173
174    iter.map(|line| {
175        line.iter()
176            .map(|ink| {
177                p.get_pen_for_ink(*ink).unwrap_or_else(|| {
178                    panic!("Unable to find a correspondance for ink {ink:?} in given palette {p:?}")
179                })
180            })
181            .collect::<Vec<Pen>>()
182    })
183    .collect::<Vec<_>>()
184}
185
186/// A ColorMatrix represents an image through a list of Inks.
187/// It has no real meaning in CPC world but can be used for image transformaton
188/// There is no mode information
189#[derive(Clone, Debug, PartialEq, Eq, Hash)]
190pub struct ColorMatrix {
191    /// List of inks
192    data: Vec<Vec<Ink>>
193}
194
195impl From<Vec<Vec<Ink>>> for ColorMatrix {
196    fn from(data: Vec<Vec<Ink>>) -> Self {
197        ColorMatrix { data }
198    }
199}
200
201/// We have to choose a strategy when reducing the number of colors of an image.
202/// This enumeration allows to set up them
203#[derive(Debug, Copy, Clone)]
204pub enum ColorConversionStrategy {
205    /// Impossible colors are replace by the first possible ink
206    ReplaceWrongColorByFirstColor,
207    /// The color is replaced by the closest one
208    ReplaceWrongColorByClosestInk,
209    /// An error is generated
210    Fail
211}
212
213impl ColorMatrix {
214    pub const INK_MASK_BACKGROUND: Ink = Ink::BRIGHTWHITE;
215    pub const INK_MASK_FOREGROUND: Ink = Ink::BLACK;
216    pub const INK_NOT_USED_IN_MASK: Ink = Ink::RED;
217
218    pub fn from_screen(data: &[u8], bytes_width: usize, mode: Mode, palette: &Palette) -> Self {
219        let pixel_height = {
220            let mut height = 0x4000 / bytes_width;
221            while !height.is_multiple_of(8) {
222                height -= 1;
223            }
224            height
225        };
226
227        let _pixel_width = mode.nb_pixels_for_bytes_width(bytes_width);
228
229        (0..pixel_height)
230            .map(|line| {
231                let screen_address = 0xC000 + ((line / 8) * bytes_width) + ((line % 8) * 0x800);
232                let data_address = screen_address - 0xC000;
233                let line_bytes = &data[data_address..(data_address + bytes_width)];
234                line_bytes
235                    .iter()
236                    .flat_map(|b| pixels::byte_to_pens(*b, mode))
237                    .collect_vec()
238            })
239            .map(move |pens| {
240                // build lines of inks
241                pens.iter()
242                    .map(|pen| palette.get(pen))
243                    .cloned()
244                    .collect_vec()
245            })
246            .collect_vec()
247            .into()
248    }
249
250    pub fn from_sprite(data: &[u8], pixels_width: u16, mode: Mode, palette: &Palette) -> Self {
251        let width = mode.nb_bytes_for_pixels_width(pixels_width as _);
252
253        // convert it
254        data.chunks_exact(width)
255            .map(|line| {
256                // build lines of pen
257                let line = line.iter();
258                line.flat_map(|b| pixels::byte_to_pens(*b, mode))
259                    .collect_vec()
260            })
261            .map(move |pens| {
262                // build lines of inks
263                pens.iter()
264                    .map(|pen| palette.get(pen))
265                    .cloned()
266                    .collect_vec()
267            })
268            .collect_vec()
269            .into()
270    }
271}
272
273#[allow(missing_docs)]
274impl ColorMatrix {
275    /// Create a new empty color matrix for the given dimensions
276    pub fn new(width: usize, height: usize) -> Self {
277        Self {
278            data: vec![vec![Ink::from(0); width]; height]
279        }
280    }
281
282    /// The matrix represents both the mask (with an unexpected color), and the sprite (<ith the expected color).
283    /// This method returns two matrices:
284    /// - The mask where bright white stands for pixels of the sprite and black stands for the pixels of the background
285    /// - The sprite where the background is replaced by a selected ink (Ideally the one that will be considered as being pen 0)
286    pub fn extract_mask_and_sprite(
287        &self,
288        mask_ink: impl Into<Ink>,
289        replacement_ink: impl Into<Ink>
290    ) -> (Self, Self) {
291        let mask_ink = mask_ink.into();
292        let replacement_ink = replacement_ink.into();
293
294        let mut mask_data = self.clone();
295        mask_data.convert_to_mask(mask_ink);
296
297        let mut sprite_data = self.clone();
298        sprite_data.replace_ink(mask_ink, replacement_ink);
299
300        (mask_data, sprite_data)
301    }
302
303    /// Destroy the image to build the mask according to the background ink
304    pub fn convert_to_mask(&mut self, mask: Ink) -> &mut Self {
305        self.data.iter_mut().for_each(|row| {
306            row.iter_mut().for_each(|ink| {
307                *ink = if *ink == mask {
308                    Self::INK_MASK_BACKGROUND
309                }
310                else {
311                    Self::INK_MASK_FOREGROUND
312                }
313            })
314        });
315        self
316    }
317
318    /// Exchange all the occurrences of `from` Ink with `to` ink
319    pub fn replace_ink(&mut self, from: Ink, to: Ink) -> &mut Self {
320        self.data.iter_mut().for_each(|row| {
321            row.iter_mut().for_each(|ink| {
322                if *ink == from {
323                    *ink = to;
324                }
325            })
326        });
327        self
328    }
329
330    pub fn empty() -> Self {
331        Self { data: Vec::new() }
332    }
333
334    /// Create a new ColorMatrix that encodes a new image full of black
335    pub fn empty_like(&self) -> Self {
336        Self {
337            data: vec![vec![Ink::from(0); self.width() as usize]; self.height() as usize]
338        }
339    }
340
341    /// Double the width (usefull for chuncky conversions)
342    #[allow(clippy::needless_range_loop, clippy::identity_op)]
343    pub fn double_horizontally(&mut self) {
344        // Create the doubled pixels
345        let mut new_data =
346            vec![vec![Ink::from(0); (2 * self.width()) as usize]; self.height() as usize];
347        for x in 0..(self.width() as usize) {
348            for y in 0..(self.height() as usize) {
349                let color = self.get_ink(x, y);
350                new_data[y][x * 2 + 0] = *color;
351                new_data[y][x * 2 + 1] = *color;
352            }
353        }
354
355        // Set them in the right position
356        std::mem::swap(&mut self.data, &mut new_data)
357    }
358
359    pub fn remove_odd_columns(&mut self) {
360        // Create the doubled pixels
361        let mut new_data =
362            vec![vec![Ink::from(0); (self.width() / 2) as usize]; self.height() as usize];
363        for x in 0..((self.width() / 2) as usize) {
364            for y in 0..(self.height() as usize) {
365                let color = self.get_ink(x * 2, y);
366                new_data[y][x] = *color;
367            }
368        }
369
370        // Set them in the right position
371        std::mem::swap(&mut self.data, &mut new_data)
372    }
373
374    /// Get the height (in pixels) of the image
375    /// TODO Use a trait for that
376    pub fn height(&self) -> u32 {
377        self.data.len() as u32
378    }
379
380    /// Returns the ink at the right position
381    pub fn get_ink(&self, x: usize, y: usize) -> &Ink {
382        &self.data[y][x]
383    }
384
385    /// Set ink
386    pub fn set_ink(&mut self, x: usize, y: usize, ink: Ink) {
387        self.data[y][x] = ink;
388    }
389
390    /// Add a line within the image
391    /// Panic if impossible
392    pub fn add_line(&mut self, position: usize, line: &[Ink]) {
393        assert_eq!(line.len(), self.width() as usize);
394        self.data.insert(position, line.to_vec());
395    }
396
397    /// Returns a reference on the wanted line of inks
398    pub fn get_line(&self, y: usize) -> &[Ink] {
399        &self.data[y]
400    }
401
402    /// Return a mutable version of the line. Care needs to be taken in order to not destroy the data structure
403    fn get_line_mut(&mut self, y: usize) -> &mut Vec<Ink> {
404        &mut self.data[y]
405    }
406
407    /// Add a column within the image
408    /// Panic if impossible
409    pub fn add_column(&mut self, position: usize, column: &[Ink]) {
410        assert_eq!(column.len(), self.height() as usize);
411        for (row, ink) in column.iter().enumerate() {
412            self.get_line_mut(row).insert(position, *ink);
413        }
414    }
415
416    /// Build a vector of Inks that contains all the inks of the given column
417    pub fn get_column(&self, x: usize) -> Vec<Ink> {
418        self.data.iter().map(|line| line[x]).collect::<Vec<Ink>>()
419    }
420
421    /// Return a copy of the inks for the given window definition
422    pub fn window(&self, start_x: usize, start_y: usize, width: usize, height: usize) -> Self {
423        let selected_lines = &self.data[start_y..start_y + height];
424        let window = selected_lines
425            .iter()
426            .map(|line| &line[start_x..start_x + width])
427            .map(|line| {
428                let mut new_line = Vec::with_capacity(line.len());
429                new_line.extend_from_slice(line);
430                new_line
431            })
432            .collect();
433        Self { data: window }
434    }
435
436    /// Return the number of different inks in the image
437    pub fn nb_inks(&self) -> usize {
438        self.data.iter().flatten().unique().count()
439    }
440
441    /// Returns the palette used (as soon as there is less than the maximum number of inks fr the requested mode)
442    pub fn extract_palette(&self, mode: Mode) -> Palette {
443        let mut p = Palette::empty();
444        for (idx, color) in self.data.iter().flatten().unique().sorted().enumerate() {
445            if idx >= mode.max_colors() {
446                // do we really want to fail ? maybe we can have special modes to handle there
447                panic!(
448                    "[ERROR] your picture uses more than 16 different colors. Palette: {p:?}. Wrong ink: {color:?}"
449                );
450            }
451            p.set(Pen::from(idx as u8), *color);
452        }
453        p
454    }
455
456    /// Modify the image in order to keep the right amount of inks
457    pub fn reduce_colors_for_mode(
458        &mut self,
459        mode: Mode,
460        strategy: ColorConversionStrategy
461    ) -> Result<(), anyhow::Error> {
462        // Get the reduced palette
463        let inks = self
464            .data
465            .iter()
466            .flatten()
467            .unique()
468            .copied()
469            .collect::<Vec<Ink>>();
470        let max_count = mode.max_colors().min(inks.len());
471        let inks = &inks[..max_count];
472
473        self.reduce_colors_with(inks, strategy)
474    }
475
476    /// Modify the image in order to use only the provided palette
477    pub fn reduce_colors_with(
478        &mut self,
479        inks: &[Ink],
480        strategy: ColorConversionStrategy
481    ) -> Result<(), anyhow::Error> {
482        for y in 0..(self.height() as usize) {
483            for x in 0..(self.width() as usize) {
484                let ink = &mut self.data[y][x];
485                if !inks.contains(ink) {
486                    match strategy {
487                        ColorConversionStrategy::ReplaceWrongColorByFirstColor => {
488                            *ink = inks[0];
489                        },
490                        ColorConversionStrategy::ReplaceWrongColorByClosestInk => unimplemented!(),
491                        ColorConversionStrategy::Fail => {
492                            return Err(anyhow::anyhow!(
493                                "{:?} not available in {:?} at [{}, {}]",
494                                ink,
495                                inks,
496                                x,
497                                y
498                            ));
499                        }
500                    }
501                }
502            }
503        }
504
505        Ok(())
506    }
507
508    /// Get the width (in bytes) of the image
509    /// TODO Use a trait for that
510    pub fn width(&self) -> u32 {
511        match self.height() {
512            0 => 0,
513            _ => self.data[0].len() as u32
514        }
515    }
516
517    pub fn convert_from_fname(fname: &str, conversion: ConversionRule) -> anyhow::Result<Self> {
518        let img = im::open(fname).with_context(|| format!("{fname} does not exists."))?;
519        Ok(Self::convert(&img.to_rgb8(), conversion))
520    }
521
522    pub fn convert(
523        img: &im::ImageBuffer<im::Rgb<u8>, Vec<u8>>,
524        conversion: ConversionRule
525    ) -> Self {
526        // Get destination image size
527        let height = img.height();
528        let width = {
529            match conversion {
530                ConversionRule::AnyModeUseAllPixels => img.width(),
531                ConversionRule::ZeroSkipOddPixels => img.width() / 2
532            }
533        };
534
535        // Make the pixels extraction
536        let mut lines = Vec::new();
537        lines.reserve(height as usize);
538        for y in 0..height {
539            let src_y = y;
540            let mut line = Vec::new();
541            line.reserve(width as usize);
542            for x in 0..width {
543                let src_x = {
544                    match conversion {
545                        ConversionRule::AnyModeUseAllPixels => x,
546                        ConversionRule::ZeroSkipOddPixels => x * 2
547                    }
548                };
549
550                let src_color = img.get_pixel(src_x, src_y);
551                let dest_ink = Ink::from(*src_color);
552
553                // Add the current ink to the current line
554                line.push(dest_ink);
555            }
556            // Add the current complete line to the current image
557            lines.push(line);
558        }
559
560        // And create the sprite structure
561        Self { data: lines }
562    }
563
564    /// Compute a difference map to see the problematic positions
565    pub fn diff(&self, other: &Self) -> Self {
566        // Create a map encoding a complete success
567        let mut data = vec![vec![Ink::from(26); other.width() as usize]; other.height() as usize];
568
569        // Set the error positions
570        for x in 0..(self.width() as usize) {
571            for y in 0..(self.height() as usize) {
572                if self.data[y][x] != other.data[y][x] {
573                    data[y][x] = Ink::from(0);
574                }
575            }
576        }
577
578        // Return the object
579        Self { data }
580    }
581
582    /// From a ColorMatrix computed with the diff method, returns the (x,y) coordinates having a difference
583    pub fn diff_to_positions(&self) -> Vec<(usize, usize)> {
584        let mut res = Vec::new();
585        for x in 0..(self.width() as usize) {
586            for y in 0..(self.height() as usize) {
587                if self.data[y][x] == Ink::from(0) {
588                    res.push((x, y));
589                }
590            }
591        }
592        res
593    }
594
595    /// Convert the buffer as an image
596    pub fn as_image(&self) -> im::ImageBuffer<im::Rgba<u8>, Vec<u8>> {
597        let mut buffer: im::ImageBuffer<im::Rgba<u8>, Vec<u8>> =
598            im::ImageBuffer::new(self.width(), self.height());
599
600        for x in 0..(self.width()) {
601            for y in 0..(self.height()) {
602                buffer.put_pixel(x, y, self.get_ink(x as usize, y as usize).color());
603            }
604        }
605
606        buffer
607    }
608
609    /// Convert the matrix as a sprite, given the right mode and an optional palette
610    pub fn as_sprite(
611        &self,
612        mode: Mode,
613        palette: Option<Palette>,
614        missing_pen: Option<Pen>
615    ) -> Sprite {
616        // Extract the palette is not provided as an argument
617        let palette = palette.unwrap_or_else(|| self.extract_palette(mode));
618
619        // Really make the conversion
620        let pens = inks_to_pens(&self.data, &palette);
621
622        // Build the sprite
623        Sprite {
624            mode: Some(mode),
625            palette: Some(palette),
626            data: encode(&pens, mode, missing_pen)
627        }
628    }
629
630    /// Convert the matrix as a sprite in mode1. Pen 1/2/3 are changed at each line. Pen 0 is constant
631    pub fn as_mode1_sprite_with_different_inks_per_line(
632        &self,
633        palette: &[(Ink, Ink, Ink, Ink)],
634        dummy_palette: &Palette,
635        missing_pen: Option<Pen>
636    ) -> Sprite {
637        // Build the matrix of pens
638        let mut data: Vec<Vec<Pen>> = Vec::new();
639        for y in 0..self.height() {
640            let y = y as usize;
641
642            // Build the palette for the current ink
643            let line_palette = {
644                let mut p = Palette::new(); // Palette full of 0
645                p.set(Pen::from(0), palette[y].0);
646                p.set(Pen::from(1), palette[y].1);
647                p.set(Pen::from(2), palette[y].2);
648                p.set(Pen::from(3), palette[y].3);
649                p
650            };
651
652            // get the pens for the current line
653            let pens = self
654                .get_line(y)
655                .iter()
656                .map(|ink| -> Pen {
657                    let pen = line_palette.get_pen_for_ink(*ink);
658                    if let Some(pen) = pen {
659                        pen
660                    }
661                    else {
662                        // eprintln!("
663                        // [ERROR] In line {}, pixel {} color ({:?}) is not in the palette {:?}. Background is used insted",
664                        // y,
665                        // x,
666                        // ink,
667                        // line_palette
668                        // );
669                        Pen::from(0)
670                    } // If the color is not in the palette, use pen 0
671                })
672                .collect::<Vec<Pen>>();
673
674            // Transform the pens in bytes
675            data.push(pens);
676        }
677
678        let encoded_pixels = encode(&data, Mode::One, missing_pen);
679
680        // Convert the matrix of pens as a sprite
681        Sprite {
682            mode: Some(Mode::One),
683            palette: Some(dummy_palette.clone()),
684            data: encoded_pixels
685        }
686    }
687
688    /// Generate an iterator on the pixels
689    pub fn inks(&self) -> Inks<'_> {
690        Inks {
691            image: self,
692            x: 0,
693            y: 0,
694            width: self.width(),
695            height: self.height()
696        }
697    }
698}
699
700/// Immutable ink iterator for generate (x, y, ink)
701#[derive(Debug)]
702pub struct Inks<'a> {
703    image: &'a ColorMatrix,
704    x: u32,
705    y: u32,
706    width: u32,
707    height: u32
708}
709
710impl Iterator for Inks<'_> {
711    type Item = (u32, u32, Ink);
712
713    fn next(&mut self) -> Option<(u32, u32, Ink)> {
714        if self.x >= self.width {
715            self.x = 0;
716            self.y += 1;
717        }
718
719        if self.y >= self.height {
720            None
721        }
722        else {
723            let ink = self.image.get_ink(self.x as _, self.y as _);
724            let i = (self.x, self.y, *ink);
725
726            self.x += 1;
727
728            Some(i)
729        }
730    }
731}
732
733/// Animation are stored in lists of ColorMatrices of same sze
734#[derive(Debug)]
735pub struct ColorMatrixList(Vec<ColorMatrix>);
736
737impl From<Vec<ColorMatrix>> for ColorMatrixList {
738    fn from(src: Vec<ColorMatrix>) -> Self {
739        ColorMatrixList(src)
740    }
741}
742
743impl From<&ColorMatrixList> for Vec<ColorMatrix> {
744    fn from(val: &ColorMatrixList) -> Self {
745        val.0.clone()
746    }
747}
748
749impl std::ops::Deref for ColorMatrixList {
750    type Target = Vec<ColorMatrix>;
751
752    fn deref(&self) -> &Self::Target {
753        &self.0
754    }
755}
756
757/// Defines potential constraints when automatically cropping the image
758#[derive(PartialEq, Copy, Clone, Debug)]
759pub enum HorizontalCropConstraint {
760    /// No constrain at all
761    None,
762    /// Consider we are working in a specific and screen mode and bytes must be full
763    CompleteByteForMode(Mode)
764}
765
766/// Defines how cropping occurs horizontally
767#[derive(PartialEq, Copy, Clone, Debug)]
768pub enum HorizontalCrop {
769    /// Cropping only on right
770    Right(HorizontalCropConstraint),
771    /// Cropping only on left
772    Left(HorizontalCropConstraint),
773    /// Cropping on left and right
774    Both(HorizontalCropConstraint, HorizontalCropConstraint),
775    /// No horinzotnalropping
776    None
777}
778
779/// Defines how cropping occurs vertically
780#[derive(PartialEq, Copy, Clone, Debug)]
781pub enum VerticalCrop {
782    /// Cropping only on top
783    Top,
784    /// Cropping only on botton
785    Bottom,
786    /// Cropping on top and bottom
787    Both,
788    /// No vertical cropping
789    None
790}
791
792impl ColorMatrixList {
793    /// Provide a Vec version of the items
794    pub fn to_vec(&self) -> Vec<ColorMatrix> {
795        self.into()
796    }
797
798    /// Animations are stored within GIF files.
799    /// TODO allow over kind of image data
800    pub fn convert_from_fname(fname: &str, conversion: ConversionRule) -> anyhow::Result<Self> {
801        use std::fs::File;
802
803        // Decode a gif into frames
804        let input = File::open(fname)?;
805        let mut options = gif::DecodeOptions::new();
806        options.set_color_output(gif::ColorOutput::Indexed);
807        let mut decoder = options.read_info(input).unwrap();
808        let mut screen = gif_dispose::Screen::new_decoder(&decoder);
809
810        let mut matrix_list = ColorMatrixList(Vec::new());
811        while let Some(frame) = decoder.read_next_frame()? {
812            screen.blit_frame(frame)?;
813
814            let content = image::ImageBuffer::<image::Rgb<u8>, Vec<u8>>::from_raw(
815                screen.pixels.width() as u32,
816                screen.pixels.height() as u32,
817                screen
818                    .pixels
819                    .buf()
820                    .iter()
821                    .flat_map(|pix| [pix.r, pix.g, pix.b].to_vec())
822                    .collect::<Vec<u8>>()
823            )
824            .unwrap();
825
826            matrix_list
827                .0
828                .push(ColorMatrix::convert(&content, conversion));
829        }
830
831        Ok(matrix_list)
832    }
833
834    /// Delegate the color reduction to the underlying ColorMatrix objects
835    pub fn reduce_colors_with(
836        &mut self,
837        inks: &[Ink],
838        strategy: ColorConversionStrategy
839    ) -> Result<(), anyhow::Error> {
840        self.0
841            .iter_mut()
842            .try_for_each(|matrix| matrix.reduce_colors_with(inks, strategy))
843    }
844
845    /// Number of frames in the animation
846    pub fn len(&self) -> usize {
847        self.0.len()
848    }
849
850    /// Assume there is one sprite at least and all of them have the same size
851    pub fn width(&self) -> u32 {
852        self.0[0].width()
853    }
854
855    /// Assume there is one sprite at least and all of them have the same size
856    pub fn height(&self) -> u32 {
857        self.0[0].height()
858    }
859
860    /// Convert each matrice as a sprite using the same conversion method
861    pub fn as_sprites(
862        &self,
863        mode: Mode,
864        palette: Option<Palette>,
865        missing_pen: Option<Pen>
866    ) -> SpriteList {
867        self.to_vec()
868            .iter()
869            .map(|matrix| matrix.as_sprite(mode, palette.clone(), missing_pen))
870            .collect::<Vec<Sprite>>()
871            .into()
872    }
873
874    /// Crop each matrix in order to only keep the maximal window where at least one pixel change over the animation
875    pub fn crop(&mut self, hor_conf: HorizontalCrop, vert_conf: VerticalCrop) -> Self {
876        use std::collections::BTreeSet;
877
878        // Collect the lines/row modified
879        let (modified_x, modified_y) = {
880            let mut modified_x = BTreeSet::new();
881            let mut modified_y = BTreeSet::new();
882
883            for (mata, matb) in self.0.iter().tuple_windows() {
884                let diff = mata.diff(matb);
885                let diff_coords = diff.diff_to_positions();
886
887                diff_coords.iter().for_each(|(x, y)| {
888                    modified_x.insert(*x);
889                    modified_y.insert(*y);
890                });
891            }
892
893            (
894                modified_x.iter().map(|x| *x as u32).collect::<Vec<_>>(),
895                modified_y.iter().map(|y| *y as u32).collect::<Vec<_>>()
896            )
897        };
898
899        // Make the croping on the left (first column to keep)
900        let mut start_x = match hor_conf {
901            HorizontalCrop::Both(..) | HorizontalCrop::Left(_) => {
902                let mut current_x = 0;
903                while current_x < self.width() - 1 && current_x < modified_x[0] {
904                    current_x += 1;
905                }
906                current_x
907            },
908            _ => 0
909        } as usize;
910
911        // Make the cropping on the right (last column to keep)
912        let mut stop_x = match hor_conf {
913            HorizontalCrop::Both(..) | HorizontalCrop::Right(_) => {
914                let mut current_x = self.width() - 1;
915                while current_x > 0 && current_x > *modified_x.last().unwrap() {
916                    current_x -= 1;
917                }
918                current_x
919            },
920            _ => self.width() - 1
921        } as usize;
922
923        // Make the cropping to the top
924        let start_y = match vert_conf {
925            VerticalCrop::Both | VerticalCrop::Top => {
926                let mut current_y = 0;
927                while current_y < self.height() - 1 && current_y < modified_y[0] {
928                    current_y += 1;
929                }
930                current_y
931            },
932            _ => 0
933        } as usize;
934
935        // Make the cropping to the bottom
936        let stop_y = match vert_conf {
937            VerticalCrop::Both | VerticalCrop::Bottom => {
938                let mut current_y = self.height() - 1;
939                while current_y > 0 && current_y > *modified_y.last().unwrap() {
940                    current_y -= 1;
941                }
942                current_y
943            },
944            _ => self.height() - 1
945        } as usize;
946
947        // Ensure horizontal start constraint is respected
948        match hor_conf {
949            HorizontalCrop::Left(HorizontalCropConstraint::CompleteByteForMode(ref mode))
950            | HorizontalCrop::Both(HorizontalCropConstraint::CompleteByteForMode(ref mode), _) => {
951                while !start_x.is_multiple_of(mode.nb_pixels_per_byte()) {
952                    start_x -= 1;
953                }
954            },
955            _ => {}
956        }
957
958        // Ensure horizontal stop contraint is respected
959        match hor_conf {
960            HorizontalCrop::Right(HorizontalCropConstraint::CompleteByteForMode(ref mode))
961            | HorizontalCrop::Both(_, HorizontalCropConstraint::CompleteByteForMode(ref mode)) => {
962                while !(stop_x + 1).is_multiple_of(mode.nb_pixels_per_byte()) {
963                    stop_x += 1;
964                }
965            },
966            _ => {}
967        }
968
969        // Return the selected window
970        self.window(start_x, start_y, stop_x - start_x + 1, stop_y - start_y + 1)
971    }
972
973    /// Apply the window operator on each ColorMatrix
974    pub fn window(&self, start_x: usize, start_y: usize, width: usize, height: usize) -> Self {
975        self.to_vec()
976            .iter()
977            .map(|matrix| matrix.window(start_x, start_y, width, height))
978            .collect::<Vec<ColorMatrix>>()
979            .into()
980    }
981}
982
983/// List of sprites for animations
984#[derive(Debug)]
985pub struct SpriteList(Vec<Sprite>);
986
987impl From<Vec<Sprite>> for SpriteList {
988    fn from(src: Vec<Sprite>) -> Self {
989        SpriteList(src)
990    }
991}
992
993impl std::ops::Deref for SpriteList {
994    type Target = Vec<Sprite>;
995
996    fn deref(&self) -> &Self::Target {
997        &self.0
998    }
999}
1000
1001/// A Sprite corresponds to a set of bytes encoded to the right CPC pixel format for a given
1002/// palette.
1003/// TODO Check why mode nad palette are optionnals. Force them if it is not mandatory to have htem
1004/// optionnal
1005#[derive(Debug)]
1006pub struct Sprite {
1007    /// Optional screen mode of the sprite
1008    pub(crate) mode: Option<Mode>,
1009    /// Optinal palete of the sprite
1010    pub(crate) palette: Option<Palette>,
1011    /// Content of the sprite
1012    pub(crate) data: Vec<Vec<u8>>
1013}
1014
1015#[allow(missing_docs)]
1016impl Sprite {
1017    pub fn from_pens(pens: &[Vec<Pen>], mode: Mode, palette: Option<Palette>) -> Self {
1018        let data = pens
1019            .iter()
1020            .map(|line| crate::pixels::pens_to_vec(line, mode))
1021            .collect_vec();
1022        Sprite {
1023            data,
1024            mode: Some(mode),
1025            palette
1026        }
1027    }
1028
1029    pub fn from_bytes(bytes: &[u8], bytes_width: usize, mode: Mode, palette: Palette) -> Self {
1030        let pens = bytes
1031            .chunks(bytes_width)
1032            .map(|chunk| pixels::bytes_to_pens(chunk, mode).collect_vec())
1033            .collect_vec();
1034
1035        Self::from_pens(&pens, mode, Some(palette))
1036    }
1037
1038    /// TODO Use TryFrom once in standard rust
1039    /// The conversion can only work if a palette and a mode is provided
1040    pub fn to_color_matrix(&self) -> Option<ColorMatrix> {
1041        if self.mode.is_none() && self.palette.is_none() {
1042            return None;
1043        }
1044
1045        let mut data = Vec::with_capacity(self.data.len());
1046        let p = self.palette.as_ref().unwrap();
1047        for line in &self.data {
1048            let inks = match self.mode {
1049                Some(Mode::Zero) | Some(Mode::Three) => {
1050                    line.iter()
1051                        .flat_map(|b: &u8| {
1052                            let pens = {
1053                                let mut pens = pixels::mode0::byte_to_pens(*b);
1054                                pens[0].limit(self.mode.unwrap());
1055                                pens[1].limit(self.mode.unwrap());
1056                                pens
1057                            };
1058                            vec![*p.get(&pens[0]), *p.get(&pens[1])]
1059                        })
1060                        .collect::<Vec<Ink>>()
1061                },
1062
1063                Some(mode) => {
1064                    bytes_to_pens(line, mode)
1065                        .map(|pen| *p.get(&pen))
1066                        .collect_vec()
1067                },
1068
1069                _ => unimplemented!()
1070            };
1071            data.push(inks);
1072        }
1073
1074        Some(ColorMatrix { data })
1075    }
1076
1077    /// Produce a linearized version of the sprite.
1078    pub fn to_linear_vec(&self) -> Vec<u8> {
1079        let size = self.height() * self.bytes_width();
1080        let mut bytes: Vec<u8> = Vec::with_capacity(size as usize);
1081
1082        for y in 0..self.height() {
1083            bytes.extend_from_slice(&self.data[y as usize]);
1084        }
1085
1086        bytes
1087    }
1088
1089    /// Get the palette of the sprite
1090    pub fn palette(&self) -> Option<Palette> {
1091        self.palette.clone()
1092    }
1093
1094    pub fn set_palette(&mut self, palette: Palette) {
1095        self.palette = Some(palette);
1096    }
1097
1098    pub fn bytes(&self) -> &Vec<Vec<u8>> {
1099        &self.data
1100    }
1101
1102    /// Get hte sprite Mode
1103    /// Cannot manage multimode sprites of course
1104    pub fn mode(&self) -> Option<Mode> {
1105        self.mode
1106    }
1107
1108    /// Get the height (in pixels) of the image
1109    /// TODO Use a trait for that
1110    pub fn height(&self) -> u32 {
1111        self.data.len() as u32
1112    }
1113
1114    /// Get the width (in bytes) of the image
1115    /// TODO Use a trait for that
1116    pub fn bytes_width(&self) -> u32 {
1117        match self.height() {
1118            0 => 0,
1119            _ => self.data[0].len() as u32
1120        }
1121    }
1122
1123    /// Get the width in pixels of the image.
1124    /// The mode must be specified
1125    pub fn pixel_width(&self) -> u32 {
1126        match self.mode {
1127            None => panic!("Unable to get the pixel width when mode is not specified"),
1128            Some(mode) => mode.nb_pixels_per_byte() as u32 * self.bytes_width()
1129        }
1130    }
1131
1132    /// Returns the byte at the right position and crash if it does not exists
1133    pub fn get_byte(&self, x: usize, y: usize) -> u8 {
1134        let line = &self.data[y];
1135        line[x]
1136    }
1137
1138    /// Returns the byte at the right position if exists
1139    pub fn get_byte_safe(&self, x: usize, y: usize) -> Option<u8> {
1140        self.data.get(y).and_then(|v| v.get(x)).copied()
1141    }
1142
1143    /// Returns the line of interest
1144    pub fn get_line(&self, y: usize) -> &[u8] {
1145        self.data[y].as_ref()
1146    }
1147
1148    /// Convert an RGB image to a sprite that code the pixels
1149    /// XXX Since 2018-06-16, most of code is delagated to ColorMatrix => maybe some bugs has been
1150    /// added
1151    pub fn convert(
1152        img: &im::ImageBuffer<im::Rgb<u8>, Vec<u8>>,
1153        mode: Mode,
1154        conversion: ConversionRule,
1155        palette: Option<Palette>,
1156        missing_pen: Option<Pen>
1157    ) -> Self {
1158        // Get the list of Inks that represent the image
1159        let matrix = ColorMatrix::convert(img, conversion);
1160        matrix.as_sprite(mode, palette, missing_pen)
1161    }
1162
1163    pub fn convert_from_fname<P: AsRef<Utf8Path>>(
1164        fname: P,
1165        mode: Mode,
1166        conversion: ConversionRule,
1167        palette: Option<Palette>,
1168        missing_pen: Option<Pen>
1169    ) -> Result<Self, im::ImageError> {
1170        let img = im::open(fname.as_ref())?;
1171        Ok(Self::convert(
1172            &img.to_rgb8(),
1173            mode,
1174            conversion,
1175            palette,
1176            missing_pen
1177        ))
1178    }
1179
1180    /// Apply a transformation function on each line
1181    /// It can change there size
1182    pub fn horizontal_transform<F>(&mut self, f: F)
1183    where F: Fn(&Vec<u8>) -> Vec<u8> {
1184        let mut transformed = self.data.iter().map(f).collect::<Vec<_>>();
1185        ::std::mem::swap(&mut transformed, &mut self.data);
1186    }
1187
1188    pub fn as_image(&self) -> im::ImageBuffer<im::Rgba<u8>, Vec<u8>> {
1189        self.to_color_matrix().unwrap().as_image()
1190    }
1191}
1192
1193/// Simple multimode sprite where each line can have its own resolution mode
1194/// The palette is assumed to be the same on all the lines
1195#[derive(Clone, Debug)]
1196#[allow(missing_docs, unused)]
1197pub struct MultiModeSprite {
1198    mode: Vec<Mode>,
1199    palette: Palette,
1200    data: Vec<Vec<u8>>
1201}
1202
1203#[derive(Copy, Clone, Debug)]
1204#[allow(missing_docs)]
1205pub enum MultiModeConversion {
1206    FirstHalfSecondHalf,
1207    OddEven
1208}
1209
1210#[allow(missing_docs)]
1211impl MultiModeSprite {
1212    /// Build an empty multimode sprite BUT provide the palette
1213    pub fn new(p: Palette) -> Self {
1214        Self {
1215            palette: p,
1216            mode: Vec::new(), // Color modes for the real lines
1217            data: Vec::new()  // Data for texture lines (twice less than real ones
1218        }
1219    }
1220
1221    pub fn bytes(&self) -> &Vec<Vec<u8>> {
1222        &self.data
1223    }
1224
1225    pub fn height(&self) -> usize {
1226        self.data.len()
1227    }
1228
1229    pub fn width(&self) -> usize {
1230        self.data[0].len()
1231    }
1232
1233    /// Build a standard mode 0 sprite from a multimode sprite
1234    /// Bytes values will be strictly the same. However representation is loss (bytes supposed to
1235    /// be displayed in mode 1, 2, 3 will be represented in mode 0)
1236    /// The multimode sprite is consummed
1237    pub fn to_mode0_sprite(&self) -> Sprite {
1238        Sprite {
1239            mode: Some(Mode::Zero),
1240            palette: Some(self.palette.clone()),
1241            data: self.data.clone()
1242        }
1243    }
1244
1245    pub fn to_mode3_sprite(&self) -> Sprite {
1246        Sprite {
1247            mode: Some(Mode::Three),
1248            palette: Some(self.palette.clone()),
1249            data: self.data.clone()
1250        }
1251    }
1252
1253    /// Generate a multimode sprite that mixes mode 0 and mode 3 and uses only 4 colors
1254    #[allow(clippy::similar_names, clippy::identity_op)]
1255    pub fn mode0_mode3_mix_from_mode0(sprite: &Sprite, conversion: MultiModeConversion) -> Self {
1256        // TODO check that there are only the first 4 inks used
1257        let p_orig = sprite.palette().unwrap();
1258
1259        //  Build the specific palette for multimode
1260        let p = {
1261            let mut p = Palette::new();
1262
1263            // First 4 inks are strictly the same
1264            for i in 0..4 {
1265                p.set(i, *p_orig.get(i.into()));
1266            }
1267
1268            // The others depends on the bits kept in mode 0 or mode 4
1269            let lut = [
1270                (0, [5, 6, 7]),
1271                (1, [8, 10, 11]),
1272                (2, [12, 13, 15]),
1273                (3, [4, 9, 14])
1274            ];
1275
1276            // Fill inks depending on the lut
1277            for (src, dsts) in &lut {
1278                dsts.iter().for_each(|dst| {
1279                    p.set(*dst, *p_orig.get((*src).into()));
1280                });
1281            }
1282
1283            p
1284        };
1285
1286        // Really makes the conversion of the lines
1287        let (modes, lines) = match conversion {
1288            MultiModeConversion::FirstHalfSecondHalf => {
1289                let sprite_height = sprite.height() as usize;
1290                let encoded_height = if sprite_height % 2 == 1 {
1291                    sprite_height / 2 + 1
1292                }
1293                else {
1294                    sprite_height / 2 + 0
1295                };
1296
1297                let mut modes = Vec::with_capacity(sprite_height);
1298                let mut lines = Vec::with_capacity(encoded_height);
1299
1300                // Create the vector of modes
1301                for i in 0..sprite_height {
1302                    let mode = if i < encoded_height {
1303                        Mode::Zero
1304                    }
1305                    else {
1306                        Mode::Three
1307                    };
1308                    modes.push(mode);
1309                }
1310
1311                // Create the vector of lines
1312                for i in 0..encoded_height {
1313                    let line1 = &sprite.data[i + 0]; // always available
1314                    let line2 = sprite.data.get(i + encoded_height); // may be absent the very last time
1315
1316                    let line = match line2 {
1317                        Some(line2) => merge_mode0_mode3(line1, line2),
1318                        None => line1.clone()
1319                    };
1320
1321                    lines.push(line);
1322                }
1323
1324                (modes, lines)
1325            },
1326
1327            _ => unimplemented!()
1328        };
1329
1330        Self {
1331            palette: p,
1332            mode: modes,
1333            data: lines
1334        }
1335    }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::ColorMatrix;
1341    use crate::ga::Ink;
1342
1343    #[test]
1344    fn test_masking() {
1345        let fg1 = Ink::BLUE;
1346        let fg2 = Ink::SKY_BLUE;
1347        let fg3 = Ink::PASTEL_BLUE;
1348        let fg4 = Ink::BRIGHT_BLUE;
1349        let bg_ = Ink::RED;
1350        let rep = Ink::BLACK;
1351
1352        let sprite_with_mask = ColorMatrix {
1353            data: vec![
1354                vec![bg_, fg2, fg3, fg4],
1355                vec![bg_, bg_, fg1, fg2],
1356                vec![bg_, bg_, bg_, fg3],
1357                vec![bg_, bg_, bg_, fg4],
1358                vec![bg_, bg_, bg_, bg_],
1359            ]
1360        };
1361
1362        let (mask, sprite) = sprite_with_mask.extract_mask_and_sprite(bg_, rep);
1363
1364        assert_eq!(
1365            sprite.data,
1366            vec![
1367                vec![rep, fg2, fg3, fg4],
1368                vec![rep, rep, fg1, fg2],
1369                vec![rep, rep, rep, fg3],
1370                vec![rep, rep, rep, fg4],
1371                vec![rep, rep, rep, rep],
1372            ]
1373        );
1374
1375        assert_eq!(
1376            mask.data,
1377            vec![
1378                vec![Ink::BRIGHT_WHITE, Ink::BLACK, Ink::BLACK, Ink::BLACK],
1379                vec![Ink::BRIGHT_WHITE, Ink::BRIGHT_WHITE, Ink::BLACK, Ink::BLACK],
1380                vec![
1381                    Ink::BRIGHT_WHITE,
1382                    Ink::BRIGHT_WHITE,
1383                    Ink::BRIGHT_WHITE,
1384                    Ink::BLACK
1385                ],
1386                vec![
1387                    Ink::BRIGHT_WHITE,
1388                    Ink::BRIGHT_WHITE,
1389                    Ink::BRIGHT_WHITE,
1390                    Ink::BLACK
1391                ],
1392                vec![
1393                    Ink::BRIGHT_WHITE,
1394                    Ink::BRIGHT_WHITE,
1395                    Ink::BRIGHT_WHITE,
1396                    Ink::BRIGHT_WHITE
1397                ],
1398            ]
1399        );
1400
1401        let mask2 = sprite_with_mask.clone().convert_to_mask(bg_).clone();
1402        let sprite2 = sprite_with_mask.clone().replace_ink(bg_, rep).clone();
1403
1404        assert_eq!(mask, mask2);
1405        assert_eq!(sprite, sprite2);
1406    }
1407}