1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! TIFF format metadata extraction
/// Helper function to read a u16 from a byte slice with proper endianness
/// Returns None if offset is out of bounds
#[inline]
fn read_u16_safe(data_ref: &[u8], offset: usize, is_little_endian: bool) -> Option<u16> {
if offset + 1 >= data_ref.len() {
return None;
}
let bytes = [data_ref[offset], data_ref[offset + 1]];
Some(if is_little_endian {
u16::from_le_bytes(bytes)
} else {
u16::from_be_bytes(bytes)
})
}
/// Helper function to read a u32 from a byte slice with proper endianness
/// Returns None if offset is out of bounds
#[inline]
fn read_u32_safe(data_ref: &[u8], offset: usize, is_little_endian: bool) -> Option<u32> {
if offset + 3 >= data_ref.len() {
return None;
}
let bytes = [
data_ref[offset],
data_ref[offset + 1],
data_ref[offset + 2],
data_ref[offset + 3],
];
Some(if is_little_endian {
u32::from_le_bytes(bytes)
} else {
u32::from_be_bytes(bytes)
})
}
use super::constants::{
COLOR_TYPE_CMYK8, COLOR_TYPE_INDEXED8, COLOR_TYPE_L8, COLOR_TYPE_RGB8, TIFF_LE_SIGNATURE,
TIFF_PHOTOMETRIC_BLACK_IS_ZERO, TIFF_PHOTOMETRIC_CIELAB, TIFF_PHOTOMETRIC_CMYK,
TIFF_PHOTOMETRIC_MASK, TIFF_PHOTOMETRIC_PALETTE, TIFF_PHOTOMETRIC_RGB,
TIFF_PHOTOMETRIC_WHITE_IS_ZERO, TIFF_PHOTOMETRIC_YCBCR, TIFF_TAG_BITS_PER_SAMPLE,
TIFF_TAG_PHOTOMETRIC_INTERPRETATION, TIFF_TYPE_LONG, TIFF_TYPE_SHORT, TIFF_VERSION,
has_tiff_signature,
};
/// Extract compression info from TIFF data
pub fn extract_compression(tiff_data_ref: &[u8]) -> Option<String> {
if tiff_data_ref.len() < 8 {
return None;
}
// Check TIFF signature
if !has_tiff_signature(tiff_data_ref) {
return None;
}
let is_little_endian = &tiff_data_ref[0..2] == TIFF_LE_SIGNATURE;
// Read version (should be 42)
let version = read_u16_safe(tiff_data_ref, 2, is_little_endian)?;
if version != TIFF_VERSION {
return None;
}
// TIFF compression is stored in IFD entries (tag TIFF_TAG_COMPRESSION)
// Parsing IFD requires reading the offset at byte 4, then traversing IFD entries
// This is complex, so for now we just note it's TIFF
// Full implementation would require parsing the IFD structure
Some("TIFF".to_string())
}
/// Extract bit depth from TIFF data
pub fn extract_bit_depth(tiff_data_ref: &[u8]) -> Option<u32> {
// TIFF bit depth is stored in IFD entries (tag TIFF_TAG_BITS_PER_SAMPLE: BitsPerSample)
// This requires parsing the IFD structure, which is complex
// For now, we'll try a simple approach: read the first IFD offset and check common values
if tiff_data_ref.len() < 8 {
return None;
}
if !has_tiff_signature(tiff_data_ref) {
return None;
}
let is_little_endian = &tiff_data_ref[0..2] == TIFF_LE_SIGNATURE;
// Read IFD offset (bytes 4-7)
let ifd_offset = match read_u32_safe(tiff_data_ref, 4, is_little_endian) {
Some(offset) => offset as usize,
None => return None,
};
// Try to read IFD entry count and look for BitsPerSample tag (258)
if ifd_offset + 2 > tiff_data_ref.len() {
return None;
}
let entry_count = match read_u16_safe(tiff_data_ref, ifd_offset, is_little_endian) {
Some(count) => count as usize,
None => return None,
};
// Each IFD entry is 12 bytes: 2 bytes tag, 2 bytes type, 4 bytes count, 4 bytes value/offset
let entry_start = ifd_offset + 2;
for i in 0..entry_count.min(20) {
// Limit search to first 20 entries to avoid excessive parsing
let entry_offset = entry_start + i * 12;
if entry_offset + 12 > tiff_data_ref.len() {
break;
}
let Some(tag) = read_u16_safe(tiff_data_ref, entry_offset, is_little_endian) else {
break;
};
// Tag TIFF_TAG_BITS_PER_SAMPLE = BitsPerSample
if tag == TIFF_TAG_BITS_PER_SAMPLE {
let Some(value) = read_u32_safe(tiff_data_ref, entry_offset + 8, is_little_endian)
else {
break;
};
return Some(value);
}
}
// Default: assume 8-bit if we can't find it
None
}
/// Extract color type from TIFF data
///
/// Note: Some TIFF files converted from JPEG (e.g., via `sips`) may have non-standard
/// IFD structures. This function attempts to parse them but falls back to RGB8 if
/// the `PhotometricInterpretation` tag cannot be found or parsed.
pub fn extract_color_type(tiff_data_ref: &[u8]) -> String {
// TIFF color type is stored in IFD entries (tag 262/0x0106: PhotometricInterpretation)
// This requires parsing the IFD structure
if tiff_data_ref.len() < 8 {
// File too small to be valid TIFF, return default
return COLOR_TYPE_RGB8.to_string();
}
if !has_tiff_signature(tiff_data_ref) {
// Not a valid TIFF signature, return default
return COLOR_TYPE_RGB8.to_string();
}
let is_little_endian = &tiff_data_ref[0..2] == TIFF_LE_SIGNATURE;
// Read IFD offset (bytes 4-7)
let ifd_offset = match read_u32_safe(tiff_data_ref, 4, is_little_endian) {
Some(offset) => offset as usize,
None => return COLOR_TYPE_RGB8.to_string(),
};
// Try to read IFD entry count and look for PhotometricInterpretation tag (262)
// If IFD offset is out of bounds, return default (RGB)
if ifd_offset + 2 > tiff_data_ref.len() {
return COLOR_TYPE_RGB8.to_string();
}
// Safely read entry count
let entry_count = match read_u16_safe(tiff_data_ref, ifd_offset, is_little_endian) {
Some(count) => count as usize,
None => return COLOR_TYPE_RGB8.to_string(),
};
// Each IFD entry is 12 bytes: 2 bytes tag, 2 bytes type, 4 bytes count, 4 bytes value/offset
let entry_start = ifd_offset + 2;
// Limit search to first 20 entries to avoid excessive parsing
let max_entries = entry_count.min(20);
for i in 0..max_entries {
let entry_offset = entry_start + i * 12;
// Bounds check: need at least 12 bytes for a complete IFD entry
if entry_offset + 12 > tiff_data_ref.len() {
break;
}
let Some(tag) = read_u16_safe(tiff_data_ref, entry_offset, is_little_endian) else {
break;
};
// Tag TIFF_TAG_PHOTOMETRIC_INTERPRETATION = PhotometricInterpretation
if tag == TIFF_TAG_PHOTOMETRIC_INTERPRETATION {
// Check type field (bytes 2-3 of IFD entry)
// Type TIFF_TYPE_SHORT = SHORT (16-bit), Type TIFF_TYPE_LONG = LONG (32-bit)
let Some(entry_type) = read_u16_safe(tiff_data_ref, entry_offset + 2, is_little_endian)
else {
continue;
};
// PhotometricInterpretation is typically a SHORT (type TIFF_TYPE_SHORT)
let value = if entry_type == TIFF_TYPE_SHORT {
// SHORT: read from bytes 8-9 (little-endian) or 10-11 (big-endian)
// For big-endian, SHORT values are in the high bytes (10-11) of the 4-byte value field
let read_offset = if is_little_endian {
entry_offset + 8
} else {
entry_offset + 10
};
match read_u16_safe(tiff_data_ref, read_offset, is_little_endian) {
Some(v) => u32::from(v),
None => continue,
}
} else if entry_type == TIFF_TYPE_LONG {
// LONG: read all 4 bytes
match read_u32_safe(tiff_data_ref, entry_offset + 8, is_little_endian) {
Some(v) => v,
None => continue,
}
} else {
// Unknown type for PhotometricInterpretation - try to read as SHORT anyway
// Some TIFF writers might use non-standard types
if is_little_endian && entry_offset + 10 <= tiff_data_ref.len() {
// Try reading as SHORT from bytes 8-9
let value =
match read_u16_safe(tiff_data_ref, entry_offset + 8, is_little_endian) {
Some(v) => u32::from(v),
None => continue,
};
return match value {
v if v == TIFF_PHOTOMETRIC_WHITE_IS_ZERO
|| v == TIFF_PHOTOMETRIC_BLACK_IS_ZERO =>
{
COLOR_TYPE_L8.to_string()
}
v if v == TIFF_PHOTOMETRIC_RGB => COLOR_TYPE_RGB8.to_string(),
v if v == TIFF_PHOTOMETRIC_PALETTE => COLOR_TYPE_INDEXED8.to_string(),
v if v == TIFF_PHOTOMETRIC_MASK => COLOR_TYPE_L8.to_string(), // Transparency mask
v if v == TIFF_PHOTOMETRIC_CMYK => COLOR_TYPE_CMYK8.to_string(),
v if v == TIFF_PHOTOMETRIC_YCBCR => COLOR_TYPE_RGB8.to_string(),
v if v == TIFF_PHOTOMETRIC_CIELAB => COLOR_TYPE_RGB8.to_string(), // CIE L*a*b*
_ => format!("Unknown({value})"),
};
}
// If we can't read it, skip this tag and continue searching
continue;
};
// PhotometricInterpretation values
return match value {
v if v == TIFF_PHOTOMETRIC_WHITE_IS_ZERO || v == TIFF_PHOTOMETRIC_BLACK_IS_ZERO => {
COLOR_TYPE_L8.to_string() // Grayscale
}
v if v == TIFF_PHOTOMETRIC_RGB => COLOR_TYPE_RGB8.to_string(), // RGB
v if v == TIFF_PHOTOMETRIC_PALETTE => COLOR_TYPE_INDEXED8.to_string(), // Palette
v if v == TIFF_PHOTOMETRIC_MASK => COLOR_TYPE_L8.to_string(), // Transparency mask (grayscale)
v if v == TIFF_PHOTOMETRIC_CMYK => COLOR_TYPE_CMYK8.to_string(), // CMYK
v if v == TIFF_PHOTOMETRIC_YCBCR => COLOR_TYPE_RGB8.to_string(), // YCbCr (treated as RGB)
v if v == TIFF_PHOTOMETRIC_CIELAB => COLOR_TYPE_RGB8.to_string(), // CIE L*a*b* (treated as RGB for display)
_ => format!("Unknown({value})"),
};
}
}
// Default: assume RGB if we can't find it
COLOR_TYPE_RGB8.to_string()
}