1use crate::error::Result;
7use crate::tag::{Tag, TagGroup, TagId};
8use crate::tags::iptc as iptc_tags;
9use crate::value::Value;
10
11const STANDARD_GROUP1: &str = "IPTC";
15
16const NONSTANDARD_GROUP1: &str = "IPTC2";
21
22pub struct IptcReader;
24
25impl IptcReader {
26 fn detect_iptc_charset(data: &[u8]) -> bool {
38 let mut pos = 0;
39 while pos + 5 <= data.len() {
40 if data[pos] != 0x1C {
41 pos += 1;
42 continue;
43 }
44 let record = data[pos + 1];
45 let dataset = data[pos + 2];
46 let length = u16::from_be_bytes([data[pos + 3], data[pos + 4]]) as usize;
47 pos += 5;
48 if length >= 0x8000 {
49 break;
50 }
51 if pos + length > data.len() {
52 break;
53 }
54 if record == 1 && dataset == 90 {
55 let val = &data[pos..pos + length];
56 return val.windows(3).any(|w| w == [0x1B, 0x25, 0x47]);
58 }
59 pos += length;
60 }
61 false
62 }
63
64 pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
66 Self::read_in_group(data, STANDARD_GROUP1)
67 }
68
69 pub fn read_nonstandard(data: &[u8]) -> Result<Vec<Tag>> {
73 Self::read_in_group(data, NONSTANDARD_GROUP1)
74 }
75
76 fn read_in_group(data: &[u8], group1: &str) -> Result<Vec<Tag>> {
77 let mut tags = Vec::new();
78 let is_utf8 = Self::detect_iptc_charset(data);
79 let mut pos = 0;
80
81 while pos + 5 <= data.len() {
82 if data[pos] != 0x1C {
84 pos += 1;
86 continue;
87 }
88
89 let record = data[pos + 1];
90 let dataset = data[pos + 2];
91 let length = u16::from_be_bytes([data[pos + 3], data[pos + 4]]) as usize;
92
93 pos += 5;
94
95 if length >= 0x8000 {
98 break;
100 }
101
102 if pos + length > data.len() {
103 break;
104 }
105
106 let value_data = &data[pos..pos + length];
107 pos += length;
108
109 if !matches!(record, 1 | 2) {
114 continue;
115 }
116
117 if record == 2 && (209..=222).contains(&dataset) {
120 let bin_value = Value::Binary(value_data.to_vec());
122 if let Some((pm_name, pm_print)) = lookup_photomechanic(dataset, &bin_value) {
123 tags.push(Tag {
124 id: TagId::Numeric(((record as u16) << 8) | dataset as u16),
125 name: pm_name.clone(),
126 description: pm_name,
127 group: TagGroup {
128 family0: "PhotoMechanic".to_string(),
129 family1: "PhotoMechanic".to_string(),
130 family2: "Image".to_string(),
131 family3: "Main".into(),
132 },
133 raw_value: bin_value,
134 print_value: pm_print,
135 priority: 0,
136 });
137 continue;
138 }
139 }
140
141 let value = if iptc_tags::is_string_tag(record, dataset) {
142 let s = if is_utf8 {
143 crate::encoding::decode_utf8_or_latin1(value_data).to_string()
144 } else {
145 crate::encoding::decode_latin1(value_data)
146 };
147 Value::String(s.trim_end_matches('\0').to_string())
148 } else if length <= 2 {
149 match length {
150 1 => Value::U8(value_data[0]),
151 2 => Value::U16(u16::from_be_bytes([value_data[0], value_data[1]])),
152 _ => Value::Binary(value_data.to_vec()),
153 }
154 } else {
155 Value::Binary(value_data.to_vec())
156 };
157
158 let tag_info = iptc_tags::lookup(record, dataset);
159 let (name, description) = match tag_info {
160 Some(info) => (info.name.to_string(), info.description.to_string()),
161 None => {
162 continue;
164 }
165 };
166
167 let base = value.to_display_string();
168 let print_value = iptc_print_conv(record, dataset, &base).unwrap_or(base);
169
170 let id_num = ((record as u16) << 8) | dataset as u16;
173 if let Some(existing) = tags
174 .iter_mut()
175 .find(|t| matches!(t.id, TagId::Numeric(n) if n == id_num))
176 {
177 let prev = std::mem::replace(&mut existing.raw_value, Value::U8(0));
178 let mut items = match prev {
179 Value::List(v) => v,
180 single => vec![single],
181 };
182 items.push(value);
183 existing.print_value = items
184 .iter()
185 .map(|v| v.to_display_string())
186 .collect::<Vec<_>>()
187 .join(", ");
188 existing.raw_value = Value::List(items);
189 continue;
190 }
191
192 tags.push(Tag {
193 id: TagId::Numeric(id_num),
194 name,
195 description,
196 group: TagGroup {
197 family0: "IPTC".to_string(),
198 family1: group1.to_string(),
199 family2: "Other".to_string(),
200 family3: "Main".into(),
201 },
202 raw_value: value,
203 print_value,
204 priority: 0,
205 });
206 }
207
208 Ok(tags)
209 }
210}
211
212fn convert_iptc_time(s: &str) -> Option<String> {
216 let b = s.as_bytes();
217 if b.len() < 6 || !b[0..6].iter().all(|c| c.is_ascii_digit()) {
218 return None;
219 }
220 let mut out = format!("{}:{}:{}", &s[0..2], &s[2..4], &s[4..6]);
221 let tz = &s[6..];
222 if !tz.is_empty() {
223 let tb = tz.as_bytes();
224 if tb.len() == 5
225 && (tb[0] == b'+' || tb[0] == b'-')
226 && tb[1..].iter().all(|c| c.is_ascii_digit())
227 {
228 out.push_str(&format!("{}{}:{}", &tz[0..1], &tz[1..3], &tz[3..5]));
229 } else {
230 return None;
231 }
232 }
233 Some(out)
234}
235
236fn iptc_print_conv(record: u8, dataset: u8, s: &str) -> Option<String> {
237 if record != 2 {
238 return None;
239 }
240 let s = s.trim();
241 match dataset {
242 200 => {
244 let name = match s {
245 "0" => Some("No ObjectData"),
246 "1" => Some("IPTC-NAA Digital Newsphoto Parameter Record"),
247 "2" => Some("IPTC7901 Recommended Message Format"),
248 "3" => Some("Tagged Image File Format (Adobe/Aldus Image data)"),
249 "4" => Some("Illustrator (Adobe Graphics data)"),
250 "5" => Some("AppleSingle (Apple Computer Inc)"),
251 _ => None,
252 };
253 Some(
254 name.map(|n| n.to_string())
255 .unwrap_or_else(|| format!("Unknown ({})", s)),
256 )
257 }
258 55 | 62 if s.len() == 8 && s.bytes().all(|b| b.is_ascii_digit()) => {
260 Some(format!("{}:{}:{}", &s[0..4], &s[4..6], &s[6..8]))
261 }
262 60 | 63 => convert_iptc_time(s),
264 10 => Some(
266 match s {
267 "0" => "0 (reserved)",
268 "1" => "1 (most urgent)",
269 "5" => "5 (normal urgency)",
270 "8" => "8 (least urgent)",
271 _ => return None,
272 }
273 .to_string(),
274 ),
275 221 => Some(prefs_print_conv(s)),
279 75 => Some(match s {
281 "a" => "Morning".to_string(),
282 "p" => "Evening".to_string(),
283 "b" => "Both Morning and Evening".to_string(),
284 other => format!("Unknown ({})", other),
285 }),
286 _ => None,
287 }
288}
289
290fn prefs_print_conv(s: &str) -> String {
295 let b = s.as_bytes();
296 for start in 0..b.len() {
298 let mut i = start;
299 while i < b.len() && b[i].is_ascii_whitespace() {
301 i += 1;
302 }
303 let mut nums: [&str; 3] = [""; 3];
304 let mut ok = true;
305 for num in &mut nums {
306 let d0 = i;
307 while i < b.len() && b[i].is_ascii_digit() {
308 i += 1;
309 }
310 if i == d0 || i >= b.len() || b[i] != b':' {
311 ok = false;
312 break;
313 }
314 *num = &s[d0..i];
315 i += 1; while i < b.len() && b[i].is_ascii_whitespace() {
318 i += 1;
319 }
320 }
321 if !ok {
322 continue;
323 }
324 let f0 = i;
326 while i < b.len() && !b[i].is_ascii_whitespace() {
327 i += 1;
328 }
329 let frame = &s[f0..i];
330 return format!(
331 "{}Tagged:{}, ColorClass:{}, Rating:{}, FrameNum:{}{}",
332 &s[..start],
333 nums[0],
334 nums[1],
335 nums[2],
336 frame,
337 &s[i..]
338 );
339 }
340 s.to_string()
341}
342
343fn lookup_photomechanic(dataset: u8, value: &Value) -> Option<(String, String)> {
346 let int_val = if let Value::Binary(ref b) = value {
348 if b.len() == 4 {
349 i32::from_be_bytes([b[0], b[1], b[2], b[3]])
350 } else {
351 return None;
352 }
353 } else {
354 return None;
355 };
356
357 let color_classes = [
358 "0 (None)",
359 "1 (Winner)",
360 "2 (Winner alt)",
361 "3 (Superior)",
362 "4 (Superior alt)",
363 "5 (Typical)",
364 "6 (Typical alt)",
365 "7 (Extras)",
366 "8 (Trash)",
367 ];
368
369 match dataset {
370 209 => Some((
371 "RawCropLeft".to_string(),
372 format!("{:.3}%", int_val as f64 / 655.36),
373 )),
374 210 => Some((
375 "RawCropTop".to_string(),
376 format!("{:.3}%", int_val as f64 / 655.36),
377 )),
378 211 => Some((
379 "RawCropRight".to_string(),
380 format!("{:.3}%", int_val as f64 / 655.36),
381 )),
382 212 => Some((
383 "RawCropBottom".to_string(),
384 format!("{:.3}%", int_val as f64 / 655.36),
385 )),
386 213 => Some(("ConstrainedCropWidth".to_string(), int_val.to_string())),
387 214 => Some(("ConstrainedCropHeight".to_string(), int_val.to_string())),
388 215 => Some(("FrameNum".to_string(), int_val.to_string())),
389 216 => {
390 let rot = match int_val {
391 0 => "0",
392 1 => "90",
393 2 => "180",
394 3 => "270",
395 _ => "0",
396 };
397 Some(("Rotation".to_string(), rot.to_string()))
398 }
399 217 => Some(("CropLeft".to_string(), int_val.to_string())),
400 218 => Some(("CropTop".to_string(), int_val.to_string())),
401 219 => Some(("CropRight".to_string(), int_val.to_string())),
402 220 => Some(("CropBottom".to_string(), int_val.to_string())),
403 221 => {
404 let v = if int_val == 0 { "No" } else { "Yes" };
405 Some(("Tagged".to_string(), v.to_string()))
406 }
407 222 => {
408 let idx = int_val as usize;
409 let class = if idx < color_classes.len() {
410 color_classes[idx].to_string()
411 } else {
412 format!("{}", int_val)
413 };
414 Some(("ColorClass".to_string(), class))
415 }
416 223 => Some(("Rating".to_string(), int_val.to_string())),
417 236 => Some((
418 "PreviewCropLeft".to_string(),
419 format!("{:.3}%", int_val as f64 / 655.36),
420 )),
421 237 => Some((
422 "PreviewCropTop".to_string(),
423 format!("{:.3}%", int_val as f64 / 655.36),
424 )),
425 238 => Some((
426 "PreviewCropRight".to_string(),
427 format!("{:.3}%", int_val as f64 / 655.36),
428 )),
429 239 => Some((
430 "PreviewCropBottom".to_string(),
431 format!("{:.3}%", int_val as f64 / 655.36),
432 )),
433 _ => None,
434 }
435}