1const DOS_EPS_MAGIC: [u8; 4] = [0xC5, 0xD0, 0xD3, 0xC6];
9
10pub fn strip_dos_eps_header(data: &[u8]) -> &[u8] {
16 if data.len() < 12 || data[..4] != DOS_EPS_MAGIC {
17 return data;
18 }
19
20 let ps_offset = u32::from_le_bytes([data[4], data[5], data[6], data[7]]) as usize;
21 let ps_length = u32::from_le_bytes([data[8], data[9], data[10], data[11]]) as usize;
22
23 let start = ps_offset.min(data.len());
24 let end = (ps_offset + ps_length).min(data.len());
25 &data[start..end]
26}
27
28pub fn content_is_epsf(data: &[u8]) -> bool {
32 let end = data
34 .iter()
35 .position(|&b| b == b'\n' || b == b'\r')
36 .unwrap_or(data.len().min(256));
37 let first_line = &data[..end];
38 first_line.windows(4).any(|w| w == b"EPSF")
39}
40
41pub fn read_eps_bounding_box(data: &[u8]) -> Option<(f64, f64, f64, f64)> {
49 let header_end = data.len().min(4096);
51 let header = &data[..header_end];
52
53 let mut bbox = None;
54 let mut hires_bbox = None;
55 let mut need_atend = false;
56
57 scan_for_bbox(header, &mut bbox, &mut hires_bbox, &mut need_atend);
58
59 if need_atend && data.len() > 4096 {
61 let trailer_start = data.len().saturating_sub(4096);
62 let trailer = &data[trailer_start..];
63 scan_for_bbox(trailer, &mut bbox, &mut hires_bbox, &mut need_atend);
64 }
65
66 hires_bbox.or(bbox)
67}
68
69fn scan_for_bbox(
71 data: &[u8],
72 bbox: &mut Option<(f64, f64, f64, f64)>,
73 hires_bbox: &mut Option<(f64, f64, f64, f64)>,
74 need_atend: &mut bool,
75) {
76 for line in data.split(|&b| b == b'\n' || b == b'\r') {
77 if line.is_empty() {
78 continue;
79 }
80
81 if line.starts_with(b"%%HiResBoundingBox:") {
82 let rest = &line[b"%%HiResBoundingBox:".len()..];
83 if let Some(values) = parse_four_numbers(rest) {
84 *hires_bbox = Some(values);
85 }
86 } else if line.starts_with(b"%%BoundingBox:") {
87 let rest = &line[b"%%BoundingBox:".len()..];
88 let trimmed = trim_ascii(rest);
89 if trimmed == b"(atend)" {
90 *need_atend = true;
91 } else if let Some(values) = parse_four_numbers(rest) {
92 *bbox = Some(values);
93 }
94 }
95 }
96}
97
98fn parse_four_numbers(data: &[u8]) -> Option<(f64, f64, f64, f64)> {
100 let s = std::str::from_utf8(data).ok()?;
101 let mut nums = s.split_whitespace().filter_map(|w| w.parse::<f64>().ok());
102 let llx = nums.next()?;
103 let lly = nums.next()?;
104 let urx = nums.next()?;
105 let ury = nums.next()?;
106 Some((llx, lly, urx, ury))
107}
108
109fn trim_ascii(data: &[u8]) -> &[u8] {
111 let start = data
112 .iter()
113 .position(|b| !b.is_ascii_whitespace())
114 .unwrap_or(data.len());
115 let end = data
116 .iter()
117 .rposition(|b| !b.is_ascii_whitespace())
118 .map_or(start, |p| p + 1);
119 &data[start..end]
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125
126 #[test]
127 fn test_strip_dos_header() {
128 let ps_content = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 100 100\n";
130 let ps_offset: u32 = 30; let ps_length = ps_content.len() as u32;
132
133 let mut data = Vec::new();
134 data.extend_from_slice(&DOS_EPS_MAGIC);
135 data.extend_from_slice(&ps_offset.to_le_bytes());
136 data.extend_from_slice(&ps_length.to_le_bytes());
137 data.resize(30, 0xFF);
139 data.extend_from_slice(ps_content);
140
141 let result = strip_dos_eps_header(&data);
142 assert_eq!(result, ps_content);
143 }
144
145 #[test]
146 fn test_strip_no_header() {
147 let ps_content = b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 200 300\n";
148 let result = strip_dos_eps_header(ps_content);
149 assert_eq!(result, ps_content);
150 }
151
152 #[test]
153 fn test_content_is_epsf() {
154 assert!(content_is_epsf(
155 b"%!PS-Adobe-2.0 EPSF-1.2\n%%BoundingBox: 0 0 100 100\n"
156 ));
157 assert!(content_is_epsf(
158 b"%!PS-Adobe-3.0 EPSF-3.0\r%%BoundingBox: 0 0 100 100\r"
159 ));
160 assert!(!content_is_epsf(
161 b"%!PS-Adobe-3.0\n%%BoundingBox: 0 0 100 100\n"
162 ));
163 assert!(!content_is_epsf(b"/Helvetica findfont\n"));
164 }
165
166 #[test]
167 fn test_bbox_integer() {
168 let data = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 300\n";
169 let bbox = read_eps_bounding_box(data);
170 assert_eq!(bbox, Some((0.0, 0.0, 200.0, 300.0)));
171 }
172
173 #[test]
174 fn test_bbox_hires_preferred() {
175 let data = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: 0 0 200 300\n%%HiResBoundingBox: 0.5 1.5 199.75 299.25\n";
176 let bbox = read_eps_bounding_box(data);
177 assert_eq!(bbox, Some((0.5, 1.5, 199.75, 299.25)));
178 }
179
180 #[test]
181 fn test_bbox_not_found() {
182 let data = b"%!PS-Adobe-3.0\n/Helvetica findfont 12 scalefont setfont\n";
183 let bbox = read_eps_bounding_box(data);
184 assert_eq!(bbox, None);
185 }
186
187 #[test]
188 fn test_bbox_negative_coords() {
189 let data = b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: -50 -25 150 275\n";
190 let bbox = read_eps_bounding_box(data);
191 assert_eq!(bbox, Some((-50.0, -25.0, 150.0, 275.0)));
192 }
193
194 #[test]
195 fn test_bbox_cr_line_endings() {
196 let data = b"%!PS-Adobe-3.0 EPSF-3.0\r%%BoundingBox: 0 9 360 135\r%%HiResBoundingBox: 0 9.001 360 135\r";
198 let bbox = read_eps_bounding_box(data);
199 assert_eq!(bbox, Some((0.0, 9.001, 360.0, 135.0)));
200 }
201
202 #[test]
203 fn test_bbox_atend() {
204 let mut data = Vec::new();
206 data.extend_from_slice(b"%!PS-Adobe-3.0 EPSF-3.0\n%%BoundingBox: (atend)\n");
207 data.resize(5000, b' ');
209 data.extend_from_slice(b"\n%%BoundingBox: 10 20 300 400\n%%EOF\n");
210
211 let bbox = read_eps_bounding_box(&data);
212 assert_eq!(bbox, Some((10.0, 20.0, 300.0, 400.0)));
213 }
214}