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
;
20 u8 df_known
, rf_known
;
22 i64 expected_dfpos
, expected_rfpos
;
23 i64 expected_dflen
, expected_rflen
;
24 de_ucstring
*filename
;
25 struct de_timestamp mod_time_from_macbinary
;
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
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
)
44 i64 nbytes_written
= 0;
47 endpos
= dcmpri
->pos
+ dcmpri
->len
;
48 d
->row_issue_flag
= 0;
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
62 if(pos
+count
> endpos
) {
68 dbuf_copy(dcmpri
->f
, pos
, count
, dcmpro
->f
);
70 nbytes_written
+= count
;
72 else if(b
>=129) { // A compressed run
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
) {
83 else if(xpos_b
> MACPAINT_WIDTH_BYTES
) {
84 d
->row_issue_flag
= 1;
85 xpos_b
= xpos_b
% MACPAINT_WIDTH_BYTES
;
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
;
100 int saved_indent_level
;
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
) {
111 de_dbg(c
, "header at %"I64_FMT
, d
->hdr_pos
);
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
);
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
;
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
,
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
{
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
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
)
208 imgstart
= vi
->pos
+512;
209 de_dbg(c
, "checking for image at offset %d", (int)imgstart
);
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
216 if(c
->infile
->len
< imgstart
+ 4) {
217 de_strlcpy(vi
->msg
, "file too small", sizeof(vi
->msg
));
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))
231 while(pos
< c
->infile
->len
) {
232 if(ypos
>=MACPAINT_HEIGHT
) {
236 b
= de_getbyte_p(&pos
);
242 if(xpos_b
==MACPAINT_WIDTH_BYTES
) {
246 else if(xpos_b
> MACPAINT_WIDTH_BYTES
) {
247 de_strlcpy(vi
->msg
, "literal too long", sizeof(vi
->msg
));
253 count
= 257 - (i64
)b
;
256 if(xpos_b
== MACPAINT_WIDTH_BYTES
) {
260 else if(xpos_b
> MACPAINT_WIDTH_BYTES
) {
261 de_strlcpy(vi
->msg
, "run too long", sizeof(vi
->msg
));
268 if(xpos_b
==0 && ypos
==MACPAINT_HEIGHT
) {
269 de_strlcpy(vi
->msg
, "decodes okay", sizeof(vi
->msg
));
274 de_snprintf(vi
->msg
, sizeof(vi
->msg
), "premature end of file (x=%d, y=%d)",
275 (int)(xpos_b
*8), (int)ypos
);
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
)
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
)
303 const i64 cell_width
= 19;
304 const i64 cell_height
= 16;
305 const i64 cells_per_row
= 19;
307 de_bitmap
*gallery
= NULL
;
308 de_bitmap
*rawimg
= NULL
;
310 const char *patsetname
;
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)");
319 pos1
= d
->hdr_pos
+ 4;
321 de_dbg(c
, "brush patterns at %"I64_FMT
, pos1
);
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) {
339 de_dbg(c
, "brush patterns are blank: not extracting");
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
++) {
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
++) {
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
);
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
) {
400 de_dbg(c
, "resource fork at %"I64_FMT
", len=%"I64_FMT
, d
->expected_rfpos
, d
->expected_rflen
);
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
)
410 de_module_params
*mparams
= NULL
;
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
419 if(b1
<1 || b1
>63) goto done
;
421 de_dbg(c
, "MacBinary header");
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) {
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) {
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
];
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
);
454 if(ucstring_isnonempty(mparams
->out_params
.fi
->name_other
) && !d
->filename
) {
455 d
->filename
= ucstring_clone(mparams
->out_params
.fi
->name_other
);
459 do_decode_rsrc(c
, d
);
464 de_finfo_destroy(c
, mparams
->out_params
.fi
);
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))
480 static void de_run_macpaint(deark
*c
, de_module_params
*mparams
)
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
);
494 d
->has_macbinary_header
= 1;
497 if(d
->has_macbinary_header
== -1) {
501 de_dbg(c
, "trying to determine if file has a MacBinary header");
504 vi1
= de_malloc(c
, sizeof(struct validity_info
));
506 test_valid_image(c
, d
, vi1
);
509 vi2
= de_malloc(c
, sizeof(struct validity_info
));
511 test_valid_image(c
, d
, vi2
);
514 de_dbg_indent(c
, -1);
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;
530 de_warn(c
, "This is probably not a MacPaint file.");
531 d
->has_macbinary_header
= 1;
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");
540 de_declare_fmt(c
, "MacPaint without MacBinary header");
542 if(d
->has_macbinary_header
) {
548 d
->img_data_pos
= d
->hdr_pos
+ 512;
550 if(d
->has_macbinary_header
) {
554 do_read_bitmap(c
, d
);
556 if(!d
->is_fmac2com
) {
557 do_read_patterns(c
, d
);
561 ucstring_destroy(d
->filename
);
568 // Note: This must be coordinated with the macbinary detection routine.
569 static int de_identify_macpaint(deark
*c
)
573 if(is_fmac2com(c
)) return 85;
576 // Not all MacPaint files can be easily identified, but this will work
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;
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;
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 "
598 void de_module_macpaint(deark
*c
, struct deark_module_info
*mi
)
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
;