cpclib_image/
convert.rs

1// This module manage high level image conversion functions
2
3use std::collections::HashSet;
4use std::fmt::Debug;
5use std::fs::File;
6use std::io::Write;
7
8use cpclib_common::bitfield::{BitRange, BitRangeMut};
9use cpclib_common::camino::Utf8Path;
10use cpclib_common::itertools::Itertools;
11use image as im;
12
13use crate::ga::*;
14use crate::image::*;
15
16/// Encode the position of a line or column to transform in the source image
17#[derive(Copy, Clone, Debug)]
18pub enum TransformationPosition {
19    /// This is the very first line or column of the image
20    First,
21    /// This is the very last line or column of the image
22    Last,
23    /// This is a specific index
24    Index(usize)
25}
26
27impl TransformationPosition {
28    /// Get the absolute position regarding the image size
29    pub fn absolute_position(self, size: usize) -> Option<usize> {
30        match self {
31            TransformationLinePosition::First => Some(0),
32            TransformationLinePosition::Last => Some(size - 1),
33            TransformationLinePosition::Index(idx) => {
34                if idx >= size {
35                    None
36                }
37                else {
38                    Some(idx)
39                }
40            },
41        }
42    }
43}
44
45/// Type that represent the position in a line of the image
46pub type TransformationLinePosition = TransformationPosition;
47/// Type that represent the position in a column of the image
48pub type TransformationColumnPosition = TransformationPosition;
49
50/// List of all the possible transformations applicable to a ColorMatrix
51#[derive(Clone, Debug)]
52pub enum Transformation {
53    /// When using mode 0, do not read all the pixel columns
54    SkipOddPixels,
55    /// Shorten lines of several pixel columns on the left
56    SkipLeftPixelColumns(u16),
57    /// Shorten columns of several pixel columns on the top
58    SkipTopPixelLines(u16),
59    KeepLeftPixelColumns(u16),
60    KeepTopPixelLines(u16),
61    /// Add artifical blank lines. The line is build by repeating the background the right amount of time
62    BlankLines {
63        /// The pattern to use to fill the background
64        pattern: Vec<Ink>,
65        /// The location of the line within the image
66        position: TransformationPosition,
67        /// The amount of lines to add
68        amount: u16
69    },
70
71    /// Add artificial blank columns given a pattern
72    BlankColumns {
73        /// The pattern to use to fill the background
74        pattern: Vec<Ink>,
75        /// The location of the column within the image
76        position: TransformationPosition,
77        /// The amount of columns to add
78        amount: u16
79    },
80
81    /// Replace one Ink by another one
82    ReplaceInk {
83        from: Ink,
84        to: Ink
85    },
86
87    /// Create a mask from the background Ink MaskFromBackgroundInk
88    MaskFromBackgroundInk(Ink)
89}
90
91impl Transformation {
92    /// Apply the transformation to the list of colormatrix
93    /// TODO find a way to use the same function name than for a ColorMatrix
94    pub fn apply_to_list(&self, list: &ColorMatrixList) -> ColorMatrixList {
95        list.to_vec()
96            .iter()
97            .map(|matrix| self.apply(matrix))
98            .collect::<Vec<ColorMatrix>>()
99            .into()
100    }
101
102    /// Apply the transformation to the given image
103    pub fn apply(&self, matrix: &ColorMatrix) -> ColorMatrix {
104        match self {
105            Transformation::SkipOddPixels => {
106                let mut res = matrix.clone();
107                res.remove_odd_columns();
108                res
109            },
110            Transformation::SkipLeftPixelColumns(amount) => {
111                matrix.window(
112                    *amount as _,
113                    0,
114                    matrix.width().saturating_sub(*amount as _) as _,
115                    matrix.height() as _
116                )
117            },
118            Transformation::SkipTopPixelLines(amount) => {
119                matrix.window(
120                    0 as _,
121                    *amount as _,
122                    matrix.width() as _,
123                    matrix.height().saturating_sub(*amount as _) as _
124                )
125            },
126            Transformation::KeepLeftPixelColumns(usize) => {
127                matrix.window(0, 0, *usize as _, matrix.height() as _)
128            },
129            Transformation::KeepTopPixelLines(usize) => {
130                matrix.window(0, 0, matrix.width() as _, *usize as _)
131            },
132            Transformation::BlankLines {
133                pattern,
134                position,
135                amount
136            } => {
137                // Build the line according to the background pattern
138                let line = {
139                    let mut lines = Vec::new();
140                    for idx in 0..(matrix.width() as usize) {
141                        lines.push(pattern[idx % pattern.len()]);
142                    }
143                    lines
144                };
145
146                // Get the real position (will not change over the additions)
147                let position = position.absolute_position(matrix.height() as _).unwrap();
148
149                // Modify the image
150                let mut res = matrix.clone();
151                (0..*amount).for_each(|_| {
152                    res.add_line(position, &line);
153                });
154                res
155            },
156            Transformation::BlankColumns {
157                pattern,
158                position,
159                amount
160            } => {
161                let column = {
162                    let mut column = Vec::new();
163                    for idx in 0..(matrix.height() as usize) {
164                        column.push(pattern[idx % pattern.len()])
165                    }
166                    column
167                };
168
169                let position = position.absolute_position(matrix.width() as _).unwrap();
170
171                let mut res = matrix.clone();
172                (0..*amount).for_each(|_| {
173                    res.add_column(position, &column);
174                });
175
176                res
177            },
178
179            Transformation::ReplaceInk { from, to } => {
180                let mut res = matrix.clone();
181                res.replace_ink(*from, *to);
182                res
183            },
184            Transformation::MaskFromBackgroundInk(ink) => {
185                let mut res = matrix.clone();
186                res.convert_to_mask(*ink);
187                res
188            }
189        }
190    }
191
192    /// Create a transformation that adds blank lines
193    pub fn blank_lines<I: Into<Ink> + Copy>(
194        pattern: &[I],
195        position: TransformationLinePosition,
196        amount: u16
197    ) -> Self {
198        Self::BlankLines {
199            pattern: pattern.iter().map(|&i| i.into()).collect::<Vec<Ink>>(),
200            position,
201            amount
202        }
203    }
204
205    /// Create a transformation that adds blanck columns
206    pub fn blank_columns<I: Into<Ink> + Copy>(
207        pattern: &[I],
208        position: TransformationColumnPosition,
209        amount: u16
210    ) -> Self {
211        Self::BlankColumns {
212            pattern: pattern.iter().map(|&i| i.into()).collect::<Vec<_>>(),
213            position,
214            amount
215        }
216    }
217}
218
219/// Container of transformations
220#[derive(Clone, Debug, Default)]
221pub struct TransformationsList {
222    /// list of transformations
223    transformations: Vec<Transformation>
224}
225
226#[allow(missing_docs)]
227impl TransformationsList {
228    /// Create an empty list of transformations
229    pub fn new(transformations: &[Transformation]) -> Self {
230        TransformationsList {
231            transformations: transformations.to_vec()
232        }
233    }
234
235    /// Add a transformation that remove one pixel column out of two
236    pub fn skip_odd_pixels(mut self) -> Self {
237        self.transformations.push(Transformation::SkipOddPixels);
238        self
239    }
240
241    pub fn column_start(mut self, count: u16) -> Self {
242        self.transformations
243            .push(Transformation::SkipLeftPixelColumns(count));
244        self
245    }
246
247    pub fn columns_kept(mut self, count: u16) -> Self {
248        self.transformations
249            .push(Transformation::KeepLeftPixelColumns(count));
250        self
251    }
252
253    pub fn lines_kept(mut self, count: u16) -> Self {
254        self.transformations
255            .push(Transformation::KeepTopPixelLines(count));
256        self
257    }
258
259    pub fn line_start(mut self, count: u16) -> Self {
260        self.transformations
261            .push(Transformation::SkipTopPixelLines(count));
262        self
263    }
264
265    pub fn replace(mut self, from: Ink, to: Ink) -> Self {
266        self.transformations
267            .push(Transformation::ReplaceInk { from, to });
268        self
269    }
270
271    pub fn build_mask_from_background_ink(mut self, background: Ink) -> Self {
272        self.transformations
273            .push(Transformation::MaskFromBackgroundInk(background));
274        self
275    }
276
277    /// Apply ALL the transformation (in order of addition)
278    pub fn apply(&self, matrix: &ColorMatrix) -> ColorMatrix {
279        let mut result = matrix.clone();
280        for transformation in &self.transformations {
281            result = transformation.apply(&result);
282        }
283        result
284    }
285}
286
287/// Encode the screen dimension in CRTC measures
288#[derive(Clone, Copy)]
289pub struct CPCScreenDimension {
290    /// Number of bytes in width
291    pub horizontal_displayed: u8,
292    /// Number of chars in height
293    pub vertical_displayed: u8,
294    /// Number of pixel line per char line
295    pub maximum_raster_address: u8
296}
297
298impl Debug for CPCScreenDimension {
299    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
300        write!(
301            fmt,
302            "CPCScreenDimension {{ horizontal_displayed: {}, vertical_displayed: {}, maximum_raster_address: {}, use_two_banks: {} }}",
303            self.horizontal_displayed,
304            self.vertical_displayed,
305            self.maximum_raster_address,
306            self.use_two_banks()
307        )
308    }
309}
310
311#[allow(missing_docs)]
312impl CPCScreenDimension {
313    /// Return screen dimension for a standard screen
314    pub fn standard() -> Self {
315        Self {
316            horizontal_displayed: 80 / 2,
317            vertical_displayed: 25,
318            /// Unsure of this value
319            maximum_raster_address: 7
320        }
321    }
322
323    /// Return the screen dimension for a standard overscan screen
324    pub fn overscan() -> Self {
325        Self {
326            horizontal_displayed: 96 / 2,
327            vertical_displayed: 39, // it was 36 before? need to find why,
328            maximum_raster_address: 7
329        }
330    }
331
332    /// Specify a tailored dimension
333    pub fn new(
334        horizontal_displayed: u8,
335        vertical_displayed: u8,
336        maximum_raster_address: u8
337    ) -> Self {
338        Self {
339            horizontal_displayed,
340            vertical_displayed,
341            maximum_raster_address
342        }
343    }
344
345    /// Number of lines to display a char
346    pub fn nb_lines_per_char(self) -> u8 {
347        1 + self.maximum_raster_address
348    }
349
350    /// Number of chars used to vertically encode the screen
351    pub fn nb_char_lines(self) -> u8 {
352        self.vertical_displayed
353    }
354
355    pub fn nb_word_columns(self) -> u8 {
356        self.horizontal_displayed
357    }
358
359    /// Number of chars used to horizontally encode the screen
360    pub fn nb_byte_columns(self) -> u8 {
361        self.nb_word_columns() * 2
362    }
363
364    /// Height of the screen in pixels
365    pub fn height(self) -> u16 {
366        u16::from(self.nb_char_lines()) * u16::from(self.nb_lines_per_char())
367    }
368
369    /// Width of the screen in pixels
370    pub fn width(self, mode: Mode) -> u16 {
371        u16::from(self.nb_byte_columns()) * mode.nb_pixels_per_byte() as u16
372    }
373
374    /// Return true if the image needs two banks
375    pub fn use_two_banks(self) -> bool {
376        u16::from(self.nb_byte_columns()) * self.height() > 0x4000
377    }
378}
379
380/// Manage the display address contained in R12-R13
381/// TODO move that later in a CRTC emulator code
382#[derive(Clone, Copy, Debug)]
383pub struct DisplayAddress(u16);
384
385#[allow(missing_docs)]
386pub type DisplayCRTCAddress = DisplayAddress;
387
388#[allow(missing_docs)]
389impl DisplayAddress {
390    const BUFFER_END: usize = 10;
391    const BUFFER_START: usize = 11;
392    const OFFSET_END: usize = 0;
393    const OFFSET_START: usize = 9;
394    const PAGE_END: usize = 12;
395    const PAGE_START: usize = 13;
396
397    /// Create the display address
398    pub fn new_from(val: u16) -> Self {
399        assert!(val < 0b1100_0000_0000_0000);
400        Self(val)
401    }
402
403    pub fn new(page: u16, is_overscan: bool, offset: u16) -> Self {
404        let mut address = Self::new_from(0);
405        address.set_page(page);
406        address.set_overscan(is_overscan);
407        address.set_offset(offset);
408
409        dbg!(address.r12(), address.r13());
410        address
411    }
412
413    pub fn new_standard_from_page(page: u16) -> Self {
414        Self::new(page, false, 0)
415    }
416
417    /// Generate an address that allow to display overscan picture from the given page
418    pub fn new_overscan_from_page(page: u16) -> Self {
419        Self::new(page, true, 0)
420    }
421
422    /// Generate an overscan address where each line is contained in a single bank
423    pub fn new_overscan_from_page_one_bank_per_line(page: u16, char_width: u16) -> Self {
424        // number of words missing
425        let delta = (0x800 % (char_width * 2)) / 2;
426        Self::new(page, true, delta)
427    }
428
429    pub fn new_standard_from_address(_address: u16) -> Self {
430        unimplemented!()
431    }
432
433    pub fn new_overscan_from_address(_address: u16) -> Self {
434        unimplemented!()
435    }
436
437    /// Return the offset part of the address
438    pub fn offset(self) -> u16 {
439        self.0.bit_range(Self::OFFSET_START, Self::OFFSET_END)
440    }
441
442    pub fn set_offset(&mut self, offset: u16) {
443        self.0
444            .set_bit_range(Self::OFFSET_START, Self::OFFSET_END, offset)
445    }
446
447    /// Return the buffer configuration
448    /// 0 0 16k
449    /// 0 1 16k
450    /// 1 0 16k
451    /// 1 1 16k
452    pub fn buffer(self) -> u16 {
453        self.0.bit_range(Self::BUFFER_START, Self::BUFFER_END)
454    }
455
456    pub fn set_buffer(&mut self, buffer: u16) {
457        self.0
458            .set_bit_range(Self::BUFFER_START, Self::BUFFER_END, buffer)
459    }
460
461    pub fn set_overscan(&mut self, is_overscan: bool) {
462        if is_overscan {
463            self.set_buffer(0b11);
464        }
465        else {
466            self.set_buffer(0b00);
467        }
468    }
469
470    /// Return the page configuration
471    /// 0 0 0x0000
472    /// 0 1 0x4000
473    /// 1 0 0x8000
474    /// 1 1 0xc000
475    pub fn page(self) -> u16 {
476        self.0.bit_range(Self::PAGE_START, Self::PAGE_END)
477    }
478
479    pub fn set_page(&mut self, page: u16) {
480        self.0.set_bit_range(Self::PAGE_START, Self::PAGE_END, page);
481    }
482
483    pub fn r12(self) -> u8 {
484        self.0.bit_range(15, 8)
485    }
486
487    pub fn r13(self) -> u8 {
488        self.0.bit_range(7, 0)
489    }
490
491    /// Return the page value
492    pub fn page_start(self) -> u16 {
493        match self.page() {
494            0 => 0x0000,
495            1 => 0x4000,
496            2 => 0x8000,
497            3 => 0xC000,
498            _ => panic!()
499        }
500    }
501
502    /// Check of the configuration correspond to an overscan
503    pub fn is_overscan(self) -> bool {
504        match self.buffer() {
505            0..=2 => false,
506            3 => true,
507            _ => panic!()
508        }
509    }
510
511    /// Returns the CPC address of the first word.
512    pub fn address(self) -> u16 {
513        self.page_start() + self.offset() * 2
514    }
515
516    /// Change the adress to point to the previous word
517    pub fn move_to_previous_word(&mut self) {
518        unimplemented!()
519    }
520
521    /// Assume the object represent the character of interest and move to next one
522    pub fn move_to_next_word(&mut self) {
523        let was_overscan = self.is_overscan();
524
525        let expected_offset = self.offset() + 1;
526        let truncated_expected_offset =
527            expected_offset.bit_range(Self::OFFSET_START, Self::OFFSET_END);
528
529        // Move the offset of one char
530        self.set_offset(truncated_expected_offset);
531        if truncated_expected_offset != expected_offset {
532            println!(
533                "From {} to {} / {} / {:?}",
534                expected_offset,
535                truncated_expected_offset,
536                self.is_overscan(),
537                self
538            );
539        }
540        // In overscan screen, change the page
541        if truncated_expected_offset != expected_offset && self.is_overscan() {
542            println!("Change of page");
543            let val = self.page() + 1;
544            self.set_page(val);
545        }
546
547        assert_eq!(was_overscan, self.is_overscan());
548    }
549}
550
551/// Specify the output format to be used
552/// TODO - add additional output format (for example zigzag sprites that can be usefull or sprite display routines)
553#[derive(Clone, Debug)]
554#[allow(missing_docs)]
555pub enum OutputFormat {
556    MaskedSprite {
557        sprite_format: SpriteEncoding,
558        mask_ink: Ink,
559        replacement_ink: Ink
560    },
561    Sprite(SpriteEncoding),
562
563    /// Chuncky output where each pixel is encoded in one byte (and is supposed to be vertically duplicated)
564    LinearEncodedChuncky,
565
566    /// CPC memory encoded. The binary can be directly included in a snapshot
567    CPCMemory {
568        output_dimension: CPCScreenDimension,
569        display_address: DisplayAddress
570    },
571
572    /// CPC memory encoded to be used with hardware splitting. The vector only contains the Variant CPCMemory
573    CPCSplittingMemory(Vec<OutputFormat>),
574
575    /// For quite complexe coding more related to very fast display
576    TileEncoded {
577        /// The width of a tile
578        tile_width: TileWidthCapture,
579        /// The height of a tile
580        tile_height: TileHeightCapture,
581        /// The way tile are horizontally captured
582        horizontal_movement: TileHorizontalCapture,
583        /// The way tile are vertically captured
584        vertical_movement: TileVerticalCapture,
585        /// The width of the grid (i.e., the number of tiles present in a row)
586        grid_width: GridWidthCapture,
587        /// The height of the gris (i.e., the number of tiles present in a column)
588        grid_height: GridHeightCapture
589    }
590}
591
592#[allow(missing_docs)]
593impl OutputFormat {
594    /// For formats manipulating a display address, modify it vertically in order to make scroll the image
595    pub fn vertically_shift_display_address(&mut self, delta: i32) {
596        if let Self::CPCMemory {
597            output_dimension,
598            display_address
599        } = self
600        {
601            if delta >= 0 {
602                for _ in 0..delta * i32::from(output_dimension.nb_word_columns()) {
603                    display_address.move_to_next_word();
604                }
605            }
606            else {
607                for _ in 0..(-delta) * i32::from(output_dimension.nb_word_columns()) {
608                    display_address.move_to_previous_word();
609                }
610            }
611        }
612    }
613
614    /// Generate output format for a linear sprite
615    pub fn create_linear_encoded_sprite() -> Self {
616        Self::Sprite(SpriteEncoding::Linear)
617    }
618
619    pub fn create_graycode_encoded_sprite() -> Self {
620        Self::Sprite(SpriteEncoding::GrayCoded)
621    }
622
623    pub fn create_zigzag_graycode_encoded_sprite() -> Self {
624        Self::Sprite(SpriteEncoding::ZigZagGrayCoded)
625    }
626
627    /// Generate output format for an overscan screen
628    pub fn create_overscan_cpc_memory() -> Self {
629        Self::CPCMemory {
630            output_dimension: CPCScreenDimension::overscan(),
631            display_address: DisplayAddress::new_overscan_from_page(2) /* we do not care of the page */
632        }
633    }
634
635    /// Generate output format for an overscan screen for which each imageline is in a single bank (this is not the case for the standard overscan)
636    pub fn create_overscan_cpc_memory_one_bank_per_line() -> Self {
637        let output_dimension = CPCScreenDimension::overscan();
638        let display_address = DisplayAddress::new_overscan_from_page_one_bank_per_line(
639            2,
640            output_dimension.nb_word_columns() as _
641        );
642        Self::CPCMemory {
643            output_dimension,
644            display_address
645        }
646    }
647
648    pub fn create_standard_cpc_memory() -> Self {
649        Self::CPCMemory {
650            output_dimension: CPCScreenDimension::standard(),
651            display_address: DisplayAddress::new_standard_from_page(2)
652        }
653    }
654}
655
656/// Defines the width of the capture
657#[derive(Debug, Clone, Copy)]
658pub enum TileWidthCapture {
659    /// All the width is captured
660    FullWidth,
661    /// Only the given number of bytes is captured
662    NbBytes(usize)
663}
664
665/// Defines the width of the capture
666#[derive(Debug, Clone, Copy)]
667pub enum TileHeightCapture {
668    /// All the height is captured
669    FullHeight,
670    /// Only the given number of lines is captured
671    NbLines(usize)
672}
673
674/// Defines the width of the capture
675#[derive(Debug, Clone, Copy)]
676pub enum GridWidthCapture {
677    /// All the width is captured
678    FullWidth,
679    /// Only the given number of tiles are capture in a row
680    TilesInRow(usize)
681}
682
683/// Defines the width of the capture
684#[derive(Debug, Clone, Copy)]
685pub enum GridHeightCapture {
686    /// All the height is captured
687    FullHeight,
688    /// Only the given number of tiles is captured in a column
689    TilesInColumn(usize)
690}
691
692/// Defines the horizontal movement when capturing bytes
693#[derive(Debug, Clone, Copy)]
694pub enum TileHorizontalCapture {
695    /// Bytes are always captured from left to right
696    AlwaysFromLeftToRight,
697    /// Bytes are always captured from right to left
698    AlwaysFromRightToLeft,
699    /// Bytes are read in a right-left left-right way
700    StartFromRightAndFlipAtTheEndOfLine,
701    /// Bytes are read in a left-right right-left way
702    StartFromLeftAndFlipAtTheEndOfLine
703}
704
705#[allow(missing_docs)]
706pub trait HorizontalWordCounter {
707    fn get_column_index(&self) -> usize {
708        unimplemented!()
709    }
710    /// goto the next position to compute (that is configuration dependant)
711    fn next(&mut self) {
712        unimplemented!();
713    }
714
715    // Acknowledge that line is ended
716    fn line_ended(&mut self) {
717        unimplemented!();
718    }
719}
720
721#[derive(Copy, Clone, Debug)]
722#[allow(missing_docs)]
723pub struct StartFromLeftAndFlipAtTheEndOfLine {
724    current_column: usize,
725    left_to_right: bool
726}
727
728#[allow(missing_docs)]
729impl Default for StartFromLeftAndFlipAtTheEndOfLine {
730    fn default() -> Self {
731        Self {
732            current_column: 0,
733            left_to_right: true
734        }
735    }
736}
737#[allow(missing_docs)]
738impl HorizontalWordCounter for StartFromLeftAndFlipAtTheEndOfLine {
739    fn get_column_index(&self) -> usize {
740        self.current_column
741    }
742
743    fn next(&mut self) {
744        if self.left_to_right {
745            self.current_column += 1;
746        }
747        else {
748            self.current_column -= 1
749        }
750    }
751
752    fn line_ended(&mut self) {
753        self.left_to_right = !self.left_to_right;
754    }
755}
756
757/// Structure to manage the horizontal movement of the sprite cursor
758#[derive(Debug, Copy, Clone)]
759pub struct StandardHorizontalCounter {
760    left_to_right: bool,
761    current_step: usize,
762    // We cannot have sprite of width
763    nb_columns: Option<std::num::NonZeroUsize>
764}
765
766impl StandardHorizontalCounter {
767    /// Generate a counter for always copy from left to right
768    pub fn always_from_left_to_right() -> StandardHorizontalCounter {
769        StandardHorizontalCounter {
770            left_to_right: true,
771            current_step: 0,
772            nb_columns: None
773        }
774    }
775
776    /// Generate a counter for always copy from right to left
777    /// The number of columns MUST be specified in some way
778    pub fn always_from_right_to_left() -> StandardHorizontalCounter {
779        StandardHorizontalCounter {
780            left_to_right: false,
781            current_step: 0,
782            nb_columns: None
783        }
784    }
785}
786
787#[allow(missing_docs)]
788impl HorizontalWordCounter for StandardHorizontalCounter {
789    fn get_column_index(&self) -> usize {
790        if self.left_to_right {
791            self.current_step
792        }
793        else {
794            usize::from(self.nb_columns.unwrap()) - self.current_step
795        }
796    }
797
798    fn next(&mut self) {
799        self.current_step += 1;
800    }
801
802    fn line_ended(&mut self) {
803        self.current_step = 0;
804    }
805}
806
807#[allow(missing_docs)]
808impl TileHorizontalCapture {
809    pub fn counter(self) -> Box<dyn HorizontalWordCounter> {
810        match self {
811            Self::AlwaysFromLeftToRight => {
812                Box::new(StandardHorizontalCounter::always_from_left_to_right())
813            },
814            Self::AlwaysFromRightToLeft => unimplemented!(),
815            Self::StartFromRightAndFlipAtTheEndOfLine => unimplemented!(),
816            Self::StartFromLeftAndFlipAtTheEndOfLine => {
817                Box::<StartFromLeftAndFlipAtTheEndOfLine>::default()
818            },
819        }
820    }
821}
822
823/// Utility structure that helps in playing with gray code movement in lines.
824/// We assume that chars are 8 lines tall. Some modification are possible for chars of 4 lines
825///
826/// Addresses ordered by lines on screen
827///
828/// 0 0x00??  000
829/// 1 0x08??  001
830/// 2 0x10??  010
831/// 3 0x18??  011
832/// 4 0x20??  100
833/// 5 0x28??  101
834/// 6 0x30??  110
835/// 7 0x38??  111
836///
837/// Adresses ordered by graycode
838///
839/// 0 000 => 0
840/// 1 001 => 1
841/// 2 011 => 3
842/// 3 010 => 2
843/// 4 110 => 6
844/// 5 111 => 7
845/// 6 101 => 5
846/// 7 100 => 4
847#[derive(Debug, Copy, Clone)]
848#[allow(missing_docs)]
849#[derive(Default)]
850pub struct GrayCodeLineCounter {
851    char_line: usize,
852    pos_in_char: u8 // in gray code space
853}
854
855/// Standard line counter
856#[derive(Copy, Clone, Debug)]
857#[allow(missing_docs)]
858pub struct StandardLineCounter {
859    pos_in_screen: usize,
860    top_to_bottom: bool
861}
862
863/// LineCounter manage the choice of the line when iterateing a sprite vertically
864pub trait LineCounter {
865    /// Return the real number of the line
866    fn get_line_index_in_screen(&self) -> usize;
867
868    /// goto the next position to compute (that is configuration dependant)
869    fn next(&mut self) {
870        unimplemented!();
871    }
872}
873
874#[allow(missing_docs)]
875impl StandardLineCounter {
876    pub fn top_to_bottom() -> Self {
877        Self {
878            pos_in_screen: 0,
879            top_to_bottom: true
880        }
881    }
882
883    pub fn bottom_to_top(start: usize) -> Self {
884        Self {
885            pos_in_screen: start,
886            top_to_bottom: false
887        }
888    }
889}
890
891#[allow(missing_docs)]
892impl LineCounter for StandardLineCounter {
893    fn get_line_index_in_screen(&self) -> usize {
894        self.pos_in_screen
895    }
896
897    fn next(&mut self) {
898        if self.top_to_bottom {
899            self.pos_in_screen += 1;
900        }
901        else {
902            self.pos_in_screen -= 1;
903        }
904    }
905}
906
907#[allow(missing_docs)]
908impl GrayCodeLineCounter {
909    pub const GRAYCODE_INDEX_TO_SCREEN_INDEX: [u8; 8] = [0, 1, 3, 2, 6, 7, 5, 4];
910    #[allow(unused)]
911    pub const SCREEN_INDEX_TO_GRAYCODE_INDEX: [u8; 8] = [0, 1, 3, 2, 7, 6, 4, 5];
912
913    pub fn new() -> Self {
914        Self::default()
915    }
916
917    pub fn get_char_line(&self) -> usize {
918        self.char_line
919    }
920
921    pub fn get_line_index_in_char(&self) -> u8 {
922        Self::GRAYCODE_INDEX_TO_SCREEN_INDEX[self.get_graycode_index_in_char() as usize]
923    }
924
925    pub fn get_graycode_index_in_char(&self) -> u8 {
926        self.pos_in_char
927    }
928
929    /// Modify the state to represent the previous line
930    pub fn goto_previous_line(&mut self) {
931        if self.pos_in_char == 0 {
932            self.pos_in_char = 7;
933            self.char_line -= 1;
934        }
935        else {
936            self.pos_in_char -= 1;
937        }
938    }
939
940    /// Modify the state to represent the next line
941    pub fn goto_next_line(&mut self) {
942        self.pos_in_char += 1;
943        if self.pos_in_char == 8 {
944            self.pos_in_char = 0;
945            self.char_line += 1;
946        }
947    }
948}
949
950impl LineCounter for GrayCodeLineCounter {
951    fn get_line_index_in_screen(&self) -> usize {
952        self.char_line * 8 + self.get_line_index_in_char() as usize
953    }
954
955    fn next(&mut self) {
956        self.goto_next_line()
957    }
958}
959
960/// Defines the vertical movement when capturing lines
961#[derive(Debug, Clone, Copy)]
962pub enum TileVerticalCapture {
963    /// Lines are always captured from the top to the bottom
964    AlwaysFromTopToBottom,
965    /// Lines are always captured from the bottom to the top
966    AlwaysFromBottomToTop,
967    /// Lines are captured in a top-bottom bottom-top way
968    StartFromTopAndFlipAtEndOfScreen,
969    /// Lines are captured in bottom-top top-bottom way
970    StartFromBottomAndFlipAtEndOfScreen,
971    /// Lines are captured following a gray-code way that starts from the top
972    GrayCodeFromTop,
973    /// Lines are captured following a gray-code way that starts from the bottom
974    GrayCodeFromBottom
975}
976
977#[allow(missing_docs)]
978impl TileVerticalCapture {
979    /// Generates the counter when it is possible.
980    /// Panics if contextual information is needed
981    pub fn counter(self) -> Box<dyn LineCounter> {
982        match self {
983            Self::AlwaysFromTopToBottom => Box::new(StandardLineCounter::top_to_bottom()),
984            Self::AlwaysFromBottomToTop => panic!("A parameter is needed there"),
985            Self::GrayCodeFromTop => Box::new(GrayCodeLineCounter::new()),
986            _ => unimplemented!()
987        }
988    }
989
990    pub fn counter_with_context(self, _screen_height: usize) -> Box<dyn LineCounter> {
991        unimplemented!("TODO once someone will code it")
992    }
993}
994
995#[derive(PartialEq, Eq, Hash, Clone, Copy)]
996pub enum SpriteEncoding {
997    Linear,
998    GrayCoded,
999    LeftToRightToLeft,
1000    ZigZagGrayCoded
1001}
1002
1003#[derive(Clone)]
1004pub struct SpriteOutput {
1005    data: Vec<u8>,
1006    palette: Palette,
1007    mode: Mode,
1008    bytes_width: usize,
1009    height: usize,
1010    encoding: SpriteEncoding
1011}
1012
1013impl AsRef<[u8]> for SpriteOutput {
1014    fn as_ref(&self) -> &[u8] {
1015        self.data()
1016    }
1017}
1018
1019impl SpriteOutput {
1020    pub fn with_encoding(&self, encoding: SpriteEncoding) -> Self {
1021        if encoding == self.encoding {
1022            return self.clone();
1023        }
1024
1025        if (encoding == SpriteEncoding::LeftToRightToLeft
1026            && self.encoding == SpriteEncoding::Linear)
1027            || (self.encoding == SpriteEncoding::LeftToRightToLeft
1028                && encoding == SpriteEncoding::Linear)
1029        {
1030            let mut res = self.clone();
1031            let width = res.bytes_width();
1032            res.data = res
1033                .data
1034                .into_iter()
1035                .chunks(width)
1036                .into_iter()
1037                .enumerate()
1038                .flat_map(|(idx, row)| {
1039                    let mut row = row.collect_vec();
1040                    if !idx.is_multiple_of(2) {
1041                        row.reverse();
1042                    }
1043                    row
1044                })
1045                .collect_vec();
1046            return res;
1047        }
1048
1049        unimplemented!(
1050            "Encoding conversion has not yet been used and is not coded. A leftRightTopBottom iterator is probably expected to ease conversion."
1051        )
1052    }
1053
1054    pub fn as_sprite(&self) -> Sprite {
1055        Sprite::from_bytes(
1056            &self.data,
1057            self.bytes_width(),
1058            self.mode,
1059            self.palette.clone()
1060        )
1061    }
1062
1063    pub fn encoding(&self) -> SpriteEncoding {
1064        self.encoding
1065    }
1066
1067    pub fn data(&self) -> &[u8] {
1068        &self.data
1069    }
1070
1071    pub fn palette(&self) -> &Palette {
1072        &self.palette
1073    }
1074
1075    pub fn bytes_width(&self) -> usize {
1076        self.bytes_width
1077    }
1078
1079    pub fn height(&self) -> usize {
1080        self.height
1081    }
1082
1083    pub fn save_sprite<P: AsRef<Utf8Path>>(&self, fname: P) -> std::io::Result<()> {
1084        let fname = fname.as_ref();
1085        let mut file = File::create(fname)?;
1086        file.write_all(self.data())
1087    }
1088}
1089
1090/// Embeds the conversion output
1091/// There must be one implementation per OuputFormat
1092#[allow(missing_docs)]
1093#[allow(clippy::large_enum_variant)]
1094pub enum Output {
1095    // Mask and sprite are encoded the very same way
1096    SpriteAndMask {
1097        sprite: SpriteOutput,
1098        mask: SpriteOutput
1099    },
1100
1101    // Any kind of sprite
1102    Sprite(SpriteOutput),
1103
1104    LinearEncodedChuncky {
1105        data: Vec<u8>,
1106        palette: Palette,
1107        bytes_width: usize,
1108        height: usize
1109    },
1110
1111    /// Result using one bank
1112    CPCMemoryStandard([u8; 0x4000], Palette),
1113
1114    /// Result using two banks
1115    CPCMemoryOverscan([u8; 0x4000], [u8; 0x4000], Palette),
1116
1117    /// Result using several chunks of memory
1118    CPCSplittingMemory(Vec<Output>),
1119
1120    /// Result containing several tiles
1121    TilesList {
1122        tile_height: u32,
1123        tile_width: u32,
1124        horizontal_movement: TileHorizontalCapture,
1125        vertical_movement: TileVerticalCapture,
1126        palette: Palette,
1127        list: Vec<Vec<u8>>
1128    }
1129}
1130
1131impl Debug for SpriteEncoding {
1132    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
1133        match self {
1134            Self::Linear => writeln!(fmt, "Linear"),
1135            Self::GrayCoded => writeln!(fmt, "GrayCoded"),
1136            Self::LeftToRightToLeft => writeln!(fmt, "ZigZag"),
1137            Self::ZigZagGrayCoded => writeln!(fmt, "ZigZagGrayCoded")
1138        }
1139    }
1140}
1141
1142impl Debug for SpriteOutput {
1143    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
1144        writeln!(fmt, "{:?}Sprite", self.encoding)
1145    }
1146}
1147
1148impl Debug for Output {
1149    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
1150        match self {
1151            Output::LinearEncodedChuncky { .. } => writeln!(fmt, "LinearEncodedChuncky"),
1152            Output::CPCMemoryStandard(..) => writeln!(fmt, "CPCMemoryStandard (16kb)"),
1153            Output::CPCMemoryOverscan(..) => writeln!(fmt, "CPCMemoryStandard (32kb)"),
1154            Output::CPCSplittingMemory(vec) => writeln!(fmt, "CPCSplitteringMemory {:?}", &vec),
1155            Output::TilesList {
1156                tile_height,
1157                tile_width,
1158                list,
1159                ..
1160            } => {
1161                writeln!(
1162                    fmt,
1163                    "{} tiles of {}x{}",
1164                    list.len(),
1165                    tile_width,
1166                    tile_height
1167                )
1168            },
1169            Output::SpriteAndMask { sprite, mask } => writeln!(fmt, "SpriteAndMask"),
1170            Output::Sprite(sprite_output) => writeln!(fmt, "{sprite_output:?}")
1171        }
1172    }
1173}
1174
1175#[allow(missing_docs)]
1176impl Output {
1177    /// Returns the bank that contains the first half of the screen
1178    pub fn overscan_screen1(&self) -> Option<&[u8; 0x4000]> {
1179        match self {
1180            Self::CPCMemoryOverscan(s1, ..) => Some(s1),
1181            _ => None
1182        }
1183    }
1184
1185    pub fn sprite(self) -> Option<SpriteOutput> {
1186        if let Self::Sprite(sprite) = self {
1187            Some(sprite)
1188        }
1189        else {
1190            None
1191        }
1192    }
1193
1194    /// Returns the bank that contains the second half of the screen
1195    pub fn overscan_screen2(&self) -> Option<&[u8; 0x4000]> {
1196        match self {
1197            Self::CPCMemoryOverscan(_, s1, _) => Some(s1),
1198            _ => None
1199        }
1200    }
1201
1202    /// Returns the list of tiles
1203    pub fn tiles_list(&self) -> Option<&[Vec<u8>]> {
1204        match self {
1205            Self::TilesList { list, .. } => Some(list),
1206            _ => None
1207        }
1208    }
1209}
1210
1211/// ImageConverter is able to make the conversion of images to several output format
1212#[derive(Debug, Clone)]
1213pub struct ImageConverter {
1214    // TODO add a crop area to not keep the complete image
1215    // cropArea: Option<???>
1216    /// A palette can be specified
1217    palette: Option<Palette>,
1218
1219    /// Screen mode
1220    mode: Mode,
1221
1222    /// Output format
1223    output: OutputFormat,
1224
1225    /// List of transformations
1226    transformations: TransformationsList,
1227
1228    /// Crop image if too large in comparison to result screen
1229    crop_if_too_large: bool
1230}
1231
1232#[allow(missing_docs)]
1233impl ImageConverter {
1234    /// Create the object that will be used to make the conversion
1235    pub fn convert<P>(
1236        input_file: P,
1237        palette: Option<Palette>,
1238        mode: Mode,
1239        transformations: TransformationsList,
1240        output: OutputFormat,
1241        crop_if_too_large: bool,
1242        missing_pen: Option<Pen>
1243    ) -> anyhow::Result<Output>
1244    where
1245        P: AsRef<Utf8Path>
1246    {
1247        Self::convert_impl(
1248            input_file.as_ref(),
1249            palette,
1250            mode,
1251            transformations,
1252            output,
1253            crop_if_too_large,
1254            missing_pen
1255        )
1256    }
1257
1258    fn convert_to_sprite(
1259        input_file: &Utf8Path,
1260        palette: Option<Palette>,
1261        mode: Mode,
1262        transformations: TransformationsList,
1263        encoding: SpriteEncoding,
1264        crop_if_too_large: bool,
1265        missing_pen: Option<Pen>
1266    ) -> anyhow::Result<SpriteOutput> {
1267        match &encoding {
1268            SpriteEncoding::Linear => {
1269                let mut converter = ImageConverter {
1270                    palette: palette.clone(),
1271                    mode,
1272                    transformations: transformations.clone(),
1273                    output: OutputFormat::Sprite(encoding),
1274                    crop_if_too_large
1275                };
1276
1277                let sprite = converter.load_sprite(input_file, missing_pen);
1278                converter
1279                    .apply_sprite_conversion(&sprite)
1280                    .map(|output| output.sprite().unwrap())
1281            },
1282
1283            SpriteEncoding::ZigZagGrayCoded => {
1284                let SpriteOutput {
1285                    data,
1286                    palette,
1287                    bytes_width,
1288                    height,
1289                    encoding,
1290                    mode
1291                } = Self::convert_impl(
1292                    input_file,
1293                    palette,
1294                    mode,
1295                    transformations,
1296                    OutputFormat::Sprite(SpriteEncoding::GrayCoded),
1297                    crop_if_too_large,
1298                    missing_pen
1299                )?
1300                .sprite()
1301                .unwrap();
1302
1303                assert_eq!(encoding, SpriteEncoding::GrayCoded);
1304
1305                let mut new_data = Vec::new();
1306                new_data.reserve_exact(data.len());
1307
1308                for j in 0..height {
1309                    let mut current_line = data[j * bytes_width..(j + 1) * bytes_width].to_vec();
1310
1311                    if j % 2 == 1 {
1312                        current_line.reverse();
1313                    }
1314
1315                    new_data.extend(current_line);
1316                }
1317
1318                Ok(SpriteOutput {
1319                    data: new_data,
1320                    palette,
1321                    bytes_width,
1322                    height,
1323                    encoding: SpriteEncoding::ZigZagGrayCoded,
1324                    mode
1325                })
1326            },
1327
1328            SpriteEncoding::GrayCoded => {
1329                // get the linear version
1330                let linear = Self::convert_impl(
1331                    input_file,
1332                    palette,
1333                    mode,
1334                    transformations,
1335                    OutputFormat::Sprite(SpriteEncoding::Linear),
1336                    crop_if_too_large,
1337                    missing_pen
1338                )?
1339                .sprite()
1340                .unwrap();
1341
1342                assert_eq!(linear.encoding, SpriteEncoding::Linear);
1343
1344                assert_eq!(linear.height() % 8, 0);
1345
1346                let nb_chars = linear.height() / 8;
1347                let mut new_data = Vec::new();
1348                for char_idx in 0..nb_chars {
1349                    for line_idx in GrayCodeLineCounter::GRAYCODE_INDEX_TO_SCREEN_INDEX.iter() {
1350                        let line_idx = *line_idx as usize;
1351                        let start = line_idx + 8 * char_idx;
1352                        new_data.extend_from_slice(
1353                            &linear.data()
1354                                [start * linear.bytes_width()..(start + 1) * linear.bytes_width()]
1355                        );
1356                    }
1357                }
1358
1359                Ok(SpriteOutput {
1360                    encoding: SpriteEncoding::GrayCoded,
1361                    mode: linear.mode,
1362                    data: new_data,
1363                    palette: linear.palette,
1364                    bytes_width: linear.bytes_width,
1365                    height: linear.height
1366                })
1367            },
1368            SpriteEncoding::LeftToRightToLeft => unimplemented!()
1369        }
1370    }
1371
1372    fn convert_impl(
1373        input_file: &Utf8Path,
1374        palette: Option<Palette>,
1375        mode: Mode,
1376        transformations: TransformationsList,
1377        output: OutputFormat,
1378        crop_if_too_large: bool,
1379        missing_pen: Option<Pen>
1380    ) -> anyhow::Result<Output> {
1381        let mut converter = ImageConverter {
1382            palette: palette.clone(),
1383            mode,
1384            transformations: transformations.clone(),
1385            output: output.clone(),
1386            crop_if_too_large
1387        };
1388
1389        if let OutputFormat::LinearEncodedChuncky = &output {
1390            let mut matrix = converter.load_color_matrix(input_file);
1391            matrix.double_horizontally();
1392            let sprite = matrix.as_sprite(mode, None, None);
1393            Ok(Output::LinearEncodedChuncky {
1394                data: sprite.to_linear_vec(),
1395                palette: sprite.palette.as_ref().unwrap().clone(), /* By definition, we expect the palette to be set */
1396                bytes_width: sprite.bytes_width() as _,
1397                height: sprite.height() as _
1398            })
1399        }
1400        else if let OutputFormat::Sprite(sprite_output_format) = &output {
1401            Self::convert_to_sprite(
1402                input_file,
1403                palette,
1404                mode,
1405                transformations,
1406                *sprite_output_format,
1407                crop_if_too_large,
1408                missing_pen
1409            )
1410            .map(Output::Sprite)
1411        }
1412        else if let OutputFormat::MaskedSprite {
1413            sprite_format,
1414            mask_ink,
1415            replacement_ink
1416        } = &output
1417        {
1418            let sprite_transformations =
1419                transformations.clone().replace(*mask_ink, *replacement_ink);
1420            let sprite = Self::convert_to_sprite(
1421                input_file,
1422                palette,
1423                mode,
1424                sprite_transformations,
1425                *sprite_format,
1426                crop_if_too_large,
1427                missing_pen
1428            )?;
1429
1430            let mask_transformations = transformations
1431                .clone()
1432                .build_mask_from_background_ink(*mask_ink);
1433            let mut mask_palette = vec![ColorMatrix::INK_NOT_USED_IN_MASK; 16];
1434            mask_palette[0] = ColorMatrix::INK_MASK_FOREGROUND; // at the position with all bits reset
1435            mask_palette[mode.max_colors() - 1] = ColorMatrix::INK_MASK_BACKGROUND; // at the position with all bits set up
1436            let mask = Self::convert_to_sprite(
1437                input_file,
1438                Some(mask_palette.into()), // we want and 0 ; or byte where we plot
1439                mode,
1440                mask_transformations,
1441                *sprite_format,
1442                crop_if_too_large,
1443                missing_pen
1444            )?;
1445
1446            // Just for debug stuff
1447            if true {
1448                let mask_img = mask.as_sprite().as_image();
1449                let sprite_img = sprite.as_sprite().as_image();
1450                mask_img.save_with_format("/tmp/mask.png", image::ImageFormat::Png);
1451                sprite_img.save_with_format("/tmp/sprite.png", image::ImageFormat::Png);
1452            }
1453
1454            Ok(Output::SpriteAndMask { sprite, mask })
1455        }
1456        else {
1457            let sprite = converter.load_sprite(input_file, missing_pen);
1458            converter.apply_sprite_conversion(&sprite)
1459        }
1460    }
1461
1462    /// Makes the conversion of the provided sprite to the expected format
1463    pub fn import(sprite: &Sprite, output: OutputFormat) -> anyhow::Result<Output> {
1464        let mut converter = ImageConverter {
1465            palette: None,
1466            mode: Mode::Zero, // TODO make the mode an optional argument,
1467            output,
1468            transformations: TransformationsList::default(),
1469            crop_if_too_large: false
1470        };
1471
1472        converter.apply_sprite_conversion(sprite)
1473    }
1474
1475    /// Load the initial image
1476    /// TODO make compatibility tests are alike
1477    /// TODO propagate errors when needed
1478    fn load_sprite(&mut self, input_file: &Utf8Path, missing_pen: Option<Pen>) -> Sprite {
1479        let matrix = self.load_color_matrix(input_file);
1480        let sprite = matrix.as_sprite(self.mode, self.palette.clone(), missing_pen);
1481        self.palette = sprite.palette();
1482
1483        sprite
1484    }
1485
1486    fn load_color_matrix(&self, input_file: &Utf8Path) -> ColorMatrix {
1487        let img = im::open(input_file)
1488            .unwrap_or_else(|_| panic!("Unable to convert {input_file:?} properly."));
1489        let mat = ColorMatrix::convert(&img.to_rgb8(), ConversionRule::AnyModeUseAllPixels);
1490        self.transformations.apply(&mat)
1491    }
1492
1493    /// Manage the conversion on the given sprite
1494    fn apply_sprite_conversion(&mut self, sprite: &Sprite) -> anyhow::Result<Output> {
1495        let output = self.output.clone();
1496
1497        match output {
1498            OutputFormat::Sprite(SpriteEncoding::Linear) => {
1499                self.linearize_sprite(sprite).map(Output::Sprite)
1500            },
1501            OutputFormat::CPCMemory {
1502                ref output_dimension,
1503                ref display_address
1504            } => self.build_memory_blocks(sprite, *output_dimension, *display_address),
1505            OutputFormat::CPCSplittingMemory(ref _vec) => unimplemented!(),
1506            OutputFormat::TileEncoded {
1507                tile_width,
1508                tile_height,
1509                horizontal_movement,
1510                vertical_movement,
1511                grid_width,
1512                grid_height
1513            } => {
1514                self.extract_tiles(
1515                    tile_width,
1516                    tile_height,
1517                    horizontal_movement,
1518                    vertical_movement,
1519                    grid_width,
1520                    grid_height,
1521                    sprite
1522                )
1523            },
1524
1525            _ => unreachable!()
1526        }
1527    }
1528
1529    /// Produce the linearized version of the sprite.
1530    /// TODO add size constraints to keep a small part of the sprite
1531    fn linearize_sprite(&mut self, sprite: &Sprite) -> anyhow::Result<SpriteOutput> {
1532        Ok(SpriteOutput {
1533            encoding: SpriteEncoding::Linear,
1534            mode: sprite.mode.unwrap(),
1535            data: sprite.to_linear_vec(),
1536            palette: sprite.palette.as_ref().unwrap().clone(), /* By definition, we expect the palette to be set */
1537            bytes_width: sprite.bytes_width() as _,
1538            height: sprite.height() as _
1539        })
1540    }
1541
1542    #[allow(clippy::too_many_arguments)]
1543    fn extract_tiles(
1544        &mut self,
1545        tile_width: TileWidthCapture,
1546        tile_height: TileHeightCapture,
1547        horizontal_movement: TileHorizontalCapture,
1548        vertical_movement: TileVerticalCapture,
1549        grid_width: GridWidthCapture,
1550        grid_height: GridHeightCapture,
1551        sprite: &Sprite
1552    ) -> anyhow::Result<Output> {
1553        // Compute the real value of the arguments
1554        let tile_width = match tile_width {
1555            TileWidthCapture::FullWidth => sprite.bytes_width(),
1556            TileWidthCapture::NbBytes(nb) => nb as _
1557        };
1558        let tile_height = match tile_height {
1559            TileHeightCapture::FullHeight => sprite.height(),
1560            TileHeightCapture::NbLines(nb) => nb as _
1561        };
1562        let nb_columns = match grid_width {
1563            GridWidthCapture::TilesInRow(nb) => nb,
1564            GridWidthCapture::FullWidth => sprite.bytes_width() as usize / tile_width as usize
1565        };
1566        let nb_rows = match grid_height {
1567            GridHeightCapture::TilesInColumn(nb) => nb,
1568            GridHeightCapture::FullHeight => sprite.height() as usize / tile_height as usize
1569        };
1570
1571        if (sprite.height() as usize) < tile_height as usize * nb_rows {
1572            return Err(anyhow::anyhow!(
1573                "{} lines expected on a tileset of {} lines.",
1574                tile_height as usize * nb_rows,
1575                sprite.height()
1576            ));
1577        }
1578        if (sprite.bytes_width() as usize) < tile_width as usize * nb_columns {
1579            return Err(anyhow::anyhow!(
1580                "{} byte-columns  expected on a tileset of {} byte-columns.",
1581                tile_width as usize * nb_columns,
1582                sprite.bytes_width()
1583            ));
1584        }
1585
1586        // Really makes the extraction
1587        let mut tiles_list: Vec<Vec<u8>> = Vec::new();
1588        for row in 0..nb_rows {
1589            for column in 0..nb_columns {
1590                // TODO add an additional parameter to read x before y
1591                // Manage the sprite in this cell
1592                let mut y_counter = vertical_movement.counter();
1593                let mut x_counter = horizontal_movement.counter();
1594                let mut current_tile: Vec<u8> = Vec::new();
1595                for _y in 0..tile_height {
1596                    for x in 0..tile_width {
1597                        // Get the line of interest
1598                        let real_line =
1599                            row * tile_height as usize + y_counter.get_line_index_in_screen();
1600
1601                        let real_col = column * tile_width as usize + x_counter.get_column_index();
1602                        if x != tile_width - 1 {
1603                            x_counter.next();
1604                        }
1605
1606                        // Get the byte from the sprite ...
1607                        //    dbg!(real_line, real_col);
1608                        let byte: u8 = sprite.get_byte(real_col, real_line);
1609
1610                        // ... and store it at the right place
1611                        current_tile.push(byte);
1612                    }
1613                    x_counter.line_ended();
1614                    y_counter.next();
1615                }
1616                tiles_list.push(current_tile);
1617            }
1618        }
1619
1620        // build the object to return
1621        Ok(Output::TilesList {
1622            tile_height,
1623            tile_width,
1624            horizontal_movement,
1625            vertical_movement,
1626            palette: sprite.palette().unwrap(),
1627            list: tiles_list
1628        })
1629    }
1630
1631    /// Manage the creation of the memory blocks
1632    /// XXX Warning, overscan is wrongly used, it is more fullscreen with 2 pages
1633    fn build_memory_blocks(
1634        &mut self,
1635        sprite: &Sprite,
1636        dim: CPCScreenDimension,
1637        display_address: DisplayAddress
1638    ) -> anyhow::Result<Output> {
1639        let screen_width = u32::from(dim.width(sprite.mode().unwrap()));
1640        let screen_height = u32::from(dim.height());
1641
1642        // Check if the destination is compatible
1643        if screen_width < sprite.pixel_width() {
1644            if !self.crop_if_too_large {
1645                return Err(anyhow::anyhow!(
1646                    "The image width ({}) is larger than the cpc screen width ({})",
1647                    sprite.pixel_width(),
1648                    screen_width
1649                ));
1650            }
1651        }
1652        else if screen_width > sprite.pixel_width() {
1653            eprintln!(
1654                "[Warning] The image width ({}) is smaller than the cpc screen width ({})",
1655                sprite.pixel_width(),
1656                screen_width
1657            );
1658        }
1659
1660        if screen_height < sprite.height() {
1661            if !self.crop_if_too_large {
1662                return Err(anyhow::anyhow!(
1663                    "The image height ({}) is larger than the cpc screen height ({})",
1664                    sprite.height(),
1665                    screen_height
1666                ));
1667            }
1668        }
1669        else if screen_height > sprite.height() {
1670            eprintln!(
1671                "[Warning] The image height ({}) is smaller than the cpc screen height ({})",
1672                sprite.height(),
1673                screen_height
1674            );
1675        }
1676
1677        // Simulate the memory
1678        let mut pages = [[0; 0x4000], [0; 0x4000], [0; 0x4000], [0; 0x4000]];
1679
1680        let mut used_pages = HashSet::new();
1681        let is_overscan = dim.use_two_banks();
1682        if !is_overscan && display_address.is_overscan() {
1683            return Err(anyhow::anyhow!(
1684                "Image requires an overscan configuration for R12/R13={:?}",
1685                display_address
1686            ));
1687        }
1688
1689        let mut current_address = display_address;
1690        used_pages.insert(current_address.page());
1691
1692        // loop over the chars vertically
1693        for char_y in 0..dim
1694            .nb_char_lines()
1695            .min((sprite.height() as f32 / 8_f32).ceil() as u8)
1696        {
1697            let char_y = char_y as usize;
1698
1699            // loop over the chars horiontally (2 bytes)
1700            for char_x in 0..dim.nb_word_columns() {
1701                let char_x = char_x as usize;
1702
1703                // Loop over the lines of the current char (8 lines for a standard screen)
1704                for line_in_char in 0..dim.nb_lines_per_char() {
1705                    let line_in_char = line_in_char as usize;
1706
1707                    // Loop over the bytes of the current char
1708                    for byte_nb in 0..2 {
1709                        let x_coord = 2 * char_x + byte_nb;
1710                        let y_coord = dim.nb_lines_per_char() as usize * char_y + line_in_char;
1711
1712                        let value = sprite.get_byte_safe(x_coord, y_coord);
1713                        // let value = Some(sprite.get_byte(x_coord as _, y_coord as _));
1714
1715                        match value {
1716                            None => {
1717                                // eprintln!("Unable to access byte in {}, {}", x_coord, y_coord);
1718                            },
1719                            Some(byte) => {
1720                                let page = current_address.page() as usize;
1721                                let address = current_address.offset() as usize * 2
1722                                    + byte_nb
1723                                    + line_in_char * 0x800;
1724
1725                                pages[page][address] = byte;
1726                            }
1727                        };
1728                    }
1729                }
1730
1731                // Manage the next word (on the same line or not)
1732                current_address.move_to_next_word();
1733                used_pages.insert(current_address.page());
1734            }
1735        }
1736
1737        // By construction, the order should be good
1738        let used_pages = used_pages
1739            .iter()
1740            .sorted()
1741            .map(|idx| pages[*idx as usize])
1742            .collect::<Vec<_>>();
1743
1744        if is_overscan && used_pages.len() != 2 {
1745            // return Err(anyhow::anyhow!(
1746            // "An overscan screen is requested but {} pages has been feed",
1747            // used_pages.len()
1748            // ));
1749
1750            eprintln!(
1751                "An overscan screen is requested but {} pages has been feed",
1752                used_pages.len()
1753            ); // TODO need to investigate
1754        }
1755
1756        // Generate the right output format
1757        let palette = sprite.palette().unwrap();
1758        if is_overscan {
1759            Ok(Output::CPCMemoryOverscan(
1760                used_pages[0],
1761                used_pages[1],
1762                palette
1763            ))
1764        }
1765        else {
1766            Ok(Output::CPCMemoryStandard(used_pages[0], palette))
1767        }
1768    }
1769}
1770
1771#[cfg(test)]
1772mod tests {
1773    use crate::convert::*;
1774
1775    #[test]
1776    fn overscan_test() {
1777        assert!(CPCScreenDimension::overscan().use_two_banks());
1778        assert!(!CPCScreenDimension::standard().use_two_banks());
1779    }
1780
1781    #[test]
1782    fn manipulation_test() {
1783        let mut address = DisplayAddress::new_from(0x3000);
1784
1785        assert_eq!(address.address(), 0xC000);
1786        assert_eq!(address.r12(), 0x30);
1787        assert_eq!(address.r13(), 0x00);
1788        assert!(!address.is_overscan());
1789
1790        address.set_page(1);
1791        assert_eq!(address.page(), 1);
1792        assert_eq!(address.address(), 0x4000);
1793
1794        address.move_to_next_word();
1795        assert_eq!(address.address(), 0x4002);
1796    }
1797
1798    #[test]
1799    fn test_masking() {
1800        let fg1 = Ink::BLUE;
1801        let fg2 = Ink::SKY_BLUE;
1802        let fg3 = Ink::PASTEL_BLUE;
1803        let fg4 = Ink::BRIGHT_BLUE;
1804        let bg_ = Ink::RED;
1805        let rep = Ink::BLACK;
1806
1807        let sprite_with_mask = ColorMatrix::from(vec![
1808            vec![bg_, fg2, fg3, fg4],
1809            vec![bg_, bg_, fg1, fg2],
1810            vec![bg_, bg_, bg_, fg3],
1811            vec![bg_, bg_, bg_, fg4],
1812            vec![bg_, bg_, bg_, bg_],
1813        ]);
1814
1815        let transformation_sprite = Transformation::ReplaceInk { from: bg_, to: rep };
1816        let transformation_mask = Transformation::MaskFromBackgroundInk(bg_);
1817
1818        let sprite = transformation_sprite.apply(&sprite_with_mask);
1819        let mask = transformation_mask.apply(&sprite_with_mask);
1820
1821        let (mask2, sprite2) = sprite_with_mask.extract_mask_and_sprite(bg_, rep);
1822
1823        assert_eq!(mask, mask2);
1824        assert_eq!(sprite, sprite2);
1825    }
1826}