web_image_meta/
jpeg.rs

1use crate::Error;
2use jpeg_decoder::Decoder;
3
4const JPEG_SOI: [u8; 2] = [0xFF, 0xD8];
5const MARKER_COM: u8 = 0xFE;
6const MARKER_APP1: u8 = 0xE1;
7const MARKER_APP2: u8 = 0xE2;
8
9/// JPEG画像のメタデータを軽量化します
10///
11/// # Arguments
12/// * `data` - JPEG画像のバイトデータ
13///
14/// # Returns
15/// * `Ok(Vec<u8>)` - 軽量化されたJPEG画像データ
16/// * `Err(Error)` - エラー
17///
18/// # Details
19/// - EXIFのオリエンテーション情報は保持
20/// - その他のEXIF情報を削除
21/// - 基本的なメタデータとEXIF・ICC以外を削除
22pub fn clean_metadata(data: &[u8]) -> Result<Vec<u8>, Error> {
23    if data.len() < 4 || data[0..2] != JPEG_SOI {
24        return Err(Error::InvalidFormat("Not a valid JPEG file".to_string()));
25    }
26
27    // JPEGが正常にデコードできるか検証
28    validate_jpeg_decode(data)?;
29
30    let mut output = Vec::new();
31    output.extend_from_slice(&JPEG_SOI);
32
33    let mut pos = 2;
34    let mut has_exif = false;
35    let mut orientation: Option<u16> = None;
36
37    // JPEGマーカーを解析
38    while pos < data.len() - 1 {
39        if data[pos] != 0xFF {
40            return Err(Error::ParseError("Invalid JPEG marker".to_string()));
41        }
42
43        let marker = data[pos + 1];
44        pos += 2;
45
46        // SOSマーカー以降は画像データなのでそのままコピー
47        if marker == 0xDA {
48            output.extend_from_slice(&[0xFF, marker]);
49            output.extend_from_slice(&data[pos..]);
50            break;
51        }
52
53        // スタンドアロンマーカーの場合
54        if (0xD0..=0xD9).contains(&marker) {
55            output.extend_from_slice(&[0xFF, marker]);
56            continue;
57        }
58
59        // セグメントサイズを読み取る
60        if pos + 2 > data.len() {
61            return Err(Error::ParseError("Unexpected end of JPEG data".to_string()));
62        }
63
64        let segment_size = ((data[pos] as u16) << 8) | (data[pos + 1] as u16);
65        if segment_size < 2 {
66            return Err(Error::ParseError("Invalid segment size".to_string()));
67        }
68
69        let segment_end = pos + segment_size as usize;
70        if segment_end > data.len() {
71            return Err(Error::ParseError("Segment extends beyond file".to_string()));
72        }
73
74        // 保持するマーカーを判定
75        let keep_segment = match marker {
76            // 基本的な構造に必要なマーカー
77            0xC0..=0xC3 | 0xC5..=0xCF => true, // SOF markers
78            0xC4 => true,                      // DHT (Huffman tables)
79            0xDB => true,                      // DQT (Quantization tables)
80            0xDD => true,                      // DRI (Restart interval)
81            // APP0 (JFIF) は保持
82            0xE0 => true,
83            // APP1 (EXIF) はオリエンテーション情報を抽出
84            MARKER_APP1 => {
85                if !has_exif && segment_size > 8 && &data[pos + 2..pos + 6] == b"Exif" {
86                    has_exif = true;
87                    // EXIFからオリエンテーションを抽出
88                    // EXIFデータを簡易的に解析してオリエンテーションを取得
89                    orientation = extract_orientation_from_exif(&data[pos + 8..segment_end]);
90                }
91                false
92            }
93            // APP2 (ICC Profile) は保持
94            MARKER_APP2 => segment_size > 14 && &data[pos + 2..pos + 14] == b"ICC_PROFILE\0",
95            // その他のAPPマーカーは削除 (0xE0は既に処理済みなので除外)
96            0xE3..=0xEF => false,
97            // コメントは削除
98            MARKER_COM => false,
99            _ => false,
100        };
101
102        if keep_segment {
103            output.extend_from_slice(&[0xFF, marker]);
104            output.extend_from_slice(&data[pos..segment_end]);
105        }
106
107        pos = segment_end;
108    }
109
110    // オリエンテーション情報がある場合は最小限のEXIFを追加
111    if let Some(orientation_value) = orientation {
112        if (1..=8).contains(&orientation_value) {
113            let exif_data = create_minimal_exif(orientation_value)?;
114            // JFIFマーカーの直後に挿入
115            let mut final_output = Vec::new();
116            let mut inserted = false;
117            let mut i = 0;
118
119            while i < output.len() - 1 {
120                if output[i] == 0xFF && output[i + 1] == 0xE0 && !inserted {
121                    // JFIFマーカーを見つけた
122                    let marker_size = ((output[i + 2] as u16) << 8) | (output[i + 3] as u16);
123                    let marker_end = i + 2 + marker_size as usize;
124                    final_output.extend_from_slice(&output[i..marker_end]);
125                    final_output.extend_from_slice(&exif_data);
126                    inserted = true;
127                    i = marker_end;
128                } else {
129                    final_output.push(output[i]);
130                    i += 1;
131                }
132            }
133            if i < output.len() {
134                final_output.push(output[i]);
135            }
136
137            if !inserted {
138                // JFIFマーカーがない場合はSOIの直後に挿入
139                let mut temp = vec![0xFF, 0xD8];
140                temp.extend_from_slice(&exif_data);
141                temp.extend_from_slice(&output[2..]);
142                return Ok(temp);
143            }
144
145            return Ok(final_output);
146        }
147    }
148
149    // 出力が有効なJPEGか検証
150    validate_jpeg_decode(&output)?;
151
152    Ok(output)
153}
154
155/// 最小限のEXIFデータを作成(オリエンテーションのみ)
156fn create_minimal_exif(orientation: u16) -> Result<Vec<u8>, Error> {
157    let mut exif = Vec::new();
158
159    // APP1マーカー
160    exif.extend_from_slice(&[0xFF, MARKER_APP1]);
161
162    // サイズは後で設定
163    exif.extend_from_slice(&[0x00, 0x00]);
164
165    // Exif識別子
166    exif.extend_from_slice(b"Exif\0\0");
167
168    // TIFF header (Little Endian)
169    exif.extend_from_slice(&[0x49, 0x49]); // "II"
170    exif.extend_from_slice(&[0x2A, 0x00]); // 42
171    exif.extend_from_slice(&[0x08, 0x00, 0x00, 0x00]); // IFD0 offset
172
173    // IFD0
174    exif.extend_from_slice(&[0x01, 0x00]); // 1 entry
175
176    // Orientation tag
177    exif.extend_from_slice(&[0x12, 0x01]); // Tag 0x0112
178    exif.extend_from_slice(&[0x03, 0x00]); // Type: SHORT
179    exif.extend_from_slice(&[0x01, 0x00, 0x00, 0x00]); // Count: 1
180    exif.extend_from_slice(&[orientation as u8, (orientation >> 8) as u8, 0x00, 0x00]); // Value
181
182    // Next IFD offset (none)
183    exif.extend_from_slice(&[0x00, 0x00, 0x00, 0x00]);
184
185    // サイズを設定
186    let size = (exif.len() - 2) as u16;
187    exif[2] = (size >> 8) as u8;
188    exif[3] = size as u8;
189
190    Ok(exif)
191}
192
193/// JPEG画像からコメントを読み取ります
194pub fn read_comment(data: &[u8]) -> Result<Option<String>, Error> {
195    if data.len() < 4 || data[0..2] != JPEG_SOI {
196        return Err(Error::InvalidFormat("Not a valid JPEG file".to_string()));
197    }
198
199    // JPEGが正常にデコードできるか検証
200    validate_jpeg_decode(data)?;
201
202    let mut pos = 2;
203
204    while pos < data.len() - 1 {
205        if data[pos] != 0xFF {
206            return Err(Error::ParseError("Invalid JPEG marker".to_string()));
207        }
208
209        let marker = data[pos + 1];
210        pos += 2;
211
212        // SOSマーカー以降は画像データ
213        if marker == 0xDA {
214            break;
215        }
216
217        // スタンドアロンマーカーの場合
218        if (0xD0..=0xD9).contains(&marker) {
219            continue;
220        }
221
222        // セグメントサイズを読み取る
223        if pos + 2 > data.len() {
224            return Err(Error::ParseError("Unexpected end of JPEG data".to_string()));
225        }
226
227        let segment_size = ((data[pos] as u16) << 8) | (data[pos + 1] as u16);
228        if segment_size < 2 {
229            return Err(Error::ParseError("Invalid segment size".to_string()));
230        }
231
232        let segment_end = pos + segment_size as usize;
233        if segment_end > data.len() {
234            return Err(Error::ParseError("Segment extends beyond file".to_string()));
235        }
236
237        // コメントマーカーの場合
238        if marker == MARKER_COM {
239            if segment_size > 2 {
240                let comment_data = &data[pos + 2..segment_end];
241                let comment = String::from_utf8_lossy(comment_data).to_string();
242                return Ok(Some(comment));
243            } else {
244                // 空のコメント(セグメントサイズが2の場合)
245                return Ok(Some(String::new()));
246            }
247        }
248
249        pos = segment_end;
250    }
251
252    Ok(None)
253}
254
255/// EXIFデータからオリエンテーション値を抽出する簡易実装
256fn extract_orientation_from_exif(exif_data: &[u8]) -> Option<u16> {
257    // 最小限のEXIF解析
258    if exif_data.len() < 8 {
259        return None;
260    }
261
262    // Tiffヘッダーを確認 (II or MM)
263    let endian = if &exif_data[0..2] == b"II" {
264        // Little Endian
265        true
266    } else if &exif_data[0..2] == b"MM" {
267        // Big Endian
268        false
269    } else {
270        return None;
271    };
272
273    // 42のマジックナンバーを確認
274    let magic = if endian {
275        u16::from_le_bytes([exif_data[2], exif_data[3]])
276    } else {
277        u16::from_be_bytes([exif_data[2], exif_data[3]])
278    };
279
280    if magic != 42 {
281        return None;
282    }
283
284    // IFD0のオフセットを取得
285    let ifd0_offset = if endian {
286        u32::from_le_bytes([exif_data[4], exif_data[5], exif_data[6], exif_data[7]]) as usize
287    } else {
288        u32::from_be_bytes([exif_data[4], exif_data[5], exif_data[6], exif_data[7]]) as usize
289    };
290
291    if ifd0_offset + 2 > exif_data.len() {
292        return None;
293    }
294
295    // エントリ数を取得
296    let entry_count = if endian {
297        u16::from_le_bytes([exif_data[ifd0_offset], exif_data[ifd0_offset + 1]]) as usize
298    } else {
299        u16::from_be_bytes([exif_data[ifd0_offset], exif_data[ifd0_offset + 1]]) as usize
300    };
301
302    // 各エントリをチェック
303    for i in 0..entry_count {
304        let entry_offset = ifd0_offset + 2 + (i * 12);
305        if entry_offset + 12 > exif_data.len() {
306            break;
307        }
308
309        // タグを確認 (0x0112 = Orientation)
310        let tag = if endian {
311            u16::from_le_bytes([exif_data[entry_offset], exif_data[entry_offset + 1]])
312        } else {
313            u16::from_be_bytes([exif_data[entry_offset], exif_data[entry_offset + 1]])
314        };
315
316        if tag == 0x0112 {
317            // オリエンテーション値を取得
318            let value_offset = entry_offset + 8;
319            let orientation = if endian {
320                u16::from_le_bytes([exif_data[value_offset], exif_data[value_offset + 1]])
321            } else {
322                u16::from_be_bytes([exif_data[value_offset], exif_data[value_offset + 1]])
323            };
324
325            return Some(orientation);
326        }
327    }
328
329    None
330}
331
332/// JPEGデータが正常にデコードできるか検証
333fn validate_jpeg_decode(data: &[u8]) -> Result<(), Error> {
334    let mut decoder = Decoder::new(data);
335
336    // ヘッダーを読み込んでデコード可能か確認
337    match decoder.read_info() {
338        Ok(_) => {
339            // 基本情報の取得を試みる
340            let info = decoder.info();
341            if info.is_none() {
342                return Err(Error::InvalidFormat("Failed to get JPEG info".to_string()));
343            }
344
345            // 画像の基本パラメータを検証
346            let info = info.unwrap();
347            if info.width == 0 || info.height == 0 {
348                return Err(Error::InvalidFormat("Invalid image dimensions".to_string()));
349            }
350
351            Ok(())
352        }
353        Err(e) => Err(Error::InvalidFormat(format!("Invalid JPEG: {e}"))),
354    }
355}
356
357/// JPEG画像にコメントを書き込みます
358pub fn write_comment(data: &[u8], comment: &str) -> Result<Vec<u8>, Error> {
359    if data.len() < 4 || data[0..2] != JPEG_SOI {
360        return Err(Error::InvalidFormat("Not a valid JPEG file".to_string()));
361    }
362
363    // JPEGが正常にデコードできるか検証
364    validate_jpeg_decode(data)?;
365
366    let comment_bytes = comment.as_bytes();
367    if comment_bytes.len() > 65533 {
368        return Err(Error::InvalidFormat("Comment too long".to_string()));
369    }
370
371    let mut output = Vec::new();
372    output.extend_from_slice(&JPEG_SOI);
373
374    // コメントセグメントを作成
375    let mut comment_segment = Vec::new();
376    comment_segment.extend_from_slice(&[0xFF, MARKER_COM]);
377    let segment_size = (comment_bytes.len() + 2) as u16;
378    comment_segment.push((segment_size >> 8) as u8);
379    comment_segment.push(segment_size as u8);
380    comment_segment.extend_from_slice(comment_bytes);
381
382    let mut pos = 2;
383    let mut comment_inserted = false;
384
385    // 既存のコメントを削除しつつ、適切な位置に新しいコメントを挿入
386    while pos < data.len() - 1 {
387        if data[pos] != 0xFF {
388            return Err(Error::ParseError("Invalid JPEG marker".to_string()));
389        }
390
391        let marker = data[pos + 1];
392        pos += 2;
393
394        // APPマーカーの後、SOSマーカーの前にコメントを挿入
395        if !comment_inserted && (marker == 0xDA || marker == 0xDB) {
396            output.extend_from_slice(&comment_segment);
397            comment_inserted = true;
398        }
399
400        // SOSマーカー以降は画像データなのでそのままコピー
401        if marker == 0xDA {
402            output.extend_from_slice(&[0xFF, marker]);
403            output.extend_from_slice(&data[pos..]);
404            break;
405        }
406
407        // スタンドアロンマーカーの場合
408        if (0xD0..=0xD9).contains(&marker) {
409            output.extend_from_slice(&[0xFF, marker]);
410            continue;
411        }
412
413        // セグメントサイズを読み取る
414        if pos + 2 > data.len() {
415            return Err(Error::ParseError("Unexpected end of JPEG data".to_string()));
416        }
417
418        let segment_size = ((data[pos] as u16) << 8) | (data[pos + 1] as u16);
419        if segment_size < 2 {
420            return Err(Error::ParseError("Invalid segment size".to_string()));
421        }
422
423        let segment_end = pos + segment_size as usize;
424        if segment_end > data.len() {
425            return Err(Error::ParseError("Segment extends beyond file".to_string()));
426        }
427
428        // 既存のコメントは削除
429        if marker != MARKER_COM {
430            output.extend_from_slice(&[0xFF, marker]);
431            output.extend_from_slice(&data[pos..segment_end]);
432        }
433
434        pos = segment_end;
435    }
436
437    // コメントがまだ挿入されていない場合(画像データがない場合)
438    if !comment_inserted {
439        output.extend_from_slice(&comment_segment);
440    }
441
442    // 出力が有効なJPEGか検証
443    validate_jpeg_decode(&output)?;
444
445    Ok(output)
446}