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") .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)?; 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
324fn 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 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
389fn 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)] fn get_output_format(matches: &ArgMatches) -> OutputFormat {
498 if let Some(sprite_matches) = matches.subcommand_matches("sprite") {
499 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 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 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 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#[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 let sub_sprite = sub_sprite.unwrap();
666
667 match &conversion {
669 Output::Sprite(sprite) | Output::SpriteAndMask { sprite, .. } => {
670 let palette = sprite.palette();
671 do_export_palette!(sub_sprite, palette);
673
674 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 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 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 match &conversion {
741 Output::TilesList {
742 palette,
743 list: tile_set,
744 ..
745 } => {
746 do_export_palette!(sub_tile, palette);
748
749 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 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 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.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}