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_HEIGHT 720
14 #define MACPAINT_IMAGE_BYTES ((MACPAINT_WIDTH/8)*MACPAINT_HEIGHT)
16 typedef struct localctx_struct
{
17 int has_macbinary_header
;
18 u8 df_known
, rf_known
;
19 i64 expected_dfpos
, expected_rfpos
;
20 i64 expected_dflen
, expected_rflen
;
21 de_ucstring
*filename
;
22 struct de_timestamp mod_time_from_macbinary
;
25 static void do_read_bitmap(deark
*c
, lctx
*d
, i64 pos
)
28 i64 cmpr_bytes_consumed
= 0;
29 dbuf
*unc_pixels
= NULL
;
32 ver_num
= de_getu32be(pos
);
33 de_dbg(c
, "version number: %u", (unsigned int)ver_num
);
34 if(ver_num
!=0 && ver_num
!=2 && ver_num
!=3) {
35 de_warn(c
, "Unrecognized version number: %u", (unsigned int)ver_num
);
40 unc_pixels
= dbuf_create_membuf(c
, MACPAINT_IMAGE_BYTES
, 1);
42 fmtutil_decompress_packbits(c
->infile
, pos
, c
->infile
->len
- pos
,
43 unc_pixels
, &cmpr_bytes_consumed
);
45 de_dbg(c
, "decompressed %d to %d bytes", (int)cmpr_bytes_consumed
,
46 (int)unc_pixels
->len
);
49 if(pos
+cmpr_bytes_consumed
> d
->expected_dfpos
+d
->expected_dflen
) {
50 de_warn(c
, "Image (ends at %"I64_FMT
") goes beyond end of "
51 "MacBinary data fork (ends at %"I64_FMT
")",
52 pos
+cmpr_bytes_consumed
, d
->expected_dfpos
+d
->expected_dflen
);
56 if(unc_pixels
->len
< MACPAINT_IMAGE_BYTES
) {
57 de_warn(c
, "Image decompressed to %d bytes, expected %d.",
58 (int)unc_pixels
->len
, (int)MACPAINT_IMAGE_BYTES
);
61 fi
= de_finfo_create(c
);
62 if(d
->filename
&& c
->filenames_from_file
) {
63 de_finfo_set_name_from_ucstring(c
, fi
, d
->filename
, 0);
66 if(d
->mod_time_from_macbinary
.is_valid
) {
67 fi
->internal_mod_time
= d
->mod_time_from_macbinary
;
70 de_convert_and_write_image_bilevel(unc_pixels
, 0,
71 MACPAINT_WIDTH
, MACPAINT_HEIGHT
, MACPAINT_WIDTH
/8,
72 DE_CVTF_WHITEISZERO
, fi
, 0);
74 dbuf_close(unc_pixels
);
75 de_finfo_destroy(c
, fi
);
78 // A function to help determine if the file has a MacBinary header.
79 // Each row is RLE-compressed independently, so once we assume one possibility
80 // or the other, we can do sanity checks to see if any code crosses a row
81 // boundary, or the image is too small to be a MacPaint image.
82 // It's inefficient to decompress whole image -- twice -- just to try to
83 // figure this out, but hopefully it's pretty reliable.
84 // Returns an integer (0, 1, 2) reflecting the likelihood that this is the
86 static int valid_file_at(deark
*c
, lctx
*d
, i64 pos1
)
96 // Minimum bytes per row is 2.
97 // For a valid (non-truncated) file, file size must be at least
98 // pos1 + 512 + 2*MACPAINT_HEIGHT. But we want to tolerate truncated
100 if(c
->infile
->len
< imgstart
+ 4) {
101 de_dbg(c
, "file too small");
108 while(pos
< c
->infile
->len
) {
109 if(ypos
>=MACPAINT_HEIGHT
) {
120 if(xpos
==MACPAINT_WIDTH
) {
124 else if(xpos
>MACPAINT_WIDTH
) {
125 de_dbg(c
, "image at offset %d: literal too long", (int)imgstart
);
133 if(xpos
==MACPAINT_WIDTH
) {
137 else if(xpos
>MACPAINT_WIDTH
) {
138 de_dbg(c
, "image at offset %d: run too long", (int)imgstart
);
144 if(xpos
==0 && ypos
==MACPAINT_HEIGHT
) {
145 de_dbg(c
, "image at offset %d decodes okay", (int)imgstart
);
149 de_dbg(c
, "image at offset %d: premature end of file (x=%d, y=%d)", (int)imgstart
, (int)xpos
, (int)ypos
);
153 static const char *get_pattern_set_info(u32 patcrc
, int *is_blank
)
157 case 0x284a7a15: return "variant 1";
158 case 0x33d2d8d6: return "standard";
159 case 0x47514647: *is_blank
= 1; return "blank";
160 case 0xb5348fd2: *is_blank
= 1; return "blank variant 1";
162 return "unrecognized";
165 // Some MacPaint files contain a collection of brush patterns.
166 // Essentially, MacPaint saves workspace settings inside image files.
167 // (But these patterns are the only setting.)
168 static void do_read_patterns(deark
*c
, lctx
*d
, i64 pos
)
173 const i64 dispwidth
= 19;
174 const i64 dispheight
= 17;
177 de_bitmap
*pat
= NULL
;
179 const char *patsetname
;
181 de_ucstring
*tmpname
= NULL
;
182 struct de_crcobj
*crc32o
;
186 crc32o
= de_crcobj_create(c
, DE_CRCOBJ_CRC32_IEEE
);
187 de_crcobj_addslice(crc32o
, c
->infile
, pos
, 38*8);
188 patcrc
= de_crcobj_getval(crc32o
);
189 de_crcobj_destroy(crc32o
);
190 patsetname
= get_pattern_set_info(patcrc
, &is_blank
);
191 de_dbg(c
, "brush patterns crc: 0x%08x (%s)", (unsigned int)patcrc
, patsetname
);
193 if(c
->extract_level
<2) {
198 de_dbg(c
, "brush patterns are blank: not extracting");
202 pat
= de_bitmap_create(c
, (dispwidth
+1)*19+1, (dispheight
+1)*2+1, 1);
204 for(cell
=0; cell
<38; cell
++) {
205 xpos
= (dispwidth
+1)*(cell
%19)+1;
206 ypos
= (dispheight
+1)*(cell
/19)+1;
208 for(j
=0; j
<dispheight
; j
++) {
209 for(i
=0; i
<dispwidth
; i
++) {
210 // TODO: Figure out the proper "brush origin" of these patterns.
211 // Some of them may be shifted differently than MacPaint displays them.
212 x
= de_get_bits_symbol(c
->infile
, 1, pos
+cell
*8+j
%8, i
%8);
214 // 0 = white. Only need to set the white pixels, since de_bitmap
215 // pixels default to black.
217 de_bitmap_setpixel_gray(pat
, xpos
+i
, ypos
+j
, 255);
223 tmpname
= ucstring_create(c
);
224 if(d
->filename
&& c
->filenames_from_file
) {
225 ucstring_append_ucstring(tmpname
, d
->filename
);
226 ucstring_append_sz(tmpname
, ".", DE_ENCODING_LATIN1
);
228 ucstring_append_sz(tmpname
, "pat", DE_ENCODING_LATIN1
);
229 fi
= de_finfo_create(c
);
230 de_finfo_set_name_from_ucstring(c
, fi
, tmpname
, 0);
231 de_bitmap_write_to_file_finfo(pat
, fi
, DE_CREATEFLAG_IS_AUX
);
234 de_bitmap_destroy(pat
);
235 de_finfo_destroy(c
, fi
);
236 ucstring_destroy(tmpname
);
239 // Not many MacPaint-in-MacBinary files have a resource fork, but a few do.
240 static void do_decode_rsrc(deark
*c
, lctx
*d
)
242 if(!d
->rf_known
) return;
243 if(d
->expected_rflen
<1) return;
244 if(d
->expected_rfpos
+d
->expected_rflen
> c
->infile
->len
) {
247 de_dbg(c
, "resource fork at %"I64_FMT
", len=%"I64_FMT
, d
->expected_rfpos
, d
->expected_rflen
);
249 de_run_module_by_id_on_slice2(c
, "macrsrc", NULL
, c
->infile
,
250 d
->expected_rfpos
, d
->expected_rflen
);
251 de_dbg_indent(c
, -1);
254 static void do_macbinary(deark
*c
, lctx
*d
)
257 de_module_params
*mparams
= NULL
;
262 // Instead of a real MacBinary header, a few macpaint files just have
263 // 128 NUL bytes, or something like that. So we'll skip MacBinary parsing
266 if(b1
<1 || b1
>63) goto done
;
268 de_dbg(c
, "MacBinary header at %d", 0);
270 mparams
= de_malloc(c
, sizeof(de_module_params
));
271 mparams
->in_params
.codes
= "D"; // = decode only, don't extract
272 mparams
->out_params
.fi
= de_finfo_create(c
); // A temporary finfo object
273 mparams
->out_params
.fi
->name_other
= ucstring_create(c
);
274 de_run_module_by_id_on_slice(c
, "macbinary", mparams
, c
->infile
, 0, c
->infile
->len
);
275 de_dbg_indent(c
, -1);
277 if(mparams
->out_params
.uint1
>0) {
279 d
->expected_dfpos
= (i64
)mparams
->out_params
.uint1
;
280 d
->expected_dflen
= (i64
)mparams
->out_params
.uint2
;
282 if(mparams
->out_params
.uint3
>0) {
284 d
->expected_rfpos
= (i64
)mparams
->out_params
.uint3
;
285 d
->expected_rflen
= (i64
)mparams
->out_params
.uint4
;
288 if(mparams
->out_params
.fi
->timestamp
[DE_TIMESTAMPIDX_MODIFY
].is_valid
) {
289 d
->mod_time_from_macbinary
= mparams
->out_params
.fi
->timestamp
[DE_TIMESTAMPIDX_MODIFY
];
293 if(d
->expected_dfpos
+d
->expected_dflen
>c
->infile
->len
) {
294 de_warn(c
, "MacBinary data fork (ends at %"I64_FMT
") "
295 "goes past end of file (%"I64_FMT
")",
296 d
->expected_dfpos
+d
->expected_dflen
, c
->infile
->len
);
301 if(ucstring_isnonempty(mparams
->out_params
.fi
->name_other
) && !d
->filename
) {
302 d
->filename
= ucstring_clone(mparams
->out_params
.fi
->name_other
);
306 do_decode_rsrc(c
, d
);
311 de_finfo_destroy(c
, mparams
->out_params
.fi
);
316 static void de_run_macpaint(deark
*c
, de_module_params
*mparams
)
321 d
= de_malloc(c
, sizeof(lctx
));
322 d
->has_macbinary_header
= de_get_ext_option_bool(c
, "macpaint:macbinary", -1);
324 if(d
->has_macbinary_header
== -1) {
327 de_dbg(c
, "trying to determine if file has a MacBinary header");
330 de_dbg(c
, "checking for image at offset 512");
332 v512
= valid_file_at(c
, d
, 0);
333 de_dbg_indent(c
, -1);
334 de_dbg(c
, "checking for image at offset 640");
336 v640
= valid_file_at(c
, d
, 128);
337 de_dbg_indent(c
, -1);
338 de_dbg_indent(c
, -1);
341 de_dbg(c
, "assuming it has no MacBinary header");
342 d
->has_macbinary_header
= 0;
344 else if(v640
> v512
) {
345 de_dbg(c
, "assuming it has a MacBinary header");
346 d
->has_macbinary_header
= 1;
348 else if(v512
&& v640
) {
349 de_warn(c
, "Can't determine if this file has a MacBinary header. "
350 "Try \"-opt macpaint:macbinary=0\".");
351 d
->has_macbinary_header
= 1;
354 de_warn(c
, "This is probably not a MacPaint file.");
355 d
->has_macbinary_header
= 1;
359 if(d
->has_macbinary_header
)
360 de_declare_fmt(c
, "MacPaint with MacBinary header");
362 de_declare_fmt(c
, "MacPaint without MacBinary header");
365 if(d
->has_macbinary_header
) {
370 do_read_bitmap(c
, d
, pos
);
372 do_read_patterns(c
, d
, pos
);
375 ucstring_destroy(d
->filename
);
380 // Note: This must be coordinated with the macbinary detection routine.
381 static int de_identify_macpaint(deark
*c
)
387 // Not all MacPaint files can be easily identified, but this will work
389 if(!de_memcmp(buf
, "PNTG", 4)) {
390 if(c
->detection_data
->is_macbinary
) return 100;
391 if(!de_memcmp(&buf
[4], "MPNT", 4)) return 80;
395 if(de_input_file_has_ext(c
, "mac")) return 10;
396 if(de_input_file_has_ext(c
, "macp")) return 15;
397 if(de_input_file_has_ext(c
, "pntg")) return 15;
401 static void de_help_macpaint(deark
*c
)
403 de_msg(c
, "-opt macpaint:macbinary=<0|1> : Assume file doesn't/does have "
404 "a MacBinary header");
405 de_msg(c
, "-m macbinary : Extract from MacBinary container, instead of "
409 void de_module_macpaint(deark
*c
, struct deark_module_info
*mi
)
412 mi
->desc
= "MacPaint image";
413 mi
->run_fn
= de_run_macpaint
;
414 mi
->identify_fn
= de_identify_macpaint
;
415 mi
->help_fn
= de_help_macpaint
;