Skip to main content

tiff_core/
constants.rs

1// Well-known TIFF tag codes.
2pub const TAG_NEW_SUBFILE_TYPE: u16 = 254;
3pub const TAG_SUBFILE_TYPE: u16 = 255;
4pub const TAG_IMAGE_WIDTH: u16 = 256;
5pub const TAG_IMAGE_LENGTH: u16 = 257;
6pub const TAG_BITS_PER_SAMPLE: u16 = 258;
7pub const TAG_COMPRESSION: u16 = 259;
8pub const TAG_PHOTOMETRIC_INTERPRETATION: u16 = 262;
9pub const TAG_STRIP_OFFSETS: u16 = 273;
10pub const TAG_SAMPLES_PER_PIXEL: u16 = 277;
11pub const TAG_ROWS_PER_STRIP: u16 = 278;
12pub const TAG_STRIP_BYTE_COUNTS: u16 = 279;
13pub const TAG_PLANAR_CONFIGURATION: u16 = 284;
14pub const TAG_PREDICTOR: u16 = 317;
15pub const TAG_COLOR_MAP: u16 = 320;
16pub const TAG_TILE_WIDTH: u16 = 322;
17pub const TAG_TILE_LENGTH: u16 = 323;
18pub const TAG_TILE_OFFSETS: u16 = 324;
19pub const TAG_TILE_BYTE_COUNTS: u16 = 325;
20pub const TAG_SUB_IFDS: u16 = 330;
21pub const TAG_INK_SET: u16 = 332;
22pub const TAG_EXTRA_SAMPLES: u16 = 338;
23pub const TAG_SAMPLE_FORMAT: u16 = 339;
24pub const TAG_JPEG_TABLES: u16 = 347;
25pub const TAG_YCBCR_SUBSAMPLING: u16 = 530;
26pub const TAG_YCBCR_POSITIONING: u16 = 531;
27pub const TAG_REFERENCE_BLACK_WHITE: u16 = 532;
28pub const TAG_LERC_PARAMETERS: u16 = 50674;
29
30/// TIFF-side LERC version value stored in `TAG_LERC_PARAMETERS`.
31///
32/// This matches libtiff/GDAL's `LERC_VERSION_2_4` value.
33pub const LERC_VERSION_2_4: u32 = 4;
34
35/// TIFF compression scheme.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
37pub enum Compression {
38    None,
39    Lzw,
40    OldJpeg,
41    Jpeg,
42    Deflate,
43    PackBits,
44    DeflateOld,
45    Lerc,
46    Zstd,
47}
48
49impl Compression {
50    pub fn from_code(code: u16) -> Option<Self> {
51        match code {
52            1 => Some(Self::None),
53            5 => Some(Self::Lzw),
54            6 => Some(Self::OldJpeg),
55            7 => Some(Self::Jpeg),
56            8 => Some(Self::Deflate),
57            32773 => Some(Self::PackBits),
58            32946 => Some(Self::DeflateOld),
59            34887 => Some(Self::Lerc),
60            50000 => Some(Self::Zstd),
61            _ => None,
62        }
63    }
64
65    pub fn to_code(self) -> u16 {
66        match self {
67            Self::None => 1,
68            Self::Lzw => 5,
69            Self::OldJpeg => 6,
70            Self::Jpeg => 7,
71            Self::Deflate => 8,
72            Self::PackBits => 32773,
73            Self::DeflateOld => 32946,
74            Self::Lerc => 34887,
75            Self::Zstd => 50000,
76        }
77    }
78
79    pub fn name(self) -> &'static str {
80        match self {
81            Self::None => "None",
82            Self::Lzw => "LZW",
83            Self::OldJpeg => "OldJpeg",
84            Self::Jpeg => "JPEG",
85            Self::Deflate => "Deflate",
86            Self::PackBits => "PackBits",
87            Self::DeflateOld => "DeflateOld",
88            Self::Lerc => "LERC",
89            Self::Zstd => "ZSTD",
90        }
91    }
92}
93
94/// TIFF predictor scheme.
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
96pub enum Predictor {
97    None,
98    Horizontal,
99    FloatingPoint,
100}
101
102impl Predictor {
103    pub fn from_code(code: u16) -> Option<Self> {
104        match code {
105            1 => Some(Self::None),
106            2 => Some(Self::Horizontal),
107            3 => Some(Self::FloatingPoint),
108            _ => None,
109        }
110    }
111
112    pub fn to_code(self) -> u16 {
113        match self {
114            Self::None => 1,
115            Self::Horizontal => 2,
116            Self::FloatingPoint => 3,
117        }
118    }
119}
120
121/// TIFF sample format.
122#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
123pub enum SampleFormat {
124    Uint,
125    Int,
126    Float,
127}
128
129impl SampleFormat {
130    pub fn from_code(code: u16) -> Option<Self> {
131        match code {
132            1 => Some(Self::Uint),
133            2 => Some(Self::Int),
134            3 => Some(Self::Float),
135            _ => None,
136        }
137    }
138
139    pub fn to_code(self) -> u16 {
140        match self {
141            Self::Uint => 1,
142            Self::Int => 2,
143            Self::Float => 3,
144        }
145    }
146}
147
148/// TIFF photometric interpretation.
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
150pub enum PhotometricInterpretation {
151    MinIsWhite,
152    MinIsBlack,
153    Rgb,
154    Palette,
155    Mask,
156    Separated,
157    YCbCr,
158    CieLab,
159}
160
161impl PhotometricInterpretation {
162    pub fn from_code(code: u16) -> Option<Self> {
163        match code {
164            0 => Some(Self::MinIsWhite),
165            1 => Some(Self::MinIsBlack),
166            2 => Some(Self::Rgb),
167            3 => Some(Self::Palette),
168            4 => Some(Self::Mask),
169            5 => Some(Self::Separated),
170            6 => Some(Self::YCbCr),
171            8 => Some(Self::CieLab),
172            _ => None,
173        }
174    }
175
176    pub fn to_code(self) -> u16 {
177        match self {
178            Self::MinIsWhite => 0,
179            Self::MinIsBlack => 1,
180            Self::Rgb => 2,
181            Self::Palette => 3,
182            Self::Mask => 4,
183            Self::Separated => 5,
184            Self::YCbCr => 6,
185            Self::CieLab => 8,
186        }
187    }
188}
189
190/// TIFF ExtraSamples semantic.
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
192pub enum ExtraSample {
193    Unspecified,
194    AssociatedAlpha,
195    UnassociatedAlpha,
196    Unknown(u16),
197}
198
199impl ExtraSample {
200    pub fn from_code(code: u16) -> Self {
201        match code {
202            0 => Self::Unspecified,
203            1 => Self::AssociatedAlpha,
204            2 => Self::UnassociatedAlpha,
205            other => Self::Unknown(other),
206        }
207    }
208
209    pub fn to_code(self) -> u16 {
210        match self {
211            Self::Unspecified => 0,
212            Self::AssociatedAlpha => 1,
213            Self::UnassociatedAlpha => 2,
214            Self::Unknown(code) => code,
215        }
216    }
217}
218
219/// TIFF YCbCr chroma sample positioning.
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221pub enum YCbCrPositioning {
222    Centered,
223    Cosited,
224    Unknown(u16),
225}
226
227impl YCbCrPositioning {
228    pub fn from_code(code: u16) -> Self {
229        match code {
230            1 => Self::Centered,
231            2 => Self::Cosited,
232            other => Self::Unknown(other),
233        }
234    }
235
236    pub fn to_code(self) -> u16 {
237        match self {
238            Self::Centered => 1,
239            Self::Cosited => 2,
240            Self::Unknown(code) => code,
241        }
242    }
243}
244
245/// TIFF InkSet semantics for separated photometric data.
246#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
247pub enum InkSet {
248    Cmyk,
249    NotCmyk,
250    Unknown(u16),
251}
252
253impl InkSet {
254    pub fn from_code(code: u16) -> Self {
255        match code {
256            1 => Self::Cmyk,
257            2 => Self::NotCmyk,
258            other => Self::Unknown(other),
259        }
260    }
261
262    pub fn to_code(self) -> u16 {
263        match self {
264            Self::Cmyk => 1,
265            Self::NotCmyk => 2,
266            Self::Unknown(code) => code,
267        }
268    }
269}
270
271/// TIFF palette ColorMap values split into RGB planes.
272#[derive(Debug, Clone, PartialEq, Eq)]
273pub struct ColorMap {
274    red: Vec<u16>,
275    green: Vec<u16>,
276    blue: Vec<u16>,
277}
278
279impl ColorMap {
280    pub fn new(red: Vec<u16>, green: Vec<u16>, blue: Vec<u16>) -> Result<Self, String> {
281        let len = red.len();
282        if green.len() != len || blue.len() != len {
283            return Err(format!(
284                "ColorMap planes must have equal length, got red={}, green={}, blue={}",
285                red.len(),
286                green.len(),
287                blue.len()
288            ));
289        }
290        Ok(Self { red, green, blue })
291    }
292
293    pub fn from_tag_values(values: &[u16]) -> Result<Self, String> {
294        if values.len() % 3 != 0 {
295            return Err(format!(
296                "ColorMap tag length must be divisible by 3, got {} values",
297                values.len()
298            ));
299        }
300        let plane_len = values.len() / 3;
301        Self::new(
302            values[..plane_len].to_vec(),
303            values[plane_len..plane_len * 2].to_vec(),
304            values[plane_len * 2..].to_vec(),
305        )
306    }
307
308    pub fn len(&self) -> usize {
309        self.red.len()
310    }
311
312    pub fn is_empty(&self) -> bool {
313        self.red.is_empty()
314    }
315
316    pub fn red(&self) -> &[u16] {
317        &self.red
318    }
319
320    pub fn green(&self) -> &[u16] {
321        &self.green
322    }
323
324    pub fn blue(&self) -> &[u16] {
325        &self.blue
326    }
327
328    pub fn encode_tag_values(&self) -> Vec<u16> {
329        let mut values = Vec::with_capacity(self.len() * 3);
330        values.extend_from_slice(&self.red);
331        values.extend_from_slice(&self.green);
332        values.extend_from_slice(&self.blue);
333        values
334    }
335}
336
337/// Structured interpretation of TIFF photometric and ancillary color tags.
338#[derive(Debug, Clone, PartialEq, Eq)]
339pub enum ColorModel {
340    Grayscale {
341        white_is_zero: bool,
342        extra_samples: Vec<ExtraSample>,
343    },
344    Palette {
345        color_map: ColorMap,
346        extra_samples: Vec<ExtraSample>,
347    },
348    Rgb {
349        extra_samples: Vec<ExtraSample>,
350    },
351    TransparencyMask,
352    Cmyk {
353        extra_samples: Vec<ExtraSample>,
354    },
355    Separated {
356        ink_set: InkSet,
357        color_channels: u16,
358        extra_samples: Vec<ExtraSample>,
359    },
360    YCbCr {
361        subsampling: [u16; 2],
362        positioning: YCbCrPositioning,
363        extra_samples: Vec<ExtraSample>,
364    },
365    CieLab {
366        extra_samples: Vec<ExtraSample>,
367    },
368}
369
370/// TIFF planar configuration.
371#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
372pub enum PlanarConfiguration {
373    Chunky,
374    Planar,
375}
376
377impl PlanarConfiguration {
378    pub fn from_code(code: u16) -> Option<Self> {
379        match code {
380            1 => Some(Self::Chunky),
381            2 => Some(Self::Planar),
382            _ => None,
383        }
384    }
385
386    pub fn to_code(self) -> u16 {
387        match self {
388            Self::Chunky => 1,
389            Self::Planar => 2,
390        }
391    }
392}
393
394/// TIFF-side LERC additional compression mode.
395///
396/// When LERC is the primary compression (tag 259 = 34887), the LERC blob
397/// may optionally be wrapped in an additional compression layer. The mode
398/// is recorded in the `LercParameters` tag (50674).
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
400pub enum LercAdditionalCompression {
401    None,
402    Deflate,
403    Zstd,
404}
405
406impl LercAdditionalCompression {
407    pub fn from_code(code: u32) -> Option<Self> {
408        match code {
409            0 => Some(Self::None),
410            1 => Some(Self::Deflate),
411            2 => Some(Self::Zstd),
412            _ => None,
413        }
414    }
415
416    pub fn to_code(self) -> u32 {
417        match self {
418            Self::None => 0,
419            Self::Deflate => 1,
420            Self::Zstd => 2,
421        }
422    }
423}
424
425#[cfg(test)]
426mod tests {
427    use super::*;
428
429    #[test]
430    fn compression_roundtrips_lerc() {
431        assert_eq!(Compression::from_code(34887), Some(Compression::Lerc));
432        assert_eq!(Compression::Lerc.to_code(), 34887);
433        assert_eq!(Compression::Lerc.name(), "LERC");
434    }
435
436    #[test]
437    fn lerc_parameters_tag_matches_registered_value() {
438        assert_eq!(TAG_LERC_PARAMETERS, 50674);
439    }
440
441    #[test]
442    fn lerc_additional_compression_roundtrips() {
443        for (code, expected) in [
444            (0, LercAdditionalCompression::None),
445            (1, LercAdditionalCompression::Deflate),
446            (2, LercAdditionalCompression::Zstd),
447        ] {
448            assert_eq!(LercAdditionalCompression::from_code(code), Some(expected));
449            assert_eq!(expected.to_code(), code);
450        }
451        assert_eq!(LercAdditionalCompression::from_code(99), None);
452    }
453
454    #[test]
455    fn photometric_roundtrips_extended_color_models() {
456        for (code, expected) in [
457            (5, PhotometricInterpretation::Separated),
458            (6, PhotometricInterpretation::YCbCr),
459            (8, PhotometricInterpretation::CieLab),
460        ] {
461            assert_eq!(PhotometricInterpretation::from_code(code), Some(expected));
462            assert_eq!(expected.to_code(), code);
463        }
464    }
465
466    #[test]
467    fn color_map_splits_tag_values_into_rgb_planes() {
468        let values = vec![1u16, 2, 10, 20, 100, 200];
469        let color_map = ColorMap::from_tag_values(&values).unwrap();
470        assert_eq!(color_map.red(), &[1, 2]);
471        assert_eq!(color_map.green(), &[10, 20]);
472        assert_eq!(color_map.blue(), &[100, 200]);
473        assert_eq!(color_map.encode_tag_values(), values);
474    }
475
476    #[test]
477    fn extra_sample_and_ink_set_roundtrip() {
478        assert_eq!(ExtraSample::from_code(1), ExtraSample::AssociatedAlpha);
479        assert_eq!(ExtraSample::UnassociatedAlpha.to_code(), 2);
480        assert_eq!(InkSet::from_code(1), InkSet::Cmyk);
481        assert_eq!(InkSet::NotCmyk.to_code(), 2);
482    }
483}