Skip to main content

cpclib_imgconverter/
lib.rs

1use std::fs::File;
2use std::io::Write;
3
4use anyhow::{self, Error};
5use camino_tempfile as tempfile;
6use clap::{Arg, ArgAction, ArgMatches, Command, value_parser};
7use cpclib::asm::preamble::defb_elements;
8use cpclib::asm::{ListingExt, assemble, assemble_to_amsdos_file};
9use cpclib::common::camino::{Utf8Path, Utf8PathBuf};
10use cpclib::common::{clap, clap_parse_any_positive_number};
11use cpclib::disc::amsdos::*;
12use cpclib::disc::disc::Disc;
13use cpclib::disc::edsk::Head;
14use cpclib::image::convert::*;
15use cpclib::image::ga::Palette;
16use cpclib::image::ocp::{self, OcpPal};
17use cpclib::sna::*;
18#[cfg(feature = "xferlib")]
19use cpclib::xfer::CpcXfer;
20use cpclib::{ExtendedDsk, Ink, Pen, sna};
21#[cfg(feature = "watch")]
22use notify::{RecommendedWatcher, RecursiveMode, Watcher};
23
24pub mod built_info {
25    include!(concat!(env!("OUT_DIR"), "/built.rs"));
26}
27
28pub fn clap_parse_ink(arg: &str) -> Result<Ink, String> {
29    let nb = clap_parse_any_positive_number(arg)?;
30    if nb > 27 {
31        Err(format!("{nb} is not a valid ink value"))
32    }
33    else {
34        Ok(nb.into())
35    }
36}
37
38#[macro_export]
39macro_rules! specify_palette {
40
41    ($e:expr) => {
42        $e.arg(
43            Arg::new("OCP_PAL")
44            .long("pal")
45            .required(false)
46            .help("OCP PAL file. The first palette among 12 is used") // TODO specify a way to select any palette
47            .value_parser(|p: &str| cpclib::common::utf8pathbuf_value_parser(true)(p))
48        )
49        .arg(
50            Arg::new("PENS")
51                .long("pens")
52                .required(false)
53                .help("Separated list of ink number. Use ',' as a separater")
54                .conflicts_with("OCP_PAL")
55        )
56        .arg(
57            Arg::new("PEN0")
58                .long("pen0")
59                .required(false)
60                .help("Ink number of the pen 0")
61                .conflicts_with("PENS")
62                .conflicts_with("OCP_PAL")
63                .value_parser(value_parser!(u8))
64        )
65        .arg(
66            Arg::new("PEN1")
67                .long("pen1")
68                .required(false)
69                .help("Ink number of the pen 1")
70                .conflicts_with("PENS")
71                .conflicts_with("OCP_PAL")
72                .value_parser(value_parser!(u8))
73        )
74        .arg(
75            Arg::new("PEN2")
76                .long("pen2")
77                .required(false)
78                .help("Ink number of the pen 2")
79                .conflicts_with("PENS")
80                .conflicts_with("OCP_PAL")
81                .value_parser(value_parser!(u8))
82        )
83        .arg(
84            Arg::new("PEN3")
85                .long("pen3")
86                .required(false)
87                .help("Ink number of the pen 3")
88                .conflicts_with("PENS")
89                .conflicts_with("OCP_PAL")
90                .value_parser(value_parser!(u8))
91        )
92        .arg(
93            Arg::new("PEN4")
94                .long("pen4")
95                .required(false)
96                .help("Ink number of the pen 4")
97                .conflicts_with("PENS")
98                .conflicts_with("OCP_PAL")
99                .value_parser(value_parser!(u8))
100        )
101        .arg(
102            Arg::new("PEN5")
103                .long("pen5")
104                .required(false)
105                .help("Ink number of the pen 5")
106                .conflicts_with("PENS")
107                .conflicts_with("OCP_PAL")
108                .value_parser(value_parser!(u8))
109        )
110        .arg(
111            Arg::new("PEN6")
112                .long("pen6")
113                .required(false)
114                .help("Ink number of the pen 6")
115                .conflicts_with("PENS")
116                .conflicts_with("OCP_PAL")
117                .value_parser(value_parser!(u8))
118        )
119        .arg(
120            Arg::new("PEN7")
121                .long("pen7")
122                .required(false)
123                .help("Ink number of the pen 7")
124                .conflicts_with("PENS")
125                .conflicts_with("OCP_PAL")
126                .value_parser(value_parser!(u8))
127        )
128        .arg(
129            Arg::new("PEN8")
130                .long("pen8")
131                .required(false)
132                .help("Ink number of the pen 8")
133                .conflicts_with("PENS")
134                .conflicts_with("OCP_PAL")
135                .value_parser(value_parser!(u8))
136        )
137        .arg(
138            Arg::new("PEN9")
139                .long("pen9")
140                .required(false)
141                .help("Ink number of the pen 9")
142                .conflicts_with("PENS")
143                .conflicts_with("OCP_PAL")
144                .value_parser(value_parser!(u8))
145        )
146        .arg(
147            Arg::new("PEN10")
148                .long("pen10")
149                .required(false)
150                .help("Ink number of the pen 10")
151                .conflicts_with("PENS")
152                .conflicts_with("OCP_PAL")
153                .value_parser(value_parser!(u8))
154        )
155        .arg(
156            Arg::new("PEN11")
157                .long("pen11")
158                .required(false)
159                .help("Ink number of the pen 11")
160                .conflicts_with("PENS")
161                .conflicts_with("OCP_PAL")
162                .value_parser(value_parser!(u8))
163        )
164        .arg(
165            Arg::new("PEN12")
166                .long("pen12")
167                .required(false)
168                .help("Ink number of the pen 12")
169                .conflicts_with("PENS")
170                .conflicts_with("OCP_PAL")
171                .value_parser(value_parser!(u8))
172        )
173        .arg(
174            Arg::new("PEN13")
175                .long("pen13")
176                .required(false)
177                .help("Ink number of the pen 13")
178                .conflicts_with("PENS")
179                .conflicts_with("OCP_PAL")
180                .value_parser(value_parser!(u8))
181        )
182        .arg(
183            Arg::new("PEN14")
184                .long("pen14")
185                .required(false)
186                .help("Ink number of the pen 14")
187                .conflicts_with("PENS")
188                .conflicts_with("OCP_PAL")
189                .value_parser(value_parser!(u8))
190        )
191        .arg(
192            Arg::new("PEN15")
193                .long("pen15")
194                .required(false)
195                .help("Ink number of the pen 15")
196                .conflicts_with("PENS")
197                .conflicts_with("OCP_PAL")
198                .value_parser(value_parser!(u8))
199        )
200    };
201}
202
203pub fn get_requested_palette(matches: &ArgMatches) -> Result<Option<Palette>, AmsdosError> {
204    if matches.contains_id("PENS") {
205        let numbers = matches
206            .get_one::<String>("PENS")
207            .unwrap()
208            .split(",")
209            .map(|ink| ink.parse::<u8>().unwrap())
210            .collect::<Vec<_>>();
211        Ok(Some(numbers.into()))
212    }
213    else if let Some(fname) = matches.get_one::<Utf8PathBuf>("OCP_PAL") {
214        let (mut data, header) = cpclib::disc::read(fname)?; // get the file content but skip the header
215        let data = data.make_contiguous();
216        let pal = OcpPal::from_buffer(data);
217        Ok(Some(pal.palette(0).clone()))
218    }
219    else {
220        let mut one_pen_set = false;
221        let mut palette = Palette::empty();
222        for i in 0..16 {
223            let key = format!("PEN{i}");
224            if matches.contains_id(&key) {
225                one_pen_set = true;
226                palette.set(i, *matches.get_one::<u8>(&key).unwrap())
227            }
228        }
229
230        if one_pen_set {
231            Ok(Some(palette))
232        }
233        else {
234            Ok(None)
235        }
236    }
237}
238
239macro_rules! export_palette {
240    ($e: expr) => {
241        $e.arg(
242            Arg::new("EXPORT_PALETTE")
243                .long("palette")
244                .short('p')
245                .required(false)
246                .action(ArgAction::Set)
247                .value_parser(clap::value_parser!(Utf8PathBuf))
248                .help("Name of the binary file that contains the palette (Gate Array format)"),
249        )
250        .arg(
251            Arg::new("EXPORT_INKS")
252            .long("inks")
253            .short('i')
254            .required(false)
255            .action(ArgAction::Set)
256            .value_parser(clap::value_parser!(Utf8PathBuf))
257            .help("Name of the binary file that will contain the ink numbers (usefull for system based color change)")
258        )
259        .arg(
260            Arg::new("EXPORT_PALETTE_FADEOUT")
261                .long("palette_fadeout")
262                .required(false)
263                .action(ArgAction::Set)
264                .value_parser(clap::value_parser!(Utf8PathBuf))
265                .help("Name of the file that will contain all the steps for a fade out transition (Gate Array format)")
266        )
267        .arg(
268            Arg::new("EXPORT_INK_FADEOUT")
269                .long("ink_fadeout")
270                .required(false)
271                .action(ArgAction::Set)
272                .value_parser(clap::value_parser!(Utf8PathBuf))
273                .help("Name of the file that will contain all the steps for a fade out transition")
274        )
275    };
276}
277
278macro_rules! do_export_palette {
279    ($arg:expr, $palette:ident) => {
280        if let Some(palette_fname) = $arg.get_one::<Utf8PathBuf>("EXPORT_PALETTE") {
281            let mut file = File::create(palette_fname).expect("Unable to create the palette file");
282            let p: Vec<u8> = $palette.into();
283            file.write_all(&p).unwrap();
284        }
285
286        if let Some(fade_fname) = $arg.get_one::<Utf8PathBuf>("EXPORT_PALETTE_FADEOUT") {
287            let palettes = $palette.rgb_fadout();
288            let bytes = palettes.iter().fold(Vec::<u8>::default(), |mut acc, x| {
289                acc.extend(&x.to_gate_array_with_default(0.into()));
290                acc
291            });
292
293            assert_eq!(palettes.len() * 17, bytes.len());
294
295            let mut file = File::create(fade_fname).expect("Unable to create the fade out file");
296            file.write_all(&bytes).unwrap();
297        }
298
299        if let Some(palette_fname) = $arg.get_one::<Utf8PathBuf>("EXPORT_INKS") {
300            let mut file = File::create(palette_fname).expect("Unable to create the inks file");
301            let inks = $palette
302                .inks()
303                .iter()
304                .map(|i| i.number())
305                .collect::<Vec<_>>();
306            file.write_all(&inks).unwrap();
307        }
308
309        if let Some(fade_fname) = $arg.get_one::<Utf8PathBuf>("EXPORT_INK_FADEOUT") {
310            let palettes = $palette.rgb_fadout();
311            let bytes = palettes
312                .iter()
313                .map(|p| p.inks().iter().map(|i| i.number()).collect::<Vec<_>>())
314                .fold(Vec::default(), |mut acc, x| {
315                    acc.extend(&x);
316                    acc
317                });
318            let mut file = File::create(fade_fname).expect("Unable to create the fade out file");
319            file.write_all(&bytes).unwrap();
320        }
321    };
322}
323
324/// Compress data using lz4 algorithm.
325/// Should be decompressed on client side.
326/// TODO test: implementation has been modified without any testing...
327fn lz4_compress(bytes: &[u8]) -> Vec<u8> {
328    cpclib::crunchers::lz4::compress(bytes)
329}
330
331fn palette_code(pal: &Palette) -> String {
332    let mut asm = " ld bc, 0x7f00\n".to_string();
333    // TODO create the linker
334
335    for idx in 0..(16 / 2) {
336        asm += &format!(
337            "\tld hl, 256*{} + {} : out (c), c : out (c), h : inc c : out (c), c: out (c), l : inc c\n",
338            pal[2 * idx].gate_array(),
339            pal[2 * idx + 1].gate_array()
340        )
341    }
342
343    asm
344}
345
346fn standard_linked_code(mode: u8, pal: &Palette, screen: &[u8]) -> String {
347    let base_code = standard_display_code(mode);
348    format!(
349        "   org 0x1000
350        di
351        ld sp, $
352
353        ; Select palette
354        {palette}
355
356        ; Copy image on screen
357        ld hl, image
358        ld de, 0xc000
359        ld bc, image_end - image
360        call lz4_uncrunch
361
362        ; Copy visualization code
363        ld hl, code
364        ld de, 0x4000
365        ld bc, code_end - code
366        ldir
367
368        ei
369        jp 0x4000
370lz4_uncrunch
371    {decompressor}
372code
373    {code}
374code_end
375        assert $ < 0x4000
376image
377    {screen}
378image_end
379
380        assert $<0xc000
381    ",
382        palette = palette_code(pal),
383        decompressor = include_str!("lz4_docent.asm"),
384        code = defb_elements(&assemble(&base_code).unwrap()),
385        screen = defb_elements(screen)
386    )
387}
388
389// Produce the code that display a standard screen
390fn standard_display_code(mode: u8) -> String {
391    format!(
392        "
393        org 0x4000
394        di
395        ld bc, 0x7f00 + 0x{:x}
396        out (c), c
397        jp $
398    ",
399        match mode {
400            0 => 0x8C,
401            1 => 0x8D,
402            2 => 0x8E,
403            _ => unreachable!()
404        }
405    )
406}
407
408fn fullscreen_display_code(mode: u8, crtc_width: usize, palette: &Palette) -> String {
409    let code_mode = match mode {
410        0 => 0x8C,
411        1 => 0x8D,
412        2 => 0x8E,
413        _ => unreachable!()
414    };
415
416    let r12 = 0x20 + 0b0000_1100;
417
418    let mut palette_code = String::new();
419    palette_code += "\tld bc, 0x7f00\n";
420    for i in 0..16 {
421        palette_code += &format!(
422            "\tld a, {}\n\t out (c), c\n\tout (c), a\n\t inc c\n",
423            palette.get(i.into()).gate_array()
424        );
425    }
426
427    format!(
428        "
429        org 0x4000
430
431        di
432        ld hl, 0xc9fb
433        ld (0x38), hl
434        ld sp, $
435        ei
436
437        ld bc, 0x7f00 + 0x{code_mode:x}
438        out (c), c
439
440        ld bc, 0xbc00 + 1
441        out (c), c
442        ld bc, 0xbd00 + {crtc_width}
443        out (c), c
444
445        ld bc, 0xbc00 + 2
446        out (c), c
447        ld bc, 0xbd00 + 50
448        out (c), c
449
450        ld bc, 0xbc00 + 12
451        out (c), c
452        ld bc, 0xbd00 + {r12}
453        out (c), c
454
455        ld bc, 0xbc00 + 13
456        out (c), c
457        ld bc, 0xbd00 + 0x00
458        out (c), c
459
460        ld bc, 0xbc00 + 7
461        out (c), c
462        ld bc, 0xbd00 + 35
463        out (c), c
464
465        ld bc, 0xbc00 + 6
466        out (c), c
467        ld bc, 0xbd00 + 38
468        out (c), c
469
470        {palette_code}
471
472frame_loop
473        ld b, 0xf5
474vsync_loop
475        in a, (c)
476        rra
477        jr nc, vsync_loop
478
479
480
481
482        jp frame_loop
483    "
484    )
485}
486
487fn overscan_display_code(mode: u8, crtc_width: usize, pal: &Palette) -> String {
488    fullscreen_display_code(mode, crtc_width, pal)
489}
490
491fn parse_int(repr: &str) -> usize {
492    repr.parse::<usize>()
493        .unwrap_or_else(|_| panic!("Error when converting {repr} as integer"))
494}
495
496#[allow(clippy::if_same_then_else)] // false positive
497fn get_output_format(matches: &ArgMatches) -> OutputFormat {
498    if let Some(sprite_matches) = matches.subcommand_matches("sprite") {
499        // Get the format for the sprite encoding
500        let sprite_format = match sprite_matches.get_one::<String>("FORMAT").unwrap().as_ref() {
501            "linear" => SpriteEncoding::Linear,
502            "graycoded" => SpriteEncoding::GrayCoded,
503            "zigazag" => SpriteEncoding::LeftToRightToLeft,
504            "zigzag+graycoded" => SpriteEncoding::ZigZagGrayCoded,
505            _ => unimplemented!()
506        };
507
508        // eventually handle sprite masking
509        if sprite_matches.contains_id("MASK_FNAME") {
510            OutputFormat::MaskedSprite {
511                sprite_format,
512                mask_ink: sprite_matches.get_one::<Ink>("MASK_INK").cloned().unwrap(),
513                replacement_ink: sprite_matches
514                    .get_one::<Ink>("REPLACEMENT_INK")
515                    .cloned()
516                    .unwrap()
517            }
518        }
519        else {
520            OutputFormat::Sprite(sprite_format)
521        }
522    }
523    else if let Some(tile_matches) = matches.subcommand_matches("tile") {
524        dbg!(OutputFormat::TileEncoded {
525            tile_width: TileWidthCapture::NbBytes(parse_int(
526                tile_matches
527                    .get_one::<String>("WIDTH")
528                    .expect("--width argument missing")
529            )),
530
531            tile_height: TileHeightCapture::NbLines(parse_int(
532                tile_matches
533                    .get_one::<String>("HEIGHT")
534                    .expect("--height argument missing")
535            )),
536
537            horizontal_movement: TileHorizontalCapture::AlwaysFromLeftToRight,
538            vertical_movement: TileVerticalCapture::AlwaysFromTopToBottom,
539
540            grid_width: tile_matches
541                .get_one::<String>("HORIZ_COUNT")
542                .map(|v| parse_int(v))
543                .map(GridWidthCapture::TilesInRow)
544                .unwrap_or(GridWidthCapture::FullWidth),
545
546            grid_height: tile_matches
547                .get_one::<String>("VERT_COUNT")
548                .map(|v| parse_int(v))
549                .map(GridHeightCapture::TilesInColumn)
550                .unwrap_or(GridHeightCapture::FullHeight)
551        })
552    }
553    else {
554        // Standard case
555        if matches.get_flag("OVERSCAN") {
556            OutputFormat::CPCMemory {
557                output_dimension: CPCScreenDimension::overscan(),
558                display_address: DisplayCRTCAddress::new_overscan_from_page(2)
559            }
560        }
561        else if matches.get_flag("FULLSCREEN") {
562            OutputFormat::CPCMemory {
563                output_dimension: CPCScreenDimension::overscan(),
564                display_address: DisplayCRTCAddress::new_overscan_from_page(2)
565            }
566        }
567        else {
568            // assume it is a standard screen
569            let mut format = CPCScreenDimension::standard();
570            if let Some(scr) = matches.subcommand_matches("scr") {
571                if let Some(&r1) = scr.get_one("R1") {
572                    format.horizontal_displayed = r1;
573                }
574                if let Some(&r6) = scr.get_one("R6") {
575                    format.vertical_displayed = r6;
576                }
577            }
578            OutputFormat::CPCMemory {
579                output_dimension: format,
580                display_address: DisplayCRTCAddress::new_standard_from_page(3)
581            }
582        }
583    }
584}
585
586// TODO - Add the ability to import a target palette
587#[allow(clippy::cast_possible_wrap)]
588#[allow(clippy::cast_possible_truncation)]
589fn convert(matches: &ArgMatches) -> anyhow::Result<()> {
590    let input_file = matches.get_one::<Utf8PathBuf>("SOURCE").unwrap();
591    let output_mode = matches
592        .get_one::<String>("MODE")
593        .unwrap()
594        .parse::<u8>()
595        .unwrap();
596    let mut transformations = TransformationsList::default();
597
598    let palette = get_requested_palette(matches)?;
599
600    if matches.get_flag("SKIP_ODD_PIXELS") {
601        transformations = transformations.skip_odd_pixels();
602    }
603    if matches.contains_id("PIXEL_COLUMN_START") {
604        transformations = transformations.column_start(
605            matches
606                .get_one::<String>("PIXEL_COLUMN_START")
607                .unwrap()
608                .parse::<u16>()
609                .unwrap()
610        )
611    }
612    if matches.contains_id("PIXEL_LINE_START") {
613        transformations = transformations.line_start(
614            matches
615                .get_one::<String>("PIXEL_LINE_START")
616                .unwrap()
617                .parse::<u16>()
618                .unwrap()
619        )
620    }
621    if matches.contains_id("PIXEL_COLUMNS_KEPT") {
622        transformations = transformations.columns_kept(
623            matches
624                .get_one::<String>("PIXEL_COLUMNS_KEPT")
625                .unwrap()
626                .parse::<u16>()
627                .unwrap()
628        )
629    }
630    if matches.contains_id("PIXEL_LINES_KEPT") {
631        transformations = transformations.lines_kept(
632            matches
633                .get_one::<String>("PIXEL_LINES_KEPT")
634                .unwrap()
635                .parse::<u16>()
636                .unwrap()
637        )
638    }
639
640    let sub_sna = matches.subcommand_matches("sna");
641    let sub_m4 = matches.subcommand_matches("m4");
642    let sub_dsk = matches.subcommand_matches("dsk");
643    let sub_sprite = matches.subcommand_matches("sprite");
644    let sub_tile = matches.subcommand_matches("tile");
645    let sub_exec = matches.subcommand_matches("exec");
646    let sub_scr = matches.subcommand_matches("scr");
647
648    let missing_pen = matches.get_one::<u8>("MISSING_PEN").map(|v| Pen::from(*v));
649
650    let crop_if_too_large = matches.get_flag("CROP_IF_TOO_LARGE");
651    let output_format = get_output_format(matches);
652    let conversion = ImageConverter::convert(
653        input_file,
654        palette,
655        output_mode.into(),
656        transformations,
657        output_format,
658        crop_if_too_large,
659        missing_pen
660    )?;
661
662    if sub_sprite.is_some() {
663        // TODO share code with the tile branch
664
665        let sub_sprite = sub_sprite.unwrap();
666
667        // handle the sprite stuff
668        match &conversion {
669            Output::Sprite(sprite) | Output::SpriteAndMask { sprite, .. } => {
670                let palette = sprite.palette();
671                // Save the binary data of the palette if any
672                do_export_palette!(sub_sprite, palette);
673
674                // Save the binary data of the sprite
675                let sprite_fname = sub_sprite.get_one::<String>("SPRITE_FNAME").unwrap();
676                sprite
677                    .save_sprite(sprite_fname)
678                    .expect("Unable to create the sprite file");
679
680                sub_sprite
681                    .get_one::<String>("CONFIGURATION")
682                    .map(|conf_fname: &String| {
683                        let mut file = File::create(conf_fname)
684                            .expect("Unable to create the configuration file");
685                        let fname = Utf8Path::new(conf_fname)
686                            .file_stem()
687                            .unwrap()
688                            .replace(".", "_");
689                        writeln!(&mut file, "{}_WIDTH equ {}", fname, sprite.bytes_width())
690                            .unwrap();
691                        writeln!(&mut file, "{}_HEIGHT equ {}", fname, sprite.height()).unwrap();
692                    });
693            },
694            _ => unreachable!("{:?} not handled", conversion)
695        }
696
697        // handle the additional mask stuff
698        if let Output::SpriteAndMask { mask, sprite } = &conversion {
699            if let Some(mask_fname) = sub_sprite.get_one::<String>("MASK_FNAME") {
700                mask.save_sprite(mask_fname)
701                    .expect("Unable to create the mask file");
702            }
703
704            if let Some(code_fname) = sub_sprite.get_one::<String>("SPRITE_ASM") {
705                assert_eq!(
706                    mask.encoding(),
707                    SpriteEncoding::Linear,
708                    "Need to implement the other cases when needed"
709                );
710
711                let r1 = sub_sprite.get_one::<u8>("R1").cloned().unwrap_or_else(|| {
712                    if matches.get_flag("OVERSCAN") || matches.get_flag("FULLSCREEN") {
713                        96 / 2
714                    }
715                    else {
716                        80 / 2
717                    }
718                });
719                let label = sub_sprite
720                    .get_one::<String>("SPRITE_ASM_LABEL")
721                    .cloned()
722                    .unwrap_or_else(|| code_fname.replace('.', "_"));
723
724                // generate the code
725                let code = match sub_sprite.get_one::<String>("SPRITE_ASM_KIND").unwrap().as_str() {
726                    "masked" => cpclib::sprite_compiler::standard_sprite_compiler(
727                        &label, sprite, mask, r1),
728                    "backup+masked" => cpclib::sprite_compiler::standard_sprite_with_background_backup_and_restore_compiler(
729                        &label, sprite, mask, r1),
730                    rest => unreachable!("{rest} unhandled")
731                };
732
733                code.save(code_fname)
734                    .expect("Unable to save generated code");
735            }
736        }
737    }
738    else if let Some(sub_tile) = sub_tile {
739        // TODO share code with the sprite branch
740        match &conversion {
741            Output::TilesList {
742                palette,
743                list: tile_set,
744                ..
745            } => {
746                // Save the palette
747                do_export_palette!(sub_tile, palette);
748
749                // Save the binary data of the tiles
750                let tile_fname = Utf8Path::new(
751                    sub_tile
752                        .get_one::<String>("SPRITE_FNAME")
753                        .expect("Missing tileset name")
754                );
755                let base = tile_fname.with_extension("").to_string();
756                let extension = tile_fname.extension().unwrap_or("");
757                for (i, data) in tile_set.iter().enumerate() {
758                    let current_filename = format!("{base}_{i:03}.{extension}");
759                    let mut file = File::create(current_filename.clone())
760                        .unwrap_or_else(|_| panic!("Unable to build {current_filename}"));
761                    file.write_all(data).unwrap();
762                }
763            },
764            _ => unreachable! {}
765        }
766    }
767    else if let Some(sub_scr) = sub_scr {
768        let fname = dbg!(sub_scr.get_one::<String>("SCR").unwrap());
769
770        match &conversion {
771            Output::CPCMemoryStandard(scr, palette) => {
772                let scr = if sub_scr.contains_id("COMPRESSED") {
773                    ocp::compress(scr)
774                }
775                else {
776                    scr.to_vec()
777                };
778
779                std::fs::write(fname, &scr)?;
780
781                do_export_palette!(sub_scr, palette);
782            },
783
784            Output::CPCMemoryOverscan(scr1, scr2, palette) => {
785                if sub_scr.contains_id("COMPRESSED") {
786                    unimplemented!();
787                }
788
789                let mut buffer = File::create(fname)?;
790                buffer.write_all(scr1)?;
791                buffer.write_all(scr2)?;
792                do_export_palette!(sub_scr, palette);
793            },
794
795            _ => unreachable!()
796        };
797    }
798    else {
799        // Make the conversion before feeding sna or dsk
800
801        /// TODO manage the presence/absence of file in the dsk, the choice of filename and so on
802        if sub_dsk.is_some() || sub_exec.is_some() {
803            let code = match &conversion {
804                Output::CPCMemoryStandard(memory, pal) => {
805                    standard_linked_code(output_mode, pal, memory)
806                },
807
808                Output::CPCMemoryOverscan(_memory1, _memory2, _pal) => unimplemented!(),
809
810                _ => unreachable!()
811            };
812
813            let filename = {
814                if sub_dsk.is_some() {
815                    "test.bin"
816                }
817                else {
818                    sub_exec
819                        .as_ref()
820                        .unwrap()
821                        .get_one::<String>("FILENAME")
822                        .unwrap()
823                }
824            };
825
826            let file = assemble_to_amsdos_file(&code, filename, Default::default()).unwrap();
827
828            if sub_exec.is_some() {
829                let filename = Utf8Path::new(filename);
830                let folder = filename.parent().unwrap();
831                let folder = if folder == Utf8Path::new("") {
832                    std::env::current_dir().unwrap()
833                }
834                else {
835                    folder.canonicalize().unwrap()
836                };
837                let folder = Utf8PathBuf::from_path_buf(folder).unwrap();
838                file.save_in_folder(folder)?;
839            }
840            else {
841                let fname = sub_dsk.unwrap().get_one::<String>("DSK").unwrap();
842                let p = Utf8Path::new(fname);
843
844                let mut dsk = {
845                    if p.exists() {
846                        ExtendedDsk::open(p).unwrap()
847                    }
848                    else {
849                        ExtendedDsk::default()
850                    }
851                };
852
853                let head = Head::A;
854                let _system = false;
855                let _read_only = false;
856
857                dsk.add_amsdos_file(
858                    &file,
859                    head,
860                    false,
861                    false,
862                    AmsdosAddBehavior::ReplaceAndEraseIfPresent
863                )
864                .unwrap();
865                dsk.save(fname).unwrap();
866            }
867        }
868        if sub_sna.is_some() || sub_m4.is_some() {
869            let (palette, code) = match &conversion {
870                Output::CPCMemoryStandard(_memory, pal) => {
871                    (pal, assemble(&standard_display_code(output_mode)).unwrap())
872                },
873
874                Output::CPCMemoryOverscan(_memory1, _memory2, pal) => {
875                    let code =
876                        assemble(&fullscreen_display_code(output_mode, 96 / 2, pal)).unwrap();
877                    (pal, code)
878                },
879
880                _ => unreachable!()
881            };
882
883            // Create a snapshot with a standard screen
884            let mut sna = Snapshot::default();
885
886            match &conversion {
887                Output::CPCMemoryStandard(memory, _) => {
888                    sna.add_data(memory.as_ref(), 0xC000)
889                        .expect("Unable to add the image in the snapshot");
890                },
891                Output::CPCMemoryOverscan(memory1, memory2, _) => {
892                    sna.add_data(memory1.as_ref(), 0x8000)
893                        .expect("Unable to add the image in the snapshot");
894                    sna.add_data(memory2.as_ref(), 0xC000)
895                        .expect("Unable to add the image in the snapshot");
896                },
897                _ => unreachable!()
898            };
899
900            sna.add_data(&code, 0x4000).unwrap();
901            sna.set_value(SnapshotFlag::Z80_PC, 0x4000).unwrap();
902            sna.set_value(SnapshotFlag::GA_PAL(Some(16)), 0x54).unwrap();
903            for i in 0..16 {
904                sna.set_value(
905                    SnapshotFlag::GA_PAL(Some(i)),
906                    u16::from(palette.get((i as i32).into()).gate_array())
907                )
908                .unwrap();
909            }
910
911            if let Some(sub_sna) = sub_sna {
912                let sna_fname = sub_sna.get_one::<String>("SNA").unwrap();
913                sna.save(sna_fname, sna::SnapshotVersion::V2)
914                    .expect("Unable to save the snapshot");
915            }
916            else if let Some(sub_m4) = sub_m4 {
917                #[cfg(feature = "xferlib")]
918                {
919                    let mut f = tempfile::Builder::new()
920                        .suffix(".sna")
921                        .tempfile()
922                        .expect("Unable to create the temporary file");
923
924                    sna.write_all(f.as_file_mut(), cpclib::sna::SnapshotVersion::V2)
925                        .expect("Unable to write the sna in the temporary file");
926
927                    let xfer = CpcXfer::new(sub_m4.get_one::<String>("CPCM4").unwrap());
928
929                    let tmp_file_name = f.path();
930                    xfer.upload_and_run(tmp_file_name, None)
931                        .expect("An error occurred while transferring the snapshot");
932                }
933            }
934        }
935    }
936
937    Ok(())
938}
939
940pub fn build_args_parser() -> clap::Command {
941    let args = specify_palette!(Command::new("CPC image conversion tool")
942                    .version(built_info::PKG_VERSION)
943                    .author("Krusty/Benediction")
944                    .about("Simple CPC image conversion tool")
945                    .arg(
946                        Arg::new("SOURCE")
947                            .help("Filename to convert")
948//                            .last(true)
949                            .required(true)
950                            .value_parser(|source: &str| {
951                              let p = Utf8PathBuf::from(source);
952                              if p.exists() {
953                                  Ok(p)
954                              }
955                              else {
956                                  Err(format!("{source} does not exists!"))
957                              }
958                            })
959                   )
960
961                .arg(
962                    Arg::new("MODE")
963                        .short('m')
964                        .long("mode")
965                        .help("Screen mode of the image to convert.")
966                        .value_name("MODE")
967                        .default_value("0")
968                        .value_parser(["0", "1", "2"])
969                )
970                .arg(
971                    Arg::new("MISSING_PEN")
972                        .long("missing-pen")
973                        .help("Pen to use when the byte is too small")
974                        .value_parser(value_parser!(u8))
975                )
976                .arg(
977                    Arg::new("CROP_IF_TOO_LARGE")
978                        .long("crop")
979                        .help("Crop the picture if it is too large according  to the destination")
980                        .action(ArgAction::SetTrue)
981                )
982                .arg(
983                    Arg::new("FULLSCREEN")
984                        .long("fullscreen")
985                        .action(ArgAction::SetTrue)
986                        .help("Specify a full screen displayed using 2 non consecutive banks.")
987                        .conflicts_with("OVERSCAN")
988                )
989                .arg(
990                    Arg::new("OVERSCAN")
991                        .long("overscan")
992                        .action(ArgAction::SetTrue)
993                        .help("Specify an overscan screen (crtc meaning).")
994                )
995                .arg(
996                    Arg::new("STANDARD")
997                        .long("standard")
998                        .action(ArgAction::SetTrue)
999                        .help("Specify a standard screen manipulation.")
1000                        .conflicts_with("OVERSCAN")
1001                        .conflicts_with("FULLSCREEN")
1002                )
1003                .arg(
1004                    Arg::new("SKIP_ODD_PIXELS")
1005                        .long("skipoddpixels")
1006                        .short('s')
1007                        .help("Skip odd pixels when reading the image (usefull when the picture is mode 0 with duplicated pixels")
1008                        .action(ArgAction::SetTrue)
1009                )
1010                .arg(
1011                    Arg::new("PIXEL_COLUMN_START")
1012                    .long("columnstart")
1013                    .required(false)
1014                    .help("Number of pixel columns to skip on the left.")
1015                )
1016                .arg(
1017                    Arg::new("PIXEL_COLUMNS_KEPT")
1018                    .long("columnskept")
1019                    .required(false)
1020                    .help("Number of pixel columns to keep.")
1021                )
1022                .arg(
1023                    Arg::new("PIXEL_LINE_START")
1024                    .long("linestart")
1025                    .required(false)
1026                    .help("Number of pixel lines to skip.")
1027                )
1028                .arg(
1029                    Arg::new("PIXEL_LINES_KEPT")
1030                    .long("lineskept")
1031                    .required(false)
1032                    .help("Number of pixel lines to keep.")
1033                )
1034                
1035                    .subcommand(
1036                        Command::new("sna")
1037                            .about("Generate a snapshot with the converted image.")
1038                            .arg(
1039                                Arg::new("SNA")
1040                                    .help("snapshot filename to generate")
1041                                    .required(true)
1042                                    .value_parser(|sna: &str| {
1043                                        if sna.to_lowercase().ends_with("sna") {
1044                                            Ok(sna.to_owned())
1045                                        }
1046                                        else {
1047                                            Err(format!("{sna} has not a snapshot extension."))
1048                                        }
1049                                    })
1050                            )
1051                    )
1052
1053                    .subcommand(
1054                        Command::new("dsk")
1055                        .about("Generate a DSK with an executable of the converted image.")
1056                        .arg(
1057                            Arg::new("DSK")
1058                            .help("dsk filename to generate")
1059                            .required(true)
1060                            .value_parser(|dsk: &str|{
1061                                if dsk.to_lowercase().ends_with("dsk") {
1062                                    Ok(dsk.to_owned())
1063                                }
1064                                else {
1065                                    Err(format!("{dsk} has not a dsk extention."))
1066                                }
1067                            })
1068                        )
1069                    )
1070
1071                    .subcommand(
1072                        export_palette!(Command::new("scr")
1073                        .about("Generate an OCP SCR file")
1074                        .arg(
1075                            Arg::new("COMPRESSED")
1076                                .help("Request a compressed screen")
1077                                .long("compress")
1078                                .short('c')
1079                                .required(false)
1080                        )
1081                        .arg(
1082                            Arg::new("R1")
1083                                .help("Screen width in number of chars")
1084                                .long("r1")
1085                                .alias("horizontal-displayed-character-number")
1086                                .alias("width")
1087                                .alias("R1")
1088                                .value_parser(clap::value_parser!(u8))
1089                        )
1090                        .arg(
1091                            Arg::new("R6")
1092                                .help("Screen height in number of chars")
1093                                .long("r6")
1094                                .alias("vertical-displayed-character-number")
1095                                .alias("width")
1096                                .value_parser(clap::value_parser!(u8))
1097                        )
1098                        .arg(
1099                            Arg::new("SCR")
1100                            .long("output")
1101                            .short('o')
1102                            .help("Filename to generate")
1103                            .required(true)
1104                        )
1105                    ))
1106
1107                    .subcommand(
1108                        Command::new("exec")
1109                        .about("Generate a binary file to manually copy in a DSK or M4 folder.")
1110                        .arg(
1111                            Arg::new("FILENAME")
1112                            .help("executable to generate")
1113                            .required(true)
1114                            .value_parser(|fname: &str|{
1115                                let fname = Utf8PathBuf::from(fname);
1116                                if let Some(ext) = fname.extension()
1117                                    && ext.len() > 3 {
1118                                        return Err(format!("{ext} is not a valid amsdos extension."));
1119                                    }
1120
1121                                if let Some(stem) = fname.file_stem()
1122                                    && stem.len() > 8 {
1123                                        return Err(format!("{stem} is not a valid amsdos file stem."))
1124                                    }
1125
1126                                Ok(fname)
1127                            })
1128                        )
1129                    )
1130
1131                    .subcommand(
1132                        export_palette!(Command::new("sprite")
1133                        .about("Generate a sprite file to be included inside an application")
1134                        .arg(
1135                            Arg::new("CONFIGURATION")
1136                            .long("configuration")
1137                            .short('c')
1138                            .required(false)
1139                            .help("Name of the assembly file that contains the size of the sprite")
1140                        )
1141                        .arg(
1142                            Arg::new("FORMAT")
1143                            .long("format")
1144                            .short('f')
1145                            .default_value("linear")
1146                            .value_parser(["linear", "graycoded", "zigzag+graycoded"])
1147                        )
1148
1149                        .arg(
1150                            Arg::new("SPRITE_FNAME")
1151                            .long("output")
1152                            .short('o')
1153                            .help("Filename where the sprite is stored")
1154                            .required_unless_present("SPRITE_ASM")
1155                        )
1156
1157                        .arg(Arg::new("R1")
1158                                .help("Screen width in number of chars")
1159                                .long("r1")
1160                                .alias("horizontal-displayed-character-number")
1161                                .alias("width")
1162                                .alias("R1")
1163                                .value_parser(clap::value_parser!(u8))
1164                                .requires("SPRITE_ASM")
1165                        )
1166
1167                        .arg(
1168                            Arg::new("SPRITE_ASM")
1169                            .long("code")
1170                            .help("Filename where to store the Z80 display code")
1171                            .required_unless_present("SPRITE_FNAME")
1172                            .requires("MASK_INK")
1173                            .requires("REPLACEMENT_INK")
1174                        )
1175
1176                        .arg(
1177                            Arg::new("SPRITE_ASM_KIND")
1178                            .long("kind")
1179                            .help("The kind of code to generate")
1180                            .requires("SPRITE_ASM")
1181                            .value_parser(["masked", "backup+masked"])
1182                            .default_value("masked")
1183                        )
1184
1185
1186                        .arg(
1187                            Arg::new("SPRITE_ASM_LABEL")
1188                            .long("label")
1189                            .short('l')
1190                            .help("Label for the generated asm code")
1191                        )
1192
1193                        .arg(
1194                            Arg::new("MASK_FNAME")
1195                            .long("mask")
1196                            .short('m')
1197                            .help("Filename where the mask is stored")
1198                            .requires("MASK_INK")
1199                            .requires("REPLACEMENT_INK")
1200                        )
1201
1202                        .arg(
1203                            Arg::new("MASK_INK")
1204                            .long("mask-ink")
1205                            .help("Ink that represents the mask in the input image")
1206                            .value_parser(clap_parse_ink)
1207                        )
1208                        .arg(
1209                            Arg::new("REPLACEMENT_INK")
1210                            .long("replacement-ink")
1211                            .help("Ink that relace the mask ink in the sprite data")
1212                            .value_parser(clap_parse_ink)
1213                        )
1214                    ))
1215
1216                    .subcommand(
1217                        export_palette!(Command::new("tile")
1218                            .about("Generate a list of sprites")
1219                            .arg(
1220                                Arg::new("WIDTH")
1221                                .long("width")
1222                                .short('W')
1223                                .required(true)
1224                                .help("Width (in bytes) of a tile")
1225                            )
1226                            .arg(
1227                                Arg::new("HEIGHT")
1228                                .long("height")
1229                                .short('H')
1230                                .required(true)
1231                                .help("Height (in lines) of a tile")
1232                            )
1233                            .arg(
1234                                Arg::new("HORIZ_COUNT")
1235                                .long("horiz_count")
1236                                .required(false)
1237                                .help("Horizontal number of tiles to extract. Extra tiles are ignored")
1238                            )
1239                            .arg(
1240                                Arg::new("VERT_COUNT")
1241                                .long("vert_count")
1242                                .required(false)
1243                                .help("Vertical number of tiles to extract. Extra tiles are ignored")
1244                            )
1245                            .arg(
1246                                Arg::new("CONFIGURATION")
1247                                .long("configuration")
1248                                .short('c')
1249                                .required(false)
1250                                .help("Name of the assembly file that contains the size of the sprite")
1251                            )
1252                            .arg(
1253                                Arg::new("FORMAT")
1254                                .long("format")
1255                                .short('f')
1256                                .value_parser(["linear", "graycoded", "zigzag+graycoded"])
1257                                .default_value("linear")
1258                            )
1259                            .arg(
1260                                Arg::new("SPRITE_FNAME")
1261                                .short('o')
1262                                .long("output")
1263                                .help("Filename to generate. Will be postfixed by the number")
1264                                .required(true)
1265                            )
1266
1267                    ))
1268
1269
1270                );
1271
1272    if cfg!(feature = "xferlib") {
1273        let subcommand = Command::new("m4")
1274            .about("Directly send the code on the M4 through a snapshot")
1275            .arg(Arg::new("CPCM4").help("Address of the M4").required(true));
1276
1277        let subcommand = if cfg!(feature = "watch") {
1278            subcommand.arg(
1279                Arg::new("WATCH")
1280                .help("Monitor the source file modification and restart the conversion and transfer automatically. Picture must ALWAYS be valid.")
1281                .long("watch")
1282            )
1283        }
1284        else {
1285            subcommand
1286        };
1287        args.subcommand(subcommand)
1288    }
1289    else {
1290        args
1291    }
1292}
1293
1294pub fn process(matches: &ArgMatches, mut args: Command) -> anyhow::Result<()> {
1295    if matches.get_flag("help") {
1296        args.print_long_help()?;
1297        return Ok(());
1298    }
1299
1300    if matches.subcommand_matches("m4").is_none()
1301        && matches.subcommand_matches("dsk").is_none()
1302        && matches.subcommand_matches("sna").is_none()
1303        && matches.subcommand_matches("sprite").is_none()
1304        && matches.subcommand_matches("tile").is_none()
1305        && matches.subcommand_matches("exec").is_none()
1306        && matches.subcommand_matches("scr").is_none()
1307    {
1308        eprintln!("[ERROR] you have not specified any action to do.");
1309        std::process::exit(exitcode::USAGE);
1310    }
1311
1312    convert(matches).expect("Unable to make the conversion");
1313
1314    if let Some(sub_m4) = matches.subcommand_matches("m4") {
1315        eprintln!("hmmm seems to not be coded yet");
1316        #[cfg(feature = "watch")]
1317        if sub_m4.contains_id("WATCH") {
1318            let (tx, rx) = std::sync::mpsc::channel();
1319            let mut watcher: RecommendedWatcher = RecommendedWatcher::new(
1320                move |res| tx.send(res).unwrap(),
1321                notify::Config::default()
1322            )?;
1323            watcher.watch(
1324                matches
1325                    .get_one::<Utf8PathBuf>("SOURCE")
1326                    .unwrap()
1327                    .as_std_path(),
1328                RecursiveMode::NonRecursive
1329            )?;
1330
1331            for res in rx {
1332                match res {
1333                    Ok(notify::event::Event {
1334                        kind: notify::event::EventKind::Modify(_),
1335                        ..
1336                    })
1337                    | Ok(notify::event::Event {
1338                        kind: notify::event::EventKind::Create(_),
1339                        ..
1340                    }) => {
1341                        if let Err(e) = convert(matches) {
1342                            return Err(Error::msg(format!(
1343                                "[ERROR] Unable to convert the image {e}"
1344                            )));
1345                        }
1346                    },
1347                    _ => {}
1348                }
1349            }
1350        }
1351    }
1352
1353    Ok(())
1354}