1use std::collections::HashSet;
4
5use tiff_core::*;
6
7use crate::encoder;
8use crate::sample::TiffWriteSample;
9
10#[derive(Debug, Clone, Copy, PartialEq)]
15pub struct LercOptions {
16 pub max_z_error: f64,
18 pub additional_compression: LercAdditionalCompression,
20}
21
22impl Default for LercOptions {
23 fn default() -> Self {
24 Self {
25 max_z_error: 0.0,
26 additional_compression: LercAdditionalCompression::None,
27 }
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct JpegOptions {
34 pub quality: u8,
36}
37
38impl Default for JpegOptions {
39 fn default() -> Self {
40 Self { quality: 75 }
41 }
42}
43
44#[derive(Debug, Clone, Copy)]
46pub enum DataLayout {
47 Strips { rows_per_strip: u32 },
49 Tiles { width: u32, height: u32 },
51}
52
53#[derive(Debug, Clone)]
55pub struct ImageBuilder {
56 pub(crate) width: u32,
57 pub(crate) height: u32,
58 pub(crate) samples_per_pixel: u16,
59 pub(crate) bits_per_sample: u16,
60 pub(crate) sample_format: SampleFormat,
61 pub(crate) compression: Compression,
62 pub(crate) predictor: Predictor,
63 pub(crate) photometric: PhotometricInterpretation,
64 pub(crate) extra_samples: Vec<ExtraSample>,
65 pub(crate) color_map: Option<ColorMap>,
66 pub(crate) ink_set: Option<InkSet>,
67 pub(crate) ycbcr_subsampling: Option<[u16; 2]>,
68 pub(crate) ycbcr_positioning: Option<YCbCrPositioning>,
69 pub(crate) planar_configuration: PlanarConfiguration,
70 pub(crate) layout: DataLayout,
71 pub(crate) extra_tags: Vec<Tag>,
72 pub(crate) subfile_type: u32,
73 pub(crate) lerc_options: Option<LercOptions>,
74 pub(crate) jpeg_options: Option<JpegOptions>,
75 pub(crate) deflate_level: Option<u32>,
76}
77
78impl ImageBuilder {
79 pub fn new(width: u32, height: u32) -> Self {
81 Self {
82 width,
83 height,
84 samples_per_pixel: 1,
85 bits_per_sample: 8,
86 sample_format: SampleFormat::Uint,
87 compression: Compression::None,
88 predictor: Predictor::None,
89 photometric: PhotometricInterpretation::MinIsBlack,
90 extra_samples: Vec::new(),
91 color_map: None,
92 ink_set: None,
93 ycbcr_subsampling: None,
94 ycbcr_positioning: None,
95 planar_configuration: PlanarConfiguration::Chunky,
96 layout: DataLayout::Strips {
97 rows_per_strip: height.min(256),
98 },
99 extra_tags: Vec::new(),
100 subfile_type: 0,
101 lerc_options: None,
102 jpeg_options: None,
103 deflate_level: None,
104 }
105 }
106
107 pub fn samples_per_pixel(mut self, spp: u16) -> Self {
108 self.samples_per_pixel = spp;
109 self
110 }
111
112 pub fn bits_per_sample(mut self, bps: u16) -> Self {
113 self.bits_per_sample = bps;
114 self
115 }
116
117 pub fn sample_format(mut self, fmt: SampleFormat) -> Self {
118 self.sample_format = fmt;
119 self
120 }
121
122 pub fn sample_type<T: TiffWriteSample>(mut self) -> Self {
124 self.bits_per_sample = T::BITS_PER_SAMPLE;
125 self.sample_format =
126 SampleFormat::from_code(T::SAMPLE_FORMAT).unwrap_or(SampleFormat::Uint);
127 self
128 }
129
130 pub fn compression(mut self, c: Compression) -> Self {
131 self.compression = c;
132 if !matches!(c, Compression::Lerc) {
133 self.lerc_options = None;
134 }
135 if !matches!(c, Compression::Jpeg) {
136 self.jpeg_options = None;
137 }
138 if matches!(c, Compression::Lerc | Compression::Jpeg) {
139 self.predictor = Predictor::None;
140 }
141 self
142 }
143
144 pub fn predictor(mut self, p: Predictor) -> Self {
145 self.predictor = p;
146 self
147 }
148
149 pub fn deflate_level(mut self, level: u32) -> Self {
155 self.deflate_level = Some(level);
156 self
157 }
158
159 pub fn photometric(mut self, p: PhotometricInterpretation) -> Self {
160 self.photometric = p;
161 self
162 }
163
164 pub fn extra_samples(mut self, extra_samples: Vec<ExtraSample>) -> Self {
166 self.extra_samples = extra_samples;
167 self
168 }
169
170 pub fn color_map(mut self, color_map: ColorMap) -> Self {
172 self.color_map = Some(color_map);
173 self
174 }
175
176 pub fn ink_set(mut self, ink_set: InkSet) -> Self {
178 self.ink_set = Some(ink_set);
179 self
180 }
181
182 pub fn ycbcr_subsampling(mut self, subsampling: [u16; 2]) -> Self {
184 self.ycbcr_subsampling = Some(subsampling);
185 self
186 }
187
188 pub fn ycbcr_positioning(mut self, positioning: YCbCrPositioning) -> Self {
190 self.ycbcr_positioning = Some(positioning);
191 self
192 }
193
194 pub fn planar_configuration(mut self, p: PlanarConfiguration) -> Self {
196 self.planar_configuration = p;
197 self
198 }
199
200 pub fn strips(mut self, rows_per_strip: u32) -> Self {
202 self.layout = DataLayout::Strips { rows_per_strip };
203 self
204 }
205
206 pub fn data_layout(&self) -> DataLayout {
208 self.layout
209 }
210
211 pub fn tiles(mut self, tile_width: u32, tile_height: u32) -> Self {
213 self.layout = DataLayout::Tiles {
214 width: tile_width,
215 height: tile_height,
216 };
217 self
218 }
219
220 pub fn tag(mut self, tag: Tag) -> Self {
222 self.extra_tags.push(tag);
223 self
224 }
225
226 pub fn overview(mut self) -> Self {
228 self.subfile_type = 1;
229 self
230 }
231
232 pub fn lerc_options(mut self, options: LercOptions) -> Self {
237 self.compression = Compression::Lerc;
238 self.predictor = Predictor::None;
239 self.lerc_options = Some(options);
240 self.jpeg_options = None;
241 self
242 }
243
244 pub fn jpeg_options(mut self, options: JpegOptions) -> Self {
252 self.compression = Compression::Jpeg;
253 self.predictor = Predictor::None;
254 self.jpeg_options = Some(options);
255 self.lerc_options = None;
256 self
257 }
258
259 pub fn checked_block_count(&self) -> crate::error::Result<usize> {
261 let blocks_per_plane = match self.checked_layout()? {
262 DataLayout::Strips { rows_per_strip } => {
263 let rps = rows_per_strip as usize;
264 (self.height as usize).div_ceil(rps)
265 }
266 DataLayout::Tiles { width, height } => {
267 let tw = width as usize;
268 let th = height as usize;
269 let tiles_across = (self.width as usize).div_ceil(tw);
270 let tiles_down = (self.height as usize).div_ceil(th);
271 tiles_across
272 .checked_mul(tiles_down)
273 .ok_or_else(|| layout_overflow("tile count"))?
274 }
275 };
276 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
277 blocks_per_plane
278 .checked_mul(self.samples_per_pixel as usize)
279 .ok_or_else(|| layout_overflow("planar block count"))
280 } else {
281 Ok(blocks_per_plane)
282 }
283 }
284
285 pub fn checked_block_sample_count(&self, index: usize) -> crate::error::Result<usize> {
287 let block_count = self.checked_block_count()?;
288 if index >= block_count {
289 return Err(crate::error::Error::BlockIndexOutOfRange {
290 index,
291 total: block_count,
292 });
293 }
294 let samples_per_pixel = self.block_samples_per_pixel() as usize;
295 let plane_block_index = self.checked_block_plane_index(index)?;
296 match self.checked_layout()? {
297 DataLayout::Strips { rows_per_strip } => {
298 let rps = rows_per_strip as usize;
299 let start_row = plane_block_index
300 .checked_mul(rps)
301 .ok_or_else(|| layout_overflow("strip start row"))?;
302 let end_row = plane_block_index
303 .checked_add(1)
304 .and_then(|value| value.checked_mul(rps))
305 .ok_or_else(|| layout_overflow("strip end row"))?
306 .min(self.height as usize);
307 let rows = end_row.saturating_sub(start_row);
308 rows.checked_mul(self.width as usize)
309 .and_then(|value| value.checked_mul(samples_per_pixel))
310 .ok_or_else(|| layout_overflow("strip sample count"))
311 }
312 DataLayout::Tiles { width, height } => {
313 (width as usize)
315 .checked_mul(height as usize)
316 .and_then(|value| value.checked_mul(samples_per_pixel))
317 .ok_or_else(|| layout_overflow("tile sample count"))
318 }
319 }
320 }
321
322 pub fn checked_estimated_uncompressed_bytes(&self) -> crate::error::Result<u64> {
324 let bps = u64::from(self.bits_per_sample.div_ceil(8));
325 (self.width as u64)
326 .checked_mul(self.height as u64)
327 .and_then(|value| value.checked_mul(self.samples_per_pixel as u64))
328 .and_then(|value| value.checked_mul(bps))
329 .ok_or_else(|| layout_overflow("estimated uncompressed byte count"))
330 }
331
332 pub fn offset_tag_codes(&self) -> (u16, u16) {
334 match self.layout {
335 DataLayout::Strips { .. } => (TAG_STRIP_OFFSETS, TAG_STRIP_BYTE_COUNTS),
336 DataLayout::Tiles { .. } => (TAG_TILE_OFFSETS, TAG_TILE_BYTE_COUNTS),
337 }
338 }
339
340 pub fn checked_layout_tags(&self) -> crate::error::Result<Vec<Tag>> {
342 match self.checked_layout()? {
343 DataLayout::Strips { rows_per_strip } => Ok(vec![Tag::new(
344 TAG_ROWS_PER_STRIP,
345 TagValue::Long(vec![rows_per_strip]),
346 )]),
347 DataLayout::Tiles { width, height } => Ok(vec![
348 Tag::new(TAG_TILE_WIDTH, TagValue::Long(vec![width])),
349 Tag::new(TAG_TILE_LENGTH, TagValue::Long(vec![height])),
350 ]),
351 }
352 }
353
354 pub fn checked_build_tags(&self, is_bigtiff: bool) -> crate::error::Result<Vec<Tag>> {
356 let mut extra_tags = self.extra_tags.clone();
357 if let Some(lerc_tag) = self.lerc_parameters_tag() {
358 extra_tags.push(lerc_tag);
359 }
360 self.validate()?;
361 let extra_samples = self.effective_extra_samples()?;
362 if !extra_samples.is_empty() {
363 extra_tags.push(Tag::new(
364 TAG_EXTRA_SAMPLES,
365 TagValue::Short(
366 extra_samples
367 .iter()
368 .copied()
369 .map(ExtraSample::to_code)
370 .collect(),
371 ),
372 ));
373 }
374 if let Some(color_map) = &self.color_map {
375 extra_tags.push(Tag::new(
376 TAG_COLOR_MAP,
377 TagValue::Short(color_map.encode_tag_values()),
378 ));
379 }
380 if let Some(ink_set) = self.ink_set {
381 extra_tags.push(Tag::new(
382 TAG_INK_SET,
383 TagValue::Short(vec![ink_set.to_code()]),
384 ));
385 }
386 if let Some([h, v]) = self.effective_ycbcr_subsampling() {
387 extra_tags.push(Tag::new(TAG_YCBCR_SUBSAMPLING, TagValue::Short(vec![h, v])));
388 }
389 if let Some(positioning) = self.ycbcr_positioning {
390 extra_tags.push(Tag::new(
391 TAG_YCBCR_POSITIONING,
392 TagValue::Short(vec![positioning.to_code()]),
393 ));
394 }
395
396 let (offsets_tag_code, byte_counts_tag_code) = self.offset_tag_codes();
397 let layout_tags = self.checked_layout_tags()?;
398
399 Ok(encoder::build_image_tags(&encoder::ImageTagParams {
400 width: self.width,
401 height: self.height,
402 samples_per_pixel: self.samples_per_pixel,
403 bits_per_sample: self.bits_per_sample,
404 sample_format: self.sample_format.to_code(),
405 compression: self.compression.to_code(),
406 photometric: self.photometric.to_code(),
407 predictor: self.predictor.to_code(),
408 planar_configuration: self.planar_configuration.to_code(),
409 subfile_type: self.subfile_type,
410 extra_tags: &extra_tags,
411 offsets_tag_code,
412 byte_counts_tag_code,
413 num_blocks: self.checked_block_count()?,
414 layout_tags: &layout_tags,
415 is_bigtiff,
416 }))
417 }
418
419 pub fn block_row_width(&self) -> usize {
421 match self.layout {
422 DataLayout::Strips { .. } => self.width as usize,
423 DataLayout::Tiles { width, .. } => width as usize,
424 }
425 }
426
427 pub fn block_samples_per_pixel(&self) -> u16 {
429 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
430 1
431 } else {
432 self.samples_per_pixel
433 }
434 }
435
436 fn checked_block_plane_index(&self, index: usize) -> crate::error::Result<usize> {
437 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
438 let blocks_per_plane = self.checked_blocks_per_plane()?;
439 if blocks_per_plane == 0 {
440 return Err(crate::error::Error::InvalidConfig(
441 "block count must be greater than zero".into(),
442 ));
443 }
444 Ok(index % blocks_per_plane)
445 } else {
446 Ok(index)
447 }
448 }
449
450 fn checked_blocks_per_plane(&self) -> crate::error::Result<usize> {
451 match self.checked_layout()? {
452 DataLayout::Strips { rows_per_strip } => {
453 let rps = rows_per_strip as usize;
454 Ok((self.height as usize).div_ceil(rps))
455 }
456 DataLayout::Tiles { width, height } => {
457 let tw = width as usize;
458 let th = height as usize;
459 let tiles_across = (self.width as usize).div_ceil(tw);
460 let tiles_down = (self.height as usize).div_ceil(th);
461 tiles_across
462 .checked_mul(tiles_down)
463 .ok_or_else(|| layout_overflow("tile count"))
464 }
465 }
466 }
467
468 pub fn checked_block_height(&self, index: usize) -> crate::error::Result<u32> {
473 let block_count = self.checked_block_count()?;
474 if index >= block_count {
475 return Err(crate::error::Error::BlockIndexOutOfRange {
476 index,
477 total: block_count,
478 });
479 }
480 match self.checked_layout()? {
481 DataLayout::Tiles { height, .. } => Ok(height),
482 DataLayout::Strips { rows_per_strip } => {
483 let plane_index = self.checked_block_plane_index(index)?;
484 let rps = rows_per_strip as usize;
485 let start_row = plane_index
486 .checked_mul(rps)
487 .ok_or_else(|| layout_overflow("strip start row"))?;
488 let remaining = (self.height as usize).saturating_sub(start_row);
489 Ok(remaining.min(rps) as u32)
490 }
491 }
492 }
493
494 pub(crate) fn effective_ycbcr_subsampling(&self) -> Option<[u16; 2]> {
499 if self.ycbcr_subsampling.is_some() {
500 return self.ycbcr_subsampling;
501 }
502 (matches!(self.photometric, PhotometricInterpretation::YCbCr)
503 && matches!(self.compression, Compression::Jpeg))
504 .then_some([2, 2])
505 }
506
507 pub fn jpeg_chroma_sampling(&self) -> Option<[u16; 2]> {
510 (matches!(self.photometric, PhotometricInterpretation::YCbCr)
511 && matches!(self.compression, Compression::Jpeg)
512 && self.block_samples_per_pixel() == 3)
513 .then(|| self.effective_ycbcr_subsampling().unwrap_or([1, 1]))
514 }
515
516 pub fn lerc_parameters_tag(&self) -> Option<Tag> {
518 if !matches!(self.compression, Compression::Lerc) {
519 return None;
520 }
521 let opts = self.lerc_options.unwrap_or_default();
522 Some(Tag::new(
523 TAG_LERC_PARAMETERS,
524 TagValue::Long(vec![
525 LERC_VERSION_2_4,
526 opts.additional_compression.to_code(),
527 ]),
528 ))
529 }
530
531 pub fn validate(&self) -> crate::error::Result<()> {
533 if self.width == 0 || self.height == 0 {
534 return Err(crate::error::Error::InvalidConfig(
535 "image dimensions must be positive".into(),
536 ));
537 }
538 if self.samples_per_pixel == 0 {
539 return Err(crate::error::Error::InvalidConfig(
540 "samples_per_pixel must be greater than zero".into(),
541 ));
542 }
543 self.validate_extra_tags()?;
544 if !matches!(self.bits_per_sample, 8 | 16 | 32 | 64) {
545 return Err(crate::error::Error::InvalidConfig(format!(
546 "bits_per_sample must be 8, 16, 32, or 64, got {}",
547 self.bits_per_sample
548 )));
549 }
550 match self.layout {
551 DataLayout::Strips { rows_per_strip: 0 } => {
552 return Err(crate::error::Error::InvalidConfig(
553 "rows_per_strip must be greater than zero".into(),
554 ));
555 }
556 DataLayout::Tiles { width, height } => {
557 if width == 0 || height == 0 {
558 return Err(crate::error::Error::InvalidConfig(format!(
559 "tile_width and tile_height must be greater than zero, got {}x{}",
560 width, height
561 )));
562 }
563 if width % 16 != 0 || height % 16 != 0 {
564 return Err(crate::error::Error::InvalidConfig(format!(
565 "tile dimensions must be multiples of 16, got {}x{}",
566 width, height
567 )));
568 }
569 }
570 _ => {}
571 }
572 self.checked_block_count()?;
573 self.checked_block_sample_count(0)?;
574 self.checked_estimated_uncompressed_bytes()?;
575 match self.compression {
576 Compression::None
577 | Compression::Lzw
578 | Compression::Deflate
579 | Compression::DeflateOld
580 | Compression::Lerc => {}
581 Compression::Jpeg if cfg!(feature = "jpeg") => {}
582 Compression::Zstd if cfg!(feature = "zstd") => {}
583 unsupported => {
584 return Err(crate::error::Error::InvalidConfig(format!(
585 "{} compression is not supported by this writer build",
586 unsupported.name()
587 )))
588 }
589 }
590 if !matches!(self.predictor, Predictor::None)
591 && matches!(self.compression, Compression::None)
592 {
593 return Err(crate::error::Error::InvalidConfig(
594 "TIFF predictors require a supported compression scheme".into(),
595 ));
596 }
597 if matches!(self.compression, Compression::Lerc)
598 && !matches!(self.predictor, Predictor::None)
599 {
600 return Err(crate::error::Error::InvalidConfig(
601 "LERC compression does not support TIFF predictors".into(),
602 ));
603 }
604 let supported_float_bits = matches!(self.bits_per_sample, 32 | 64)
605 || (cfg!(feature = "f16") && self.bits_per_sample == 16);
606 if matches!(self.sample_format, SampleFormat::Float) && !supported_float_bits {
607 let supported = if cfg!(feature = "f16") {
608 "16, 32, or 64"
609 } else {
610 "32 or 64"
611 };
612 return Err(crate::error::Error::InvalidConfig(format!(
613 "float sample format requires {supported} bits per sample, got {}",
614 self.bits_per_sample
615 )));
616 }
617 if cfg!(feature = "f16")
618 && matches!(self.compression, Compression::Lerc)
619 && matches!(self.sample_format, SampleFormat::Float)
620 && self.bits_per_sample == 16
621 {
622 return Err(crate::error::Error::InvalidConfig(
623 "LERC compression does not support 16-bit float samples".into(),
624 ));
625 }
626 match self.predictor {
627 Predictor::Horizontal => {
628 if matches!(self.sample_format, SampleFormat::Float) {
629 return Err(crate::error::Error::InvalidConfig(
630 "horizontal predictor requires integer sample formats; \
631 use Predictor::FloatingPoint for float samples"
632 .into(),
633 ));
634 }
635 }
636 Predictor::FloatingPoint => {
637 if !matches!(self.sample_format, SampleFormat::Float) {
638 return Err(crate::error::Error::InvalidConfig(
639 "floating-point predictor requires float sample formats".into(),
640 ));
641 }
642 }
643 Predictor::None => {}
644 }
645 if let Some(level) = self.deflate_level {
646 if level > 9 {
647 return Err(crate::error::Error::InvalidConfig(format!(
648 "deflate_level must be 0-9, got {level}"
649 )));
650 }
651 if !matches!(
652 self.compression,
653 Compression::Deflate | Compression::DeflateOld
654 ) {
655 return Err(crate::error::Error::InvalidConfig(
656 "deflate_level requires Deflate compression".into(),
657 ));
658 }
659 }
660 self.validate_color_model()?;
661 if matches!(self.compression, Compression::Jpeg) {
662 self.validate_jpeg_config()?;
663 }
664 Ok(())
665 }
666
667 fn validate_extra_tags(&self) -> crate::error::Result<()> {
668 const MANAGED_TAGS: &[u16] = &[
669 TAG_NEW_SUBFILE_TYPE,
670 TAG_IMAGE_WIDTH,
671 TAG_IMAGE_LENGTH,
672 TAG_BITS_PER_SAMPLE,
673 TAG_COMPRESSION,
674 TAG_PHOTOMETRIC_INTERPRETATION,
675 TAG_STRIP_OFFSETS,
676 TAG_SAMPLES_PER_PIXEL,
677 TAG_ROWS_PER_STRIP,
678 TAG_STRIP_BYTE_COUNTS,
679 TAG_PLANAR_CONFIGURATION,
680 TAG_PREDICTOR,
681 TAG_COLOR_MAP,
682 TAG_TILE_WIDTH,
683 TAG_TILE_LENGTH,
684 TAG_TILE_OFFSETS,
685 TAG_TILE_BYTE_COUNTS,
686 TAG_INK_SET,
687 TAG_EXTRA_SAMPLES,
688 TAG_SAMPLE_FORMAT,
689 TAG_YCBCR_SUBSAMPLING,
690 TAG_YCBCR_POSITIONING,
691 TAG_LERC_PARAMETERS,
692 ];
693
694 let mut seen = HashSet::with_capacity(self.extra_tags.len());
695 for tag in &self.extra_tags {
696 if !seen.insert(tag.code) {
697 return Err(crate::error::Error::InvalidConfig(format!(
698 "extra TIFF tag {} is defined more than once",
699 tag.code
700 )));
701 }
702 if MANAGED_TAGS.contains(&tag.code) {
703 return Err(crate::error::Error::InvalidConfig(format!(
704 "TIFF tag {} is managed by ImageBuilder and cannot be supplied as an extra tag",
705 tag.code
706 )));
707 }
708 if tag.tag_type != tag.value.tag_type() || tag.count != tag.value.count() {
709 return Err(crate::error::Error::InvalidConfig(format!(
710 "extra TIFF tag {} has type/count metadata inconsistent with its value",
711 tag.code
712 )));
713 }
714 }
715 Ok(())
716 }
717
718 fn checked_layout(&self) -> crate::error::Result<DataLayout> {
719 match self.layout {
720 DataLayout::Strips { rows_per_strip: 0 } => Err(crate::error::Error::InvalidConfig(
721 "rows_per_strip must be greater than zero".into(),
722 )),
723 DataLayout::Tiles { width, height } if width == 0 || height == 0 => {
724 Err(crate::error::Error::InvalidConfig(format!(
725 "tile_width and tile_height must be greater than zero, got {}x{}",
726 width, height
727 )))
728 }
729 DataLayout::Tiles { width, height } if width % 16 != 0 || height % 16 != 0 => {
730 Err(crate::error::Error::InvalidConfig(format!(
731 "tile dimensions must be multiples of 16, got {}x{}",
732 width, height
733 )))
734 }
735 layout => Ok(layout),
736 }
737 }
738
739 fn validate_color_model(&self) -> crate::error::Result<()> {
740 if !matches!(self.photometric, PhotometricInterpretation::Palette)
741 && self.color_map.is_some()
742 {
743 return Err(crate::error::Error::InvalidConfig(
744 "ColorMap is only valid with palette photometric interpretation".into(),
745 ));
746 }
747
748 if !matches!(self.photometric, PhotometricInterpretation::Separated)
749 && self.ink_set.is_some()
750 {
751 return Err(crate::error::Error::InvalidConfig(
752 "InkSet is only valid with separated photometric interpretation".into(),
753 ));
754 }
755
756 let base_samples: u16 = match self.photometric {
757 PhotometricInterpretation::MinIsWhite | PhotometricInterpretation::MinIsBlack => 1,
758 PhotometricInterpretation::Rgb => 3,
759 PhotometricInterpretation::Palette => {
760 let color_map =
761 self.color_map
762 .as_ref()
763 .ok_or(crate::error::Error::InvalidConfig(
764 "palette photometric interpretation requires a ColorMap".into(),
765 ))?;
766 let expected_entries =
767 1usize
768 .checked_shl(self.bits_per_sample as u32)
769 .ok_or_else(|| {
770 crate::error::Error::InvalidConfig(format!(
771 "palette BitsPerSample {} exceeds usize shift width",
772 self.bits_per_sample
773 ))
774 })?;
775 if color_map.len() != expected_entries {
776 return Err(crate::error::Error::InvalidConfig(format!(
777 "palette ColorMap has {} entries but BitsPerSample={} requires {}",
778 color_map.len(),
779 self.bits_per_sample,
780 expected_entries
781 )));
782 }
783 1
784 }
785 PhotometricInterpretation::Mask => 1,
786 PhotometricInterpretation::Separated => match self.ink_set.unwrap_or(InkSet::Cmyk) {
787 InkSet::Cmyk => 4,
788 InkSet::NotCmyk | InkSet::Unknown(_) => {
789 return Err(crate::error::Error::InvalidConfig(
790 "separated photometric interpretation currently requires InkSet::Cmyk"
791 .into(),
792 ))
793 }
794 },
795 PhotometricInterpretation::YCbCr => 3,
796 PhotometricInterpretation::CieLab => 3,
797 };
798
799 let _ = self.effective_extra_samples_for_base(base_samples)?;
800
801 if matches!(self.photometric, PhotometricInterpretation::YCbCr) {
802 if !matches!(self.sample_format, SampleFormat::Uint) || self.bits_per_sample != 8 {
803 return Err(crate::error::Error::InvalidConfig(
804 "YCbCr photometric interpretation requires 8-bit unsigned samples".into(),
805 ));
806 }
807 if let Some(subsampling) = self.ycbcr_subsampling {
808 let supported = subsampling == [1, 1]
809 || (matches!(self.compression, Compression::Jpeg) && subsampling == [2, 2]);
810 if !supported {
811 return Err(crate::error::Error::InvalidConfig(format!(
812 "YCbCr subsampling {:?} is not supported by the current writer; \
813 supported values are [1, 1], and [2, 2] with JPEG compression",
814 subsampling
815 )));
816 }
817 }
818 } else if self.ycbcr_subsampling.is_some() || self.ycbcr_positioning.is_some() {
819 return Err(crate::error::Error::InvalidConfig(
820 "YCbCr-specific tags require YCbCr photometric interpretation".into(),
821 ));
822 }
823
824 Ok(())
825 }
826
827 fn effective_extra_samples(&self) -> crate::error::Result<Vec<ExtraSample>> {
828 let base_samples = match self.photometric {
829 PhotometricInterpretation::MinIsWhite | PhotometricInterpretation::MinIsBlack => 1,
830 PhotometricInterpretation::Rgb => 3,
831 PhotometricInterpretation::Palette => 1,
832 PhotometricInterpretation::Mask => 1,
833 PhotometricInterpretation::Separated => 4,
834 PhotometricInterpretation::YCbCr => 3,
835 PhotometricInterpretation::CieLab => 3,
836 };
837 self.effective_extra_samples_for_base(base_samples)
838 }
839
840 fn effective_extra_samples_for_base(
841 &self,
842 base_samples: u16,
843 ) -> crate::error::Result<Vec<ExtraSample>> {
844 let implied_extra_samples = self
845 .samples_per_pixel
846 .checked_sub(base_samples)
847 .ok_or_else(|| {
848 crate::error::Error::InvalidConfig(format!(
849 "{} photometric interpretation requires at least {} samples, got {}",
850 photometric_name(self.photometric),
851 base_samples,
852 self.samples_per_pixel
853 ))
854 })?;
855 if self.extra_samples.len() > implied_extra_samples as usize {
856 return Err(crate::error::Error::InvalidConfig(format!(
857 "{} photometric interpretation has {} total channels but {} ExtraSamples",
858 photometric_name(self.photometric),
859 self.samples_per_pixel,
860 self.extra_samples.len()
861 )));
862 }
863
864 let mut extra_samples = self.extra_samples.clone();
865 extra_samples.resize(implied_extra_samples as usize, ExtraSample::Unspecified);
866 Ok(extra_samples)
867 }
868
869 fn validate_jpeg_config(&self) -> crate::error::Result<()> {
870 let options = self.jpeg_options.unwrap_or_default();
871 if !(1..=100).contains(&options.quality) {
872 return Err(crate::error::Error::InvalidConfig(format!(
873 "JPEG quality must be in the range 1..=100, got {}",
874 options.quality
875 )));
876 }
877 if self.bits_per_sample != 8 {
878 return Err(crate::error::Error::InvalidConfig(format!(
879 "JPEG compression requires 8-bit samples, got {} bits",
880 self.bits_per_sample
881 )));
882 }
883 if !matches!(self.sample_format, SampleFormat::Uint) {
884 return Err(crate::error::Error::InvalidConfig(format!(
885 "JPEG compression requires unsigned integer samples, got {:?}",
886 self.sample_format
887 )));
888 }
889 if !matches!(self.predictor, Predictor::None) {
890 return Err(crate::error::Error::InvalidConfig(
891 "JPEG compression does not support TIFF predictors".into(),
892 ));
893 }
894
895 let block_width = self.block_row_width();
896 if block_width > u16::MAX as usize {
897 return Err(crate::error::Error::InvalidConfig(format!(
898 "JPEG block width must be <= {}, got {}",
899 u16::MAX,
900 block_width
901 )));
902 }
903 let max_block_height = match self.layout {
904 DataLayout::Strips { rows_per_strip } => rows_per_strip.max(1),
905 DataLayout::Tiles { height, .. } => height,
906 };
907 if max_block_height > u16::MAX as u32 {
908 return Err(crate::error::Error::InvalidConfig(format!(
909 "JPEG block height must be <= {}, got {}",
910 u16::MAX,
911 max_block_height
912 )));
913 }
914
915 let block_samples_per_pixel = self.block_samples_per_pixel();
916 match block_samples_per_pixel {
917 1 => {}
918 3 => {
919 if !matches!(self.photometric, PhotometricInterpretation::YCbCr) {
920 return Err(crate::error::Error::InvalidConfig(
921 "interleaved 3-sample JPEG blocks require YCbCr photometric \
922 interpretation; use planar configuration for other color models"
923 .into(),
924 ));
925 }
926 }
927 other => {
928 return Err(crate::error::Error::InvalidConfig(format!(
929 "JPEG write supports 1 or 3 samples per encoded block, got {other}; \
930 use planar configuration for other band counts"
931 )));
932 }
933 }
934
935 if matches!(
936 self.photometric,
937 PhotometricInterpretation::Palette | PhotometricInterpretation::Mask
938 ) {
939 return Err(crate::error::Error::InvalidConfig(format!(
940 "{:?} photometric interpretation is not supported with JPEG compression",
941 self.photometric
942 )));
943 }
944
945 Ok(())
946 }
947}
948
949fn photometric_name(photometric: PhotometricInterpretation) -> &'static str {
950 match photometric {
951 PhotometricInterpretation::MinIsWhite => "MinIsWhite",
952 PhotometricInterpretation::MinIsBlack => "MinIsBlack",
953 PhotometricInterpretation::Rgb => "RGB",
954 PhotometricInterpretation::Palette => "Palette",
955 PhotometricInterpretation::Mask => "TransparencyMask",
956 PhotometricInterpretation::Separated => "Separated",
957 PhotometricInterpretation::YCbCr => "YCbCr",
958 PhotometricInterpretation::CieLab => "CIELab",
959 }
960}
961
962fn layout_overflow(context: &'static str) -> crate::error::Error {
963 crate::error::Error::InvalidConfig(format!("{context} overflows layout size limits"))
964}
965
966#[cfg(test)]
967mod tests {
968 use super::ImageBuilder;
969 use tiff_core::{
970 PhotometricInterpretation, PlanarConfiguration, Tag, TagType, TagValue, TAG_IMAGE_WIDTH,
971 };
972
973 #[test]
974 fn validate_rejects_zero_strip_and_tile_dimensions() {
975 let err = ImageBuilder::new(16, 16).strips(0).validate().unwrap_err();
976 assert!(
977 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
978 );
979
980 let err = ImageBuilder::new(16, 16)
981 .tiles(0, 16)
982 .validate()
983 .unwrap_err();
984 assert!(
985 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
986 );
987
988 let err = ImageBuilder::new(16, 16)
989 .tiles(16, 0)
990 .validate()
991 .unwrap_err();
992 assert!(
993 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
994 );
995
996 let err = ImageBuilder::new(16, 16)
997 .tiles(0, 0)
998 .validate()
999 .unwrap_err();
1000 assert!(
1001 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width") && message.contains("tile_height"))
1002 );
1003 }
1004
1005 #[test]
1006 fn checked_helpers_reject_zero_strip_and_tile_dimensions() {
1007 let builder = ImageBuilder::new(16, 16).strips(0);
1008 let err = builder.checked_block_count().unwrap_err();
1009 assert!(
1010 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
1011 );
1012 let err = builder.checked_layout_tags().unwrap_err();
1013 assert!(
1014 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
1015 );
1016 let err = builder.checked_build_tags(false).unwrap_err();
1017 assert!(
1018 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
1019 );
1020
1021 let builder = ImageBuilder::new(16, 16).tiles(0, 16);
1022 let err = builder.checked_block_count().unwrap_err();
1023 assert!(
1024 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
1025 );
1026 let err = builder.checked_layout_tags().unwrap_err();
1027 assert!(
1028 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
1029 );
1030 let err = builder.checked_build_tags(false).unwrap_err();
1031 assert!(
1032 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
1033 );
1034
1035 let builder = ImageBuilder::new(16, 16).tiles(16, 0);
1036 let err = builder.checked_block_count().unwrap_err();
1037 assert!(
1038 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1039 );
1040 let err = builder.checked_layout_tags().unwrap_err();
1041 assert!(
1042 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1043 );
1044 let err = builder.checked_build_tags(false).unwrap_err();
1045 assert!(
1046 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1047 );
1048
1049 let builder = ImageBuilder::new(16, 16).tiles(15, 16);
1050 let err = builder.checked_layout_tags().unwrap_err();
1051 assert!(
1052 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("multiples of 16"))
1053 );
1054 }
1055
1056 #[test]
1057 fn checked_build_tags_returns_color_model_errors() {
1058 let err = ImageBuilder::new(16, 16)
1059 .photometric(PhotometricInterpretation::Rgb)
1060 .samples_per_pixel(1)
1061 .checked_build_tags(false)
1062 .unwrap_err();
1063 assert!(
1064 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("requires at least 3 samples"))
1065 );
1066 }
1067
1068 #[test]
1069 fn checked_helpers_reject_out_of_range_block_indices() {
1070 let builder = ImageBuilder::new(16, 16).sample_type::<u8>().tiles(16, 16);
1071 assert!(matches!(
1072 builder.checked_block_sample_count(1),
1073 Err(crate::error::Error::BlockIndexOutOfRange { index: 1, total: 1 })
1074 ));
1075 assert!(matches!(
1076 builder.checked_block_height(1),
1077 Err(crate::error::Error::BlockIndexOutOfRange { index: 1, total: 1 })
1078 ));
1079 }
1080
1081 #[test]
1082 fn validation_rejects_conflicting_duplicate_and_incoherent_extra_tags() {
1083 let managed =
1084 ImageBuilder::new(1, 1).tag(Tag::new(TAG_IMAGE_WIDTH, TagValue::Long(vec![2])));
1085 assert!(matches!(
1086 managed.validate(),
1087 Err(crate::error::Error::InvalidConfig(message)) if message.contains("managed")
1088 ));
1089
1090 let duplicate = ImageBuilder::new(1, 1)
1091 .tag(Tag::new(65000, TagValue::Short(vec![1])))
1092 .tag(Tag::new(65000, TagValue::Short(vec![2])));
1093 assert!(matches!(
1094 duplicate.validate(),
1095 Err(crate::error::Error::InvalidConfig(message)) if message.contains("more than once")
1096 ));
1097
1098 let mut incoherent = Tag::new(65000, TagValue::Short(vec![1]));
1099 incoherent.tag_type = TagType::Long;
1100 let incoherent = ImageBuilder::new(1, 1).tag(incoherent);
1101 assert!(matches!(
1102 incoherent.validate(),
1103 Err(crate::error::Error::InvalidConfig(message)) if message.contains("inconsistent")
1104 ));
1105 }
1106
1107 #[test]
1108 fn unsupported_predictor_requests_are_reported_instead_of_ignored() {
1109 let err = ImageBuilder::new(1, 1)
1110 .sample_type::<u8>()
1111 .compression(tiff_core::Compression::Jpeg)
1112 .predictor(tiff_core::Predictor::Horizontal)
1113 .validate()
1114 .unwrap_err();
1115 #[cfg(feature = "jpeg")]
1116 assert!(
1117 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("JPEG compression does not support"))
1118 );
1119 #[cfg(not(feature = "jpeg"))]
1120 assert!(
1121 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("not supported"))
1122 );
1123
1124 let err = ImageBuilder::new(1, 1)
1125 .sample_type::<u8>()
1126 .predictor(tiff_core::Predictor::Horizontal)
1127 .validate()
1128 .unwrap_err();
1129 assert!(
1130 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("require a supported compression"))
1131 );
1132 }
1133
1134 #[test]
1135 fn validation_rejects_unsupported_writer_codecs_before_block_writes() {
1136 for compression in [
1137 tiff_core::Compression::OldJpeg,
1138 tiff_core::Compression::PackBits,
1139 tiff_core::Compression::WebP,
1140 ] {
1141 assert!(matches!(
1142 ImageBuilder::new(1, 1)
1143 .sample_type::<u8>()
1144 .compression(compression)
1145 .validate(),
1146 Err(crate::error::Error::InvalidConfig(message))
1147 if message.contains("not supported")
1148 ));
1149 }
1150
1151 #[cfg(not(feature = "jpeg"))]
1152 assert!(ImageBuilder::new(1, 1)
1153 .sample_type::<u8>()
1154 .compression(tiff_core::Compression::Jpeg)
1155 .validate()
1156 .is_err());
1157 #[cfg(not(feature = "zstd"))]
1158 assert!(ImageBuilder::new(1, 1)
1159 .sample_type::<u8>()
1160 .compression(tiff_core::Compression::Zstd)
1161 .validate()
1162 .is_err());
1163 }
1164
1165 #[test]
1166 fn validate_rejects_mismatched_predictor_and_sample_format() {
1167 let err = ImageBuilder::new(4, 4)
1168 .sample_type::<f32>()
1169 .compression(tiff_core::Compression::Deflate)
1170 .predictor(tiff_core::Predictor::Horizontal)
1171 .validate()
1172 .unwrap_err();
1173 assert!(
1174 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("horizontal predictor"))
1175 );
1176
1177 let err = ImageBuilder::new(4, 4)
1178 .sample_type::<u16>()
1179 .compression(tiff_core::Compression::Deflate)
1180 .predictor(tiff_core::Predictor::FloatingPoint)
1181 .validate()
1182 .unwrap_err();
1183 assert!(
1184 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("floating-point predictor"))
1185 );
1186
1187 let f16_builder = ImageBuilder::new(4, 4)
1188 .bits_per_sample(16)
1189 .sample_format(tiff_core::SampleFormat::Float);
1190 #[cfg(not(feature = "f16"))]
1191 {
1192 let err = f16_builder.validate().unwrap_err();
1193 assert!(
1194 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("32 or 64 bits"))
1195 );
1196 }
1197 #[cfg(feature = "f16")]
1198 assert!(f16_builder.validate().is_ok());
1199
1200 assert!(ImageBuilder::new(4, 4)
1201 .sample_type::<u16>()
1202 .compression(tiff_core::Compression::Deflate)
1203 .predictor(tiff_core::Predictor::Horizontal)
1204 .validate()
1205 .is_ok());
1206 assert!(ImageBuilder::new(4, 4)
1207 .sample_type::<f32>()
1208 .compression(tiff_core::Compression::Deflate)
1209 .predictor(tiff_core::Predictor::FloatingPoint)
1210 .validate()
1211 .is_ok());
1212 }
1213
1214 #[cfg(feature = "f16")]
1215 #[test]
1216 fn validate_rejects_lerc_with_f16_samples() {
1217 let err = ImageBuilder::new(4, 4)
1218 .sample_type::<half::f16>()
1219 .compression(tiff_core::Compression::Lerc)
1220 .validate()
1221 .unwrap_err();
1222 assert!(
1223 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("LERC compression does not support 16-bit float samples"))
1224 );
1225 }
1226
1227 #[test]
1228 fn validate_rejects_overflowing_layout_sizes() {
1229 let err = ImageBuilder::new(u32::MAX, u32::MAX)
1230 .sample_type::<u8>()
1231 .samples_per_pixel(u16::MAX)
1232 .planar_configuration(PlanarConfiguration::Planar)
1233 .tiles(16, 16)
1234 .validate()
1235 .unwrap_err();
1236 assert!(
1237 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("block count"))
1238 );
1239
1240 let large_multiple_of_16 = u32::MAX - 15;
1241 let err = ImageBuilder::new(1, 1)
1242 .sample_type::<u8>()
1243 .samples_per_pixel(2)
1244 .tiles(large_multiple_of_16, large_multiple_of_16)
1245 .validate()
1246 .unwrap_err();
1247 assert!(
1248 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("sample count"))
1249 );
1250
1251 let err = ImageBuilder::new(u32::MAX, u32::MAX)
1252 .sample_type::<u64>()
1253 .samples_per_pixel(2)
1254 .strips(256)
1255 .validate()
1256 .unwrap_err();
1257 assert!(
1258 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("byte count"))
1259 );
1260 }
1261}