exiftool_rs/formats/
postscript.rs1use crate::error::{Error, Result};
7use crate::metadata::XmpReader;
8use crate::tag::{Tag, TagGroup, TagId};
9use crate::value::Value;
10
11pub fn read_postscript(data: &[u8]) -> Result<Vec<Tag>> {
12 let mut tags = Vec::new();
13 let mut offset = 0;
14
15 if data.len() >= 30 && data.starts_with(&[0xC5, 0xD0, 0xD3, 0xC6]) {
17 let ps_offset = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
18 let ps_length = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
19
20 if ps_offset + ps_length <= data.len() {
21 offset = ps_offset;
22 }
23 tags.push(mk("EPSFormat", "EPS Format", Value::String("DOS Binary".into())));
24 }
25
26 if offset + 4 > data.len() || (!data[offset..].starts_with(b"%!PS") && !data[offset..].starts_with(b"%!Ad")) {
28 return Err(Error::InvalidData("not a PostScript file".into()));
29 }
30
31 let text = String::from_utf8_lossy(&data[offset..data.len().min(offset + 65536)]);
33 let text = text.replace('\r', "\n");
34
35 for line in text.lines() {
36 if !line.starts_with("%%") && !line.starts_with("%!") {
37 if !line.starts_with('%') && !line.is_empty() {
39 break;
40 }
41 continue;
42 }
43
44 let line = line.trim();
45
46 if let Some(rest) = line.strip_prefix("%%Title:") {
47 tags.push(mk("Title", "Title", Value::String(rest.trim().trim_matches('(').trim_matches(')').to_string())));
48 } else if let Some(rest) = line.strip_prefix("%%Creator:") {
49 tags.push(mk("Creator", "Creator", Value::String(rest.trim().trim_matches('(').trim_matches(')').to_string())));
50 } else if let Some(rest) = line.strip_prefix("%%CreationDate:") {
51 tags.push(mk("CreateDate", "Create Date", Value::String(rest.trim().trim_matches('(').trim_matches(')').to_string())));
52 } else if let Some(rest) = line.strip_prefix("%%For:") {
53 tags.push(mk("Author", "Author", Value::String(rest.trim().trim_matches('(').trim_matches(')').to_string())));
54 } else if let Some(rest) = line.strip_prefix("%%BoundingBox:") {
55 tags.push(mk("BoundingBox", "Bounding Box", Value::String(rest.trim().to_string())));
56 } else if let Some(rest) = line.strip_prefix("%%HiResBoundingBox:") {
57 tags.push(mk("HiResBoundingBox", "HiRes Bounding Box", Value::String(rest.trim().to_string())));
58 } else if let Some(rest) = line.strip_prefix("%%Pages:") {
59 tags.push(mk("Pages", "Pages", Value::String(rest.trim().to_string())));
60 } else if let Some(rest) = line.strip_prefix("%%LanguageLevel:") {
61 tags.push(mk("LanguageLevel", "Language Level", Value::String(rest.trim().to_string())));
62 } else if let Some(rest) = line.strip_prefix("%%DocumentData:") {
63 tags.push(mk("DocumentData", "Document Data", Value::String(rest.trim().to_string())));
64 } else if line.starts_with("%!PS-Adobe-") {
65 let version = line.strip_prefix("%!PS-Adobe-").unwrap_or("").trim();
66 tags.push(mk("PSVersion", "PostScript Version", Value::String(version.to_string())));
67 if version.contains("EPSF") {
69 tags.push(mk("EPSVersion", "EPS Version", Value::String(version.to_string())));
70 }
71 }
72 }
73
74 if let Some(xmp_start) = find_bytes(&data[offset..], b"<?xpacket begin") {
76 let xmp_data = &data[offset + xmp_start..];
77 if let Some(xmp_end) = find_bytes(xmp_data, b"<?xpacket end") {
78 let end = xmp_end + 20; if let Ok(xmp_tags) = XmpReader::read(&xmp_data[..end.min(xmp_data.len())]) {
80 tags.extend(xmp_tags);
81 }
82 }
83 }
84
85 Ok(tags)
86}
87
88fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
89 haystack.windows(needle.len()).position(|w| w == needle)
90}
91
92fn mk(name: &str, description: &str, value: Value) -> Tag {
93 let pv = value.to_display_string();
94 Tag {
95 id: TagId::Text(name.to_string()),
96 name: name.to_string(),
97 description: description.to_string(),
98 group: TagGroup {
99 family0: "PostScript".into(),
100 family1: "PostScript".into(),
101 family2: "Document".into(),
102 },
103 raw_value: value,
104 print_value: pv,
105 priority: 0,
106 }
107}