1use crate::error::Result;
7use crate::tag::{Tag, TagGroup, TagId};
8use crate::tags::iptc as iptc_tags;
9use crate::value::Value;
10
11pub struct IptcReader;
13
14impl IptcReader {
15 pub fn read(data: &[u8]) -> Result<Vec<Tag>> {
24 let mut tags = Vec::new();
25 let mut pos = 0;
26
27 while pos + 5 <= data.len() {
28 if data[pos] != 0x1C {
30 pos += 1;
32 continue;
33 }
34
35 let record = data[pos + 1];
36 let dataset = data[pos + 2];
37 let length = u16::from_be_bytes([data[pos + 3], data[pos + 4]]) as usize;
38
39 pos += 5;
40
41 if length >= 0x8000 {
44 break;
46 }
47
48 if pos + length > data.len() {
49 break;
50 }
51
52 let value_data = &data[pos..pos + length];
53 pos += length;
54
55 let ifd_name = match record {
57 1 => "IPTCEnvelope",
58 2 => "IPTCApplication",
59 _ => continue,
60 };
61
62 if record == 2 && dataset >= 209 && dataset <= 222 {
65 let bin_value = Value::Binary(value_data.to_vec());
67 if let Some((pm_name, pm_print)) = lookup_photomechanic(dataset, &bin_value) {
68 tags.push(Tag {
69 id: TagId::Numeric(((record as u16) << 8) | dataset as u16),
70 name: pm_name.clone(),
71 description: pm_name,
72 group: TagGroup {
73 family0: "PhotoMechanic".to_string(),
74 family1: "PhotoMechanic".to_string(),
75 family2: "Image".to_string(),
76 },
77 raw_value: bin_value,
78 print_value: pm_print,
79 priority: 0,
80 });
81 continue;
82 }
83 }
84
85 let value = if iptc_tags::is_string_tag(record, dataset) {
86 Value::String(
87 String::from_utf8_lossy(value_data)
88 .trim_end_matches('\0')
89 .to_string(),
90 )
91 } else if length <= 2 {
92 match length {
93 1 => Value::U8(value_data[0]),
94 2 => Value::U16(u16::from_be_bytes([value_data[0], value_data[1]])),
95 _ => Value::Binary(value_data.to_vec()),
96 }
97 } else {
98 Value::Binary(value_data.to_vec())
99 };
100
101 let tag_info = iptc_tags::lookup(record, dataset);
102 let (name, description) = match tag_info {
103 Some(info) => (info.name.to_string(), info.description.to_string()),
104 None => {
105 continue;
107 },
108 };
109
110 let print_value = value.to_display_string();
111
112 tags.push(Tag {
113 id: TagId::Numeric(((record as u16) << 8) | dataset as u16),
114 name,
115 description,
116 group: TagGroup {
117 family0: "IPTC".to_string(),
118 family1: ifd_name.to_string(),
119 family2: "Other".to_string(),
120 },
121 raw_value: value,
122 print_value,
123 priority: 0,
124 });
125 }
126
127 Ok(tags)
128 }
129}
130
131fn lookup_photomechanic(dataset: u8, value: &Value) -> Option<(String, String)> {
134 let int_val = if let Value::Binary(ref b) = value {
136 if b.len() == 4 {
137 i32::from_be_bytes([b[0], b[1], b[2], b[3]])
138 } else {
139 return None;
140 }
141 } else {
142 return None;
143 };
144
145 let color_classes = [
146 "0 (None)", "1 (Winner)", "2 (Winner alt)", "3 (Superior)",
147 "4 (Superior alt)", "5 (Typical)", "6 (Typical alt)", "7 (Extras)", "8 (Trash)",
148 ];
149
150 match dataset {
151 209 => Some(("RawCropLeft".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
152 210 => Some(("RawCropTop".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
153 211 => Some(("RawCropRight".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
154 212 => Some(("RawCropBottom".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
155 213 => Some(("ConstrainedCropWidth".to_string(), int_val.to_string())),
156 214 => Some(("ConstrainedCropHeight".to_string(), int_val.to_string())),
157 215 => Some(("FrameNum".to_string(), int_val.to_string())),
158 216 => {
159 let rot = match int_val {
160 0 => "0", 1 => "90", 2 => "180", 3 => "270", _ => "0",
161 };
162 Some(("Rotation".to_string(), rot.to_string()))
163 }
164 217 => Some(("CropLeft".to_string(), int_val.to_string())),
165 218 => Some(("CropTop".to_string(), int_val.to_string())),
166 219 => Some(("CropRight".to_string(), int_val.to_string())),
167 220 => Some(("CropBottom".to_string(), int_val.to_string())),
168 221 => {
169 let v = if int_val == 0 { "No" } else { "Yes" };
170 Some(("Tagged".to_string(), v.to_string()))
171 }
172 222 => {
173 let idx = int_val as usize;
174 let class = if idx < color_classes.len() {
175 color_classes[idx].to_string()
176 } else {
177 format!("{}", int_val)
178 };
179 Some(("ColorClass".to_string(), class))
180 }
181 223 => Some(("Rating".to_string(), int_val.to_string())),
182 236 => Some(("PreviewCropLeft".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
183 237 => Some(("PreviewCropTop".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
184 238 => Some(("PreviewCropRight".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
185 239 => Some(("PreviewCropBottom".to_string(), format!("{:.3}%", int_val as f64 / 655.36))),
186 _ => None,
187 }
188}