Coverage Report

Created: 2025-10-10 07:21

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/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
}