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 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 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 width as usize * height as usize * samples_per_pixel
280 }
281 }
282 }
283
284 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 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 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 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 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 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 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 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 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}