/src/image/src/codecs/pnm/encoder.rs
Line | Count | Source |
1 | | //! Encoding of PNM Images |
2 | | use crate::utils::vec_try_with_capacity; |
3 | | use std::fmt; |
4 | | use std::io; |
5 | | use std::io::Write; |
6 | | |
7 | | use super::AutoBreak; |
8 | | use super::{ArbitraryHeader, ArbitraryTuplType, BitmapHeader, GraymapHeader, PixmapHeader}; |
9 | | use super::{HeaderRecord, PnmHeader, PnmSubtype, SampleEncoding}; |
10 | | |
11 | | use crate::color::ExtendedColorType; |
12 | | use crate::error::{ |
13 | | ImageError, ImageResult, ParameterError, ParameterErrorKind, UnsupportedError, |
14 | | UnsupportedErrorKind, |
15 | | }; |
16 | | use crate::{ImageEncoder, ImageFormat}; |
17 | | |
18 | | use byteorder_lite::{BigEndian, WriteBytesExt}; |
19 | | |
20 | | enum HeaderStrategy { |
21 | | Dynamic, |
22 | | Subtype(PnmSubtype), |
23 | | Chosen(PnmHeader), |
24 | | } |
25 | | |
26 | | #[derive(Clone, Copy)] |
27 | | pub enum FlatSamples<'a> { |
28 | | U8(&'a [u8]), |
29 | | U16(&'a [u16]), |
30 | | } |
31 | | |
32 | | /// Encodes images to any of the `pnm` image formats. |
33 | | pub struct PnmEncoder<W: Write> { |
34 | | writer: W, |
35 | | header: HeaderStrategy, |
36 | | } |
37 | | |
38 | | /// Encapsulate the checking system in the type system. Non of the fields are actually accessed |
39 | | /// but requiring them forces us to validly construct the struct anyways. |
40 | | struct CheckedImageBuffer<'a> { |
41 | | _image: FlatSamples<'a>, |
42 | | _width: u32, |
43 | | _height: u32, |
44 | | _color: ExtendedColorType, |
45 | | } |
46 | | |
47 | | // Check the header against the buffer. Each struct produces the next after a check. |
48 | | struct UncheckedHeader<'a> { |
49 | | header: &'a PnmHeader, |
50 | | } |
51 | | |
52 | | struct CheckedDimensions<'a> { |
53 | | unchecked: UncheckedHeader<'a>, |
54 | | width: u32, |
55 | | height: u32, |
56 | | } |
57 | | |
58 | | struct CheckedHeaderColor<'a> { |
59 | | dimensions: CheckedDimensions<'a>, |
60 | | color: ExtendedColorType, |
61 | | } |
62 | | |
63 | | struct CheckedHeader<'a> { |
64 | | color: CheckedHeaderColor<'a>, |
65 | | encoding: TupleEncoding<'a>, |
66 | | _image: CheckedImageBuffer<'a>, |
67 | | } |
68 | | |
69 | | enum TupleEncoding<'a> { |
70 | | PbmBits { |
71 | | samples: FlatSamples<'a>, |
72 | | width: u32, |
73 | | }, |
74 | | Ascii { |
75 | | samples: FlatSamples<'a>, |
76 | | }, |
77 | | Bytes { |
78 | | samples: FlatSamples<'a>, |
79 | | }, |
80 | | } |
81 | | |
82 | | impl<W: Write> PnmEncoder<W> { |
83 | | /// Create new `PnmEncoder` from the `writer`. |
84 | | /// |
85 | | /// The encoded images will have some `pnm` format. If more control over the image type is |
86 | | /// required, use either one of `with_subtype` or `with_header`. For more information on the |
87 | | /// behaviour, see `with_dynamic_header`. |
88 | 0 | pub fn new(writer: W) -> Self { |
89 | 0 | PnmEncoder { |
90 | 0 | writer, |
91 | 0 | header: HeaderStrategy::Dynamic, |
92 | 0 | } |
93 | 0 | } |
94 | | |
95 | | /// Encode a specific pnm subtype image. |
96 | | /// |
97 | | /// The magic number and encoding type will be chosen as provided while the rest of the header |
98 | | /// data will be generated dynamically. Trying to encode incompatible images (e.g. encoding an |
99 | | /// RGB image as Graymap) will result in an error. |
100 | | /// |
101 | | /// This will overwrite the effect of earlier calls to `with_header` and `with_dynamic_header`. |
102 | | pub fn with_subtype(self, subtype: PnmSubtype) -> Self { |
103 | | PnmEncoder { |
104 | | writer: self.writer, |
105 | | header: HeaderStrategy::Subtype(subtype), |
106 | | } |
107 | | } |
108 | | |
109 | | /// Enforce the use of a chosen header. |
110 | | /// |
111 | | /// While this option gives the most control over the actual written data, the encoding process |
112 | | /// will error in case the header data and image parameters do not agree. It is the users |
113 | | /// obligation to ensure that the width and height are set accordingly, for example. |
114 | | /// |
115 | | /// Choose this option if you want a lossless decoding/encoding round trip. |
116 | | /// |
117 | | /// This will overwrite the effect of earlier calls to `with_subtype` and `with_dynamic_header`. |
118 | | pub fn with_header(self, header: PnmHeader) -> Self { |
119 | | PnmEncoder { |
120 | | writer: self.writer, |
121 | | header: HeaderStrategy::Chosen(header), |
122 | | } |
123 | | } |
124 | | |
125 | | /// Create the header dynamically for each image. |
126 | | /// |
127 | | /// This is the default option upon creation of the encoder. With this, most images should be |
128 | | /// encodable but the specific format chosen is out of the users control. The pnm subtype is |
129 | | /// chosen arbitrarily by the library. |
130 | | /// |
131 | | /// This will overwrite the effect of earlier calls to `with_subtype` and `with_header`. |
132 | | pub fn with_dynamic_header(self) -> Self { |
133 | | PnmEncoder { |
134 | | writer: self.writer, |
135 | | header: HeaderStrategy::Dynamic, |
136 | | } |
137 | | } |
138 | | |
139 | | /// Encode an image whose samples are represented as a sequence of `u8` or `u16` data. |
140 | | /// |
141 | | /// If `image` is a slice of `u8`, the samples will be interpreted based on the chosen `color` option. |
142 | | /// Color types of 16-bit precision means that the bytes are reinterpreted as 16-bit samples, |
143 | | /// otherwise they are treated as 8-bit samples. |
144 | | /// If `image` is a slice of `u16`, the samples will be interpreted as 16-bit samples directly. |
145 | | /// |
146 | | /// Some `pnm` subtypes are incompatible with some color options, a chosen header most |
147 | | /// certainly with any deviation from the original decoded image. |
148 | 0 | pub fn encode<'s, S>( |
149 | 0 | &mut self, |
150 | 0 | image: S, |
151 | 0 | width: u32, |
152 | 0 | height: u32, |
153 | 0 | color: ExtendedColorType, |
154 | 0 | ) -> ImageResult<()> |
155 | 0 | where |
156 | 0 | S: Into<FlatSamples<'s>>, |
157 | | { |
158 | 0 | let image = image.into(); |
159 | | |
160 | | // adapt samples so that they are aligned even in 16-bit samples, |
161 | | // required due to the narrowing of the image buffer to &[u8] |
162 | | // on dynamic image writing |
163 | 0 | let image = match (image, color) { |
164 | | ( |
165 | 0 | FlatSamples::U8(samples), |
166 | | ExtendedColorType::L16 |
167 | | | ExtendedColorType::La16 |
168 | | | ExtendedColorType::Rgb16 |
169 | | | ExtendedColorType::Rgba16, |
170 | | ) => { |
171 | 0 | match bytemuck::try_cast_slice(samples) { |
172 | | // proceed with aligned 16-bit samples |
173 | 0 | Ok(samples) => FlatSamples::U16(samples), |
174 | 0 | Err(_e) => { |
175 | | // reallocation is required |
176 | 0 | let new_samples: Vec<u16> = samples |
177 | 0 | .chunks(2) |
178 | 0 | .map(|chunk| u16::from_ne_bytes([chunk[0], chunk[1]])) |
179 | 0 | .collect(); |
180 | | |
181 | 0 | let image = FlatSamples::U16(&new_samples); |
182 | | |
183 | | // make a separate encoding path, |
184 | | // because the image buffer lifetime has changed |
185 | 0 | return self.encode_impl(image, width, height, color); |
186 | | } |
187 | | } |
188 | | } |
189 | | // should not be necessary for any other case |
190 | 0 | _ => image, |
191 | | }; |
192 | | |
193 | 0 | self.encode_impl(image, width, height, color) |
194 | 0 | } |
195 | | |
196 | | /// Encode an image whose samples are already interpreted correctly. |
197 | 0 | fn encode_impl( |
198 | 0 | &mut self, |
199 | 0 | samples: FlatSamples<'_>, |
200 | 0 | width: u32, |
201 | 0 | height: u32, |
202 | 0 | color: ExtendedColorType, |
203 | 0 | ) -> ImageResult<()> { |
204 | 0 | match self.header { |
205 | 0 | HeaderStrategy::Dynamic => self.write_dynamic_header(samples, width, height, color), |
206 | 0 | HeaderStrategy::Subtype(subtype) => { |
207 | 0 | self.write_subtyped_header(subtype, samples, width, height, color) |
208 | | } |
209 | 0 | HeaderStrategy::Chosen(ref header) => { |
210 | 0 | Self::write_with_header(&mut self.writer, header, samples, width, height, color) |
211 | | } |
212 | | } |
213 | 0 | } |
214 | | |
215 | | /// Choose any valid pnm format that the image can be expressed in and write its header. |
216 | | /// |
217 | | /// Returns how the body should be written if successful. |
218 | 0 | fn write_dynamic_header( |
219 | 0 | &mut self, |
220 | 0 | image: FlatSamples, |
221 | 0 | width: u32, |
222 | 0 | height: u32, |
223 | 0 | color: ExtendedColorType, |
224 | 0 | ) -> ImageResult<()> { |
225 | 0 | let depth = u32::from(color.channel_count()); |
226 | 0 | let (maxval, tupltype) = match color { |
227 | 0 | ExtendedColorType::L1 => (1, ArbitraryTuplType::BlackAndWhite), |
228 | 0 | ExtendedColorType::L8 => (0xff, ArbitraryTuplType::Grayscale), |
229 | 0 | ExtendedColorType::L16 => (0xffff, ArbitraryTuplType::Grayscale), |
230 | 0 | ExtendedColorType::La1 => (1, ArbitraryTuplType::BlackAndWhiteAlpha), |
231 | 0 | ExtendedColorType::La8 => (0xff, ArbitraryTuplType::GrayscaleAlpha), |
232 | 0 | ExtendedColorType::La16 => (0xffff, ArbitraryTuplType::GrayscaleAlpha), |
233 | 0 | ExtendedColorType::Rgb8 => (0xff, ArbitraryTuplType::RGB), |
234 | 0 | ExtendedColorType::Rgb16 => (0xffff, ArbitraryTuplType::RGB), |
235 | 0 | ExtendedColorType::Rgba8 => (0xff, ArbitraryTuplType::RGBAlpha), |
236 | 0 | ExtendedColorType::Rgba16 => (0xffff, ArbitraryTuplType::RGBAlpha), |
237 | | _ => { |
238 | 0 | return Err(ImageError::Unsupported( |
239 | 0 | UnsupportedError::from_format_and_kind( |
240 | 0 | ImageFormat::Pnm.into(), |
241 | 0 | UnsupportedErrorKind::Color(color), |
242 | 0 | ), |
243 | 0 | )) |
244 | | } |
245 | | }; |
246 | | |
247 | 0 | let header = PnmHeader { |
248 | 0 | decoded: HeaderRecord::Arbitrary(ArbitraryHeader { |
249 | 0 | width, |
250 | 0 | height, |
251 | 0 | depth, |
252 | 0 | maxval, |
253 | 0 | tupltype: Some(tupltype), |
254 | 0 | }), |
255 | 0 | encoded: None, |
256 | 0 | }; |
257 | | |
258 | 0 | Self::write_with_header(&mut self.writer, &header, image, width, height, color) |
259 | 0 | } |
260 | | |
261 | | /// Try to encode the image with the chosen format, give its corresponding pixel encoding type. |
262 | 0 | fn write_subtyped_header( |
263 | 0 | &mut self, |
264 | 0 | subtype: PnmSubtype, |
265 | 0 | image: FlatSamples, |
266 | 0 | width: u32, |
267 | 0 | height: u32, |
268 | 0 | color: ExtendedColorType, |
269 | 0 | ) -> ImageResult<()> { |
270 | 0 | let header = match (subtype, color) { |
271 | 0 | (PnmSubtype::ArbitraryMap, color) => { |
272 | 0 | return self.write_dynamic_header(image, width, height, color) |
273 | | } |
274 | 0 | (PnmSubtype::Pixmap(encoding), ExtendedColorType::Rgb8) => PnmHeader { |
275 | 0 | decoded: HeaderRecord::Pixmap(PixmapHeader { |
276 | 0 | encoding, |
277 | 0 | width, |
278 | 0 | height, |
279 | 0 | maxval: 255, |
280 | 0 | }), |
281 | 0 | encoded: None, |
282 | 0 | }, |
283 | 0 | (PnmSubtype::Graymap(encoding), ExtendedColorType::L8) => PnmHeader { |
284 | 0 | decoded: HeaderRecord::Graymap(GraymapHeader { |
285 | 0 | encoding, |
286 | 0 | width, |
287 | 0 | height, |
288 | 0 | maxwhite: 255, |
289 | 0 | }), |
290 | 0 | encoded: None, |
291 | 0 | }, |
292 | 0 | (PnmSubtype::Bitmap(encoding), ExtendedColorType::L8 | ExtendedColorType::L1) => { |
293 | 0 | PnmHeader { |
294 | 0 | decoded: HeaderRecord::Bitmap(BitmapHeader { |
295 | 0 | encoding, |
296 | 0 | height, |
297 | 0 | width, |
298 | 0 | }), |
299 | 0 | encoded: None, |
300 | 0 | } |
301 | | } |
302 | | (_, _) => { |
303 | 0 | return Err(ImageError::Unsupported( |
304 | 0 | UnsupportedError::from_format_and_kind( |
305 | 0 | ImageFormat::Pnm.into(), |
306 | 0 | UnsupportedErrorKind::Color(color), |
307 | 0 | ), |
308 | 0 | )) |
309 | | } |
310 | | }; |
311 | | |
312 | 0 | Self::write_with_header(&mut self.writer, &header, image, width, height, color) |
313 | 0 | } |
314 | | |
315 | | /// Try to encode the image with the chosen header, checking if values are correct. |
316 | | /// |
317 | | /// Returns how the body should be written if successful. |
318 | 0 | fn write_with_header( |
319 | 0 | writer: &mut dyn Write, |
320 | 0 | header: &PnmHeader, |
321 | 0 | image: FlatSamples, |
322 | 0 | width: u32, |
323 | 0 | height: u32, |
324 | 0 | color: ExtendedColorType, |
325 | 0 | ) -> ImageResult<()> { |
326 | 0 | let unchecked = UncheckedHeader { header }; |
327 | | |
328 | 0 | unchecked |
329 | 0 | .check_header_dimensions(width, height)? |
330 | 0 | .check_header_color(color)? |
331 | 0 | .check_sample_values(image)? |
332 | 0 | .write_header(writer)? |
333 | 0 | .write_image(writer) |
334 | 0 | } |
335 | | } |
336 | | |
337 | | impl<W: Write> ImageEncoder for PnmEncoder<W> { |
338 | | #[track_caller] |
339 | 0 | fn write_image( |
340 | 0 | mut self, |
341 | 0 | buf: &[u8], |
342 | 0 | width: u32, |
343 | 0 | height: u32, |
344 | 0 | color_type: ExtendedColorType, |
345 | 0 | ) -> ImageResult<()> { |
346 | 0 | let expected_buffer_len = color_type.buffer_size(width, height); |
347 | 0 | assert_eq!( |
348 | | expected_buffer_len, |
349 | 0 | buf.len() as u64, |
350 | 0 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image", |
351 | 0 | buf.len(), |
352 | | ); |
353 | | |
354 | 0 | self.encode(buf, width, height, color_type) |
355 | 0 | } |
356 | | } |
357 | | |
358 | | impl<'a> CheckedImageBuffer<'a> { |
359 | 0 | fn check( |
360 | 0 | image: FlatSamples<'a>, |
361 | 0 | width: u32, |
362 | 0 | height: u32, |
363 | 0 | color: ExtendedColorType, |
364 | 0 | ) -> ImageResult<CheckedImageBuffer<'a>> { |
365 | 0 | let components = color.channel_count() as usize; |
366 | 0 | let uwidth = width as usize; |
367 | 0 | let uheight = height as usize; |
368 | 0 | let expected_len = components |
369 | 0 | .checked_mul(uwidth) |
370 | 0 | .and_then(|v| v.checked_mul(uheight)); |
371 | 0 | if Some(image.len()) != expected_len { |
372 | | // Image buffer does not correspond to size and colour. |
373 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
374 | 0 | ParameterErrorKind::DimensionMismatch, |
375 | 0 | ))); |
376 | 0 | } |
377 | 0 | Ok(CheckedImageBuffer { |
378 | 0 | _image: image, |
379 | 0 | _width: width, |
380 | 0 | _height: height, |
381 | 0 | _color: color, |
382 | 0 | }) |
383 | 0 | } |
384 | | } |
385 | | |
386 | | impl<'a> UncheckedHeader<'a> { |
387 | 0 | fn check_header_dimensions( |
388 | 0 | self, |
389 | 0 | width: u32, |
390 | 0 | height: u32, |
391 | 0 | ) -> ImageResult<CheckedDimensions<'a>> { |
392 | 0 | if self.header.width() != width || self.header.height() != height { |
393 | | // Chosen header does not match Image dimensions. |
394 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
395 | 0 | ParameterErrorKind::DimensionMismatch, |
396 | 0 | ))); |
397 | 0 | } |
398 | | |
399 | 0 | Ok(CheckedDimensions { |
400 | 0 | unchecked: self, |
401 | 0 | width, |
402 | 0 | height, |
403 | 0 | }) |
404 | 0 | } |
405 | | } |
406 | | |
407 | | impl<'a> CheckedDimensions<'a> { |
408 | | // Check color compatibility with the header. This will only error when we are certain that |
409 | | // the combination is bogus (e.g. combining Pixmap and Palette) but allows uncertain |
410 | | // combinations (basically a ArbitraryTuplType::Custom with any color of fitting depth). |
411 | 0 | fn check_header_color(self, color: ExtendedColorType) -> ImageResult<CheckedHeaderColor<'a>> { |
412 | 0 | let components = u32::from(color.channel_count()); |
413 | | |
414 | 0 | match *self.unchecked.header { |
415 | | PnmHeader { |
416 | | decoded: HeaderRecord::Bitmap(_), |
417 | | .. |
418 | 0 | } => match color { |
419 | 0 | ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), |
420 | | _ => { |
421 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
422 | 0 | ParameterErrorKind::Generic( |
423 | 0 | "PBM format only support luma color types".to_owned(), |
424 | 0 | ), |
425 | 0 | ))) |
426 | | } |
427 | | }, |
428 | | PnmHeader { |
429 | | decoded: HeaderRecord::Graymap(_), |
430 | | .. |
431 | 0 | } => match color { |
432 | 0 | ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), |
433 | | _ => { |
434 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
435 | 0 | ParameterErrorKind::Generic( |
436 | 0 | "PGM format only support luma color types".to_owned(), |
437 | 0 | ), |
438 | 0 | ))) |
439 | | } |
440 | | }, |
441 | | PnmHeader { |
442 | | decoded: HeaderRecord::Pixmap(_), |
443 | | .. |
444 | 0 | } => match color { |
445 | 0 | ExtendedColorType::Rgb8 => (), |
446 | | _ => { |
447 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
448 | 0 | ParameterErrorKind::Generic( |
449 | 0 | "PPM format only support ExtendedColorType::Rgb8".to_owned(), |
450 | 0 | ), |
451 | 0 | ))) |
452 | | } |
453 | | }, |
454 | | PnmHeader { |
455 | | decoded: |
456 | | HeaderRecord::Arbitrary(ArbitraryHeader { |
457 | 0 | depth, |
458 | 0 | ref tupltype, |
459 | | .. |
460 | | }), |
461 | | .. |
462 | 0 | } => match (tupltype, color) { |
463 | 0 | (&Some(ArbitraryTuplType::BlackAndWhite), ExtendedColorType::L1) => (), |
464 | 0 | (&Some(ArbitraryTuplType::BlackAndWhiteAlpha), ExtendedColorType::La8) => (), |
465 | | |
466 | 0 | (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L1) => (), |
467 | 0 | (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L8) => (), |
468 | 0 | (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L16) => (), |
469 | 0 | (&Some(ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La8) => (), |
470 | | |
471 | 0 | (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb8) => (), |
472 | 0 | (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb16) => (), |
473 | 0 | (&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba8) => (), |
474 | 0 | (&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba16) => (), |
475 | | |
476 | 0 | (&None, _) if depth == components => (), |
477 | 0 | (&Some(ArbitraryTuplType::Custom(_)), _) if depth == components => (), |
478 | 0 | _ if depth != components => { |
479 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
480 | 0 | ParameterErrorKind::Generic(format!( |
481 | 0 | "Depth mismatch: header {depth} vs. color {components}" |
482 | 0 | )), |
483 | 0 | ))) |
484 | | } |
485 | | _ => { |
486 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
487 | 0 | ParameterErrorKind::Generic( |
488 | 0 | "Invalid color type for selected PAM color type".to_owned(), |
489 | 0 | ), |
490 | 0 | ))) |
491 | | } |
492 | | }, |
493 | | } |
494 | | |
495 | 0 | Ok(CheckedHeaderColor { |
496 | 0 | dimensions: self, |
497 | 0 | color, |
498 | 0 | }) |
499 | 0 | } |
500 | | } |
501 | | |
502 | | impl<'a> CheckedHeaderColor<'a> { |
503 | 0 | fn check_sample_values(self, image: FlatSamples<'a>) -> ImageResult<CheckedHeader<'a>> { |
504 | 0 | let header_maxval = match self.dimensions.unchecked.header.decoded { |
505 | 0 | HeaderRecord::Bitmap(_) => 1, |
506 | 0 | HeaderRecord::Graymap(GraymapHeader { maxwhite, .. }) => maxwhite, |
507 | 0 | HeaderRecord::Pixmap(PixmapHeader { maxval, .. }) => maxval, |
508 | 0 | HeaderRecord::Arbitrary(ArbitraryHeader { maxval, .. }) => maxval, |
509 | | }; |
510 | | |
511 | | // We trust the image color bit count to be correct at least. |
512 | 0 | let max_sample = match self.color { |
513 | 0 | ExtendedColorType::Unknown(n) if n <= 16 => (1 << n) - 1, |
514 | 0 | ExtendedColorType::L1 => 1, |
515 | | ExtendedColorType::L8 |
516 | | | ExtendedColorType::La8 |
517 | | | ExtendedColorType::Rgb8 |
518 | | | ExtendedColorType::Rgba8 |
519 | | | ExtendedColorType::Bgr8 |
520 | 0 | | ExtendedColorType::Bgra8 => 0xff, |
521 | | ExtendedColorType::L16 |
522 | | | ExtendedColorType::La16 |
523 | | | ExtendedColorType::Rgb16 |
524 | 0 | | ExtendedColorType::Rgba16 => 0xffff, |
525 | | _ => { |
526 | | // Unsupported target color type. |
527 | 0 | return Err(ImageError::Unsupported( |
528 | 0 | UnsupportedError::from_format_and_kind( |
529 | 0 | ImageFormat::Pnm.into(), |
530 | 0 | UnsupportedErrorKind::Color(self.color), |
531 | 0 | ), |
532 | 0 | )); |
533 | | } |
534 | | }; |
535 | | |
536 | | // Avoid the performance heavy check if possible, e.g. if the header has been chosen by us. |
537 | 0 | if header_maxval < max_sample && !image.all_smaller(header_maxval) { |
538 | | // Sample value greater than allowed for chosen header. |
539 | 0 | return Err(ImageError::Unsupported( |
540 | 0 | UnsupportedError::from_format_and_kind( |
541 | 0 | ImageFormat::Pnm.into(), |
542 | 0 | UnsupportedErrorKind::GenericFeature( |
543 | 0 | "Sample value greater than allowed for chosen header".to_owned(), |
544 | 0 | ), |
545 | 0 | ), |
546 | 0 | )); |
547 | 0 | } |
548 | | |
549 | 0 | let encoding = image.encoding_for(&self.dimensions.unchecked.header.decoded); |
550 | | |
551 | 0 | let image = CheckedImageBuffer::check( |
552 | 0 | image, |
553 | 0 | self.dimensions.width, |
554 | 0 | self.dimensions.height, |
555 | 0 | self.color, |
556 | 0 | )?; |
557 | | |
558 | 0 | Ok(CheckedHeader { |
559 | 0 | color: self, |
560 | 0 | encoding, |
561 | 0 | _image: image, |
562 | 0 | }) |
563 | 0 | } |
564 | | } |
565 | | |
566 | | impl<'a> CheckedHeader<'a> { |
567 | 0 | fn write_header(self, writer: &mut dyn Write) -> ImageResult<TupleEncoding<'a>> { |
568 | 0 | self.header().write(writer)?; |
569 | 0 | Ok(self.encoding) |
570 | 0 | } |
571 | | |
572 | 0 | fn header(&self) -> &PnmHeader { |
573 | 0 | self.color.dimensions.unchecked.header |
574 | 0 | } |
575 | | } |
576 | | |
577 | | struct SampleWriter<'a>(&'a mut dyn Write); |
578 | | |
579 | | impl SampleWriter<'_> { |
580 | 0 | fn write_samples_ascii<V>(self, samples: V) -> io::Result<()> |
581 | 0 | where |
582 | 0 | V: Iterator, |
583 | 0 | V::Item: fmt::Display, |
584 | | { |
585 | 0 | let mut auto_break_writer = AutoBreak::new(self.0, 70)?; |
586 | 0 | for value in samples { |
587 | 0 | write!(auto_break_writer, "{value} ")?; |
588 | | } |
589 | 0 | auto_break_writer.flush() |
590 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::SampleWriter>::write_samples_ascii::<core::slice::iter::Iter<u8>> Unexecuted instantiation: <image::codecs::pnm::encoder::SampleWriter>::write_samples_ascii::<core::slice::iter::Iter<u16>> |
591 | | |
592 | 0 | fn write_pbm_bits<V>(self, samples: &[V], width: u32) -> io::Result<()> |
593 | 0 | /* Default gives 0 for all primitives. TODO: replace this with `Zeroable` once it hits stable */ |
594 | 0 | where |
595 | 0 | V: Default + Eq + Copy, |
596 | | { |
597 | | // The length of an encoded scanline |
598 | 0 | let line_width = (width - 1) / 8 + 1; |
599 | | |
600 | | // We'll be writing single bytes, so buffer |
601 | 0 | let mut line_buffer = vec_try_with_capacity(line_width as usize)?; |
602 | | |
603 | 0 | for line in samples.chunks(width as usize) { |
604 | 0 | for byte_bits in line.chunks(8) { |
605 | 0 | let mut byte = 0u8; |
606 | 0 | for i in 0..8 { |
607 | | // Black pixels are encoded as 1s |
608 | 0 | if let Some(&v) = byte_bits.get(i) { |
609 | 0 | if v == V::default() { |
610 | 0 | byte |= 1u8 << (7 - i); |
611 | 0 | } |
612 | 0 | } |
613 | | } |
614 | 0 | line_buffer.push(byte); |
615 | | } |
616 | 0 | self.0.write_all(line_buffer.as_slice())?; |
617 | 0 | line_buffer.clear(); |
618 | | } |
619 | | |
620 | 0 | self.0.flush() |
621 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::SampleWriter>::write_pbm_bits::<u8> Unexecuted instantiation: <image::codecs::pnm::encoder::SampleWriter>::write_pbm_bits::<u16> |
622 | | } |
623 | | |
624 | | impl<'a> FlatSamples<'a> { |
625 | 0 | fn len(&self) -> usize { |
626 | 0 | match *self { |
627 | 0 | FlatSamples::U8(arr) => arr.len(), |
628 | 0 | FlatSamples::U16(arr) => arr.len(), |
629 | | } |
630 | 0 | } |
631 | | |
632 | 0 | fn all_smaller(&self, max_val: u32) -> bool { |
633 | 0 | match *self { |
634 | 0 | FlatSamples::U8(arr) => arr.iter().all(|&val| u32::from(val) <= max_val), |
635 | 0 | FlatSamples::U16(arr) => arr.iter().all(|&val| u32::from(val) <= max_val), |
636 | | } |
637 | 0 | } |
638 | | |
639 | 0 | fn encoding_for(&self, header: &HeaderRecord) -> TupleEncoding<'a> { |
640 | 0 | match *header { |
641 | | HeaderRecord::Bitmap(BitmapHeader { |
642 | | encoding: SampleEncoding::Binary, |
643 | 0 | width, |
644 | | .. |
645 | 0 | }) => TupleEncoding::PbmBits { |
646 | 0 | samples: *self, |
647 | 0 | width, |
648 | 0 | }, |
649 | | |
650 | | HeaderRecord::Bitmap(BitmapHeader { |
651 | | encoding: SampleEncoding::Ascii, |
652 | | .. |
653 | 0 | }) => TupleEncoding::Ascii { samples: *self }, |
654 | | |
655 | 0 | HeaderRecord::Arbitrary(_) => TupleEncoding::Bytes { samples: *self }, |
656 | | |
657 | | HeaderRecord::Graymap(GraymapHeader { |
658 | | encoding: SampleEncoding::Ascii, |
659 | | .. |
660 | | }) |
661 | | | HeaderRecord::Pixmap(PixmapHeader { |
662 | | encoding: SampleEncoding::Ascii, |
663 | | .. |
664 | 0 | }) => TupleEncoding::Ascii { samples: *self }, |
665 | | |
666 | | HeaderRecord::Graymap(GraymapHeader { |
667 | | encoding: SampleEncoding::Binary, |
668 | | .. |
669 | | }) |
670 | | | HeaderRecord::Pixmap(PixmapHeader { |
671 | | encoding: SampleEncoding::Binary, |
672 | | .. |
673 | 0 | }) => TupleEncoding::Bytes { samples: *self }, |
674 | | } |
675 | 0 | } |
676 | | } |
677 | | |
678 | | impl<'a> From<&'a [u8]> for FlatSamples<'a> { |
679 | 0 | fn from(samples: &'a [u8]) -> Self { |
680 | 0 | FlatSamples::U8(samples) |
681 | 0 | } |
682 | | } |
683 | | |
684 | | impl<'a> From<&'a [u16]> for FlatSamples<'a> { |
685 | 0 | fn from(samples: &'a [u16]) -> Self { |
686 | 0 | FlatSamples::U16(samples) |
687 | 0 | } |
688 | | } |
689 | | |
690 | | impl TupleEncoding<'_> { |
691 | 0 | fn write_image(&self, writer: &mut dyn Write) -> ImageResult<()> { |
692 | 0 | match *self { |
693 | | TupleEncoding::PbmBits { |
694 | 0 | samples: FlatSamples::U8(samples), |
695 | 0 | width, |
696 | 0 | } => SampleWriter(writer) |
697 | 0 | .write_pbm_bits(samples, width) |
698 | 0 | .map_err(ImageError::IoError), |
699 | | TupleEncoding::PbmBits { |
700 | 0 | samples: FlatSamples::U16(samples), |
701 | 0 | width, |
702 | 0 | } => SampleWriter(writer) |
703 | 0 | .write_pbm_bits(samples, width) |
704 | 0 | .map_err(ImageError::IoError), |
705 | | |
706 | | TupleEncoding::Bytes { |
707 | 0 | samples: FlatSamples::U8(samples), |
708 | 0 | } => writer.write_all(samples).map_err(ImageError::IoError), |
709 | | TupleEncoding::Bytes { |
710 | 0 | samples: FlatSamples::U16(samples), |
711 | 0 | } => samples.iter().try_for_each(|&sample| { |
712 | 0 | writer |
713 | 0 | .write_u16::<BigEndian>(sample) |
714 | 0 | .map_err(ImageError::IoError) |
715 | 0 | }), |
716 | | |
717 | | TupleEncoding::Ascii { |
718 | 0 | samples: FlatSamples::U8(samples), |
719 | 0 | } => SampleWriter(writer) |
720 | 0 | .write_samples_ascii(samples.iter()) |
721 | 0 | .map_err(ImageError::IoError), |
722 | | TupleEncoding::Ascii { |
723 | 0 | samples: FlatSamples::U16(samples), |
724 | 0 | } => SampleWriter(writer) |
725 | 0 | .write_samples_ascii(samples.iter()) |
726 | 0 | .map_err(ImageError::IoError), |
727 | | } |
728 | 0 | } |
729 | | } |
730 | | |
731 | | #[test] |
732 | | fn pbm_allows_black() { |
733 | | let imgbuf = crate::DynamicImage::new_luma8(50, 50); |
734 | | |
735 | | let mut buffer = vec![]; |
736 | | let encoder = |
737 | | PnmEncoder::new(&mut buffer).with_subtype(PnmSubtype::Bitmap(SampleEncoding::Ascii)); |
738 | | |
739 | | imgbuf |
740 | | .write_with_encoder(encoder) |
741 | | .expect("all-zeroes is a black image"); |
742 | | } |
743 | | |
744 | | #[test] |
745 | | fn pbm_allows_white() { |
746 | | let imgbuf = |
747 | | crate::DynamicImage::ImageLuma8(crate::ImageBuffer::from_pixel(50, 50, crate::Luma([1]))); |
748 | | |
749 | | let mut buffer = vec![]; |
750 | | let encoder = |
751 | | PnmEncoder::new(&mut buffer).with_subtype(PnmSubtype::Bitmap(SampleEncoding::Ascii)); |
752 | | |
753 | | imgbuf |
754 | | .write_with_encoder(encoder) |
755 | | .expect("all-zeroes is a white image"); |
756 | | } |
757 | | |
758 | | #[test] |
759 | | fn pbm_verifies_pixels() { |
760 | | let imgbuf = |
761 | | crate::DynamicImage::ImageLuma8(crate::ImageBuffer::from_pixel(50, 50, crate::Luma([255]))); |
762 | | |
763 | | let mut buffer = vec![]; |
764 | | let encoder = |
765 | | PnmEncoder::new(&mut buffer).with_subtype(PnmSubtype::Bitmap(SampleEncoding::Ascii)); |
766 | | |
767 | | imgbuf |
768 | | .write_with_encoder(encoder) |
769 | | .expect_err("failed to catch violating samples"); |
770 | | } |