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