1use std::collections::HashMap;
2use std::ops::Range;
3
4use bytes::Bytes;
5use num_enum::TryFromPrimitive;
6
7use crate::error::{AsyncTiffError, AsyncTiffResult, TiffError};
8use crate::geo::{GeoKeyDirectory, GeoKeyTag};
9use crate::predictor::PredictorInfo;
10use crate::reader::{AsyncFileReader, Endianness};
11use crate::tag_value::TagValue;
12use crate::tags::{
13 CompressionMethod, PhotometricInterpretation, PlanarConfiguration, Predictor, ResolutionUnit,
14 SampleFormat, Tag,
15};
16use crate::{DataType, Tile};
17
18const DOCUMENT_NAME: u16 = 269;
19
20#[allow(dead_code)]
23#[derive(Debug, Clone, PartialEq)]
24pub struct ImageFileDirectory {
25 pub(crate) endianness: Endianness,
26
27 pub(crate) new_subfile_type: Option<u32>,
28
29 pub(crate) image_width: u32,
31
32 pub(crate) image_height: u32,
34
35 pub(crate) bits_per_sample: Vec<u16>,
36
37 pub(crate) compression: CompressionMethod,
38
39 pub(crate) photometric_interpretation: PhotometricInterpretation,
40
41 pub(crate) document_name: Option<String>,
42
43 pub(crate) image_description: Option<String>,
44
45 pub(crate) strip_offsets: Option<Vec<u64>>,
46
47 pub(crate) orientation: Option<u16>,
48
49 pub(crate) samples_per_pixel: u16,
55
56 pub(crate) rows_per_strip: Option<u32>,
57
58 pub(crate) strip_byte_counts: Option<Vec<u64>>,
59
60 pub(crate) min_sample_value: Option<Vec<u16>>,
61 pub(crate) max_sample_value: Option<Vec<u16>>,
62
63 pub(crate) x_resolution: Option<f64>,
65
66 pub(crate) y_resolution: Option<f64>,
68
69 pub(crate) planar_configuration: PlanarConfiguration,
84
85 pub(crate) resolution_unit: Option<ResolutionUnit>,
86
87 pub(crate) software: Option<String>,
89
90 pub(crate) date_time: Option<String>,
96 pub(crate) artist: Option<String>,
97 pub(crate) host_computer: Option<String>,
98
99 pub(crate) predictor: Option<Predictor>,
100
101 pub(crate) color_map: Option<Vec<u16>>,
121
122 pub(crate) tile_width: Option<u32>,
123 pub(crate) tile_height: Option<u32>,
124
125 pub(crate) tile_offsets: Option<Vec<u64>>,
126 pub(crate) tile_byte_counts: Option<Vec<u64>>,
127
128 pub(crate) extra_samples: Option<Vec<u16>>,
129
130 pub(crate) sample_format: Vec<SampleFormat>,
131
132 pub(crate) jpeg_tables: Option<Bytes>,
133
134 pub(crate) copyright: Option<String>,
135
136 pub(crate) geo_key_directory: Option<GeoKeyDirectory>,
138 pub(crate) model_pixel_scale: Option<Vec<f64>>,
139 pub(crate) model_tiepoint: Option<Vec<f64>>,
140 pub(crate) model_transformation: Option<Vec<f64>>,
141
142 pub(crate) gdal_nodata: Option<String>,
144 pub(crate) gdal_metadata: Option<String>,
145 pub(crate) other_tags: HashMap<Tag, TagValue>,
146}
147
148impl ImageFileDirectory {
149 pub fn from_tags(
151 tag_data: HashMap<Tag, TagValue>,
152 endianness: Endianness,
153 ) -> AsyncTiffResult<Self> {
154 let mut new_subfile_type = None;
155 let mut image_width = None;
156 let mut image_height = None;
157 let mut bits_per_sample = None;
158 let mut compression = None;
159 let mut photometric_interpretation = None;
160 let mut document_name = None;
161 let mut image_description = None;
162 let mut strip_offsets = None;
163 let mut orientation = None;
164 let mut samples_per_pixel = None;
165 let mut rows_per_strip = None;
166 let mut strip_byte_counts = None;
167 let mut min_sample_value = None;
168 let mut max_sample_value = None;
169 let mut x_resolution = None;
170 let mut y_resolution = None;
171 let mut planar_configuration = None;
172 let mut resolution_unit = None;
173 let mut software = None;
174 let mut date_time = None;
175 let mut artist = None;
176 let mut host_computer = None;
177 let mut predictor = None;
178 let mut color_map = None;
179 let mut tile_width = None;
180 let mut tile_height = None;
181 let mut tile_offsets = None;
182 let mut tile_byte_counts = None;
183 let mut extra_samples = None;
184 let mut sample_format = None;
185 let mut jpeg_tables = None;
186 let mut copyright = None;
187 let mut geo_key_directory_data = None;
188 let mut model_pixel_scale = None;
189 let mut model_tiepoint = None;
190 let mut model_transformation = None;
191 let mut geo_ascii_params: Option<String> = None;
192 let mut geo_double_params: Option<Vec<f64>> = None;
193 let mut gdal_nodata = None;
194 let mut gdal_metadata = None;
195
196 let mut other_tags = HashMap::new();
197
198 tag_data.into_iter().try_for_each(|(tag, value)| {
199 match tag {
200 Tag::NewSubfileType => new_subfile_type = Some(value.into_u32()?),
201 Tag::ImageWidth => image_width = Some(value.into_u32()?),
202 Tag::ImageLength => image_height = Some(value.into_u32()?),
203 Tag::BitsPerSample => bits_per_sample = Some(value.into_u16_vec()?),
204 Tag::Compression => {
205 compression = Some(CompressionMethod::from_u16_exhaustive(value.into_u16()?))
206 }
207 Tag::PhotometricInterpretation => {
208 photometric_interpretation =
209 PhotometricInterpretation::from_u16(value.into_u16()?)
210 }
211 Tag::ImageDescription => image_description = Some(value.into_string()?),
212 Tag::StripOffsets => strip_offsets = Some(value.into_u64_vec()?),
213 Tag::Orientation => orientation = Some(value.into_u16()?),
214 Tag::SamplesPerPixel => samples_per_pixel = Some(value.into_u16()?),
215 Tag::RowsPerStrip => rows_per_strip = Some(value.into_u32()?),
216 Tag::StripByteCounts => strip_byte_counts = Some(value.into_u64_vec()?),
217 Tag::MinSampleValue => min_sample_value = Some(value.into_u16_vec()?),
218 Tag::MaxSampleValue => max_sample_value = Some(value.into_u16_vec()?),
219 Tag::XResolution => match value {
220 TagValue::Rational(n, d) => x_resolution = Some(n as f64 / d as f64),
221 _ => unreachable!("Expected rational type for XResolution."),
222 },
223 Tag::YResolution => match value {
224 TagValue::Rational(n, d) => y_resolution = Some(n as f64 / d as f64),
225 _ => unreachable!("Expected rational type for YResolution."),
226 },
227 Tag::PlanarConfiguration => {
228 planar_configuration = PlanarConfiguration::from_u16(value.into_u16()?)
229 }
230 Tag::ResolutionUnit => {
231 resolution_unit = ResolutionUnit::from_u16(value.into_u16()?)
232 }
233 Tag::Software => software = Some(value.into_string()?),
234 Tag::DateTime => date_time = Some(value.into_string()?),
235 Tag::Artist => artist = Some(value.into_string()?),
236 Tag::HostComputer => host_computer = Some(value.into_string()?),
237 Tag::Predictor => predictor = Predictor::from_u16(value.into_u16()?),
238 Tag::ColorMap => color_map = Some(value.into_u16_vec()?),
239 Tag::TileWidth => tile_width = Some(value.into_u32()?),
240 Tag::TileLength => tile_height = Some(value.into_u32()?),
241 Tag::TileOffsets => tile_offsets = Some(value.into_u64_vec()?),
242 Tag::TileByteCounts => tile_byte_counts = Some(value.into_u64_vec()?),
243 Tag::ExtraSamples => extra_samples = Some(value.into_u16_vec()?),
244 Tag::SampleFormat => {
245 let values = value.into_u16_vec()?;
246 sample_format = Some(
247 values
248 .into_iter()
249 .map(SampleFormat::from_u16_exhaustive)
250 .collect(),
251 );
252 }
253 Tag::JPEGTables => jpeg_tables = Some(value.into_u8_vec()?.into()),
254 Tag::Copyright => copyright = Some(value.into_string()?),
255
256 Tag::GeoKeyDirectory => geo_key_directory_data = Some(value.into_u16_vec()?),
259 Tag::ModelPixelScale => model_pixel_scale = Some(value.into_f64_vec()?),
260 Tag::ModelTiepoint => model_tiepoint = Some(value.into_f64_vec()?),
261 Tag::ModelTransformation => model_transformation = Some(value.into_f64_vec()?),
262 Tag::GeoAsciiParams => geo_ascii_params = Some(value.into_string()?),
263 Tag::GeoDoubleParams => geo_double_params = Some(value.into_f64_vec()?),
264 Tag::GdalNodata => gdal_nodata = Some(value.into_string()?),
265 Tag::GdalMetadata => gdal_metadata = Some(value.into_string()?),
266 Tag::Unknown(DOCUMENT_NAME) => document_name = Some(value.into_string()?),
268 _ => {
269 other_tags.insert(tag, value);
270 }
271 };
272 Ok::<_, TiffError>(())
273 })?;
274
275 let mut geo_key_directory = None;
276
277 if let Some(data) = geo_key_directory_data {
280 let mut chunks = data.chunks(4);
281
282 let header = chunks
283 .next()
284 .expect("If the geo key directory exists, a header should exist.");
285 let key_directory_version = header[0];
286 assert_eq!(key_directory_version, 1);
287
288 let key_revision = header[1];
289 assert_eq!(key_revision, 1);
290
291 let _key_minor_revision = header[2];
292 let number_of_keys = header[3];
293
294 let mut tags = HashMap::with_capacity(number_of_keys as usize);
295 for _ in 0..number_of_keys {
296 let chunk = chunks
297 .next()
298 .expect("There should be a chunk for each key.");
299
300 let key_id = chunk[0];
301 let tag_name = if let Ok(tag_name) = GeoKeyTag::try_from_primitive(key_id) {
302 tag_name
303 } else {
304 continue;
309 };
310
311 let tag_location = chunk[1];
312 let count = chunk[2];
313 let value_offset = chunk[3];
314
315 if tag_location == 0 {
316 tags.insert(tag_name, TagValue::Short(value_offset));
317 } else if Tag::from_u16_exhaustive(tag_location) == Tag::GeoAsciiParams {
318 let geo_ascii_params = geo_ascii_params
322 .as_ref()
323 .expect("GeoAsciiParamsTag exists but geo_ascii_params does not.");
324 let value_offset = value_offset as usize;
325 let mut s = &geo_ascii_params[value_offset..value_offset + count as usize];
326
327 if s.ends_with('|') {
330 s = &s[0..s.len() - 1];
331 }
332
333 tags.insert(tag_name, TagValue::Ascii(s.to_string()));
334 } else if Tag::from_u16_exhaustive(tag_location) == Tag::GeoDoubleParams {
335 let geo_double_params = geo_double_params
339 .as_ref()
340 .expect("GeoDoubleParamsTag exists but geo_double_params does not.");
341 let value_offset = value_offset as usize;
342 let value = if count == 1 {
343 TagValue::Double(geo_double_params[value_offset])
344 } else {
345 let x = geo_double_params[value_offset..value_offset + count as usize]
346 .iter()
347 .map(|val| TagValue::Double(*val))
348 .collect();
349 TagValue::List(x)
350 };
351 tags.insert(tag_name, value);
352 }
353 }
354 geo_key_directory = Some(GeoKeyDirectory::from_tags(tags)?);
355 }
356
357 let samples_per_pixel = samples_per_pixel.expect("samples_per_pixel not found");
358 let planar_configuration = if let Some(planar_configuration) = planar_configuration {
359 planar_configuration
360 } else if samples_per_pixel == 1 {
361 PlanarConfiguration::Chunky
364 } else {
365 PlanarConfiguration::Chunky
366 };
367 Ok(Self {
368 endianness,
369 new_subfile_type,
370 image_width: image_width.expect("image_width not found"),
371 image_height: image_height.expect("image_height not found"),
372 bits_per_sample: bits_per_sample.expect("bits per sample not found"),
373 compression: compression.unwrap_or(CompressionMethod::None),
376 photometric_interpretation: photometric_interpretation
377 .expect("photometric interpretation not found"),
378 document_name,
379 image_description,
380 strip_offsets,
381 orientation,
382 samples_per_pixel,
383 rows_per_strip,
384 strip_byte_counts,
385 min_sample_value,
386 max_sample_value,
387 x_resolution,
388 y_resolution,
389 planar_configuration,
390 resolution_unit,
391 software,
392 date_time,
393 artist,
394 host_computer,
395 predictor,
396 color_map,
397 tile_width,
398 tile_height,
399 tile_offsets,
400 tile_byte_counts,
401 extra_samples,
402 sample_format: sample_format
405 .unwrap_or(vec![SampleFormat::Uint; samples_per_pixel as _]),
406 copyright,
407 jpeg_tables,
408 geo_key_directory,
409 model_pixel_scale,
410 model_tiepoint,
411 model_transformation,
412 gdal_nodata,
413 gdal_metadata,
414 other_tags,
415 })
416 }
417
418 pub fn new_subfile_type(&self) -> Option<u32> {
421 self.new_subfile_type
422 }
423
424 pub fn image_width(&self) -> u32 {
427 self.image_width
428 }
429
430 pub fn image_height(&self) -> u32 {
433 self.image_height
434 }
435
436 pub fn bits_per_sample(&self) -> &[u16] {
439 &self.bits_per_sample
440 }
441
442 pub fn compression(&self) -> CompressionMethod {
445 self.compression
446 }
447
448 pub fn photometric_interpretation(&self) -> PhotometricInterpretation {
451 self.photometric_interpretation
452 }
453
454 pub fn document_name(&self) -> Option<&str> {
456 self.document_name.as_deref()
457 }
458
459 pub fn image_description(&self) -> Option<&str> {
462 self.image_description.as_deref()
463 }
464
465 pub fn strip_offsets(&self) -> Option<&[u64]> {
468 self.strip_offsets.as_deref()
469 }
470
471 pub fn orientation(&self) -> Option<u16> {
474 self.orientation
475 }
476
477 pub fn samples_per_pixel(&self) -> u16 {
483 self.samples_per_pixel
484 }
485
486 pub fn rows_per_strip(&self) -> Option<u32> {
489 self.rows_per_strip
490 }
491
492 pub fn strip_byte_counts(&self) -> Option<&[u64]> {
495 self.strip_byte_counts.as_deref()
496 }
497
498 pub fn min_sample_value(&self) -> Option<&[u16]> {
501 self.min_sample_value.as_deref()
502 }
503
504 pub fn max_sample_value(&self) -> Option<&[u16]> {
507 self.max_sample_value.as_deref()
508 }
509
510 pub fn x_resolution(&self) -> Option<f64> {
513 self.x_resolution
514 }
515
516 pub fn y_resolution(&self) -> Option<f64> {
519 self.y_resolution
520 }
521
522 pub fn planar_configuration(&self) -> PlanarConfiguration {
539 self.planar_configuration
540 }
541
542 pub fn resolution_unit(&self) -> Option<ResolutionUnit> {
545 self.resolution_unit
546 }
547
548 pub fn software(&self) -> Option<&str> {
551 self.software.as_deref()
552 }
553
554 pub fn date_time(&self) -> Option<&str> {
562 self.date_time.as_deref()
563 }
564
565 pub fn artist(&self) -> Option<&str> {
568 self.artist.as_deref()
569 }
570
571 pub fn host_computer(&self) -> Option<&str> {
574 self.host_computer.as_deref()
575 }
576
577 pub fn predictor(&self) -> Option<Predictor> {
581 self.predictor
582 }
583
584 pub fn tile_width(&self) -> Option<u32> {
587 self.tile_width
588 }
589
590 pub fn tile_height(&self) -> Option<u32> {
593 self.tile_height
594 }
595
596 pub fn tile_offsets(&self) -> Option<&[u64]> {
599 self.tile_offsets.as_deref()
600 }
601
602 pub fn tile_byte_counts(&self) -> Option<&[u64]> {
605 self.tile_byte_counts.as_deref()
606 }
607
608 pub fn extra_samples(&self) -> Option<&[u16]> {
611 self.extra_samples.as_deref()
612 }
613
614 pub fn sample_format(&self) -> &[SampleFormat] {
617 &self.sample_format
618 }
619
620 pub fn jpeg_tables(&self) -> Option<&[u8]> {
623 self.jpeg_tables.as_deref()
624 }
625
626 pub fn copyright(&self) -> Option<&str> {
629 self.copyright.as_deref()
630 }
631
632 pub fn geo_key_directory(&self) -> Option<&GeoKeyDirectory> {
635 self.geo_key_directory.as_ref()
636 }
637
638 pub fn model_pixel_scale(&self) -> Option<&[f64]> {
641 self.model_pixel_scale.as_deref()
642 }
643
644 pub fn model_tiepoint(&self) -> Option<&[f64]> {
647 self.model_tiepoint.as_deref()
648 }
649
650 pub fn model_transformation(&self) -> Option<&[f64]> {
653 self.model_transformation.as_deref()
654 }
655
656 pub fn gdal_nodata(&self) -> Option<&str> {
659 self.gdal_nodata.as_deref()
660 }
661
662 pub fn gdal_metadata(&self) -> Option<&str> {
667 self.gdal_metadata.as_deref()
668 }
669
670 pub fn other_tags(&self) -> &HashMap<Tag, TagValue> {
672 &self.other_tags
673 }
674
675 pub fn colormap(&self) -> Option<HashMap<usize, [u8; 3]>> {
677 fn cmap_transform(val: u16) -> u8 {
678 let val = ((val as f64 / 65535.0) * 255.0).floor();
679 if val >= 255.0 {
680 255
681 } else if val < 0.0 {
682 0
683 } else {
684 val as u8
685 }
686 }
687
688 if let Some(cmap_data) = &self.color_map {
689 let bits_per_sample = self.bits_per_sample[0];
690 let count = 2_usize.pow(bits_per_sample as u32);
691 let mut result = HashMap::new();
692
693 for idx in 0..count {
695 let color: [u8; 3] =
696 std::array::from_fn(|i| cmap_transform(cmap_data[idx + i * count]));
697 result.insert(idx, color);
700 }
701
702 Some(result)
703 } else {
704 None
705 }
706 }
707
708 fn get_tile_byte_range(&self, x: usize, y: usize) -> Option<Range<u64>> {
709 let tile_offsets = self.tile_offsets.as_deref()?;
710 let tile_byte_counts = self.tile_byte_counts.as_deref()?;
711 let idx = (y * self.tile_count()?.0) + x;
712 let offset = tile_offsets[idx] as usize;
713 let byte_count = tile_byte_counts[idx] as usize;
715 Some(offset as _..(offset + byte_count) as _)
716 }
717
718 pub async fn fetch_tile(
720 &self,
721 x: usize,
722 y: usize,
723 reader: &dyn AsyncFileReader,
724 ) -> AsyncTiffResult<Tile> {
725 let range = self
726 .get_tile_byte_range(x, y)
727 .ok_or(AsyncTiffError::General("Not a tiled TIFF".to_string()))?;
728 let compressed_bytes = reader.get_bytes(range).await?;
729 let data_type = DataType::from_tags(&self.sample_format, &self.bits_per_sample);
730 Ok(Tile {
731 x,
732 y,
733 data_type,
734 width: self.tile_width.unwrap_or(self.image_width),
735 height: self.tile_height.unwrap_or(self.image_height),
736 planar_configuration: self.planar_configuration,
737 samples_per_pixel: self.samples_per_pixel,
738 predictor: self.predictor.unwrap_or(Predictor::None),
739 predictor_info: PredictorInfo::from_ifd(self),
740 compressed_bytes,
741 compression_method: self.compression,
742 photometric_interpretation: self.photometric_interpretation,
743 jpeg_tables: self.jpeg_tables.clone(),
744 })
745 }
746
747 pub async fn fetch_tiles(
749 &self,
750 x: &[usize],
751 y: &[usize],
752 reader: &dyn AsyncFileReader,
753 ) -> AsyncTiffResult<Vec<Tile>> {
754 assert_eq!(x.len(), y.len(), "x and y should have same len");
755
756 let predictor_info = PredictorInfo::from_ifd(self);
757 let data_type = DataType::from_tags(&self.sample_format, &self.bits_per_sample);
758
759 let byte_ranges = x
761 .iter()
762 .zip(y)
763 .map(|(x, y)| {
764 self.get_tile_byte_range(*x, *y)
765 .ok_or(AsyncTiffError::General("Not a tiled TIFF".to_string()))
766 })
767 .collect::<AsyncTiffResult<Vec<_>>>()?;
768
769 let buffers = reader.get_byte_ranges(byte_ranges).await?;
771
772 let mut tiles = vec![];
774 for ((compressed_bytes, &x), &y) in buffers.into_iter().zip(x).zip(y) {
775 let tile = Tile {
776 x,
777 y,
778 data_type,
779 width: self.tile_width.unwrap_or(self.image_width),
780 height: self.tile_height.unwrap_or(self.image_height),
781 planar_configuration: self.planar_configuration,
782 samples_per_pixel: self.samples_per_pixel,
783 predictor: self.predictor.unwrap_or(Predictor::None),
784 predictor_info,
785 compressed_bytes,
786 compression_method: self.compression,
787 photometric_interpretation: self.photometric_interpretation,
788 jpeg_tables: self.jpeg_tables.clone(),
789 };
790 tiles.push(tile);
791 }
792 Ok(tiles)
793 }
794
795 pub fn tile_count(&self) -> Option<(usize, usize)> {
798 let x_count = (self.image_width as f64 / self.tile_width? as f64).ceil();
799 let y_count = (self.image_height as f64 / self.tile_height? as f64).ceil();
800 Some((x_count as usize, y_count as usize))
801 }
802}
803
804#[allow(dead_code)]
821pub(crate) fn compute_tile_dimensions(
825 x: usize,
826 y: usize,
827 image_width: u32,
828 image_height: u32,
829 tile_width: Option<u32>,
830 tile_height: Option<u32>,
831 rows_per_strip: Option<u32>,
832) -> (u32, u32) {
833 if let (Some(tile_width), Some(tile_height)) = (tile_width, tile_height) {
835 let x_offset = (x as u32) * tile_width;
836 let y_offset = (y as u32) * tile_height;
837
838 let actual_width = std::cmp::min(tile_width, image_width.saturating_sub(x_offset));
839 let actual_height = std::cmp::min(tile_height, image_height.saturating_sub(y_offset));
840
841 (actual_width, actual_height)
842 } else {
843 let strip_height = rows_per_strip.unwrap_or(image_height);
845 let y_offset = (y as u32) * strip_height;
846 let actual_height = std::cmp::min(strip_height, image_height.saturating_sub(y_offset));
847
848 (image_width, actual_height)
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 #[test]
857 fn test_tile_dimensions_full_tiles() {
858 assert_eq!(
860 compute_tile_dimensions(0, 0, 512, 512, Some(256), Some(256), None),
861 (256, 256),
862 "Top-left tile should be full size"
863 );
864 assert_eq!(
865 compute_tile_dimensions(1, 0, 512, 512, Some(256), Some(256), None),
866 (256, 256),
867 "Top-right tile should be full size"
868 );
869 assert_eq!(
870 compute_tile_dimensions(0, 1, 512, 512, Some(256), Some(256), None),
871 (256, 256),
872 "Bottom-left tile should be full size"
873 );
874 assert_eq!(
875 compute_tile_dimensions(1, 1, 512, 512, Some(256), Some(256), None),
876 (256, 256),
877 "Bottom-right tile should be full size"
878 );
879 }
880
881 #[test]
882 fn test_tile_dimensions_edge_tiles() {
883 assert_eq!(
885 compute_tile_dimensions(0, 0, 500, 500, Some(256), Some(256), None),
886 (256, 256),
887 "Top-left tile should be full size"
888 );
889 assert_eq!(
890 compute_tile_dimensions(1, 0, 500, 500, Some(256), Some(256), None),
891 (244, 256),
892 "Top-right edge tile should be 244 pixels wide"
893 );
894 assert_eq!(
895 compute_tile_dimensions(0, 1, 500, 500, Some(256), Some(256), None),
896 (256, 244),
897 "Bottom-left edge tile should be 244 pixels tall"
898 );
899 assert_eq!(
900 compute_tile_dimensions(1, 1, 500, 500, Some(256), Some(256), None),
901 (244, 244),
902 "Bottom-right corner tile should be 244x244"
903 );
904 }
905
906 #[test]
907 fn test_strip_dimensions() {
908 assert_eq!(
910 compute_tile_dimensions(0, 0, 1024, 768, None, None, Some(128)),
911 (1024, 128),
912 "First strip should be full width and height"
913 );
914 assert_eq!(
915 compute_tile_dimensions(0, 5, 1024, 768, None, None, Some(128)),
916 (1024, 128),
917 "Middle strip should be full size"
918 );
919 assert_eq!(
920 compute_tile_dimensions(0, 5, 1024, 768, None, None, Some(128)),
921 (1024, 128),
922 "Last strip (768 / 128 = 6 strips, index 5) should be full height"
923 );
924 }
925
926 #[test]
927 fn test_strip_dimensions_partial_last_strip() {
928 assert_eq!(
931 compute_tile_dimensions(0, 0, 1024, 700, None, None, Some(128)),
932 (1024, 128),
933 "First strip should be full height"
934 );
935 assert_eq!(
936 compute_tile_dimensions(0, 5, 1024, 700, None, None, Some(128)),
937 (1024, 60),
938 "Last strip should be 60 pixels tall"
939 );
940 }
941
942 #[test]
943 fn test_single_tile_image() {
944 assert_eq!(
946 compute_tile_dimensions(0, 0, 100, 100, Some(256), Some(256), None),
947 (100, 100),
948 "Single tile should match image dimensions"
949 );
950 }
951
952 #[test]
953 fn test_strip_default_height() {
954 assert_eq!(
956 compute_tile_dimensions(0, 0, 1024, 768, None, None, None),
957 (1024, 768),
958 "Strip should default to full image height"
959 );
960 }
961}