1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
//! MIFF (Magick Image File Format) reader.
//!
//! Parses MIFF headers (text key=value pairs) and embedded profiles
//! (IPTC/Photoshop 8BIM blocks, EXIF APP1, XMP APP1).
//! Mirrors ExifTool's MIFF.pm ProcessMIFF().
use crate::error::{Error, Result};
use crate::metadata::{ExifReader, XmpReader};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
/// XMP APP1 header as used in MIFF (and PNG): "http://ns.adobe.com/xap/1.0/\0"
const XMP_APP1_HDR: &[u8] = b"http://ns.adobe.com/xap/1.0/\x00";
/// EXIF APP1 header: "Exif\0\0"
const EXIF_APP1_HDR: &[u8] = b"Exif\x00\x00";
/// Map a lower-case MIFF header key to an ExifTool tag name.
/// Returns None for profile-* keys (handled separately) and unknown keys
/// (which are dynamically named).
fn miff_tag_name(key: &str) -> Option<&'static str> {
match key {
"background-color" => Some("BackgroundColor"),
"blue-primary" => Some("BluePrimary"),
"border-color" => Some("BorderColor"),
"matt-color" => Some("MattColor"),
"class" => Some("Class"),
"colors" => Some("Colors"),
"colorspace" => Some("ColorSpace"),
"columns" => Some("ImageWidth"),
"compression" => Some("Compression"),
"delay" => Some("Delay"),
"depth" => Some("Depth"),
"dispose" => Some("Dispose"),
"gamma" => Some("Gamma"),
"green-primary" => Some("GreenPrimary"),
"id" => Some("ID"),
"iterations" => Some("Iterations"),
"label" => Some("Label"),
"matte" => Some("Matte"),
"montage" => Some("Montage"),
"packets" => Some("Packets"),
"page" => Some("Page"),
"red-primary" => Some("RedPrimary"),
"rendering-intent" => Some("RenderingIntent"),
"resolution" => Some("Resolution"),
"rows" => Some("ImageHeight"),
"scene" => Some("Scene"),
"signature" => Some("Signature"),
"units" => Some("Units"),
"white-point" => Some("WhitePoint"),
_ => None,
}
}
fn make_miff_tag(name: &str, value: String) -> Tag {
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: name.to_string(),
group: TagGroup {
family0: "MIFF".into(),
family1: "MIFF".into(),
family2: "Image".into(),
},
raw_value: Value::String(value.clone()),
print_value: value,
priority: 0,
}
}
/// Extract all metadata tags from a MIFF file.
pub fn read_miff(data: &[u8]) -> Result<Vec<Tag>> {
// MIFF files must start with "id=ImageMagick"
if data.len() < 14 || &data[..14] != b"id=ImageMagick" {
return Err(Error::InvalidData("not a MIFF file".into()));
}
let mut tags = Vec::new();
// Find end of header: ":\x1a" (new-style) or ":\n" (old-style)
// The Perl code reads until ":\x1a" (Colon+Ctrl-Z).
// For old-style files it may just use ":\n".
let header_end = find_header_end(data);
if header_end.is_none() {
return Err(Error::InvalidData(
"MIFF header end marker not found".into(),
));
}
let (header_data_end, profile_data_start) = header_end.unwrap();
// Parse the header text
let header_bytes = &data[..header_data_end];
let header_str = crate::encoding::decode_utf8_or_latin1(header_bytes);
// Collect profiles: list of (profile_type, byte_length)
let mut profiles: Vec<(String, usize)> = Vec::new();
// Parse header: lines of "key=value" pairs separated by whitespace/newlines.
// The Perl code splits on whitespace: split ' ', $buff
// This means all tokens (split by any whitespace) are processed.
// Multi-word values are enclosed in braces: key={value with spaces}
// Comments are in braces starting with {.
parse_header_tokens(&header_str, &mut tags, &mut profiles);
// Process profile data
let mut pos = profile_data_start;
for (profile_type, profile_len) in &profiles {
if pos + profile_len > data.len() {
break;
}
let profile_data = &data[pos..pos + profile_len];
pos += profile_len;
match profile_type.as_str() {
"iptc" => {
// IPTC profile: if starts with "8BIM", process as Photoshop IRB blocks
if profile_data.starts_with(b"8BIM") {
crate::formats::psd::read_irb_resources(
profile_data,
0,
profile_data.len(),
&mut tags,
);
// Perl ExifTool does not emit CurrentIPTCDigest for MIFF files
tags.retain(|t| t.name != "CurrentIPTCDigest");
}
}
"APP1" | "exif" => {
if profile_data.starts_with(EXIF_APP1_HDR) {
// APP1 EXIF: skip "Exif\0\0" header, then parse TIFF
let exif_data = &profile_data[EXIF_APP1_HDR.len()..];
if let Ok(exif_tags) = ExifReader::read(exif_data) {
tags.extend(exif_tags);
}
} else if profile_data.starts_with(XMP_APP1_HDR) {
// APP1 XMP: skip header, then parse XMP
let xmp_data = &profile_data[XMP_APP1_HDR.len()..];
if let Ok(xmp_tags) = XmpReader::read(xmp_data) {
tags.extend(xmp_tags);
}
}
}
"xmp" => {
if let Ok(xmp_tags) = XmpReader::read(profile_data) {
tags.extend(xmp_tags);
}
}
"icc" => {
if let Ok(icc_tags) = crate::formats::icc::read_icc(profile_data) {
tags.extend(icc_tags);
}
}
_ => {}
}
}
// Perl ExifTool doesn't emit CurrentIPTCDigest for MIFF
tags.retain(|t| t.name != "CurrentIPTCDigest");
Ok(tags)
}
/// Find the end of the MIFF header.
/// Returns (offset of end-of-header-content, offset of first profile byte).
/// The header ends with ":\x1a" or (old-style) ":\n" as terminator.
fn find_header_end(data: &[u8]) -> Option<(usize, usize)> {
// Look for ":\x1a" first (new-style)
if let Some(idx) = find_bytes(data, b":\x1a") {
return Some((idx, idx + 2));
}
// Old-style: look for standalone ":\n"
// The Perl code: local $/ = ":\x1a"; but old files end with ":\n"
// We'll just use the entire file as header in that case (no profile data)
if let Some(idx) = find_bytes(data, b":\n") {
return Some((idx, idx + 2));
}
None
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
if needle.is_empty() || haystack.len() < needle.len() {
return None;
}
haystack.windows(needle.len()).position(|w| w == needle)
}
/// Parse MIFF header tokens and populate tags and profiles lists.
/// The Perl code: split ' ', $buff — splits on any whitespace.
/// Then iterates through tokens, building key=value pairs.
fn parse_header_tokens(header: &str, tags: &mut Vec<Tag>, profiles: &mut Vec<(String, usize)>) {
// Tokenize: split on any whitespace (space, tab, newline, CR, form-feed)
let tokens: Vec<&str> = header.split_whitespace().collect();
let mut i = 0;
let mut mode = ""; // "" normal, "com" in comment, "val" in multi-word value
let mut current_tag: Option<String> = None;
let mut current_val = String::new();
while i < tokens.len() {
let token = tokens[i];
i += 1;
if mode == "com" {
if token.ends_with('}') {
mode = "";
}
continue;
}
if token.starts_with('{') && current_tag.is_none() {
// A comment block
if !token.ends_with('}') {
mode = "com";
}
continue;
}
if mode == "val" {
// Continuation of a brace-enclosed value
current_val.push(' ');
current_val.push_str(token);
if token.ends_with('}') {
mode = "";
// Remove surrounding braces from accumulated value
if let Some(ref tag) = current_tag {
let val = current_val
.trim_start_matches('{')
.trim_end_matches('}')
.to_string();
emit_tag(tag, val, tags, profiles);
}
current_tag = None;
current_val.clear();
}
continue;
}
// Normal token: should be "key=value"
if let Some(eq_pos) = token.find('=') {
let key = &token[..eq_pos];
let val = &token[eq_pos + 1..];
if val.starts_with('{') {
if val.ends_with('}') && val.len() > 1 {
// Single-token brace value
let inner = &val[1..val.len() - 1];
emit_tag(key, inner.to_string(), tags, profiles);
} else {
// Multi-token brace value
mode = "val";
current_tag = Some(key.to_string());
current_val = val.to_string();
}
} else {
emit_tag(key, val.to_string(), tags, profiles);
}
} else if token.starts_with(':') {
// End of old-style MIFF
break;
}
// Unknown token: ignore (Perl warns but continues)
}
}
fn emit_tag(key: &str, val: String, tags: &mut Vec<Tag>, profiles: &mut Vec<(String, usize)>) {
// Check for profile-* keys
if let Some(profile_type) = key.strip_prefix("profile-") {
// val is the length as a string
if let Ok(len) = val.parse::<usize>() {
profiles.push((profile_type.to_string(), len));
}
return;
}
// Map key to ExifTool tag name
let tag_name = if let Some(mapped) = miff_tag_name(key) {
mapped.to_string()
} else {
// Dynamic tag: use the key as-is (Perl adds it to the tag table)
key.to_string()
};
tags.push(make_miff_tag(&tag_name, val));
}