1use tiff_core::*;
4
5use crate::encoder;
6use crate::sample::TiffWriteSample;
7
8#[derive(Debug, Clone, Copy, PartialEq)]
13pub struct LercOptions {
14 pub max_z_error: f64,
16 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub struct JpegOptions {
32 pub quality: u8,
34}
35
36impl Default for JpegOptions {
37 fn default() -> Self {
38 Self { quality: 75 }
39 }
40}
41
42#[derive(Debug, Clone, Copy)]
44pub enum DataLayout {
45 Strips { rows_per_strip: u32 },
47 Tiles { width: u32, height: u32 },
49}
50
51#[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 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 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 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 pub fn extra_samples(mut self, extra_samples: Vec<ExtraSample>) -> Self {
155 self.extra_samples = extra_samples;
156 self
157 }
158
159 pub fn color_map(mut self, color_map: ColorMap) -> Self {
161 self.color_map = Some(color_map);
162 self
163 }
164
165 pub fn ink_set(mut self, ink_set: InkSet) -> Self {
167 self.ink_set = Some(ink_set);
168 self
169 }
170
171 pub fn ycbcr_subsampling(mut self, subsampling: [u16; 2]) -> Self {
173 self.ycbcr_subsampling = Some(subsampling);
174 self
175 }
176
177 pub fn ycbcr_positioning(mut self, positioning: YCbCrPositioning) -> Self {
179 self.ycbcr_positioning = Some(positioning);
180 self
181 }
182
183 pub fn planar_configuration(mut self, p: PlanarConfiguration) -> Self {
185 self.planar_configuration = p;
186 self
187 }
188
189 pub fn strips(mut self, rows_per_strip: u32) -> Self {
191 self.layout = DataLayout::Strips { rows_per_strip };
192 self
193 }
194
195 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 pub fn tag(mut self, tag: Tag) -> Self {
206 self.extra_tags.push(tag);
207 self
208 }
209
210 pub fn overview(mut self) -> Self {
212 self.subfile_type = 1;
213 self
214 }
215
216 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 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 #[deprecated(
249 since = "0.6.0",
250 note = "use ImageBuilder::checked_block_count() to handle invalid layouts without best-effort fallback"
251 )]
252 pub fn block_count(&self) -> usize {
253 let blocks_per_plane = self.legacy_blocks_per_plane();
254 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
255 blocks_per_plane.saturating_mul(self.samples_per_pixel as usize)
256 } else {
257 blocks_per_plane
258 }
259 }
260
261 pub fn checked_block_count(&self) -> crate::error::Result<usize> {
263 let blocks_per_plane = match self.checked_layout()? {
264 DataLayout::Strips { rows_per_strip } => {
265 let rps = rows_per_strip as usize;
266 (self.height as usize).div_ceil(rps)
267 }
268 DataLayout::Tiles { width, height } => {
269 let tw = width as usize;
270 let th = height as usize;
271 let tiles_across = (self.width as usize).div_ceil(tw);
272 let tiles_down = (self.height as usize).div_ceil(th);
273 tiles_across
274 .checked_mul(tiles_down)
275 .ok_or_else(|| layout_overflow("tile count"))?
276 }
277 };
278 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
279 blocks_per_plane
280 .checked_mul(self.samples_per_pixel as usize)
281 .ok_or_else(|| layout_overflow("planar block count"))
282 } else {
283 Ok(blocks_per_plane)
284 }
285 }
286
287 #[deprecated(
293 since = "0.6.0",
294 note = "use ImageBuilder::checked_block_sample_count() to handle invalid layouts without best-effort fallback"
295 )]
296 pub fn block_sample_count(&self, index: usize) -> usize {
297 let samples_per_pixel = self.block_samples_per_pixel() as usize;
298 let plane_block_index = self.block_plane_index(index);
299 match self.layout {
300 DataLayout::Strips { rows_per_strip } => {
301 let rps = rows_per_strip.max(1) as usize;
302 let start_row = plane_block_index.saturating_mul(rps);
303 let end_row = plane_block_index
304 .saturating_add(1)
305 .saturating_mul(rps)
306 .min(self.height as usize);
307 let rows = end_row.saturating_sub(start_row);
308 rows.saturating_mul(self.width as usize)
309 .saturating_mul(samples_per_pixel)
310 }
311 DataLayout::Tiles { width, height } => {
312 (width.max(1) as usize)
314 .saturating_mul(height.max(1) as usize)
315 .saturating_mul(samples_per_pixel)
316 }
317 }
318 }
319
320 pub fn checked_block_sample_count(&self, index: usize) -> crate::error::Result<usize> {
322 let samples_per_pixel = self.block_samples_per_pixel() as usize;
323 let plane_block_index = self.checked_block_plane_index(index)?;
324 match self.checked_layout()? {
325 DataLayout::Strips { rows_per_strip } => {
326 let rps = rows_per_strip as usize;
327 let start_row = plane_block_index
328 .checked_mul(rps)
329 .ok_or_else(|| layout_overflow("strip start row"))?;
330 let end_row = plane_block_index
331 .checked_add(1)
332 .and_then(|value| value.checked_mul(rps))
333 .ok_or_else(|| layout_overflow("strip end row"))?
334 .min(self.height as usize);
335 let rows = end_row.saturating_sub(start_row);
336 rows.checked_mul(self.width as usize)
337 .and_then(|value| value.checked_mul(samples_per_pixel))
338 .ok_or_else(|| layout_overflow("strip sample count"))
339 }
340 DataLayout::Tiles { width, height } => {
341 (width as usize)
343 .checked_mul(height as usize)
344 .and_then(|value| value.checked_mul(samples_per_pixel))
345 .ok_or_else(|| layout_overflow("tile sample count"))
346 }
347 }
348 }
349
350 #[deprecated(
356 since = "0.6.0",
357 note = "use ImageBuilder::checked_estimated_uncompressed_bytes() to handle overflow without saturation"
358 )]
359 pub fn estimated_uncompressed_bytes(&self) -> u64 {
360 let bps = (self.bits_per_sample / 8).max(1) as u64;
361 (self.width as u64)
362 .saturating_mul(self.height as u64)
363 .saturating_mul(self.samples_per_pixel as u64)
364 .saturating_mul(bps)
365 }
366
367 pub fn checked_estimated_uncompressed_bytes(&self) -> crate::error::Result<u64> {
369 let bps = (self.bits_per_sample / 8).max(1) as u64;
370 (self.width as u64)
371 .checked_mul(self.height as u64)
372 .and_then(|value| value.checked_mul(self.samples_per_pixel as u64))
373 .and_then(|value| value.checked_mul(bps))
374 .ok_or_else(|| layout_overflow("estimated uncompressed byte count"))
375 }
376
377 pub fn offset_tag_codes(&self) -> (u16, u16) {
379 match self.layout {
380 DataLayout::Strips { .. } => (TAG_STRIP_OFFSETS, TAG_STRIP_BYTE_COUNTS),
381 DataLayout::Tiles { .. } => (TAG_TILE_OFFSETS, TAG_TILE_BYTE_COUNTS),
382 }
383 }
384
385 #[deprecated(
391 since = "0.6.0",
392 note = "use ImageBuilder::checked_layout_tags() to handle invalid layouts without best-effort fallback"
393 )]
394 pub fn layout_tags(&self) -> Vec<Tag> {
395 self.legacy_layout_tags()
396 }
397
398 pub fn checked_layout_tags(&self) -> crate::error::Result<Vec<Tag>> {
400 match self.checked_layout()? {
401 DataLayout::Strips { rows_per_strip } => Ok(vec![Tag::new(
402 TAG_ROWS_PER_STRIP,
403 TagValue::Long(vec![rows_per_strip]),
404 )]),
405 DataLayout::Tiles { width, height } => Ok(vec![
406 Tag::new(TAG_TILE_WIDTH, TagValue::Long(vec![width])),
407 Tag::new(TAG_TILE_LENGTH, TagValue::Long(vec![height])),
408 ]),
409 }
410 }
411
412 #[deprecated(
418 since = "0.6.0",
419 note = "use ImageBuilder::checked_build_tags() to handle invalid layouts and color models without best-effort fallback"
420 )]
421 pub fn build_tags(&self, is_bigtiff: bool) -> Vec<Tag> {
422 let mut extra_tags = self.extra_tags.clone();
423 if let Some(lerc_tag) = self.lerc_parameters_tag() {
424 extra_tags.push(lerc_tag);
425 }
426 if let Ok(extra_samples) = self.effective_extra_samples() {
427 if !extra_samples.is_empty() {
428 extra_tags.push(Tag::new(
429 TAG_EXTRA_SAMPLES,
430 TagValue::Short(
431 extra_samples
432 .iter()
433 .copied()
434 .map(ExtraSample::to_code)
435 .collect(),
436 ),
437 ));
438 }
439 }
440 if let Some(color_map) = &self.color_map {
441 extra_tags.push(Tag::new(
442 TAG_COLOR_MAP,
443 TagValue::Short(color_map.encode_tag_values()),
444 ));
445 }
446 if let Some(ink_set) = self.ink_set {
447 extra_tags.push(Tag::new(
448 TAG_INK_SET,
449 TagValue::Short(vec![ink_set.to_code()]),
450 ));
451 }
452 if let Some([h, v]) = self.ycbcr_subsampling {
453 extra_tags.push(Tag::new(TAG_YCBCR_SUBSAMPLING, TagValue::Short(vec![h, v])));
454 }
455 if let Some(positioning) = self.ycbcr_positioning {
456 extra_tags.push(Tag::new(
457 TAG_YCBCR_POSITIONING,
458 TagValue::Short(vec![positioning.to_code()]),
459 ));
460 }
461
462 let (offsets_tag_code, byte_counts_tag_code) = self.offset_tag_codes();
463 let layout_tags = self.legacy_layout_tags();
464 let num_blocks = self.checked_block_count().unwrap_or(0);
465
466 encoder::build_image_tags(&encoder::ImageTagParams {
467 width: self.width,
468 height: self.height,
469 samples_per_pixel: self.samples_per_pixel,
470 bits_per_sample: self.bits_per_sample,
471 sample_format: self.sample_format.to_code(),
472 compression: self.compression.to_code(),
473 photometric: self.photometric.to_code(),
474 predictor: self.predictor.to_code(),
475 planar_configuration: self.planar_configuration.to_code(),
476 subfile_type: self.subfile_type,
477 extra_tags: &extra_tags,
478 offsets_tag_code,
479 byte_counts_tag_code,
480 num_blocks,
481 layout_tags: &layout_tags,
482 is_bigtiff,
483 })
484 }
485
486 pub fn checked_build_tags(&self, is_bigtiff: bool) -> crate::error::Result<Vec<Tag>> {
488 let mut extra_tags = self.extra_tags.clone();
489 if let Some(lerc_tag) = self.lerc_parameters_tag() {
490 extra_tags.push(lerc_tag);
491 }
492 self.validate()?;
493 let extra_samples = self.effective_extra_samples()?;
494 if !extra_samples.is_empty() {
495 extra_tags.push(Tag::new(
496 TAG_EXTRA_SAMPLES,
497 TagValue::Short(
498 extra_samples
499 .iter()
500 .copied()
501 .map(ExtraSample::to_code)
502 .collect(),
503 ),
504 ));
505 }
506 if let Some(color_map) = &self.color_map {
507 extra_tags.push(Tag::new(
508 TAG_COLOR_MAP,
509 TagValue::Short(color_map.encode_tag_values()),
510 ));
511 }
512 if let Some(ink_set) = self.ink_set {
513 extra_tags.push(Tag::new(
514 TAG_INK_SET,
515 TagValue::Short(vec![ink_set.to_code()]),
516 ));
517 }
518 if let Some([h, v]) = self.ycbcr_subsampling {
519 extra_tags.push(Tag::new(TAG_YCBCR_SUBSAMPLING, TagValue::Short(vec![h, v])));
520 }
521 if let Some(positioning) = self.ycbcr_positioning {
522 extra_tags.push(Tag::new(
523 TAG_YCBCR_POSITIONING,
524 TagValue::Short(vec![positioning.to_code()]),
525 ));
526 }
527
528 let (offsets_tag_code, byte_counts_tag_code) = self.offset_tag_codes();
529 let layout_tags = self.checked_layout_tags()?;
530
531 Ok(encoder::build_image_tags(&encoder::ImageTagParams {
532 width: self.width,
533 height: self.height,
534 samples_per_pixel: self.samples_per_pixel,
535 bits_per_sample: self.bits_per_sample,
536 sample_format: self.sample_format.to_code(),
537 compression: self.compression.to_code(),
538 photometric: self.photometric.to_code(),
539 predictor: self.predictor.to_code(),
540 planar_configuration: self.planar_configuration.to_code(),
541 subfile_type: self.subfile_type,
542 extra_tags: &extra_tags,
543 offsets_tag_code,
544 byte_counts_tag_code,
545 num_blocks: self.checked_block_count()?,
546 layout_tags: &layout_tags,
547 is_bigtiff,
548 }))
549 }
550
551 pub fn block_row_width(&self) -> usize {
553 match self.layout {
554 DataLayout::Strips { .. } => self.width as usize,
555 DataLayout::Tiles { width, .. } => width as usize,
556 }
557 }
558
559 pub fn block_samples_per_pixel(&self) -> u16 {
561 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
562 1
563 } else {
564 self.samples_per_pixel
565 }
566 }
567
568 fn block_plane_index(&self, index: usize) -> usize {
569 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
570 let blocks_per_plane = self.legacy_blocks_per_plane();
571 if blocks_per_plane == 0 {
572 0
573 } else {
574 index % blocks_per_plane
575 }
576 } else {
577 index
578 }
579 }
580
581 fn checked_block_plane_index(&self, index: usize) -> crate::error::Result<usize> {
582 if matches!(self.planar_configuration, PlanarConfiguration::Planar) {
583 let blocks_per_plane = self.checked_blocks_per_plane()?;
584 if blocks_per_plane == 0 {
585 return Err(crate::error::Error::InvalidConfig(
586 "block count must be greater than zero".into(),
587 ));
588 }
589 Ok(index % blocks_per_plane)
590 } else {
591 Ok(index)
592 }
593 }
594
595 fn checked_blocks_per_plane(&self) -> crate::error::Result<usize> {
596 match self.checked_layout()? {
597 DataLayout::Strips { rows_per_strip } => {
598 let rps = rows_per_strip as usize;
599 Ok((self.height as usize).div_ceil(rps))
600 }
601 DataLayout::Tiles { width, height } => {
602 let tw = width as usize;
603 let th = height as usize;
604 let tiles_across = (self.width as usize).div_ceil(tw);
605 let tiles_down = (self.height as usize).div_ceil(th);
606 tiles_across
607 .checked_mul(tiles_down)
608 .ok_or_else(|| layout_overflow("tile count"))
609 }
610 }
611 }
612
613 fn legacy_blocks_per_plane(&self) -> usize {
614 match self.layout {
615 DataLayout::Strips { rows_per_strip } => {
616 let rps = rows_per_strip.max(1) as usize;
617 (self.height as usize).div_ceil(rps)
618 }
619 DataLayout::Tiles { width, height } => {
620 let tw = width.max(1) as usize;
621 let th = height.max(1) as usize;
622 let tiles_across = (self.width as usize).div_ceil(tw);
623 let tiles_down = (self.height as usize).div_ceil(th);
624 tiles_across.saturating_mul(tiles_down)
625 }
626 }
627 }
628
629 fn legacy_layout_tags(&self) -> Vec<Tag> {
630 match self.layout {
631 DataLayout::Strips { rows_per_strip } => {
632 vec![Tag::new(
633 TAG_ROWS_PER_STRIP,
634 TagValue::Long(vec![rows_per_strip]),
635 )]
636 }
637 DataLayout::Tiles { width, height } => {
638 vec![
639 Tag::new(TAG_TILE_WIDTH, TagValue::Long(vec![width])),
640 Tag::new(TAG_TILE_LENGTH, TagValue::Long(vec![height])),
641 ]
642 }
643 }
644 }
645
646 pub fn block_height(&self, index: usize) -> u32 {
651 match self.layout {
652 DataLayout::Tiles { height, .. } => height,
653 DataLayout::Strips { rows_per_strip } => {
654 let plane_index = self.block_plane_index(index);
655 let rps = rows_per_strip.max(1) as usize;
656 let start_row = plane_index.saturating_mul(rps);
657 let remaining = (self.height as usize).saturating_sub(start_row);
658 remaining.min(rps) as u32
659 }
660 }
661 }
662
663 pub fn lerc_parameters_tag(&self) -> Option<Tag> {
665 if !matches!(self.compression, Compression::Lerc) {
666 return None;
667 }
668 let opts = self.lerc_options.unwrap_or_default();
669 Some(Tag::new(
670 TAG_LERC_PARAMETERS,
671 TagValue::Long(vec![
672 LERC_VERSION_2_4,
673 opts.additional_compression.to_code(),
674 ]),
675 ))
676 }
677
678 pub fn validate(&self) -> crate::error::Result<()> {
680 if self.width == 0 || self.height == 0 {
681 return Err(crate::error::Error::InvalidConfig(
682 "image dimensions must be positive".into(),
683 ));
684 }
685 if self.samples_per_pixel == 0 {
686 return Err(crate::error::Error::InvalidConfig(
687 "samples_per_pixel must be greater than zero".into(),
688 ));
689 }
690 if !matches!(self.bits_per_sample, 8 | 16 | 32 | 64) {
691 return Err(crate::error::Error::InvalidConfig(format!(
692 "bits_per_sample must be 8, 16, 32, or 64, got {}",
693 self.bits_per_sample
694 )));
695 }
696 match self.layout {
697 DataLayout::Strips { rows_per_strip: 0 } => {
698 return Err(crate::error::Error::InvalidConfig(
699 "rows_per_strip must be greater than zero".into(),
700 ));
701 }
702 DataLayout::Tiles { width, height } => {
703 if width == 0 || height == 0 {
704 return Err(crate::error::Error::InvalidConfig(format!(
705 "tile_width and tile_height must be greater than zero, got {}x{}",
706 width, height
707 )));
708 }
709 if width % 16 != 0 || height % 16 != 0 {
710 return Err(crate::error::Error::InvalidConfig(format!(
711 "tile dimensions must be multiples of 16, got {}x{}",
712 width, height
713 )));
714 }
715 }
716 _ => {}
717 }
718 self.checked_block_count()?;
719 self.checked_block_sample_count(0)?;
720 self.checked_estimated_uncompressed_bytes()?;
721 if matches!(self.compression, Compression::Lerc)
722 && !matches!(self.predictor, Predictor::None)
723 {
724 return Err(crate::error::Error::InvalidConfig(
725 "LERC compression does not support TIFF predictors".into(),
726 ));
727 }
728 if matches!(self.compression, Compression::OldJpeg) {
729 return Err(crate::error::Error::InvalidConfig(
730 "Old-style JPEG compression is not supported for writing; use Compression::Jpeg"
731 .into(),
732 ));
733 }
734 self.validate_color_model()?;
735 if matches!(self.compression, Compression::Jpeg) {
736 self.validate_jpeg_config()?;
737 }
738 Ok(())
739 }
740
741 fn checked_layout(&self) -> crate::error::Result<DataLayout> {
742 match self.layout {
743 DataLayout::Strips { rows_per_strip: 0 } => Err(crate::error::Error::InvalidConfig(
744 "rows_per_strip must be greater than zero".into(),
745 )),
746 DataLayout::Tiles { width, height } if width == 0 || height == 0 => {
747 Err(crate::error::Error::InvalidConfig(format!(
748 "tile_width and tile_height must be greater than zero, got {}x{}",
749 width, height
750 )))
751 }
752 DataLayout::Tiles { width, height } if width % 16 != 0 || height % 16 != 0 => {
753 Err(crate::error::Error::InvalidConfig(format!(
754 "tile dimensions must be multiples of 16, got {}x{}",
755 width, height
756 )))
757 }
758 layout => Ok(layout),
759 }
760 }
761
762 fn validate_color_model(&self) -> crate::error::Result<()> {
763 if !matches!(self.photometric, PhotometricInterpretation::Palette)
764 && self.color_map.is_some()
765 {
766 return Err(crate::error::Error::InvalidConfig(
767 "ColorMap is only valid with palette photometric interpretation".into(),
768 ));
769 }
770
771 if !matches!(self.photometric, PhotometricInterpretation::Separated)
772 && self.ink_set.is_some()
773 {
774 return Err(crate::error::Error::InvalidConfig(
775 "InkSet is only valid with separated photometric interpretation".into(),
776 ));
777 }
778
779 let base_samples: u16 = match self.photometric {
780 PhotometricInterpretation::MinIsWhite | PhotometricInterpretation::MinIsBlack => 1,
781 PhotometricInterpretation::Rgb => 3,
782 PhotometricInterpretation::Palette => {
783 let color_map =
784 self.color_map
785 .as_ref()
786 .ok_or(crate::error::Error::InvalidConfig(
787 "palette photometric interpretation requires a ColorMap".into(),
788 ))?;
789 let expected_entries =
790 1usize
791 .checked_shl(self.bits_per_sample as u32)
792 .ok_or_else(|| {
793 crate::error::Error::InvalidConfig(format!(
794 "palette BitsPerSample {} exceeds usize shift width",
795 self.bits_per_sample
796 ))
797 })?;
798 if color_map.len() != expected_entries {
799 return Err(crate::error::Error::InvalidConfig(format!(
800 "palette ColorMap has {} entries but BitsPerSample={} requires {}",
801 color_map.len(),
802 self.bits_per_sample,
803 expected_entries
804 )));
805 }
806 1
807 }
808 PhotometricInterpretation::Mask => 1,
809 PhotometricInterpretation::Separated => match self.ink_set.unwrap_or(InkSet::Cmyk) {
810 InkSet::Cmyk => 4,
811 InkSet::NotCmyk | InkSet::Unknown(_) => {
812 return Err(crate::error::Error::InvalidConfig(
813 "separated photometric interpretation currently requires InkSet::Cmyk"
814 .into(),
815 ))
816 }
817 },
818 PhotometricInterpretation::YCbCr => 3,
819 PhotometricInterpretation::CieLab => 3,
820 };
821
822 let _ = self.effective_extra_samples_for_base(base_samples)?;
823
824 if matches!(self.photometric, PhotometricInterpretation::YCbCr) {
825 if !matches!(self.sample_format, SampleFormat::Uint) || self.bits_per_sample != 8 {
826 return Err(crate::error::Error::InvalidConfig(
827 "YCbCr photometric interpretation requires 8-bit unsigned samples".into(),
828 ));
829 }
830 if let Some(subsampling) = self.ycbcr_subsampling {
831 if subsampling != [1, 1] {
832 return Err(crate::error::Error::InvalidConfig(format!(
833 "YCbCr subsampling {:?} is not supported by the current writer",
834 subsampling
835 )));
836 }
837 }
838 } else if self.ycbcr_subsampling.is_some() || self.ycbcr_positioning.is_some() {
839 return Err(crate::error::Error::InvalidConfig(
840 "YCbCr-specific tags require YCbCr photometric interpretation".into(),
841 ));
842 }
843
844 Ok(())
845 }
846
847 fn effective_extra_samples(&self) -> crate::error::Result<Vec<ExtraSample>> {
848 let base_samples = match self.photometric {
849 PhotometricInterpretation::MinIsWhite | PhotometricInterpretation::MinIsBlack => 1,
850 PhotometricInterpretation::Rgb => 3,
851 PhotometricInterpretation::Palette => 1,
852 PhotometricInterpretation::Mask => 1,
853 PhotometricInterpretation::Separated => 4,
854 PhotometricInterpretation::YCbCr => 3,
855 PhotometricInterpretation::CieLab => 3,
856 };
857 self.effective_extra_samples_for_base(base_samples)
858 }
859
860 fn effective_extra_samples_for_base(
861 &self,
862 base_samples: u16,
863 ) -> crate::error::Result<Vec<ExtraSample>> {
864 let implied_extra_samples = self
865 .samples_per_pixel
866 .checked_sub(base_samples)
867 .ok_or_else(|| {
868 crate::error::Error::InvalidConfig(format!(
869 "{} photometric interpretation requires at least {} samples, got {}",
870 photometric_name(self.photometric),
871 base_samples,
872 self.samples_per_pixel
873 ))
874 })?;
875 if self.extra_samples.len() > implied_extra_samples as usize {
876 return Err(crate::error::Error::InvalidConfig(format!(
877 "{} photometric interpretation has {} total channels but {} ExtraSamples",
878 photometric_name(self.photometric),
879 self.samples_per_pixel,
880 self.extra_samples.len()
881 )));
882 }
883
884 let mut extra_samples = self.extra_samples.clone();
885 extra_samples.resize(implied_extra_samples as usize, ExtraSample::Unspecified);
886 Ok(extra_samples)
887 }
888
889 fn validate_jpeg_config(&self) -> crate::error::Result<()> {
890 let options = self.jpeg_options.unwrap_or_default();
891 if !(1..=100).contains(&options.quality) {
892 return Err(crate::error::Error::InvalidConfig(format!(
893 "JPEG quality must be in the range 1..=100, got {}",
894 options.quality
895 )));
896 }
897 if self.bits_per_sample != 8 {
898 return Err(crate::error::Error::InvalidConfig(format!(
899 "JPEG compression requires 8-bit samples, got {} bits",
900 self.bits_per_sample
901 )));
902 }
903 if !matches!(self.sample_format, SampleFormat::Uint) {
904 return Err(crate::error::Error::InvalidConfig(format!(
905 "JPEG compression requires unsigned integer samples, got {:?}",
906 self.sample_format
907 )));
908 }
909 if !matches!(self.predictor, Predictor::None) {
910 return Err(crate::error::Error::InvalidConfig(
911 "JPEG compression does not support TIFF predictors".into(),
912 ));
913 }
914
915 let block_width = self.block_row_width();
916 if block_width > u16::MAX as usize {
917 return Err(crate::error::Error::InvalidConfig(format!(
918 "JPEG block width must be <= {}, got {}",
919 u16::MAX,
920 block_width
921 )));
922 }
923 let max_block_height = match self.layout {
924 DataLayout::Strips { rows_per_strip } => rows_per_strip.max(1),
925 DataLayout::Tiles { height, .. } => height,
926 };
927 if max_block_height > u16::MAX as u32 {
928 return Err(crate::error::Error::InvalidConfig(format!(
929 "JPEG block height must be <= {}, got {}",
930 u16::MAX,
931 max_block_height
932 )));
933 }
934
935 let block_samples_per_pixel = self.block_samples_per_pixel();
936 if block_samples_per_pixel != 1 {
937 return Err(crate::error::Error::InvalidConfig(format!(
938 "JPEG write currently supports one sample per encoded block, got {}; use planar configuration for multi-band JPEG",
939 block_samples_per_pixel
940 )));
941 }
942
943 if matches!(
944 self.photometric,
945 PhotometricInterpretation::Palette | PhotometricInterpretation::Mask
946 ) {
947 return Err(crate::error::Error::InvalidConfig(format!(
948 "{:?} photometric interpretation is not supported with JPEG compression",
949 self.photometric
950 )));
951 }
952
953 Ok(())
954 }
955}
956
957fn photometric_name(photometric: PhotometricInterpretation) -> &'static str {
958 match photometric {
959 PhotometricInterpretation::MinIsWhite => "MinIsWhite",
960 PhotometricInterpretation::MinIsBlack => "MinIsBlack",
961 PhotometricInterpretation::Rgb => "RGB",
962 PhotometricInterpretation::Palette => "Palette",
963 PhotometricInterpretation::Mask => "TransparencyMask",
964 PhotometricInterpretation::Separated => "Separated",
965 PhotometricInterpretation::YCbCr => "YCbCr",
966 PhotometricInterpretation::CieLab => "CIELab",
967 }
968}
969
970fn layout_overflow(context: &'static str) -> crate::error::Error {
971 crate::error::Error::InvalidConfig(format!("{context} overflows layout size limits"))
972}
973
974#[cfg(test)]
975mod tests {
976 use super::ImageBuilder;
977 use std::panic;
978 use tiff_core::{PhotometricInterpretation, PlanarConfiguration};
979
980 #[test]
981 fn validate_rejects_zero_strip_and_tile_dimensions() {
982 let err = ImageBuilder::new(16, 16).strips(0).validate().unwrap_err();
983 assert!(
984 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
985 );
986
987 let err = ImageBuilder::new(16, 16)
988 .tiles(0, 16)
989 .validate()
990 .unwrap_err();
991 assert!(
992 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
993 );
994
995 let err = ImageBuilder::new(16, 16)
996 .tiles(16, 0)
997 .validate()
998 .unwrap_err();
999 assert!(
1000 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1001 );
1002
1003 let err = ImageBuilder::new(16, 16)
1004 .tiles(0, 0)
1005 .validate()
1006 .unwrap_err();
1007 assert!(
1008 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width") && message.contains("tile_height"))
1009 );
1010 }
1011
1012 #[test]
1013 fn checked_helpers_reject_zero_strip_and_tile_dimensions() {
1014 let builder = ImageBuilder::new(16, 16).strips(0);
1015 let err = builder.checked_block_count().unwrap_err();
1016 assert!(
1017 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
1018 );
1019 let err = builder.checked_layout_tags().unwrap_err();
1020 assert!(
1021 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
1022 );
1023 let err = builder.checked_build_tags(false).unwrap_err();
1024 assert!(
1025 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("rows_per_strip"))
1026 );
1027
1028 let builder = ImageBuilder::new(16, 16).tiles(0, 16);
1029 let err = builder.checked_block_count().unwrap_err();
1030 assert!(
1031 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
1032 );
1033 let err = builder.checked_layout_tags().unwrap_err();
1034 assert!(
1035 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
1036 );
1037 let err = builder.checked_build_tags(false).unwrap_err();
1038 assert!(
1039 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_width"))
1040 );
1041
1042 let builder = ImageBuilder::new(16, 16).tiles(16, 0);
1043 let err = builder.checked_block_count().unwrap_err();
1044 assert!(
1045 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1046 );
1047 let err = builder.checked_layout_tags().unwrap_err();
1048 assert!(
1049 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1050 );
1051 let err = builder.checked_build_tags(false).unwrap_err();
1052 assert!(
1053 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("tile_height"))
1054 );
1055
1056 let builder = ImageBuilder::new(16, 16).tiles(15, 16);
1057 let err = builder.checked_layout_tags().unwrap_err();
1058 assert!(
1059 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("multiples of 16"))
1060 );
1061 }
1062
1063 #[test]
1064 fn checked_build_tags_returns_color_model_errors() {
1065 let err = ImageBuilder::new(16, 16)
1066 .photometric(PhotometricInterpretation::Rgb)
1067 .samples_per_pixel(1)
1068 .checked_build_tags(false)
1069 .unwrap_err();
1070 assert!(
1071 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("requires at least 3 samples"))
1072 );
1073 }
1074
1075 #[test]
1076 #[allow(deprecated)]
1077 fn legacy_infallible_helpers_do_not_panic_on_invalid_builders() {
1078 let builders = vec![
1079 ImageBuilder::new(16, 16).strips(0),
1080 ImageBuilder::new(16, 16).tiles(0, 16),
1081 ImageBuilder::new(16, 16).tiles(16, 0),
1082 ImageBuilder::new(16, 16).tiles(15, 16),
1083 ImageBuilder::new(16, 16)
1084 .photometric(PhotometricInterpretation::Rgb)
1085 .samples_per_pixel(1),
1086 ImageBuilder::new(u32::MAX, u32::MAX)
1087 .sample_type::<u8>()
1088 .samples_per_pixel(u16::MAX)
1089 .planar_configuration(PlanarConfiguration::Planar)
1090 .tiles(16, 16),
1091 ];
1092
1093 for builder in builders {
1094 let result = panic::catch_unwind(|| {
1095 let _ = builder.block_count();
1096 let _ = builder.block_sample_count(usize::MAX);
1097 let _ = builder.estimated_uncompressed_bytes();
1098 let _ = builder.layout_tags();
1099 let _ = builder.build_tags(false);
1100 let _ = builder.build_tags(true);
1101 let _ = builder.block_height(usize::MAX);
1102 });
1103 assert!(result.is_ok());
1104 }
1105 }
1106
1107 #[test]
1108 fn validate_rejects_overflowing_layout_sizes() {
1109 let err = ImageBuilder::new(u32::MAX, u32::MAX)
1110 .sample_type::<u8>()
1111 .samples_per_pixel(u16::MAX)
1112 .planar_configuration(PlanarConfiguration::Planar)
1113 .tiles(16, 16)
1114 .validate()
1115 .unwrap_err();
1116 assert!(
1117 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("block count"))
1118 );
1119
1120 let large_multiple_of_16 = u32::MAX - 15;
1121 let err = ImageBuilder::new(1, 1)
1122 .sample_type::<u8>()
1123 .samples_per_pixel(2)
1124 .tiles(large_multiple_of_16, large_multiple_of_16)
1125 .validate()
1126 .unwrap_err();
1127 assert!(
1128 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("sample count"))
1129 );
1130
1131 let err = ImageBuilder::new(u32::MAX, u32::MAX)
1132 .sample_type::<u64>()
1133 .samples_per_pixel(2)
1134 .strips(256)
1135 .validate()
1136 .unwrap_err();
1137 assert!(
1138 matches!(err, crate::error::Error::InvalidConfig(message) if message.contains("byte count"))
1139 );
1140 }
1141}