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