Skip to main content

tiff_writer/
builder.rs

1//! Image builder for configuring a single TIFF IFD.
2
3use tiff_core::*;
4
5use crate::encoder;
6use crate::sample::TiffWriteSample;
7
8/// LERC encoding options for the TIFF writer.
9///
10/// Controls the LERC2 error tolerance and optional additional compression
11/// applied to the encoded LERC blob before storage in the TIFF block.
12#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct LercOptions {
14    /// Maximum encoding error per sample value. Set to `0.0` for lossless.
15    pub max_z_error: f64,
16    /// Optional additional compression applied to the LERC blob.
17    pub additional_compression: LercAdditionalCompression,
18}
19
20impl Default for LercOptions {
21    fn default() -> Self {
22        Self {
23            max_z_error: 0.0,
24            additional_compression: LercAdditionalCompression::None,
25        }
26    }
27}
28
29/// JPEG encoding options for the TIFF writer.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct JpegOptions {
32    /// Quality in the range 1..=100.
33    pub quality: u8,
34}
35
36impl Default for JpegOptions {
37    fn default() -> Self {
38        Self { quality: 75 }
39    }
40}
41
42/// Describes how image data is organized: strips or tiles.
43#[derive(Debug, Clone, Copy)]
44pub enum DataLayout {
45    /// Strip-based: each strip contains `rows_per_strip` rows.
46    Strips { rows_per_strip: u32 },
47    /// Tile-based: each tile is `width x height` pixels.
48    Tiles { width: u32, height: u32 },
49}
50
51/// Builder for configuring a single image (IFD) within a TIFF file.
52#[derive(Debug, Clone)]
53pub struct ImageBuilder {
54    pub(crate) width: u32,
55    pub(crate) height: u32,
56    pub(crate) samples_per_pixel: u16,
57    pub(crate) bits_per_sample: u16,
58    pub(crate) sample_format: SampleFormat,
59    pub(crate) compression: Compression,
60    pub(crate) predictor: Predictor,
61    pub(crate) photometric: PhotometricInterpretation,
62    pub(crate) extra_samples: Vec<ExtraSample>,
63    pub(crate) color_map: Option<ColorMap>,
64    pub(crate) ink_set: Option<InkSet>,
65    pub(crate) ycbcr_subsampling: Option<[u16; 2]>,
66    pub(crate) ycbcr_positioning: Option<YCbCrPositioning>,
67    pub(crate) planar_configuration: PlanarConfiguration,
68    pub(crate) layout: DataLayout,
69    pub(crate) extra_tags: Vec<Tag>,
70    pub(crate) subfile_type: u32,
71    pub(crate) lerc_options: Option<LercOptions>,
72    pub(crate) jpeg_options: Option<JpegOptions>,
73}
74
75impl ImageBuilder {
76    /// Create a new image builder with required dimensions.
77    pub fn new(width: u32, height: u32) -> Self {
78        Self {
79            width,
80            height,
81            samples_per_pixel: 1,
82            bits_per_sample: 8,
83            sample_format: SampleFormat::Uint,
84            compression: Compression::None,
85            predictor: Predictor::None,
86            photometric: PhotometricInterpretation::MinIsBlack,
87            extra_samples: Vec::new(),
88            color_map: None,
89            ink_set: None,
90            ycbcr_subsampling: None,
91            ycbcr_positioning: None,
92            planar_configuration: PlanarConfiguration::Chunky,
93            layout: DataLayout::Strips {
94                rows_per_strip: height.min(256),
95            },
96            extra_tags: Vec::new(),
97            subfile_type: 0,
98            lerc_options: None,
99            jpeg_options: None,
100        }
101    }
102
103    pub fn samples_per_pixel(mut self, spp: u16) -> Self {
104        self.samples_per_pixel = spp;
105        self
106    }
107
108    pub fn bits_per_sample(mut self, bps: u16) -> Self {
109        self.bits_per_sample = bps;
110        self
111    }
112
113    pub fn sample_format(mut self, fmt: SampleFormat) -> Self {
114        self.sample_format = fmt;
115        self
116    }
117
118    /// Configure from a TiffWriteSample type. Sets bits_per_sample and sample_format.
119    pub fn sample_type<T: TiffWriteSample>(mut self) -> Self {
120        self.bits_per_sample = T::BITS_PER_SAMPLE;
121        self.sample_format =
122            SampleFormat::from_code(T::SAMPLE_FORMAT).unwrap_or(SampleFormat::Uint);
123        self
124    }
125
126    pub fn compression(mut self, c: Compression) -> Self {
127        self.compression = c;
128        if !matches!(c, Compression::Lerc) {
129            self.lerc_options = None;
130        }
131        if !matches!(c, Compression::Jpeg) {
132            self.jpeg_options = None;
133        }
134        if matches!(c, Compression::Lerc | Compression::Jpeg) {
135            self.predictor = Predictor::None;
136        }
137        self
138    }
139
140    pub fn predictor(mut self, p: Predictor) -> Self {
141        // LERC and JPEG do not use TIFF predictors; ignore the request.
142        if !matches!(self.compression, Compression::Lerc | Compression::Jpeg) {
143            self.predictor = p;
144        }
145        self
146    }
147
148    pub fn photometric(mut self, p: PhotometricInterpretation) -> Self {
149        self.photometric = p;
150        self
151    }
152
153    /// Set TIFF ExtraSamples semantics for channels beyond the base color model.
154    pub fn extra_samples(mut self, extra_samples: Vec<ExtraSample>) -> Self {
155        self.extra_samples = extra_samples;
156        self
157    }
158
159    /// Set a palette ColorMap for `PhotometricInterpretation::Palette`.
160    pub fn color_map(mut self, color_map: ColorMap) -> Self {
161        self.color_map = Some(color_map);
162        self
163    }
164
165    /// Set the InkSet tag for separated photometric data.
166    pub fn ink_set(mut self, ink_set: InkSet) -> Self {
167        self.ink_set = Some(ink_set);
168        self
169    }
170
171    /// Set TIFF YCbCr chroma subsampling factors.
172    pub fn ycbcr_subsampling(mut self, subsampling: [u16; 2]) -> Self {
173        self.ycbcr_subsampling = Some(subsampling);
174        self
175    }
176
177    /// Set TIFF YCbCr sample positioning.
178    pub fn ycbcr_positioning(mut self, positioning: YCbCrPositioning) -> Self {
179        self.ycbcr_positioning = Some(positioning);
180        self
181    }
182
183    /// Set chunky (interleaved) or separate planar sample layout for multi-band images.
184    pub fn planar_configuration(mut self, p: PlanarConfiguration) -> Self {
185        self.planar_configuration = p;
186        self
187    }
188
189    /// Configure strip-based layout.
190    pub fn strips(mut self, rows_per_strip: u32) -> Self {
191        self.layout = DataLayout::Strips { rows_per_strip };
192        self
193    }
194
195    /// Configure tile-based layout.
196    pub fn tiles(mut self, tile_width: u32, tile_height: u32) -> Self {
197        self.layout = DataLayout::Tiles {
198            width: tile_width,
199            height: tile_height,
200        };
201        self
202    }
203
204    /// Add an arbitrary extra tag to the IFD.
205    pub fn tag(mut self, tag: Tag) -> Self {
206        self.extra_tags.push(tag);
207        self
208    }
209
210    /// Mark this IFD as a reduced-resolution overview.
211    pub fn overview(mut self) -> Self {
212        self.subfile_type = 1;
213        self
214    }
215
216    /// Set LERC compression with the given options.
217    ///
218    /// This sets `compression = Lerc` and `predictor = None` (LERC performs
219    /// its own quantization and does not use TIFF predictors).
220    pub fn lerc_options(mut self, options: LercOptions) -> Self {
221        self.compression = Compression::Lerc;
222        self.predictor = Predictor::None;
223        self.lerc_options = Some(options);
224        self.jpeg_options = None;
225        self
226    }
227
228    /// Set JPEG compression with the given options.
229    ///
230    /// This sets `compression = Jpeg` and `predictor = None` (JPEG uses its
231    /// own transform and entropy coding pipeline rather than TIFF predictors).
232    ///
233    /// Multi-band JPEG requires `planar_configuration(Planar)` so each encoded
234    /// strip/tile is a single grayscale component.
235    pub fn jpeg_options(mut self, options: JpegOptions) -> Self {
236        self.compression = Compression::Jpeg;
237        self.predictor = Predictor::None;
238        self.jpeg_options = Some(options);
239        self.lerc_options = None;
240        self
241    }
242
243    /// Total number of blocks (strips or tiles) for this image configuration.
244    pub fn block_count(&self) -> usize {
245        let blocks_per_plane = match self.layout {
246            DataLayout::Strips { rows_per_strip } => {
247                let rps = rows_per_strip.max(1) as usize;
248                (self.height as usize).div_ceil(rps)
249            }
250            DataLayout::Tiles { width, height } => {
251                let tw = width.max(1) as usize;
252                let th = height.max(1) as usize;
253                let tiles_across = (self.width as usize).div_ceil(tw);
254                let tiles_down = (self.height as usize).div_ceil(th);
255                tiles_across * tiles_down
256            }
257        };
258        if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
259            blocks_per_plane * self.samples_per_pixel as usize
260        } else {
261            blocks_per_plane
262        }
263    }
264
265    /// Expected number of samples for the block at `index`.
266    pub fn block_sample_count(&self, index: usize) -> usize {
267        let samples_per_pixel = self.block_samples_per_pixel() as usize;
268        let plane_block_index = self.block_plane_index(index);
269        match self.layout {
270            DataLayout::Strips { rows_per_strip } => {
271                let rps = rows_per_strip.max(1) as usize;
272                let start_row = plane_block_index * rps;
273                let end_row = ((plane_block_index + 1) * rps).min(self.height as usize);
274                let rows = end_row.saturating_sub(start_row);
275                rows * self.width as usize * samples_per_pixel
276            }
277            DataLayout::Tiles { width, height } => {
278                // Tiles are always full-sized (padded at edges)
279                width as usize * height as usize * samples_per_pixel
280            }
281        }
282    }
283
284    /// Estimated uncompressed image bytes.
285    pub fn estimated_uncompressed_bytes(&self) -> u64 {
286        let bps = (self.bits_per_sample / 8).max(1) as u64;
287        self.width as u64 * self.height as u64 * self.samples_per_pixel as u64 * bps
288    }
289
290    /// The TIFF tag codes for offset and bytecount arrays.
291    pub fn offset_tag_codes(&self) -> (u16, u16) {
292        match self.layout {
293            DataLayout::Strips { .. } => (TAG_STRIP_OFFSETS, TAG_STRIP_BYTE_COUNTS),
294            DataLayout::Tiles { .. } => (TAG_TILE_OFFSETS, TAG_TILE_BYTE_COUNTS),
295        }
296    }
297
298    /// Build the layout-specific tags (RowsPerStrip or TileWidth/TileLength).
299    pub fn layout_tags(&self) -> Vec<Tag> {
300        match self.layout {
301            DataLayout::Strips { rows_per_strip } => {
302                vec![Tag::new(
303                    TAG_ROWS_PER_STRIP,
304                    TagValue::Long(vec![rows_per_strip]),
305                )]
306            }
307            DataLayout::Tiles { width, height } => {
308                vec![
309                    Tag::new(TAG_TILE_WIDTH, TagValue::Long(vec![width])),
310                    Tag::new(TAG_TILE_LENGTH, TagValue::Long(vec![height])),
311                ]
312            }
313        }
314    }
315
316    /// Build the serialized TIFF tags for this image definition.
317    pub fn build_tags(&self, is_bigtiff: bool) -> Vec<Tag> {
318        let mut extra_tags = self.extra_tags.clone();
319        if let Some(lerc_tag) = self.lerc_parameters_tag() {
320            extra_tags.push(lerc_tag);
321        }
322        let extra_samples = self
323            .effective_extra_samples()
324            .expect("ImageBuilder::build_tags requires a validated color model");
325        if !extra_samples.is_empty() {
326            extra_tags.push(Tag::new(
327                TAG_EXTRA_SAMPLES,
328                TagValue::Short(
329                    extra_samples
330                        .iter()
331                        .copied()
332                        .map(ExtraSample::to_code)
333                        .collect(),
334                ),
335            ));
336        }
337        if let Some(color_map) = &self.color_map {
338            extra_tags.push(Tag::new(
339                TAG_COLOR_MAP,
340                TagValue::Short(color_map.encode_tag_values()),
341            ));
342        }
343        if let Some(ink_set) = self.ink_set {
344            extra_tags.push(Tag::new(
345                TAG_INK_SET,
346                TagValue::Short(vec![ink_set.to_code()]),
347            ));
348        }
349        if let Some([h, v]) = self.ycbcr_subsampling {
350            extra_tags.push(Tag::new(TAG_YCBCR_SUBSAMPLING, TagValue::Short(vec![h, v])));
351        }
352        if let Some(positioning) = self.ycbcr_positioning {
353            extra_tags.push(Tag::new(
354                TAG_YCBCR_POSITIONING,
355                TagValue::Short(vec![positioning.to_code()]),
356            ));
357        }
358
359        let (offsets_tag_code, byte_counts_tag_code) = self.offset_tag_codes();
360        let layout_tags = self.layout_tags();
361
362        encoder::build_image_tags(&encoder::ImageTagParams {
363            width: self.width,
364            height: self.height,
365            samples_per_pixel: self.samples_per_pixel,
366            bits_per_sample: self.bits_per_sample,
367            sample_format: self.sample_format.to_code(),
368            compression: self.compression.to_code(),
369            photometric: self.photometric.to_code(),
370            predictor: self.predictor.to_code(),
371            planar_configuration: self.planar_configuration.to_code(),
372            subfile_type: self.subfile_type,
373            extra_tags: &extra_tags,
374            offsets_tag_code,
375            byte_counts_tag_code,
376            num_blocks: self.block_count(),
377            layout_tags: &layout_tags,
378            is_bigtiff,
379        })
380    }
381
382    /// Row width in pixels for compression pipeline (tile_width or image_width).
383    pub fn block_row_width(&self) -> usize {
384        match self.layout {
385            DataLayout::Strips { .. } => self.width as usize,
386            DataLayout::Tiles { width, .. } => width as usize,
387        }
388    }
389
390    /// Samples per pixel represented in a single block.
391    pub fn block_samples_per_pixel(&self) -> u16 {
392        if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
393            1
394        } else {
395            self.samples_per_pixel
396        }
397    }
398
399    fn block_plane_index(&self, index: usize) -> usize {
400        if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
401            index % self.blocks_per_plane()
402        } else {
403            index
404        }
405    }
406
407    fn blocks_per_plane(&self) -> usize {
408        match self.layout {
409            DataLayout::Strips { rows_per_strip } => {
410                let rps = rows_per_strip.max(1) as usize;
411                (self.height as usize).div_ceil(rps)
412            }
413            DataLayout::Tiles { width, height } => {
414                let tw = width.max(1) as usize;
415                let th = height.max(1) as usize;
416                let tiles_across = (self.width as usize).div_ceil(tw);
417                let tiles_down = (self.height as usize).div_ceil(th);
418                tiles_across * tiles_down
419            }
420        }
421    }
422
423    /// Height of the block at `index` in pixels.
424    ///
425    /// Tiles are always full-sized (padded at edges). Strips may be shorter
426    /// for the final strip.
427    pub fn block_height(&self, index: usize) -> u32 {
428        match self.layout {
429            DataLayout::Tiles { height, .. } => height,
430            DataLayout::Strips { rows_per_strip } => {
431                let plane_index = self.block_plane_index(index);
432                let rps = rows_per_strip.max(1) as usize;
433                let start_row = plane_index * rps;
434                let remaining = (self.height as usize).saturating_sub(start_row);
435                remaining.min(rps) as u32
436            }
437        }
438    }
439
440    /// Build the `TAG_LERC_PARAMETERS` tag if LERC compression is configured.
441    pub fn lerc_parameters_tag(&self) -> Option<Tag> {
442        if !matches!(self.compression, Compression::Lerc) {
443            return None;
444        }
445        let opts = self.lerc_options.unwrap_or_default();
446        Some(Tag::new(
447            TAG_LERC_PARAMETERS,
448            TagValue::Long(vec![2, opts.additional_compression.to_code()]),
449        ))
450    }
451
452    /// Validate the configuration.
453    pub fn validate(&self) -> crate::error::Result<()> {
454        if self.width == 0 || self.height == 0 {
455            return Err(crate::error::Error::InvalidConfig(
456                "image dimensions must be positive".into(),
457            ));
458        }
459        if self.samples_per_pixel == 0 {
460            return Err(crate::error::Error::InvalidConfig(
461                "samples_per_pixel must be greater than zero".into(),
462            ));
463        }
464        if !matches!(self.bits_per_sample, 8 | 16 | 32 | 64) {
465            return Err(crate::error::Error::InvalidConfig(format!(
466                "bits_per_sample must be 8, 16, 32, or 64, got {}",
467                self.bits_per_sample
468            )));
469        }
470        if let DataLayout::Tiles { width, height } = self.layout {
471            if width % 16 != 0 || height % 16 != 0 {
472                return Err(crate::error::Error::InvalidConfig(format!(
473                    "tile dimensions must be multiples of 16, got {}x{}",
474                    width, height
475                )));
476            }
477        }
478        if matches!(self.compression, Compression::Lerc)
479            && !matches!(self.predictor, Predictor::None)
480        {
481            return Err(crate::error::Error::InvalidConfig(
482                "LERC compression does not support TIFF predictors".into(),
483            ));
484        }
485        if matches!(self.compression, Compression::OldJpeg) {
486            return Err(crate::error::Error::InvalidConfig(
487                "Old-style JPEG compression is not supported for writing; use Compression::Jpeg"
488                    .into(),
489            ));
490        }
491        self.validate_color_model()?;
492        if matches!(self.compression, Compression::Jpeg) {
493            self.validate_jpeg_config()?;
494        }
495        Ok(())
496    }
497
498    fn validate_color_model(&self) -> crate::error::Result<()> {
499        if !matches!(self.photometric, PhotometricInterpretation::Palette)
500            && self.color_map.is_some()
501        {
502            return Err(crate::error::Error::InvalidConfig(
503                "ColorMap is only valid with palette photometric interpretation".into(),
504            ));
505        }
506
507        if !matches!(self.photometric, PhotometricInterpretation::Separated)
508            && self.ink_set.is_some()
509        {
510            return Err(crate::error::Error::InvalidConfig(
511                "InkSet is only valid with separated photometric interpretation".into(),
512            ));
513        }
514
515        let base_samples: u16 = match self.photometric {
516            PhotometricInterpretation::MinIsWhite | PhotometricInterpretation::MinIsBlack => 1,
517            PhotometricInterpretation::Rgb => 3,
518            PhotometricInterpretation::Palette => {
519                let color_map =
520                    self.color_map
521                        .as_ref()
522                        .ok_or(crate::error::Error::InvalidConfig(
523                            "palette photometric interpretation requires a ColorMap".into(),
524                        ))?;
525                let expected_entries =
526                    1usize
527                        .checked_shl(self.bits_per_sample as u32)
528                        .ok_or_else(|| {
529                            crate::error::Error::InvalidConfig(format!(
530                                "palette BitsPerSample {} exceeds usize shift width",
531                                self.bits_per_sample
532                            ))
533                        })?;
534                if color_map.len() != expected_entries {
535                    return Err(crate::error::Error::InvalidConfig(format!(
536                        "palette ColorMap has {} entries but BitsPerSample={} requires {}",
537                        color_map.len(),
538                        self.bits_per_sample,
539                        expected_entries
540                    )));
541                }
542                1
543            }
544            PhotometricInterpretation::Mask => 1,
545            PhotometricInterpretation::Separated => match self.ink_set.unwrap_or(InkSet::Cmyk) {
546                InkSet::Cmyk => 4,
547                InkSet::NotCmyk | InkSet::Unknown(_) => {
548                    return Err(crate::error::Error::InvalidConfig(
549                        "separated photometric interpretation currently requires InkSet::Cmyk"
550                            .into(),
551                    ))
552                }
553            },
554            PhotometricInterpretation::YCbCr => 3,
555            PhotometricInterpretation::CieLab => 3,
556        };
557
558        let _ = self.effective_extra_samples_for_base(base_samples)?;
559
560        if matches!(self.photometric, PhotometricInterpretation::YCbCr) {
561            if !matches!(self.sample_format, SampleFormat::Uint) || self.bits_per_sample != 8 {
562                return Err(crate::error::Error::InvalidConfig(
563                    "YCbCr photometric interpretation requires 8-bit unsigned samples".into(),
564                ));
565            }
566            if let Some(subsampling) = self.ycbcr_subsampling {
567                if subsampling != [1, 1] {
568                    return Err(crate::error::Error::InvalidConfig(format!(
569                        "YCbCr subsampling {:?} is not supported by the current writer",
570                        subsampling
571                    )));
572                }
573            }
574        } else if self.ycbcr_subsampling.is_some() || self.ycbcr_positioning.is_some() {
575            return Err(crate::error::Error::InvalidConfig(
576                "YCbCr-specific tags require YCbCr photometric interpretation".into(),
577            ));
578        }
579
580        Ok(())
581    }
582
583    fn effective_extra_samples(&self) -> crate::error::Result<Vec<ExtraSample>> {
584        let base_samples = match self.photometric {
585            PhotometricInterpretation::MinIsWhite | PhotometricInterpretation::MinIsBlack => 1,
586            PhotometricInterpretation::Rgb => 3,
587            PhotometricInterpretation::Palette => 1,
588            PhotometricInterpretation::Mask => 1,
589            PhotometricInterpretation::Separated => 4,
590            PhotometricInterpretation::YCbCr => 3,
591            PhotometricInterpretation::CieLab => 3,
592        };
593        self.effective_extra_samples_for_base(base_samples)
594    }
595
596    fn effective_extra_samples_for_base(
597        &self,
598        base_samples: u16,
599    ) -> crate::error::Result<Vec<ExtraSample>> {
600        let implied_extra_samples = self
601            .samples_per_pixel
602            .checked_sub(base_samples)
603            .ok_or_else(|| {
604                crate::error::Error::InvalidConfig(format!(
605                    "{} photometric interpretation requires at least {} samples, got {}",
606                    photometric_name(self.photometric),
607                    base_samples,
608                    self.samples_per_pixel
609                ))
610            })?;
611        if self.extra_samples.len() > implied_extra_samples as usize {
612            return Err(crate::error::Error::InvalidConfig(format!(
613                "{} photometric interpretation has {} total channels but {} ExtraSamples",
614                photometric_name(self.photometric),
615                self.samples_per_pixel,
616                self.extra_samples.len()
617            )));
618        }
619
620        let mut extra_samples = self.extra_samples.clone();
621        extra_samples.resize(implied_extra_samples as usize, ExtraSample::Unspecified);
622        Ok(extra_samples)
623    }
624
625    fn validate_jpeg_config(&self) -> crate::error::Result<()> {
626        let options = self.jpeg_options.unwrap_or_default();
627        if !(1..=100).contains(&options.quality) {
628            return Err(crate::error::Error::InvalidConfig(format!(
629                "JPEG quality must be in the range 1..=100, got {}",
630                options.quality
631            )));
632        }
633        if self.bits_per_sample != 8 {
634            return Err(crate::error::Error::InvalidConfig(format!(
635                "JPEG compression requires 8-bit samples, got {} bits",
636                self.bits_per_sample
637            )));
638        }
639        if !matches!(self.sample_format, SampleFormat::Uint) {
640            return Err(crate::error::Error::InvalidConfig(format!(
641                "JPEG compression requires unsigned integer samples, got {:?}",
642                self.sample_format
643            )));
644        }
645        if !matches!(self.predictor, Predictor::None) {
646            return Err(crate::error::Error::InvalidConfig(
647                "JPEG compression does not support TIFF predictors".into(),
648            ));
649        }
650
651        let block_width = self.block_row_width();
652        if block_width > u16::MAX as usize {
653            return Err(crate::error::Error::InvalidConfig(format!(
654                "JPEG block width must be <= {}, got {}",
655                u16::MAX,
656                block_width
657            )));
658        }
659        let max_block_height = match self.layout {
660            DataLayout::Strips { rows_per_strip } => rows_per_strip.max(1),
661            DataLayout::Tiles { height, .. } => height,
662        };
663        if max_block_height > u16::MAX as u32 {
664            return Err(crate::error::Error::InvalidConfig(format!(
665                "JPEG block height must be <= {}, got {}",
666                u16::MAX,
667                max_block_height
668            )));
669        }
670
671        let block_samples_per_pixel = self.block_samples_per_pixel();
672        if block_samples_per_pixel != 1 {
673            return Err(crate::error::Error::InvalidConfig(format!(
674                "JPEG write currently supports one sample per encoded block, got {}; use planar configuration for multi-band JPEG",
675                block_samples_per_pixel
676            )));
677        }
678
679        if matches!(
680            self.photometric,
681            PhotometricInterpretation::Palette | PhotometricInterpretation::Mask
682        ) {
683            return Err(crate::error::Error::InvalidConfig(format!(
684                "{:?} photometric interpretation is not supported with JPEG compression",
685                self.photometric
686            )));
687        }
688
689        Ok(())
690    }
691}
692
693fn photometric_name(photometric: PhotometricInterpretation) -> &'static str {
694    match photometric {
695        PhotometricInterpretation::MinIsWhite => "MinIsWhite",
696        PhotometricInterpretation::MinIsBlack => "MinIsBlack",
697        PhotometricInterpretation::Rgb => "RGB",
698        PhotometricInterpretation::Palette => "Palette",
699        PhotometricInterpretation::Mask => "TransparencyMask",
700        PhotometricInterpretation::Separated => "Separated",
701        PhotometricInterpretation::YCbCr => "YCbCr",
702        PhotometricInterpretation::CieLab => "CIELab",
703    }
704}