1use crate::error::{Error, Result};
7use crate::metadata::XmpReader;
8use crate::tag::{Tag, TagGroup, TagId};
9use crate::value::Value;
10
11fn decode_hex(s: &str) -> Vec<u8> {
13 let s: String = s.chars().filter(|c| c.is_ascii_hexdigit()).collect();
14 (0..s.len() / 2)
15 .filter_map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).ok())
16 .collect()
17}
18
19pub fn read_postscript(data: &[u8], extract_embedded: u8) -> Result<Vec<Tag>> {
23 let mut tags = Vec::new();
24 let mut offset = 0;
25
26 if data.len() >= 30 && data.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) {
28 let ps_offset = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
29 let ps_length = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
30
31 if ps_offset + ps_length <= data.len() {
32 offset = ps_offset;
33 }
34 tags.push(mk(
35 "EPSFormat",
36 "EPS Format",
37 Value::String("DOS Binary".into()),
38 ));
39 }
40
41 if offset + 4 > data.len()
43 || (!data[offset..].starts_with(b"%!PS") && !data[offset..].starts_with(b"%!Ad"))
44 {
45 return Err(Error::InvalidData("not a PostScript file".into()));
46 }
47
48 scan_dsc(&data[offset..], extract_embedded, &mut tags);
53
54 let xmp_at = find_bytes(&data[offset..], b"<?xpacket begin");
62 let irb_at = find_bytes(&data[offset..], b"%BeginPhotoshop:");
63 let xmp_first = match (xmp_at, irb_at) {
64 (Some(x), Some(i)) => x < i,
65 _ => true,
66 };
67
68 let full_text = crate::encoding::decode_utf8_or_latin1(&data[offset..]);
69 let full_text = full_text.replace('\r', "\n");
70
71 let read_xmp = |tags: &mut Vec<Tag>| {
72 if let Some(xmp_start) = xmp_at {
73 let xmp_data = &data[offset + xmp_start..];
74 if let Some(xmp_end) = find_bytes(xmp_data, b"<?xpacket end") {
75 let end = xmp_end + 20; if let Ok(xmp_tags) = XmpReader::read(&xmp_data[..end.min(xmp_data.len())]) {
77 tags.extend(xmp_tags);
78 }
79 }
80 }
81 };
82
83 if xmp_first {
84 read_xmp(&mut tags);
85 parse_photoshop_blocks(&full_text, &mut tags);
86 } else {
87 parse_photoshop_blocks(&full_text, &mut tags);
88 read_xmp(&mut tags);
89 }
90
91 parse_image_data_comment(&full_text, &mut tags);
93
94 Ok(tags)
95}
96
97fn dsc_tag(name: &str) -> Option<(&'static str, &'static str)> {
103 Some(match name {
104 "Author" => ("Author", "Author"),
105 "BoundingBox" => ("BoundingBox", "Bounding Box"),
106 "Copyright" => ("Copyright", "Copyright"),
107 "CreationDate" => ("CreateDate", "Create Date"),
108 "Creator" => ("Creator", "Creator"),
109 "ImageData" => ("ImageData", "Image Data"),
110 "For" => ("For", "For"),
111 "Keywords" => ("Keywords", "Keywords"),
112 "ModDate" => ("ModifyDate", "Modify Date"),
113 "Pages" => ("Pages", "Pages"),
114 "Routing" => ("Routing", "Routing"),
115 "Subject" => ("Subject", "Subject"),
116 "Title" => ("Title", "Title"),
117 "Version" => ("Version", "Version"),
118 "AI9_ColorModel" => ("AIColorModel", "AI Color Model"),
120 "AI3_ColorUsage" => ("AIColorUsage", "AI Color Usage"),
121 "AI5_RulerUnits" => ("AIRulerUnits", "AI Ruler Units"),
122 "AI5_TargetResolution" => ("AITargetResolution", "AI Target Resolution"),
123 "AI5_NumLayers" => ("AINumLayers", "AI Num Layers"),
124 "AI5_FileFormat" => ("AIFileFormat", "AI File Format"),
125 "AI8_CreatorVersion" => ("AICreatorVersion", "AI Creator Version"),
126 "AI12_BuildNumber" => ("AIBuildNumber", "AI Build Number"),
127 _ => return None,
128 })
129}
130
131fn dsc_print_conv(tag: &str, val: &str) -> Option<&'static str> {
133 Some(match (tag, val.trim()) {
134 ("AIColorModel", "1") => "RGB",
135 ("AIColorModel", "2") => "CMYK",
136 ("AIRulerUnits", "0") => "Inches",
137 ("AIRulerUnits", "1") => "Millimeters",
138 ("AIRulerUnits", "2") => "Points",
139 ("AIRulerUnits", "3") => "Picas",
140 ("AIRulerUnits", "4") => "Centimeters",
141 ("AIRulerUnits", "6") => "Pixels",
142 _ => return None,
143 })
144}
145
146fn ps_lines(data: &[u8]) -> Vec<(usize, usize)> {
150 let mut out = Vec::new();
151 let mut start = 0;
152 let mut i = 0;
153 while i < data.len() {
154 if data[i] == b'\n' || data[i] == b'\r' {
155 out.push((start, i));
156 if data[i] == b'\r' && i + 1 < data.len() && data[i + 1] == b'\n' {
157 i += 1;
158 }
159 i += 1;
160 start = i;
161 } else {
162 i += 1;
163 }
164 }
165 if start < data.len() {
166 out.push((start, data.len()));
167 }
168 out
169}
170
171fn starts_with_ci(line: &str, prefix: &str) -> bool {
172 line.len() >= prefix.len() && line[..prefix.len()].eq_ignore_ascii_case(prefix)
173}
174
175fn pop_doc_level(doc_num: &mut String) -> u32 {
178 let digits_at = doc_num
179 .rfind(|c: char| !c.is_ascii_digit())
180 .map_or(0, |p| p + 1);
181 if digits_at == doc_num.len() {
182 return 0;
183 }
184 let num = doc_num[digits_at..].parse::<u32>().unwrap_or(0);
185 let cut = if digits_at > 0 && doc_num.as_bytes()[digits_at - 1] == b'-' {
186 digits_at - 1
187 } else {
188 digits_at
189 };
190 doc_num.truncate(cut);
191 num
192}
193
194fn match_begin(line: &str) -> Option<(&str, &str, &str)> {
198 let pct_len = if line.starts_with("%%") {
199 2
200 } else if line.starts_with('%') {
201 1
202 } else {
203 return None;
204 };
205 let rest = &line[pct_len..];
206 if rest.len() < 5 || !rest[..5].eq_ignore_ascii_case("Begin") {
207 return None;
208 }
209 let after = &rest[5..];
210 for kw in [
211 "_xml_packet",
212 "Photoshop",
213 "ICCProfile",
214 "Document",
215 "Binary",
216 ] {
217 if after.len() >= kw.len() && after[..kw.len()].eq_ignore_ascii_case(kw) {
218 return Some((&line[..pct_len], &rest[..5], &after[..kw.len()]));
219 }
220 }
221 None
222}
223
224fn match_dsc_comment(line: &str) -> Option<(usize, &str, &str)> {
227 let pct_len = if line.starts_with("%%") {
228 2
229 } else if line.starts_with('%') {
230 1
231 } else {
232 return None;
233 };
234 let rest = &line[pct_len..];
235 let colon = rest.find(':')?;
236 let name = &rest[..colon];
237 if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_') {
238 return None;
239 }
240 let mut val = &rest[colon + 1..];
241 val = val.strip_prefix(' ').unwrap_or(val);
243 Some((pct_len, name, val))
244}
245
246fn decode_comment(val: &str) -> String {
250 let val = val.trim_end_matches(['\r', '\n']);
251 if !(val.starts_with('(') && val.ends_with(')') && val.len() >= 2) {
252 return val.to_string();
253 }
254 let inner = &val[1..val.len() - 1];
255 let mut vals: Vec<String> = Vec::new();
256 let mut cur = String::new();
257 let mut nesting = 1usize;
258 let mut chars = inner.chars().peekable();
259 while let Some(c) = chars.next() {
260 match c {
261 '\\' => {
262 cur.push('\\');
263 if let Some(n) = chars.next() {
264 cur.push(n);
265 }
266 }
267 '(' => {
268 nesting += 1;
269 cur.push(c);
270 }
271 ')' => {
272 nesting -= 1;
273 if nesting == 0 {
274 vals.push(std::mem::take(&mut cur));
275 while chars.peek().is_some_and(|c| c.is_whitespace()) {
277 chars.next();
278 }
279 if chars.peek() == Some(&'(') {
280 chars.next();
281 nesting = 1;
282 }
283 } else {
284 cur.push(c);
285 }
286 }
287 _ => cur.push(c),
288 }
289 }
290 vals.push(cur);
291 let decoded: Vec<String> = vals.iter().map(|v| unescape_ps(v)).collect();
292 decoded.join(", ")
293}
294
295fn unescape_ps(s: &str) -> String {
298 let mut out = String::new();
299 let mut chars = s.chars().peekable();
300 while let Some(c) = chars.next() {
301 if c != '\\' {
302 out.push(c);
303 continue;
304 }
305 match chars.next() {
306 None => break,
307 Some(d) if d.is_digit(8) => {
308 let mut oct = d.to_digit(8).unwrap();
309 for _ in 0..2 {
310 match chars.peek() {
311 Some(e) if e.is_digit(8) => {
312 oct = oct * 8 + e.to_digit(8).unwrap();
313 chars.next();
314 }
315 _ => break,
316 }
317 }
318 out.push((oct & 0xff) as u8 as char);
319 }
320 Some('n') => out.push('\n'),
321 Some('r') => out.push('\r'),
322 Some('t') => out.push('\t'),
323 Some('b') => out.push('\u{8}'),
324 Some('f') => out.push('\u{c}'),
325 Some(d) => out.push(d),
326 }
327 }
328 out
329}
330
331fn scan_dsc(data: &[u8], extract_embedded: u8, tags: &mut Vec<Tag>) {
339 let embedded = extract_embedded > 0;
340 let lines = ps_lines(data);
341
342 let mut mode: Option<&'static str> = None;
344 let mut end_token: Option<String> = None;
345 let mut begin_token = String::new();
346 let mut doc_num = String::new();
347 let mut sub_doc_num: u32 = 0;
348 let mut doc_count: u32 = 0;
349 let mut end_doc: Option<String> = None;
350 let mut skip_to: usize = 0;
351
352 let mut i = 0;
353 while i < lines.len() {
354 let (start, end) = lines[i];
355 i += 1;
356 if start < skip_to {
357 continue;
358 }
359 let line = crate::encoding::decode_utf8_or_latin1(&data[start..end]);
360 let line = line.as_str();
361
362 if let Some(m) = mode {
363 match &end_token {
364 None => {
366 if !line.contains("<?xpacket end") {
367 continue;
368 }
369 mode = None;
370 continue;
371 }
372 Some(et) => {
373 if !starts_with_ci(line, et) {
374 if m == "Document" {
375 if starts_with_ci(line, &begin_token) {
377 doc_num.push_str("-1");
378 }
379 }
380 continue;
381 }
382 if m == "Document" {
383 pop_doc_level(&mut doc_num);
384 if doc_num.is_empty() {
385 mode = None;
386 }
387 continue;
388 }
389 mode = None;
391 end_token = None;
392 continue;
393 }
394 }
395 }
396
397 if let Some(ed) = end_doc.clone() {
398 if starts_with_ci(line, &ed) {
399 sub_doc_num = pop_doc_level(&mut doc_num);
400 if doc_num.is_empty() {
401 end_doc = None;
402 }
403 continue;
404 }
405 }
406
407 if let Some((pct, begin, kw)) = match_begin(line) {
408 let kind = match kw.to_ascii_lowercase().as_str() {
409 "_xml_packet" => "XMP",
410 "photoshop" => "Photoshop",
411 "iccprofile" => "ICC_Profile",
412 "document" => "Document",
413 _ => {
414 if let Some((_, name, val)) = match_dsc_comment(line) {
416 if name.eq_ignore_ascii_case("BeginBinary") {
417 if let Ok(n) = val.trim().parse::<usize>() {
418 skip_to = end + n;
419 }
420 }
421 }
422 continue;
423 }
424 };
425 let bt = format!("{pct}{begin}{kw}");
426 let et = format!("{pct}{}{kw}", if begin == "begin" { "end" } else { "End" });
427 begin_token = bt.clone();
428 end_token = Some(et.clone());
429 if kind != "Document" {
430 mode = Some(kind);
431 continue;
432 }
433 if doc_num.is_empty() {
435 doc_count += 1;
436 doc_num = doc_count.to_string();
437 } else {
438 sub_doc_num += 1;
439 doc_num.push('-');
440 doc_num.push_str(&sub_doc_num.to_string());
441 }
442 sub_doc_num = 0;
443 if !embedded {
444 mode = Some("Document");
445 continue;
446 }
447 end_doc = Some(et);
448 end_token = None;
449 mode = None;
450 if let Some(rest) = line.get(bt.len()..).and_then(|r| r.strip_prefix(':')) {
453 let name = rest.trim_start();
454 if name.len() < rest.len() && !name.is_empty() {
455 let name = if name.starts_with('(') && name.ends_with(')') {
456 &name[1..name.len() - 1]
457 } else {
458 name
459 };
460 tags.push(mk_doc(
461 "EmbeddedFileName",
462 "Embedded File Name",
463 Value::String(name.to_string()),
464 &doc_num,
465 ));
466 }
467 }
468 continue;
469 }
470
471 if line.starts_with("<?xpacket begin") && line.contains("W5M0MpCehiHzreSzNTczkc9d") {
472 if !line.contains("<?xpacket end") {
473 mode = Some("XMP");
474 end_token = None;
475 }
476 continue;
477 }
478
479 let Some((pct_len, name, raw)) = match_dsc_comment(line) else {
480 continue;
481 };
482 let Some((tag_name, desc)) = dsc_tag(name) else {
483 continue;
484 };
485 if pct_len == 1 && tag_name != "ImageData" && !name.starts_with("AI") {
488 continue;
489 }
490 let mut val = raw.to_string();
492 while i < lines.len() {
493 let (cs, ce) = lines[i];
494 let cont = crate::encoding::decode_utf8_or_latin1(&data[cs..ce]);
495 if !cont.starts_with("%%+") {
496 break;
497 }
498 val.push_str(&cont[3..]);
499 i += 1;
500 }
501 let val = decode_comment(&val);
502 let print = dsc_print_conv(tag_name, &val).map(str::to_string);
503 let mut tag = mk_doc(tag_name, desc, Value::String(val), &doc_num);
504 if let Some(p) = print {
505 tag.print_value = p;
506 }
507 tags.push(tag);
508 }
509}
510
511fn parse_photoshop_blocks(text: &str, tags: &mut Vec<Tag>) {
513 let mut search: &str = text;
514 while let Some(start) = search.find("%BeginPhotoshop:") {
515 let block = &search[start..];
516 let end = block.find("%EndPhotoshop").unwrap_or(block.len());
517 let block = &block[..end];
518
519 let mut hex_str = String::new();
521 let mut first = true;
522 for line in block.lines() {
523 if first {
524 first = false;
525 continue;
526 } let line = line.trim();
528 if let Some(hex_part) = line.strip_prefix("% ") {
529 hex_str.push_str(hex_part);
530 }
531 }
532
533 if !hex_str.is_empty() {
534 let irb_data = decode_hex(&hex_str);
535 parse_photoshop_irb(&irb_data, tags);
536 }
537
538 let advance = start + end + 13; if advance >= search.len() {
540 break;
541 }
542 search = &search[advance..];
543 }
544}
545
546fn parse_photoshop_irb(data: &[u8], tags: &mut Vec<Tag>) {
548 let mut pos = 0;
549 while pos + 12 <= data.len() {
550 if &data[pos..pos + 4] != b"8BIM" {
551 break;
552 }
553 let res_type = u16::from_be_bytes([data[pos + 4], data[pos + 5]]);
554
555 let name_len = data[pos + 6] as usize;
557 let name_total = 1 + name_len;
558 let name_total = if name_total % 2 != 0 {
559 name_total + 1
560 } else {
561 name_total
562 };
563 let data_start = pos + 6 + name_total;
564 if data_start + 4 > data.len() {
565 break;
566 }
567 let data_size = u32::from_be_bytes([
568 data[data_start],
569 data[data_start + 1],
570 data[data_start + 2],
571 data[data_start + 3],
572 ]) as usize;
573 let data_end = data_start + 4 + data_size;
574 if data_end > data.len() {
575 break;
576 }
577 let block_data = &data[data_start + 4..data_end];
578
579 match res_type {
580 0x0404 => {
581 let digest = crate::md5::md5_hex(block_data);
583 tags.push(mk(
584 "CurrentIPTCDigest",
585 "Current IPTC Digest",
586 Value::String(digest),
587 ));
588 if let Ok(iptc_tags) = crate::metadata::IptcReader::read(block_data) {
589 tags.extend(iptc_tags);
590 }
591 }
592 0x0425
593 if block_data.len() >= 16 => {
595 let digest = block_data[..16]
596 .iter()
597 .map(|b| format!("{:02x}", b))
598 .collect::<String>();
599 let mut tag = mk("IPTCDigest", "IPTC Digest", Value::String(digest));
603 tag.group.family0 = "Photoshop".into();
604 tag.group.family1 = "Photoshop".into();
605 tag.group.family2 = "Image".into();
606 tags.push(tag);
607 }
608 _ => {}
609 }
610
611 pos = data_end;
612 if pos % 2 != 0 {
613 pos += 1;
614 }
615 }
616}
617
618fn parse_image_data_comment(text: &str, tags: &mut Vec<Tag>) {
620 for line in text.lines() {
621 if let Some(rest) = line.strip_prefix("%ImageData:") {
622 let parts: Vec<&str> = rest.split_whitespace().collect();
623 if parts.len() >= 2 {
624 if let Ok(h) = parts[1].parse::<u32>() {
631 tags.push(mk_composite("ImageHeight", "Image Height", Value::U32(h)));
632 }
633 if let Ok(w) = parts[0].parse::<u32>() {
634 tags.push(mk_composite("ImageWidth", "Image Width", Value::U32(w)));
635 }
636 }
637 break;
638 }
639 }
640}
641
642fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
643 haystack.windows(needle.len()).position(|w| w == needle)
644}
645
646fn mk(name: &str, description: &str, value: Value) -> Tag {
647 let pv = value.to_display_string();
648 Tag {
649 id: TagId::Text(name.to_string()),
650 name: name.to_string(),
651 description: description.to_string(),
652 group: TagGroup {
653 family0: "PostScript".into(),
654 family1: "PostScript".into(),
655 family2: "Document".into(),
656 family3: "Main".into(),
657 },
658 raw_value: value,
659 print_value: pv,
660 priority: 0,
661 }
662}
663
664fn mk_composite(name: &str, description: &str, value: Value) -> Tag {
666 let mut tag = mk(name, description, value);
667 tag.group.family0 = "Composite".into();
668 tag.group.family1 = "Composite".into();
669 tag.group.family2 = "Image".into();
670 tag
671}
672
673fn mk_doc(name: &str, description: &str, value: Value, doc_num: &str) -> Tag {
676 let mut tag = mk(name, description, value);
677 if !doc_num.is_empty() {
678 tag.group.family3 = format!("Doc{doc_num}");
679 }
680 tag
681}