1use crate::core::{normalise_pixel_value, Image, ImageBase, PixelBound, RGB};
2use crate::format::{Decoder, Encoder};
3use ndarray::Data;
4use num_traits::cast::{FromPrimitive, NumCast};
5use num_traits::{Num, NumAssignOps};
6use std::fmt::Display;
7use std::io::{Error, ErrorKind};
8
9#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
10enum EncodingType {
11 Binary,
12 Plaintext,
13}
14
15#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
17pub struct PpmEncoder {
18 encoding: EncodingType,
19}
20
21#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, Default)]
23pub struct PpmDecoder;
24
25impl Default for PpmEncoder {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl<T, U> Encoder<T, U, RGB> for PpmEncoder
37where
38 U: Data<Elem = T>,
39 T: Copy
40 + Clone
41 + Num
42 + NumAssignOps
43 + NumCast
44 + PartialOrd
45 + Display
46 + PixelBound
47 + FromPrimitive,
48{
49 fn encode(&self, image: &ImageBase<U, RGB>) -> Vec<u8> {
50 use EncodingType::*;
51 match self.encoding {
52 Plaintext => self.encode_plaintext(image),
53 Binary => self.encode_binary(image),
54 }
55 }
56}
57
58impl PpmEncoder {
59 pub fn new() -> Self {
61 PpmEncoder {
62 encoding: EncodingType::Binary,
63 }
64 }
65
66 pub fn new_plaintext_encoder() -> Self {
69 PpmEncoder {
70 encoding: EncodingType::Plaintext,
71 }
72 }
73
74 fn get_max_value<T, U>(image: &ImageBase<U, RGB>) -> Option<u8>
77 where
78 U: Data<Elem = T>,
79 T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound,
80 {
81 image
82 .data
83 .iter()
84 .fold(T::zero(), |ref acc, x| if x > acc { *x } else { *acc })
85 .to_u8()
86 }
87
88 fn generate_header(self, rows: usize, cols: usize, max_value: u8) -> String {
90 use EncodingType::*;
91 match self.encoding {
92 Plaintext => format!("P3\n{} {} {}\n", rows, cols, max_value),
93 Binary => format!("P6\n{} {} {}\n", rows, cols, max_value),
94 }
95 }
96
97 fn encode_binary<T, U>(self, image: &ImageBase<U, RGB>) -> Vec<u8>
99 where
100 U: Data<Elem = T>,
101 T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound,
102 {
103 let max_val = Self::get_max_value(image).unwrap_or(255);
104
105 let mut result = self
106 .generate_header(image.rows(), image.cols(), max_val)
107 .into_bytes();
108
109 result.reserve(result.len() + (image.rows() * image.cols() * 3));
110
111 for data in image.data.iter() {
112 let value = (normalise_pixel_value(*data) * 255.0f64) as u8;
113 result.push(value);
114 }
115 result
116 }
117
118 fn encode_plaintext<T, U>(self, image: &ImageBase<U, RGB>) -> Vec<u8>
121 where
122 U: Data<Elem = T>,
123 T: Copy + Clone + Num + NumAssignOps + NumCast + PartialOrd + Display + PixelBound,
124 {
125 let max_val = 255;
126
127 let mut result = self.generate_header(image.rows(), image.cols(), max_val);
128 result.reserve(image.rows() * image.cols() * 5);
131
132 let mut temp = String::new();
134 let max_margin = 70 - 12;
135 temp.reserve(max_margin);
136
137 for data in image.data.iter() {
138 let value = (normalise_pixel_value(*data) * 255.0f64) as u8;
139 temp.push_str(&format!("{} ", value));
140 if temp.len() > max_margin {
141 result.push_str(&temp);
142 result.push('\n');
143 temp.clear();
144 }
145 }
146 if !temp.is_empty() {
147 result.push_str(&temp);
148 }
149 result.into_bytes()
150 }
151}
152
153impl<T> Decoder<T, RGB> for PpmDecoder
159where
160 T: Copy
161 + Clone
162 + Num
163 + NumAssignOps
164 + NumCast
165 + PartialOrd
166 + Display
167 + PixelBound
168 + FromPrimitive,
169{
170 fn decode(&self, bytes: &[u8]) -> std::io::Result<Image<T, RGB>> {
171 if bytes.len() < 9 {
172 Err(Error::new(
173 ErrorKind::InvalidData,
174 "File is below minimum size of ppm",
175 ))
176 } else if bytes.starts_with(b"P3") {
177 Self::decode_plaintext(&bytes[2..])
178 } else if bytes.starts_with(b"P6") {
179 Self::decode_binary(&bytes[2..])
180 } else {
181 Err(Error::new(
182 ErrorKind::InvalidData,
183 "File is below minimum size of ppm",
184 ))
185 }
186 }
187}
188
189impl PpmDecoder {
190 fn decode_header(bytes: &[u8]) -> std::io::Result<(usize, usize, usize)> {
193 let err = || Error::new(ErrorKind::InvalidData, "Error in file header");
194 let mut keep = true;
195 let bytes = bytes
196 .iter()
197 .filter(|x| {
198 if *x == &b'#' {
199 keep = false;
200 false
201 } else if !keep {
202 if *x == &b'\n' || *x == &b'\r' {
203 keep = true;
204 }
205 false
206 } else {
207 true
208 }
209 })
210 .cloned()
211 .collect::<Vec<_>>();
212
213 if let Ok(s) = String::from_utf8(bytes) {
214 let res = s
215 .split_whitespace()
216 .map(|x| x.parse::<usize>().unwrap_or(0))
217 .collect::<Vec<_>>();
218 if res.len() == 3 {
219 Ok((res[0], res[1], res[2]))
220 } else {
221 Err(err())
222 }
223 } else {
224 Err(err())
225 }
226 }
227
228 fn decode_binary<T>(bytes: &[u8]) -> std::io::Result<Image<T, RGB>>
229 where
230 T: Copy
231 + Clone
232 + Num
233 + NumAssignOps
234 + NumCast
235 + PartialOrd
236 + Display
237 + PixelBound
238 + FromPrimitive,
239 {
240 let err = || Error::new(ErrorKind::InvalidData, "Error in file encoding");
241 const WHITESPACE: &[u8] = b" \t\n\r";
242
243 let mut image_bytes = Vec::<T>::new();
244
245 let mut last_saw_whitespace = false;
246 let mut is_comment = false;
247 let mut val_count = 0;
248 let header_end = bytes
249 .iter()
250 .position(|&b| {
251 if b == b'#' {
252 is_comment = true;
253 } else if is_comment {
254 if b == b'\r' || b == b'\n' {
255 is_comment = false;
256 }
257 } else if last_saw_whitespace && !WHITESPACE.contains(&b) {
258 val_count += 1;
259 last_saw_whitespace = false;
260 } else if WHITESPACE.contains(&b) {
261 last_saw_whitespace = true;
262 }
263 val_count == 3 && WHITESPACE.contains(&b)
264 })
265 .ok_or_else(err)?;
266
267 let (rows, cols, max_val) = Self::decode_header(&bytes[0..header_end])?;
268 for b in bytes.iter().skip(header_end + 1) {
269 let real_pixel = (*b as f64) * (255.0f64 / (max_val as f64));
270 image_bytes.push(T::from_u8(real_pixel as u8).unwrap_or_else(T::zero));
271 }
272
273 if image_bytes.is_empty() || image_bytes.len() != (rows * cols * 3) {
274 Err(err())
275 } else {
276 let image = Image::<T, RGB>::from_shape_data(rows, cols, image_bytes);
277 Ok(image)
278 }
279 }
280
281 fn decode_plaintext<T>(bytes: &[u8]) -> std::io::Result<Image<T, RGB>>
282 where
283 T: Copy
284 + Clone
285 + Num
286 + NumAssignOps
287 + NumCast
288 + PartialOrd
289 + Display
290 + PixelBound
291 + FromPrimitive,
292 {
293 let err = || Error::new(ErrorKind::InvalidData, "Error in file encoding");
294 let data = String::from_utf8(bytes.to_vec()).map_err(|_| err())?;
296
297 let mut rows = -1;
298 let mut cols = -1;
299 let mut max_val = -1;
300 let mut image_bytes = Vec::<T>::new();
301 for line in data.lines().filter(|l| !l.starts_with('#')) {
302 for value in line.split_whitespace().take_while(|x| !x.starts_with('#')) {
303 let temp = value.parse::<isize>().map_err(|_| err())?;
304 if rows < 0 {
305 rows = temp;
306 } else if cols < 0 {
307 cols = temp;
308 image_bytes.reserve((rows * cols * 3) as usize);
309 } else if max_val < 0 {
310 max_val = temp;
311 } else {
312 let real_pixel = (temp as f64) * (255.0f64 / (max_val as f64));
313 image_bytes.push(T::from_f64(real_pixel).unwrap_or_else(T::zero));
314 }
315 }
316 }
317 if image_bytes.is_empty() || image_bytes.len() != ((rows * cols * 3) as usize) {
318 Err(err())
319 } else {
320 let image = Image::<T, RGB>::from_shape_data(rows as usize, cols as usize, image_bytes);
321 Ok(image)
322 }
323 }
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329 use crate::core::colour_models::*;
330 use ndarray::prelude::*;
331 use ndarray_rand::RandomExt;
332 use rand::distributions::Uniform;
333 use std::fs::remove_file;
334
335 #[test]
336 fn max_value_test() {
337 let full_range = "P3 1 1 255 0 255 0";
338 let clamped = "P3 1 1 1 0 1 0";
339
340 let decoder = PpmDecoder::default();
341 let full_image: Image<u8, RGB> = decoder.decode(full_range.as_bytes()).unwrap();
342 let clamp_image: Image<u8, RGB> = decoder.decode(clamped.as_bytes()).unwrap();
343
344 assert_eq!(full_image, clamp_image);
345 assert_eq!(full_image.pixel(0, 0), arr1(&[0, 255, 0]));
346 }
347
348 #[test]
349 fn encoding_consistency() {
350 let image_str = "P3
351 3 3 255
352 255 255 255 0 0 0 255 0 0
353 0 255 0 0 0 255 255 255 0
354 0 255 255 127 127 127 0 0 0";
355
356 let decoder = PpmDecoder::default();
357 let image: Image<u8, RGB> = decoder.decode(image_str.as_bytes()).unwrap();
358
359 let encoder = PpmEncoder::new();
360 let image_bytes = encoder.encode(&image);
361
362 let restored: Image<u8, RGB> = decoder.decode(&image_bytes).unwrap();
363
364 assert_eq!(image, restored);
365
366 let encoder = PpmEncoder::new_plaintext_encoder();
367 let image_bytes = encoder.encode(&image);
368 let restored: Image<u8, RGB> = decoder.decode(&image_bytes).unwrap();
369
370 assert_eq!(image, restored);
371 }
372
373 #[test]
374 fn binary_comments() {
375 let image_str = "P3
376 3 3 255
377 255 255 255 0 0 0 255 0 0
378 0 255 0 0 0 255 255 255 0
379 0 255 255 127 127 127 0 0 0";
380
381 let decoder = PpmDecoder::default();
382 let image: Image<u8, RGB> = decoder.decode(image_str.as_bytes()).unwrap();
383
384 let encoder = PpmEncoder::new();
385 let mut image_bytes = encoder.encode(&image);
386 let comment = b"# This is a comment\n";
387 for i in 0..comment.len() {
388 image_bytes.insert(2 + i, comment[i]);
389 }
390 let restored: Image<u8, RGB> = decoder.decode(&image_bytes).unwrap();
391
392 assert_eq!(image, restored);
393 }
394
395 #[test]
396 fn binary_file_save() {
397 let mut image = Image::<u8, RGB>::new(480, 640);
398 let new_data = Array3::<u8>::random(image.data.dim(), Uniform::new(0, 255));
399 image.data = new_data;
400
401 let bin_encoder = PpmEncoder::new();
402
403 let filename = "bintest.ppm";
404
405 bin_encoder.encode_file(&image, filename).unwrap();
406
407 let decoder = PpmDecoder::default();
408 let new_image = decoder.decode_file(filename).unwrap();
409 let _ = remove_file(filename);
410
411 image_compare(&new_image, &image);
412 }
413
414 #[test]
415 fn plaintext_file_save() {
416 let mut image = Image::<u8, RGB>::new(480, 640);
417 let new_data = Array3::<u8>::random(image.data.dim(), Uniform::new(0, 255));
418 image.data = new_data;
419
420 let bin_encoder = PpmEncoder::new_plaintext_encoder();
421 let filename = "texttest.ppm";
422
423 bin_encoder.encode_file(&image, filename).unwrap();
424
425 let decoder = PpmDecoder::default();
426 let new_image = decoder.decode_file(filename).unwrap();
427 let _ = remove_file(filename);
428
429 image_compare(&new_image, &image);
430 }
431
432 fn image_compare<C>(actual: &Image<u8, C>, expected: &Image<u8, C>)
433 where
434 C: ColourModel,
435 {
436 assert_eq!(actual.data.shape(), expected.data.shape());
437
438 for (act, exp) in actual.data.iter().zip(expected.data.iter()) {
439 let delta = (*act as i16 - *exp as i16).abs();
440 assert!(delta < 2);
442 }
443 }
444}