fnt: Improved error handling, etc.
[deark.git] / modules / macpaint.c
blob22156ee27b33f7b0fb2b8607cf231ed0c899f105
1 // This file is part of Deark.
2 // Copyright (C) 2016 Jason Summers
3 // See the file COPYING for terms of use.
5 // MacPaint image format
7 #include <deark-config.h>
8 #include <deark-private.h>
9 #include <deark-fmtutil.h>
10 DE_DECLARE_MODULE(de_module_macpaint);
12 #define MACPAINT_WIDTH 576
13 #define MACPAINT_WIDTH_BYTES (MACPAINT_WIDTH/8)
14 #define MACPAINT_HEIGHT 720
15 #define MACPAINT_IMAGE_BYTES (MACPAINT_WIDTH_BYTES*MACPAINT_HEIGHT)
17 typedef struct localctx_struct {
18 int has_macbinary_header;
19 u8 is_fmac2com;
20 u8 df_known, rf_known;
21 u8 row_issue_flag;
22 i64 expected_dfpos, expected_rfpos;
23 i64 expected_dflen, expected_rflen;
24 de_ucstring *filename;
25 struct de_timestamp mod_time_from_macbinary;
26 i64 hdr_pos;
27 i64 img_data_pos;
28 } lctx;
30 // We'd like to use fmtutil_decompress_packbits_ex() instead of this custom
31 // decompressor, but it would make it difficult to test for row boundary
32 // violations.
33 // Alternatively, merging this and test_valid_image() is something to consider,
34 // but probably not worth it.
35 static void decompress_packbits_for_macpaint(deark *c, lctx *d,
36 struct de_dfilter_in_params *dcmpri, struct de_dfilter_out_params *dcmpro,
37 struct de_dfilter_results *dres)
39 i64 pos;
40 u8 b, b2;
41 i64 count;
42 i64 endpos;
43 i64 xpos_b = 0;
44 i64 nbytes_written = 0;
46 pos = dcmpri->pos;
47 endpos = dcmpri->pos + dcmpri->len;
48 d->row_issue_flag = 0;
50 while(1) {
51 if(dcmpro->len_known && nbytes_written >= dcmpro->expected_len) {
52 goto done; // Decompressed the requested amount of dst data.
54 if(pos+2 > endpos) { // Min item size is 2 bytes
55 goto done; // Reached the end of source data
58 b = dbuf_getbyte_p(dcmpri->f, &pos);
60 if(b<=127) { // An uncompressed run
61 count = 1 + (i64)b;
62 if(pos+count > endpos) {
63 pos--;
64 goto done;
67 xpos_b += count;
68 dbuf_copy(dcmpri->f, pos, count, dcmpro->f);
69 pos += count;
70 nbytes_written += count;
72 else if(b>=129) { // A compressed run
73 count = 257 - (i64)b;
74 xpos_b += count;
75 b2 = dbuf_getbyte_p(dcmpri->f, &pos);
76 dbuf_write_run(dcmpro->f, b2, count);
77 nbytes_written += count;
80 if(xpos_b==MACPAINT_WIDTH_BYTES) {
81 xpos_b = 0;
83 else if(xpos_b > MACPAINT_WIDTH_BYTES) {
84 d->row_issue_flag = 1;
85 xpos_b = xpos_b % MACPAINT_WIDTH_BYTES;
89 done:
90 dbuf_flush(dcmpro->f);
91 dres->bytes_consumed = pos - dcmpri->pos;
92 dres->bytes_consumed_valid = 1;
95 static void do_read_bitmap(deark *c, lctx *d)
97 i64 cmpr_bytes_consumed = 0;
98 dbuf *unc_pixels = NULL;
99 de_finfo *fi = NULL;
100 int saved_indent_level;
101 i64 ipos;
102 struct de_packbits_params *pbparams = NULL;
103 struct de_dfilter_in_params dcmpri;
104 struct de_dfilter_out_params dcmpro;
105 struct de_dfilter_results dres;
107 de_dbg_indent_save(c, &saved_indent_level);
108 if(!d->is_fmac2com) {
109 i64 ver_num;
111 de_dbg(c, "header at %"I64_FMT, d->hdr_pos);
112 de_dbg_indent(c, 1);
113 ver_num = de_getu32be(d->hdr_pos);
114 de_dbg(c, "version number: %u", (unsigned int)ver_num);
115 if(ver_num!=0 && ver_num!=2 && ver_num!=3) {
116 de_warn(c, "Unrecognized version number: %u", (unsigned int)ver_num);
119 // We wait until later to read the brush patterns, only so that the patterns
120 // won't be the first file extracted.
122 de_dbg_indent(c, -1);
125 ipos = d->img_data_pos;
126 de_dbg(c, "image data at %"I64_FMT, ipos);
127 de_dbg_indent(c, 1);
128 unc_pixels = dbuf_create_membuf(c, MACPAINT_IMAGE_BYTES, 1);
129 dbuf_enable_wbuffer(unc_pixels);
131 de_dfilter_init_objects(c, &dcmpri, &dcmpro, &dres);
132 dcmpri.f = c->infile;
133 dcmpri.pos = ipos;
134 dcmpri.len = c->infile->len - ipos;
135 dcmpro.f = unc_pixels;
136 dcmpro.len_known = 1;
137 dcmpro.expected_len = MACPAINT_IMAGE_BYTES;
138 pbparams = de_malloc(c, sizeof(struct de_packbits_params));
140 decompress_packbits_for_macpaint(c, d, &dcmpri, &dcmpro, &dres);
141 dbuf_flush(unc_pixels);
143 cmpr_bytes_consumed = dres.bytes_consumed;
144 de_dbg(c, "decompressed %"I64_FMT" to %"I64_FMT" bytes", cmpr_bytes_consumed,
145 unc_pixels->len);
147 if(d->df_known) {
148 if(ipos+cmpr_bytes_consumed > d->expected_dfpos+d->expected_dflen) {
149 de_warn(c, "Image (ends at %"I64_FMT") goes beyond end of "
150 "MacBinary data fork (ends at %"I64_FMT")",
151 ipos+cmpr_bytes_consumed, d->expected_dfpos+d->expected_dflen);
155 if(unc_pixels->len < MACPAINT_IMAGE_BYTES) {
156 de_warn(c, "Image decompressed to %"I64_FMT" bytes, expected %u.",
157 unc_pixels->len, (UI)MACPAINT_IMAGE_BYTES);
159 else if(d->row_issue_flag) {
160 de_warn(c, "Rows not compressed independently. Decompression may have failed.");
163 fi = de_finfo_create(c);
164 if(d->filename && c->filenames_from_file) {
165 de_finfo_set_name_from_ucstring(c, fi, d->filename, 0);
168 if(d->mod_time_from_macbinary.is_valid) {
169 fi->internal_mod_time = d->mod_time_from_macbinary;
172 de_convert_and_write_image_bilevel(unc_pixels, 0,
173 MACPAINT_WIDTH, MACPAINT_HEIGHT, MACPAINT_WIDTH/8,
174 DE_CVTF_WHITEISZERO, fi, 0);
176 dbuf_close(unc_pixels);
177 de_finfo_destroy(c, fi);
178 de_free(c, pbparams);
179 de_dbg_indent_restore(c, saved_indent_level);
182 struct validity_info {
183 i64 pos;
184 int retval;
185 char msg[80];
188 // A function to help determine if the file has a MacBinary header.
189 // Each row is RLE-compressed independently, so once we assume one possibility
190 // or the other, we can do sanity checks to see if any code crosses a row
191 // boundary, or the image is too small to be a MacPaint image.
192 // It's inefficient to decompress whole image -- twice -- just to try to
193 // figure this out, but hopefully it's pretty reliable.
194 // Returns an integer (0, 1, 2...) reflecting the likelihood that this is the
195 // correct position.
196 // The special 'struct validity_info' is overkill, but it makes it easier to
197 // try different things.
198 static void test_valid_image(deark *c, lctx *d, struct validity_info *vi)
200 u8 b;
201 i64 count;
202 i64 pos;
203 i64 imgstart;
204 i64 xpos_b = 0;
205 i64 ypos = 0;
206 int v = 0;
208 imgstart = vi->pos+512;
209 de_dbg(c, "checking for image at offset %d", (int)imgstart);
210 de_dbg_indent(c, 1);
212 // Minimum bytes per row is 2.
213 // For a valid (non-truncated) file, file size must be at least
214 // pos1 + 512 + 2*MACPAINT_HEIGHT. But we want to tolerate truncated
215 // files as well.
216 if(c->infile->len < imgstart + 4) {
217 de_strlcpy(vi->msg, "file too small", sizeof(vi->msg));
218 vi->retval = 0;
219 goto done;
222 // Look for a boundary between all-0 bytes, and not-all-0 bytes.
223 if(dbuf_is_all_zeroes(c->infile, imgstart-16, 16) &&
224 !dbuf_is_all_zeroes(c->infile, imgstart, 8))
226 v++;
229 pos = imgstart;
231 while(pos < c->infile->len) {
232 if(ypos>=MACPAINT_HEIGHT) {
233 break;
236 b = de_getbyte_p(&pos);
238 if(b<=127) {
239 count = 1+(i64)b;
240 pos += count;
241 xpos_b += count;
242 if(xpos_b==MACPAINT_WIDTH_BYTES) {
243 xpos_b = 0;
244 ypos++;
246 else if(xpos_b > MACPAINT_WIDTH_BYTES) {
247 de_strlcpy(vi->msg, "literal too long", sizeof(vi->msg));
248 vi->retval = v+0;
249 goto done;
252 else if(b>=129) {
253 count = 257 - (i64)b;
254 pos++;
255 xpos_b += count;
256 if(xpos_b == MACPAINT_WIDTH_BYTES) {
257 xpos_b = 0;
258 ypos++;
260 else if(xpos_b > MACPAINT_WIDTH_BYTES) {
261 de_strlcpy(vi->msg, "run too long", sizeof(vi->msg));
262 vi->retval = v+0;
263 goto done;
268 if(xpos_b==0 && ypos==MACPAINT_HEIGHT) {
269 de_strlcpy(vi->msg, "decodes okay", sizeof(vi->msg));
270 vi->retval = v+2;
271 goto done;
274 de_snprintf(vi->msg, sizeof(vi->msg), "premature end of file (x=%d, y=%d)",
275 (int)(xpos_b*8), (int)ypos);
276 vi->retval = v+1;
278 done:
279 de_dbg(c, "image at offset %d: %s", (int)(vi->pos+512), vi->msg);
280 de_dbg_indent(c, -1);
283 static const char *get_pattern_set_info(u32 patcrc, int *is_blank)
285 *is_blank = 0;
286 switch(patcrc) {
287 case 0x284a7a15: return "variant 1";
288 case 0x33d2d8d6: return "standard";
289 case 0x47514647: *is_blank = 1; return "blank";
290 case 0xb5348fd2: *is_blank = 1; return "blank variant 1";
292 return "unrecognized";
295 // Some MacPaint files contain a collection of brush patterns.
296 // Essentially, MacPaint saves workspace settings inside image files.
297 // (But these patterns are the only setting.)
298 static void do_read_patterns(deark *c, lctx *d)
300 i64 pos1;
301 i64 cell_idx;
302 i64 i, j;
303 const i64 cell_width = 19;
304 const i64 cell_height = 16;
305 const i64 cells_per_row = 19;
306 int is_blank;
307 de_bitmap *gallery = NULL;
308 de_bitmap *rawimg = NULL;
309 u32 patcrc;
310 const char *patsetname;
311 de_finfo *fi = NULL;
312 de_ucstring *tmpname = NULL;
313 struct de_crcobj *crc32o;
314 int saved_indent_level;
316 de_dbg_indent_save(c, &saved_indent_level);
317 de_dbg(c, "header (continued)");
318 de_dbg_indent(c, 1);
319 pos1 = d->hdr_pos + 4;
321 de_dbg(c, "brush patterns at %"I64_FMT, pos1);
322 de_dbg_indent(c, 1);
323 crc32o = de_crcobj_create(c, DE_CRCOBJ_CRC32_IEEE);
324 de_crcobj_addslice(crc32o, c->infile, pos1, 38*8);
325 patcrc = de_crcobj_getval(crc32o);
326 de_crcobj_destroy(crc32o);
327 patsetname = get_pattern_set_info(patcrc, &is_blank);
328 de_dbg(c, "brush patterns crc: 0x%08x (%s)", (unsigned int)patcrc, patsetname);
330 rawimg = de_bitmap_create(c, 8, 38*8, 1);
331 rawimg->is_internal = 1;
332 de_convert_image_bilevel(c->infile, pos1, 1, rawimg, DE_CVTF_WHITEISZERO);
334 if(c->extract_level<2) {
335 goto done;
338 if(is_blank) {
339 de_dbg(c, "brush patterns are blank: not extracting");
340 goto done;
343 gallery = de_bitmap_create(c, (cell_width+1)*19+1, (cell_height+1)*2+1, 1);
345 for(cell_idx=0; cell_idx<38; cell_idx++) {
346 i64 cell_x, cell_y; // dst cell pos in cells
347 i64 cell_xpos, cell_ypos; // dst cell pos in pixels
349 cell_x = cell_idx%cells_per_row;
350 cell_y = cell_idx/cells_per_row;
352 cell_xpos = (cell_width+1)*cell_x+1;
353 cell_ypos = (cell_height+1)*cell_y+1;
355 for(j=0; j<cell_height; j++) {
356 i64 yoffs;
358 // The cell sizes and "brush origin" are believed to be correct for
359 // at least one version of MacPaint, under some conditions.
360 yoffs = (cell_ypos+j+4)%8;
362 for(i=0; i<cell_width; i++) {
363 de_colorsample x;
364 i64 xoffs;
366 xoffs = (cell_xpos+i+7)%8;
368 x = DE_COLOR_K(de_bitmap_getpixel(rawimg, xoffs, 8*cell_idx + yoffs));
369 de_bitmap_setpixel_gray(gallery, cell_xpos+i, cell_ypos+j, x);
374 tmpname = ucstring_create(c);
375 if(d->filename && c->filenames_from_file) {
376 ucstring_append_ucstring(tmpname, d->filename);
377 ucstring_append_sz(tmpname, ".", DE_ENCODING_LATIN1);
379 ucstring_append_sz(tmpname, "pat", DE_ENCODING_LATIN1);
380 fi = de_finfo_create(c);
381 de_finfo_set_name_from_ucstring(c, fi, tmpname, 0);
382 de_bitmap_write_to_file_finfo(gallery, fi, DE_CREATEFLAG_IS_AUX|DE_CREATEFLAG_IS_BWIMG);
384 done:
385 de_bitmap_destroy(gallery);
386 de_bitmap_destroy(rawimg);
387 de_finfo_destroy(c, fi);
388 ucstring_destroy(tmpname);
389 de_dbg_indent_restore(c, saved_indent_level);
392 // Not many MacPaint-in-MacBinary files have a resource fork, but a few do.
393 static void do_decode_rsrc(deark *c, lctx *d)
395 if(!d->rf_known) return;
396 if(d->expected_rflen<1) return;
397 if(d->expected_rfpos+d->expected_rflen > c->infile->len) {
398 return;
400 de_dbg(c, "resource fork at %"I64_FMT", len=%"I64_FMT, d->expected_rfpos, d->expected_rflen);
401 de_dbg_indent(c, 1);
402 de_run_module_by_id_on_slice2(c, "macrsrc", NULL, c->infile,
403 d->expected_rfpos, d->expected_rflen);
404 de_dbg_indent(c, -1);
407 static void do_macbinary(deark *c, lctx *d)
409 u8 b0, b1;
410 de_module_params *mparams = NULL;
412 b0 = de_getbyte(0);
413 b1 = de_getbyte(1);
415 // Instead of a real MacBinary header, a few macpaint files just have
416 // 128 NUL bytes, or something like that. So we'll skip MacBinary parsing
417 // in some cases.
418 if(b0!=0) goto done;
419 if(b1<1 || b1>63) goto done;
421 de_dbg(c, "MacBinary header");
422 de_dbg_indent(c, 1);
423 mparams = de_malloc(c, sizeof(de_module_params));
424 mparams->in_params.codes = "D"; // = decode only, don't extract
425 mparams->out_params.fi = de_finfo_create(c); // A temporary finfo object
426 mparams->out_params.fi->name_other = ucstring_create(c);
427 de_run_module_by_id_on_slice(c, "macbinary", mparams, c->infile, 0, c->infile->len);
428 de_dbg_indent(c, -1);
430 if(mparams->out_params.uint1>0) {
431 d->df_known = 1;
432 d->expected_dfpos = (i64)mparams->out_params.uint1;
433 d->expected_dflen = (i64)mparams->out_params.uint2;
435 if(mparams->out_params.uint3>0) {
436 d->rf_known = 1;
437 d->expected_rfpos = (i64)mparams->out_params.uint3;
438 d->expected_rflen = (i64)mparams->out_params.uint4;
441 if(mparams->out_params.fi->timestamp[DE_TIMESTAMPIDX_MODIFY].is_valid) {
442 d->mod_time_from_macbinary = mparams->out_params.fi->timestamp[DE_TIMESTAMPIDX_MODIFY];
445 if(d->df_known) {
446 if(d->expected_dfpos+d->expected_dflen>c->infile->len) {
447 de_warn(c, "MacBinary data fork (ends at %"I64_FMT") "
448 "goes past end of file (%"I64_FMT")",
449 d->expected_dfpos+d->expected_dflen, c->infile->len);
450 d->df_known = 0;
454 if(ucstring_isnonempty(mparams->out_params.fi->name_other) && !d->filename) {
455 d->filename = ucstring_clone(mparams->out_params.fi->name_other);
458 if(d->rf_known) {
459 do_decode_rsrc(c, d);
462 done:
463 if(mparams) {
464 de_finfo_destroy(c, mparams->out_params.fi);
465 de_free(c, mparams);
469 static u8 is_fmac2com(deark *c)
471 if(dbuf_memcmp(c->infile, 0, (const void*)"\xeb\x79\x90", 3)) return 0;
472 if(dbuf_memcmp(c->infile, 608,
473 (const void*)"\x80\x03\x83\xc3\x0f\xb1\x04\xd3\xeb\xb4\x4a\xcd\x21\xb4\x48\xbb", 16))
475 return 0;
477 return 1;
480 static void de_run_macpaint(deark *c, de_module_params *mparams)
482 lctx *d;
483 struct validity_info *vi1 = NULL;
484 struct validity_info *vi2 = NULL;
486 d = de_malloc(c, sizeof(lctx));
487 d->has_macbinary_header = de_get_ext_option_bool(c, "macpaint:macbinary", -1);
489 if(d->has_macbinary_header == -1) {
490 d->is_fmac2com = is_fmac2com(c);
493 if(d->is_fmac2com) {
494 d->has_macbinary_header = 1;
497 if(d->has_macbinary_header == -1) {
498 int v512;
499 int v640;
501 de_dbg(c, "trying to determine if file has a MacBinary header");
502 de_dbg_indent(c, 1);
504 vi1 = de_malloc(c, sizeof(struct validity_info));
505 vi1->pos = 0;
506 test_valid_image(c, d, vi1);
507 v512 = vi1->retval;
509 vi2 = de_malloc(c, sizeof(struct validity_info));
510 vi2->pos = 128;
511 test_valid_image(c, d, vi2);
512 v640 = vi2->retval;
514 de_dbg_indent(c, -1);
516 if(v512 > v640) {
517 de_dbg(c, "assuming it has no MacBinary header");
518 d->has_macbinary_header = 0;
520 else if(v640 > v512) {
521 de_dbg(c, "assuming it has a MacBinary header");
522 d->has_macbinary_header = 1;
524 else if(v512 && v640) {
525 de_warn(c, "Can't determine if this file has a MacBinary header. "
526 "Try \"-opt macpaint:macbinary=0\".");
527 d->has_macbinary_header = 1;
529 else {
530 de_warn(c, "This is probably not a MacPaint file.");
531 d->has_macbinary_header = 1;
535 if(d->is_fmac2com)
536 de_declare_fmt(c, "FMAC2COM self-displaying MacPaint");
537 else if(d->has_macbinary_header)
538 de_declare_fmt(c, "MacPaint with MacBinary header");
539 else
540 de_declare_fmt(c, "MacPaint without MacBinary header");
542 if(d->has_macbinary_header) {
543 d->hdr_pos = 128;
545 else {
546 d->hdr_pos = 0;
548 d->img_data_pos = d->hdr_pos + 512;
550 if(d->has_macbinary_header) {
551 do_macbinary(c, d);
554 do_read_bitmap(c, d);
556 if(!d->is_fmac2com) {
557 do_read_patterns(c, d);
560 if(d) {
561 ucstring_destroy(d->filename);
562 de_free(c, d);
564 de_free(c, vi1);
565 de_free(c, vi2);
568 // Note: This must be coordinated with the macbinary detection routine.
569 static int de_identify_macpaint(deark *c)
571 u8 buf[8];
573 if(is_fmac2com(c)) return 85;
574 de_read(buf, 65, 8);
576 // Not all MacPaint files can be easily identified, but this will work
577 // for some of them.
578 if(!de_memcmp(buf, "PNTG", 4)) {
579 if(c->detection_data->is_macbinary) return 100;
580 if(!de_memcmp(&buf[4], "MPNT", 4)) return 80;
581 return 70;
584 if(de_input_file_has_ext(c, "mac")) return 10;
585 if(de_input_file_has_ext(c, "macp")) return 15;
586 if(de_input_file_has_ext(c, "pntg")) return 15;
587 return 0;
590 static void de_help_macpaint(deark *c)
592 de_msg(c, "-opt macpaint:macbinary=<0|1> : Assume file doesn't/does have "
593 "a MacBinary header");
594 de_msg(c, "-m macbinary : Extract from MacBinary container, instead of "
595 "decoding");
598 void de_module_macpaint(deark *c, struct deark_module_info *mi)
600 mi->id = "macpaint";
601 mi->desc = "MacPaint image";
602 mi->run_fn = de_run_macpaint;
603 mi->identify_fn = de_identify_macpaint;
604 mi->help_fn = de_help_macpaint;