1use 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#[derive(Copy, Clone, Debug)]
18pub enum TransformationPosition {
19 First,
21 Last,
23 Index(usize)
25}
26
27impl TransformationPosition {
28 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
45pub type TransformationLinePosition = TransformationPosition;
47pub type TransformationColumnPosition = TransformationPosition;
49
50#[derive(Clone, Debug)]
52pub enum Transformation {
53 SkipOddPixels,
55 SkipLeftPixelColumns(u16),
57 SkipTopPixelLines(u16),
59 KeepLeftPixelColumns(u16),
60 KeepTopPixelLines(u16),
61 BlankLines {
63 pattern: Vec<Ink>,
65 position: TransformationPosition,
67 amount: u16
69 },
70
71 BlankColumns {
73 pattern: Vec<Ink>,
75 position: TransformationPosition,
77 amount: u16
79 },
80
81 ReplaceInk {
83 from: Ink,
84 to: Ink
85 },
86
87 MaskFromBackgroundInk(Ink)
89}
90
91impl Transformation {
92 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 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 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 let position = position.absolute_position(matrix.height() as _).unwrap();
148
149 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 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 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#[derive(Clone, Debug, Default)]
221pub struct TransformationsList {
222 transformations: Vec<Transformation>
224}
225
226#[allow(missing_docs)]
227impl TransformationsList {
228 pub fn new(transformations: &[Transformation]) -> Self {
230 TransformationsList {
231 transformations: transformations.to_vec()
232 }
233 }
234
235 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 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#[derive(Clone, Copy)]
289pub struct CPCScreenDimension {
290 pub horizontal_displayed: u8,
292 pub vertical_displayed: u8,
294 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 pub fn standard() -> Self {
315 Self {
316 horizontal_displayed: 80 / 2,
317 vertical_displayed: 25,
318 maximum_raster_address: 7
320 }
321 }
322
323 pub fn overscan() -> Self {
325 Self {
326 horizontal_displayed: 96 / 2,
327 vertical_displayed: 39, maximum_raster_address: 7
329 }
330 }
331
332 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 pub fn nb_lines_per_char(self) -> u8 {
347 1 + self.maximum_raster_address
348 }
349
350 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 pub fn nb_byte_columns(self) -> u8 {
361 self.nb_word_columns() * 2
362 }
363
364 pub fn height(self) -> u16 {
366 u16::from(self.nb_char_lines()) * u16::from(self.nb_lines_per_char())
367 }
368
369 pub fn width(self, mode: Mode) -> u16 {
371 u16::from(self.nb_byte_columns()) * mode.nb_pixels_per_byte() as u16
372 }
373
374 pub fn use_two_banks(self) -> bool {
376 u16::from(self.nb_byte_columns()) * self.height() > 0x4000
377 }
378}
379
380#[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 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 pub fn new_overscan_from_page(page: u16) -> Self {
419 Self::new(page, true, 0)
420 }
421
422 pub fn new_overscan_from_page_one_bank_per_line(page: u16, char_width: u16) -> Self {
424 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 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 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 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 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 pub fn is_overscan(self) -> bool {
504 match self.buffer() {
505 0..=2 => false,
506 3 => true,
507 _ => panic!()
508 }
509 }
510
511 pub fn address(self) -> u16 {
513 self.page_start() + self.offset() * 2
514 }
515
516 pub fn move_to_previous_word(&mut self) {
518 unimplemented!()
519 }
520
521 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 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 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#[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 LinearEncodedChuncky,
565
566 CPCMemory {
568 output_dimension: CPCScreenDimension,
569 display_address: DisplayAddress
570 },
571
572 CPCSplittingMemory(Vec<OutputFormat>),
574
575 TileEncoded {
577 tile_width: TileWidthCapture,
579 tile_height: TileHeightCapture,
581 horizontal_movement: TileHorizontalCapture,
583 vertical_movement: TileVerticalCapture,
585 grid_width: GridWidthCapture,
587 grid_height: GridHeightCapture
589 }
590}
591
592#[allow(missing_docs)]
593impl OutputFormat {
594 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 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 pub fn create_overscan_cpc_memory() -> Self {
629 Self::CPCMemory {
630 output_dimension: CPCScreenDimension::overscan(),
631 display_address: DisplayAddress::new_overscan_from_page(2) }
633 }
634
635 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#[derive(Debug, Clone, Copy)]
658pub enum TileWidthCapture {
659 FullWidth,
661 NbBytes(usize)
663}
664
665#[derive(Debug, Clone, Copy)]
667pub enum TileHeightCapture {
668 FullHeight,
670 NbLines(usize)
672}
673
674#[derive(Debug, Clone, Copy)]
676pub enum GridWidthCapture {
677 FullWidth,
679 TilesInRow(usize)
681}
682
683#[derive(Debug, Clone, Copy)]
685pub enum GridHeightCapture {
686 FullHeight,
688 TilesInColumn(usize)
690}
691
692#[derive(Debug, Clone, Copy)]
694pub enum TileHorizontalCapture {
695 AlwaysFromLeftToRight,
697 AlwaysFromRightToLeft,
699 StartFromRightAndFlipAtTheEndOfLine,
701 StartFromLeftAndFlipAtTheEndOfLine
703}
704
705#[allow(missing_docs)]
706pub trait HorizontalWordCounter {
707 fn get_column_index(&self) -> usize {
708 unimplemented!()
709 }
710 fn next(&mut self) {
712 unimplemented!();
713 }
714
715 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#[derive(Debug, Copy, Clone)]
759pub struct StandardHorizontalCounter {
760 left_to_right: bool,
761 current_step: usize,
762 nb_columns: Option<std::num::NonZeroUsize>
764}
765
766impl StandardHorizontalCounter {
767 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 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#[derive(Debug, Copy, Clone)]
848#[allow(missing_docs)]
849#[derive(Default)]
850pub struct GrayCodeLineCounter {
851 char_line: usize,
852 pos_in_char: u8 }
854
855#[derive(Copy, Clone, Debug)]
857#[allow(missing_docs)]
858pub struct StandardLineCounter {
859 pos_in_screen: usize,
860 top_to_bottom: bool
861}
862
863pub trait LineCounter {
865 fn get_line_index_in_screen(&self) -> usize;
867
868 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 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 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#[derive(Debug, Clone, Copy)]
962pub enum TileVerticalCapture {
963 AlwaysFromTopToBottom,
965 AlwaysFromBottomToTop,
967 StartFromTopAndFlipAtEndOfScreen,
969 StartFromBottomAndFlipAtEndOfScreen,
971 GrayCodeFromTop,
973 GrayCodeFromBottom
975}
976
977#[allow(missing_docs)]
978impl TileVerticalCapture {
979 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#[allow(missing_docs)]
1093#[allow(clippy::large_enum_variant)]
1094pub enum Output {
1095 SpriteAndMask {
1097 sprite: SpriteOutput,
1098 mask: SpriteOutput
1099 },
1100
1101 Sprite(SpriteOutput),
1103
1104 LinearEncodedChuncky {
1105 data: Vec<u8>,
1106 palette: Palette,
1107 bytes_width: usize,
1108 height: usize
1109 },
1110
1111 CPCMemoryStandard([u8; 0x4000], Palette),
1113
1114 CPCMemoryOverscan([u8; 0x4000], [u8; 0x4000], Palette),
1116
1117 CPCSplittingMemory(Vec<Output>),
1119
1120 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 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 pub fn overscan_screen2(&self) -> Option<&[u8; 0x4000]> {
1196 match self {
1197 Self::CPCMemoryOverscan(_, s1, _) => Some(s1),
1198 _ => None
1199 }
1200 }
1201
1202 pub fn tiles_list(&self) -> Option<&[Vec<u8>]> {
1204 match self {
1205 Self::TilesList { list, .. } => Some(list),
1206 _ => None
1207 }
1208 }
1209}
1210
1211#[derive(Debug, Clone)]
1213pub struct ImageConverter {
1214 palette: Option<Palette>,
1218
1219 mode: Mode,
1221
1222 output: OutputFormat,
1224
1225 transformations: TransformationsList,
1227
1228 crop_if_too_large: bool
1230}
1231
1232#[allow(missing_docs)]
1233impl ImageConverter {
1234 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 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(), 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; mask_palette[mode.max_colors() - 1] = ColorMatrix::INK_MASK_BACKGROUND; let mask = Self::convert_to_sprite(
1437 input_file,
1438 Some(mask_palette.into()), mode,
1440 mask_transformations,
1441 *sprite_format,
1442 crop_if_too_large,
1443 missing_pen
1444 )?;
1445
1446 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 pub fn import(sprite: &Sprite, output: OutputFormat) -> anyhow::Result<Output> {
1464 let mut converter = ImageConverter {
1465 palette: None,
1466 mode: Mode::Zero, output,
1468 transformations: TransformationsList::default(),
1469 crop_if_too_large: false
1470 };
1471
1472 converter.apply_sprite_conversion(sprite)
1473 }
1474
1475 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 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 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(), 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 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 let mut tiles_list: Vec<Vec<u8>> = Vec::new();
1588 for row in 0..nb_rows {
1589 for column in 0..nb_columns {
1590 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 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 let byte: u8 = sprite.get_byte(real_col, real_line);
1609
1610 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 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 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 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 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 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 for char_x in 0..dim.nb_word_columns() {
1701 let char_x = char_x as usize;
1702
1703 for line_in_char in 0..dim.nb_lines_per_char() {
1705 let line_in_char = line_in_char as usize;
1706
1707 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 match value {
1716 None => {
1717 },
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 current_address.move_to_next_word();
1733 used_pages.insert(current_address.page());
1734 }
1735 }
1736
1737 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 eprintln!(
1751 "An overscan screen is requested but {} pages has been feed",
1752 used_pages.len()
1753 ); }
1755
1756 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}