/src/image/src/codecs/pnm/encoder.rs
Line | Count | Source (jump to first uncovered line) |
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 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::new Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::new |
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 | 0 | pub fn with_subtype(self, subtype: PnmSubtype) -> Self { |
103 | 0 | PnmEncoder { |
104 | 0 | writer: self.writer, |
105 | 0 | header: HeaderStrategy::Subtype(subtype), |
106 | 0 | } |
107 | 0 | } |
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 | 0 | pub fn with_header(self, header: PnmHeader) -> Self { |
119 | 0 | PnmEncoder { |
120 | 0 | writer: self.writer, |
121 | 0 | header: HeaderStrategy::Chosen(header), |
122 | 0 | } |
123 | 0 | } |
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 | 0 | pub fn with_dynamic_header(self) -> Self { |
133 | 0 | PnmEncoder { |
134 | 0 | writer: self.writer, |
135 | 0 | header: HeaderStrategy::Dynamic, |
136 | 0 | } |
137 | 0 | } |
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 | 0 | { |
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 | 0 | ExtendedColorType::L16 |
167 | 0 | | ExtendedColorType::La16 |
168 | 0 | | ExtendedColorType::Rgb16 |
169 | 0 | | ExtendedColorType::Rgba16, |
170 | 0 | ) => { |
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 | 0 | // 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]])) Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::encode::<_>::{closure#0} Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode::<&[u8]>::{closure#0} |
179 | 0 | .collect(); |
180 | 0 |
|
181 | 0 | let image = FlatSamples::U16(&new_samples); |
182 | 0 |
|
183 | 0 | // make a separate encoding path, |
184 | 0 | // 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 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::encode::<_> Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode::<&[u8]> |
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 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::encode_impl Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::encode_impl |
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 | 0 |
|
258 | 0 | Self::write_with_header(&mut self.writer, &header, image, width, height, color) |
259 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::write_dynamic_header Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::write_dynamic_header |
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::Parameter(ParameterError::from_kind( |
304 | 0 | ParameterErrorKind::Generic( |
305 | 0 | "Color type can not be represented in the chosen format".to_owned(), |
306 | 0 | ), |
307 | 0 | ))); |
308 | | } |
309 | | }; |
310 | | |
311 | 0 | Self::write_with_header(&mut self.writer, &header, image, width, height, color) |
312 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::write_subtyped_header Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::write_subtyped_header |
313 | | |
314 | | /// Try to encode the image with the chosen header, checking if values are correct. |
315 | | /// |
316 | | /// Returns how the body should be written if successful. |
317 | 0 | fn write_with_header( |
318 | 0 | writer: &mut dyn Write, |
319 | 0 | header: &PnmHeader, |
320 | 0 | image: FlatSamples, |
321 | 0 | width: u32, |
322 | 0 | height: u32, |
323 | 0 | color: ExtendedColorType, |
324 | 0 | ) -> ImageResult<()> { |
325 | 0 | let unchecked = UncheckedHeader { header }; |
326 | 0 |
|
327 | 0 | unchecked |
328 | 0 | .check_header_dimensions(width, height)? |
329 | 0 | .check_header_color(color)? |
330 | 0 | .check_sample_values(image)? |
331 | 0 | .write_header(writer)? |
332 | 0 | .write_image(writer) |
333 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_>>::write_with_header Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>>>::write_with_header |
334 | | } |
335 | | |
336 | | impl<W: Write> ImageEncoder for PnmEncoder<W> { |
337 | | #[track_caller] |
338 | 0 | fn write_image( |
339 | 0 | mut self, |
340 | 0 | buf: &[u8], |
341 | 0 | width: u32, |
342 | 0 | height: u32, |
343 | 0 | color_type: ExtendedColorType, |
344 | 0 | ) -> ImageResult<()> { |
345 | 0 | let expected_buffer_len = color_type.buffer_size(width, height); |
346 | 0 | assert_eq!( |
347 | 0 | expected_buffer_len, |
348 | 0 | buf.len() as u64, |
349 | 0 | "Invalid buffer length: expected {expected_buffer_len} got {} for {width}x{height} image", |
350 | 0 | buf.len(), |
351 | | ); |
352 | | |
353 | 0 | self.encode(buf, width, height, color_type) |
354 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<_> as image::io::encoder::ImageEncoder>::write_image Unexecuted instantiation: <image::codecs::pnm::encoder::PnmEncoder<&mut std::io::cursor::Cursor<alloc::vec::Vec<u8>>> as image::io::encoder::ImageEncoder>::write_image |
355 | | } |
356 | | |
357 | | impl<'a> CheckedImageBuffer<'a> { |
358 | 0 | fn check( |
359 | 0 | image: FlatSamples<'a>, |
360 | 0 | width: u32, |
361 | 0 | height: u32, |
362 | 0 | color: ExtendedColorType, |
363 | 0 | ) -> ImageResult<CheckedImageBuffer<'a>> { |
364 | 0 | let components = color.channel_count() as usize; |
365 | 0 | let uwidth = width as usize; |
366 | 0 | let uheight = height as usize; |
367 | 0 | let expected_len = components |
368 | 0 | .checked_mul(uwidth) |
369 | 0 | .and_then(|v| v.checked_mul(uheight)); |
370 | 0 | if Some(image.len()) != expected_len { |
371 | | // Image buffer does not correspond to size and colour. |
372 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
373 | 0 | ParameterErrorKind::DimensionMismatch, |
374 | 0 | ))); |
375 | 0 | } |
376 | 0 | Ok(CheckedImageBuffer { |
377 | 0 | _image: image, |
378 | 0 | _width: width, |
379 | 0 | _height: height, |
380 | 0 | _color: color, |
381 | 0 | }) |
382 | 0 | } |
383 | | } |
384 | | |
385 | | impl<'a> UncheckedHeader<'a> { |
386 | 0 | fn check_header_dimensions( |
387 | 0 | self, |
388 | 0 | width: u32, |
389 | 0 | height: u32, |
390 | 0 | ) -> ImageResult<CheckedDimensions<'a>> { |
391 | 0 | if self.header.width() != width || self.header.height() != height { |
392 | | // Chosen header does not match Image dimensions. |
393 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
394 | 0 | ParameterErrorKind::DimensionMismatch, |
395 | 0 | ))); |
396 | 0 | } |
397 | 0 |
|
398 | 0 | Ok(CheckedDimensions { |
399 | 0 | unchecked: self, |
400 | 0 | width, |
401 | 0 | height, |
402 | 0 | }) |
403 | 0 | } |
404 | | } |
405 | | |
406 | | impl<'a> CheckedDimensions<'a> { |
407 | | // Check color compatibility with the header. This will only error when we are certain that |
408 | | // the combination is bogus (e.g. combining Pixmap and Palette) but allows uncertain |
409 | | // combinations (basically a ArbitraryTuplType::Custom with any color of fitting depth). |
410 | 0 | fn check_header_color(self, color: ExtendedColorType) -> ImageResult<CheckedHeaderColor<'a>> { |
411 | 0 | let components = u32::from(color.channel_count()); |
412 | 0 |
|
413 | 0 | match *self.unchecked.header { |
414 | | PnmHeader { |
415 | | decoded: HeaderRecord::Bitmap(_), |
416 | | .. |
417 | 0 | } => match color { |
418 | 0 | ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), |
419 | | _ => { |
420 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
421 | 0 | ParameterErrorKind::Generic( |
422 | 0 | "PBM format only support luma color types".to_owned(), |
423 | 0 | ), |
424 | 0 | ))) |
425 | | } |
426 | | }, |
427 | | PnmHeader { |
428 | | decoded: HeaderRecord::Graymap(_), |
429 | | .. |
430 | 0 | } => match color { |
431 | 0 | ExtendedColorType::L1 | ExtendedColorType::L8 | ExtendedColorType::L16 => (), |
432 | | _ => { |
433 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
434 | 0 | ParameterErrorKind::Generic( |
435 | 0 | "PGM format only support luma color types".to_owned(), |
436 | 0 | ), |
437 | 0 | ))) |
438 | | } |
439 | | }, |
440 | | PnmHeader { |
441 | | decoded: HeaderRecord::Pixmap(_), |
442 | | .. |
443 | 0 | } => match color { |
444 | 0 | ExtendedColorType::Rgb8 => (), |
445 | | _ => { |
446 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
447 | 0 | ParameterErrorKind::Generic( |
448 | 0 | "PPM format only support ExtendedColorType::Rgb8".to_owned(), |
449 | 0 | ), |
450 | 0 | ))) |
451 | | } |
452 | | }, |
453 | | PnmHeader { |
454 | | decoded: |
455 | | HeaderRecord::Arbitrary(ArbitraryHeader { |
456 | 0 | depth, |
457 | 0 | ref tupltype, |
458 | 0 | .. |
459 | 0 | }), |
460 | 0 | .. |
461 | 0 | } => match (tupltype, color) { |
462 | 0 | (&Some(ArbitraryTuplType::BlackAndWhite), ExtendedColorType::L1) => (), |
463 | 0 | (&Some(ArbitraryTuplType::BlackAndWhiteAlpha), ExtendedColorType::La8) => (), |
464 | | |
465 | 0 | (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L1) => (), |
466 | 0 | (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L8) => (), |
467 | 0 | (&Some(ArbitraryTuplType::Grayscale), ExtendedColorType::L16) => (), |
468 | 0 | (&Some(ArbitraryTuplType::GrayscaleAlpha), ExtendedColorType::La8) => (), |
469 | | |
470 | 0 | (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb8) => (), |
471 | 0 | (&Some(ArbitraryTuplType::RGB), ExtendedColorType::Rgb16) => (), |
472 | 0 | (&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba8) => (), |
473 | 0 | (&Some(ArbitraryTuplType::RGBAlpha), ExtendedColorType::Rgba16) => (), |
474 | | |
475 | 0 | (&None, _) if depth == components => (), |
476 | 0 | (&Some(ArbitraryTuplType::Custom(_)), _) if depth == components => (), |
477 | 0 | _ if depth != components => { |
478 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
479 | 0 | ParameterErrorKind::Generic(format!( |
480 | 0 | "Depth mismatch: header {depth} vs. color {components}" |
481 | 0 | )), |
482 | 0 | ))) |
483 | | } |
484 | | _ => { |
485 | 0 | return Err(ImageError::Parameter(ParameterError::from_kind( |
486 | 0 | ParameterErrorKind::Generic( |
487 | 0 | "Invalid color type for selected PAM color type".to_owned(), |
488 | 0 | ), |
489 | 0 | ))) |
490 | | } |
491 | | }, |
492 | | } |
493 | | |
494 | 0 | Ok(CheckedHeaderColor { |
495 | 0 | dimensions: self, |
496 | 0 | color, |
497 | 0 | }) |
498 | 0 | } |
499 | | } |
500 | | |
501 | | impl<'a> CheckedHeaderColor<'a> { |
502 | 0 | fn check_sample_values(self, image: FlatSamples<'a>) -> ImageResult<CheckedHeader<'a>> { |
503 | 0 | let header_maxval = match self.dimensions.unchecked.header.decoded { |
504 | 0 | HeaderRecord::Bitmap(_) => 1, |
505 | 0 | HeaderRecord::Graymap(GraymapHeader { maxwhite, .. }) => maxwhite, |
506 | 0 | HeaderRecord::Pixmap(PixmapHeader { maxval, .. }) => maxval, |
507 | 0 | HeaderRecord::Arbitrary(ArbitraryHeader { maxval, .. }) => maxval, |
508 | | }; |
509 | | |
510 | | // We trust the image color bit count to be correct at least. |
511 | 0 | let max_sample = match self.color { |
512 | 0 | ExtendedColorType::Unknown(n) if n <= 16 => (1 << n) - 1, |
513 | 0 | ExtendedColorType::L1 => 1, |
514 | | ExtendedColorType::L8 |
515 | | | ExtendedColorType::La8 |
516 | | | ExtendedColorType::Rgb8 |
517 | | | ExtendedColorType::Rgba8 |
518 | | | ExtendedColorType::Bgr8 |
519 | 0 | | ExtendedColorType::Bgra8 => 0xff, |
520 | | ExtendedColorType::L16 |
521 | | | ExtendedColorType::La16 |
522 | | | ExtendedColorType::Rgb16 |
523 | 0 | | ExtendedColorType::Rgba16 => 0xffff, |
524 | | _ => { |
525 | | // Unsupported target color type. |
526 | 0 | return Err(ImageError::Unsupported( |
527 | 0 | UnsupportedError::from_format_and_kind( |
528 | 0 | ImageFormat::Pnm.into(), |
529 | 0 | UnsupportedErrorKind::Color(self.color), |
530 | 0 | ), |
531 | 0 | )); |
532 | | } |
533 | | }; |
534 | | |
535 | | // Avoid the performance heavy check if possible, e.g. if the header has been chosen by us. |
536 | 0 | if header_maxval < max_sample && !image.all_smaller(header_maxval) { |
537 | | // Sample value greater than allowed for chosen header. |
538 | 0 | return Err(ImageError::Unsupported( |
539 | 0 | UnsupportedError::from_format_and_kind( |
540 | 0 | ImageFormat::Pnm.into(), |
541 | 0 | UnsupportedErrorKind::GenericFeature( |
542 | 0 | "Sample value greater than allowed for chosen header".to_owned(), |
543 | 0 | ), |
544 | 0 | ), |
545 | 0 | )); |
546 | 0 | } |
547 | 0 |
|
548 | 0 | let encoding = image.encoding_for(&self.dimensions.unchecked.header.decoded); |
549 | | |
550 | 0 | let image = CheckedImageBuffer::check( |
551 | 0 | image, |
552 | 0 | self.dimensions.width, |
553 | 0 | self.dimensions.height, |
554 | 0 | self.color, |
555 | 0 | )?; |
556 | | |
557 | 0 | Ok(CheckedHeader { |
558 | 0 | color: self, |
559 | 0 | encoding, |
560 | 0 | _image: image, |
561 | 0 | }) |
562 | 0 | } |
563 | | } |
564 | | |
565 | | impl<'a> CheckedHeader<'a> { |
566 | 0 | fn write_header(self, writer: &mut dyn Write) -> ImageResult<TupleEncoding<'a>> { |
567 | 0 | self.header().write(writer)?; |
568 | 0 | Ok(self.encoding) |
569 | 0 | } |
570 | | |
571 | 0 | fn header(&self) -> &PnmHeader { |
572 | 0 | self.color.dimensions.unchecked.header |
573 | 0 | } |
574 | | } |
575 | | |
576 | | struct SampleWriter<'a>(&'a mut dyn Write); |
577 | | |
578 | | impl SampleWriter<'_> { |
579 | 0 | fn write_samples_ascii<V>(self, samples: V) -> io::Result<()> |
580 | 0 | where |
581 | 0 | V: Iterator, |
582 | 0 | V::Item: fmt::Display, |
583 | 0 | { |
584 | 0 | let mut auto_break_writer = AutoBreak::new(self.0, 70)?; |
585 | 0 | for value in samples { |
586 | 0 | write!(auto_break_writer, "{value} ")?; |
587 | | } |
588 | 0 | auto_break_writer.flush() |
589 | 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>> |
590 | | |
591 | 0 | fn write_pbm_bits<V>(self, samples: &[V], width: u32) -> io::Result<()> |
592 | 0 | /* Default gives 0 for all primitives. TODO: replace this with `Zeroable` once it hits stable */ |
593 | 0 | where |
594 | 0 | V: Default + Eq + Copy, |
595 | 0 | { |
596 | 0 | // The length of an encoded scanline |
597 | 0 | let line_width = (width - 1) / 8 + 1; |
598 | | |
599 | | // We'll be writing single bytes, so buffer |
600 | 0 | let mut line_buffer = vec_try_with_capacity(line_width as usize)?; |
601 | | |
602 | 0 | for line in samples.chunks(width as usize) { |
603 | 0 | for byte_bits in line.chunks(8) { |
604 | 0 | let mut byte = 0u8; |
605 | 0 | for i in 0..8 { |
606 | | // Black pixels are encoded as 1s |
607 | 0 | if let Some(&v) = byte_bits.get(i) { |
608 | 0 | if v == V::default() { |
609 | 0 | byte |= 1u8 << (7 - i); |
610 | 0 | } |
611 | 0 | } |
612 | | } |
613 | 0 | line_buffer.push(byte); |
614 | | } |
615 | 0 | self.0.write_all(line_buffer.as_slice())?; |
616 | 0 | line_buffer.clear(); |
617 | | } |
618 | | |
619 | 0 | self.0.flush() |
620 | 0 | } Unexecuted instantiation: <image::codecs::pnm::encoder::SampleWriter>::write_pbm_bits::<u8> Unexecuted instantiation: <image::codecs::pnm::encoder::SampleWriter>::write_pbm_bits::<u16> |
621 | | } |
622 | | |
623 | | impl<'a> FlatSamples<'a> { |
624 | 0 | fn len(&self) -> usize { |
625 | 0 | match *self { |
626 | 0 | FlatSamples::U8(arr) => arr.len(), |
627 | 0 | FlatSamples::U16(arr) => arr.len(), |
628 | | } |
629 | 0 | } |
630 | | |
631 | 0 | fn all_smaller(&self, max_val: u32) -> bool { |
632 | 0 | match *self { |
633 | 0 | FlatSamples::U8(arr) => arr.iter().any(|&val| u32::from(val) > max_val), |
634 | 0 | FlatSamples::U16(arr) => arr.iter().any(|&val| u32::from(val) > max_val), |
635 | | } |
636 | 0 | } |
637 | | |
638 | 0 | fn encoding_for(&self, header: &HeaderRecord) -> TupleEncoding<'a> { |
639 | 0 | match *header { |
640 | | HeaderRecord::Bitmap(BitmapHeader { |
641 | | encoding: SampleEncoding::Binary, |
642 | 0 | width, |
643 | 0 | .. |
644 | 0 | }) => TupleEncoding::PbmBits { |
645 | 0 | samples: *self, |
646 | 0 | width, |
647 | 0 | }, |
648 | | |
649 | | HeaderRecord::Bitmap(BitmapHeader { |
650 | | encoding: SampleEncoding::Ascii, |
651 | | .. |
652 | 0 | }) => TupleEncoding::Ascii { samples: *self }, |
653 | | |
654 | 0 | HeaderRecord::Arbitrary(_) => TupleEncoding::Bytes { samples: *self }, |
655 | | |
656 | | HeaderRecord::Graymap(GraymapHeader { |
657 | | encoding: SampleEncoding::Ascii, |
658 | | .. |
659 | | }) |
660 | | | HeaderRecord::Pixmap(PixmapHeader { |
661 | | encoding: SampleEncoding::Ascii, |
662 | | .. |
663 | 0 | }) => TupleEncoding::Ascii { samples: *self }, |
664 | | |
665 | | HeaderRecord::Graymap(GraymapHeader { |
666 | | encoding: SampleEncoding::Binary, |
667 | | .. |
668 | | }) |
669 | | | HeaderRecord::Pixmap(PixmapHeader { |
670 | | encoding: SampleEncoding::Binary, |
671 | | .. |
672 | 0 | }) => TupleEncoding::Bytes { samples: *self }, |
673 | | } |
674 | 0 | } |
675 | | } |
676 | | |
677 | | impl<'a> From<&'a [u8]> for FlatSamples<'a> { |
678 | 0 | fn from(samples: &'a [u8]) -> Self { |
679 | 0 | FlatSamples::U8(samples) |
680 | 0 | } |
681 | | } |
682 | | |
683 | | impl<'a> From<&'a [u16]> for FlatSamples<'a> { |
684 | 0 | fn from(samples: &'a [u16]) -> Self { |
685 | 0 | FlatSamples::U16(samples) |
686 | 0 | } |
687 | | } |
688 | | |
689 | | impl TupleEncoding<'_> { |
690 | 0 | fn write_image(&self, writer: &mut dyn Write) -> ImageResult<()> { |
691 | 0 | match *self { |
692 | | TupleEncoding::PbmBits { |
693 | 0 | samples: FlatSamples::U8(samples), |
694 | 0 | width, |
695 | 0 | } => SampleWriter(writer) |
696 | 0 | .write_pbm_bits(samples, width) |
697 | 0 | .map_err(ImageError::IoError), |
698 | | TupleEncoding::PbmBits { |
699 | 0 | samples: FlatSamples::U16(samples), |
700 | 0 | width, |
701 | 0 | } => SampleWriter(writer) |
702 | 0 | .write_pbm_bits(samples, width) |
703 | 0 | .map_err(ImageError::IoError), |
704 | | |
705 | | TupleEncoding::Bytes { |
706 | 0 | samples: FlatSamples::U8(samples), |
707 | 0 | } => writer.write_all(samples).map_err(ImageError::IoError), |
708 | | TupleEncoding::Bytes { |
709 | 0 | samples: FlatSamples::U16(samples), |
710 | 0 | } => samples.iter().try_for_each(|&sample| { |
711 | 0 | writer |
712 | 0 | .write_u16::<BigEndian>(sample) |
713 | 0 | .map_err(ImageError::IoError) |
714 | 0 | }), |
715 | | |
716 | | TupleEncoding::Ascii { |
717 | 0 | samples: FlatSamples::U8(samples), |
718 | 0 | } => SampleWriter(writer) |
719 | 0 | .write_samples_ascii(samples.iter()) |
720 | 0 | .map_err(ImageError::IoError), |
721 | | TupleEncoding::Ascii { |
722 | 0 | samples: FlatSamples::U16(samples), |
723 | 0 | } => SampleWriter(writer) |
724 | 0 | .write_samples_ascii(samples.iter()) |
725 | 0 | .map_err(ImageError::IoError), |
726 | | } |
727 | 0 | } |
728 | | } |