Skip to main content

oxihuman_export/
tiff_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! TIFF image stub export.
6
7/// TIFF compression type.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum TiffCompression {
10    None,
11    Lzw,
12    Deflate,
13}
14
15impl TiffCompression {
16    /// Return TIFF compression tag value.
17    pub fn tag_value(&self) -> u16 {
18        match self {
19            Self::None => 1,
20            Self::Lzw => 5,
21            Self::Deflate => 8,
22        }
23    }
24}
25
26/// TIFF export options.
27#[derive(Debug, Clone)]
28pub struct TiffOptions {
29    pub width: u32,
30    pub height: u32,
31    pub bits_per_sample: u8,
32    pub samples_per_pixel: u8,
33    pub compression: TiffCompression,
34}
35
36impl Default for TiffOptions {
37    fn default() -> Self {
38        Self {
39            width: 512,
40            height: 512,
41            bits_per_sample: 8,
42            samples_per_pixel: 4,
43            compression: TiffCompression::None,
44        }
45    }
46}
47
48/// TIFF image stub.
49#[derive(Debug, Clone)]
50pub struct TiffExport {
51    pub options: TiffOptions,
52    pub pixels: Vec<u8>,
53}
54
55impl TiffExport {
56    /// Create TIFF export filled with solid RGBA color.
57    pub fn new_solid(width: u32, height: u32, color: [u8; 4]) -> Self {
58        let n = (width * height) as usize;
59        let mut pixels = Vec::with_capacity(n * 4);
60        for _ in 0..n {
61            pixels.extend_from_slice(&color);
62        }
63        Self {
64            options: TiffOptions {
65                width,
66                height,
67                ..Default::default()
68            },
69            pixels,
70        }
71    }
72
73    /// Pixel count.
74    pub fn pixel_count(&self) -> usize {
75        (self.options.width * self.options.height) as usize
76    }
77
78    /// Raw byte count of pixel data.
79    pub fn raw_bytes(&self) -> usize {
80        self.pixels.len()
81    }
82}
83
84/// Estimate TIFF file size including header (stub).
85pub fn estimate_tiff_bytes(export: &TiffExport) -> usize {
86    let raw = export.raw_bytes();
87    match export.options.compression {
88        TiffCompression::None => raw + 512,
89        TiffCompression::Lzw => raw / 3 + 512,
90        TiffCompression::Deflate => raw / 4 + 512,
91    }
92}
93
94/// Validate TIFF options.
95pub fn validate_tiff(export: &TiffExport) -> bool {
96    export.options.width > 0
97        && export.options.height > 0
98        && export.options.bits_per_sample > 0
99        && export.options.samples_per_pixel > 0
100}
101
102/// Serialize TIFF metadata to JSON (stub).
103pub fn tiff_metadata_json(export: &TiffExport) -> String {
104    format!(
105        "{{\"width\":{},\"height\":{},\"bps\":{},\"spp\":{},\"compression\":{}}}",
106        export.options.width,
107        export.options.height,
108        export.options.bits_per_sample,
109        export.options.samples_per_pixel,
110        export.options.compression.tag_value()
111    )
112}
113
114#[cfg(test)]
115mod tests {
116    use super::*;
117
118    fn sample() -> TiffExport {
119        TiffExport::new_solid(8, 8, [200, 150, 100, 255])
120    }
121
122    #[test]
123    fn test_pixel_count() {
124        /* pixel count matches dimensions */
125        assert_eq!(sample().pixel_count(), 64);
126    }
127
128    #[test]
129    fn test_raw_bytes() {
130        /* raw bytes = pixel_count * 4 for RGBA */
131        assert_eq!(sample().raw_bytes(), 256);
132    }
133
134    #[test]
135    fn test_validate_valid() {
136        /* valid export passes */
137        assert!(validate_tiff(&sample()));
138    }
139
140    #[test]
141    fn test_compression_tag() {
142        /* compression tags are distinct */
143        assert_ne!(
144            TiffCompression::None.tag_value(),
145            TiffCompression::Lzw.tag_value()
146        );
147    }
148
149    #[test]
150    fn test_estimate_bytes_none_largest() {
151        /* no-compression estimate is larger than compressed */
152        let mut e = sample();
153        let none_size = estimate_tiff_bytes(&e);
154        e.options.compression = TiffCompression::Deflate;
155        let deflate_size = estimate_tiff_bytes(&e);
156        assert!(none_size > deflate_size);
157    }
158
159    #[test]
160    fn test_metadata_json_has_bps() {
161        /* metadata JSON contains bps field */
162        assert!(tiff_metadata_json(&sample()).contains("bps"));
163    }
164
165    #[test]
166    fn test_validate_zero_dim() {
167        /* zero dimension fails validation */
168        let mut e = sample();
169        e.options.width = 0;
170        assert!(!validate_tiff(&e));
171    }
172
173    #[test]
174    fn test_default_options() {
175        /* default options are valid */
176        let opts = TiffOptions::default();
177        assert_eq!(opts.bits_per_sample, 8);
178    }
179}