|
1 | 1 | import Foundation |
2 | 2 |
|
3 | 3 | enum JPEGContainer { |
4 | | - /// Extract EXIF TIFF data from a JPEG file. |
5 | | - static func extractEXIF(from data: Data) throws -> Data { |
6 | | - guard data.count >= 4, |
7 | | - data[data.startIndex] == 0xFF, |
8 | | - data[data.startIndex + 1] == 0xD8 |
9 | | - else { |
| 4 | + /// Extract EXIF TIFF data from a JPEG file handle, reading only metadata segments. |
| 5 | + static func extractEXIF(from fileHandle: FileHandle) throws -> Data { |
| 6 | + let soi = fileHandle.readData(ofLength: 2) |
| 7 | + guard soi.count == 2, soi[0] == 0xFF, soi[1] == 0xD8 else { |
10 | 8 | throw EXIFError.invalidJPEGStructure |
11 | 9 | } |
12 | 10 |
|
13 | | - var offset = 2 |
14 | | - |
15 | | - while offset + 4 <= data.count { |
16 | | - let start = data.startIndex + offset |
17 | | - |
18 | | - // Each marker starts with 0xFF |
19 | | - guard data[start] == 0xFF else { |
| 11 | + while true { |
| 12 | + // Read marker (2 bytes) |
| 13 | + let markerData = fileHandle.readData(ofLength: 2) |
| 14 | + guard markerData.count == 2, markerData[0] == 0xFF else { |
20 | 15 | throw EXIFError.invalidJPEGStructure |
21 | 16 | } |
22 | 17 |
|
23 | | - let marker = data[start + 1] |
| 18 | + let marker = markerData[1] |
24 | 19 |
|
25 | | - // Skip padding bytes (0xFF 0xFF...) |
| 20 | + // Skip padding bytes |
26 | 21 | if marker == 0xFF { |
27 | | - offset += 1 |
| 22 | + fileHandle.seek(toFileOffset: fileHandle.offsetInFile - 1) |
28 | 23 | continue |
29 | 24 | } |
30 | 25 |
|
31 | | - // SOS (Start of Scan) or EOI — no more metadata segments |
32 | | - if marker == 0xDA || marker == 0xD9 { |
33 | | - break |
34 | | - } |
| 26 | + // SOS or EOI — no more metadata segments |
| 27 | + if marker == 0xDA || marker == 0xD9 { break } |
35 | 28 |
|
36 | | - // Markers without a length field (RST, TEM, SOI) |
| 29 | + // Markers without a length field |
37 | 30 | if marker == 0x00 || marker == 0x01 || (0xD0 ... 0xD7).contains(marker) { |
38 | | - offset += 2 |
39 | 31 | continue |
40 | 32 | } |
41 | 33 |
|
42 | | - // Read segment length (includes the 2 length bytes but not the marker) |
43 | | - guard offset + 4 <= data.count else { break } |
44 | | - let length = Int(data[start + 2]) << 8 | Int(data[start + 3]) |
| 34 | + // Read segment length |
| 35 | + let lengthData = fileHandle.readData(ofLength: 2) |
| 36 | + guard lengthData.count == 2 else { break } |
| 37 | + let length = Int(lengthData[0]) << 8 | Int(lengthData[1]) |
45 | 38 | guard length >= 2 else { throw EXIFError.invalidJPEGStructure } |
46 | 39 |
|
47 | | - // APP1 marker (0xE1) — check for EXIF signature |
48 | | - if marker == 0xE1 { |
49 | | - let payloadStart = offset + 4 |
50 | | - let payloadLength = length - 2 |
| 40 | + let payloadLength = length - 2 |
51 | 41 |
|
52 | | - if payloadLength >= 6, payloadStart + 6 <= data.count { |
53 | | - let sigStart = data.startIndex + payloadStart |
54 | | - // "Exif\0\0" |
55 | | - if data[sigStart] == 0x45, data[sigStart + 1] == 0x78, |
56 | | - data[sigStart + 2] == 0x69, data[sigStart + 3] == 0x66, |
57 | | - data[sigStart + 4] == 0x00, data[sigStart + 5] == 0x00 |
58 | | - { |
59 | | - let tiffStart = payloadStart + 6 |
60 | | - let tiffLength = payloadLength - 6 |
61 | | - guard tiffStart + tiffLength <= data.count else { throw EXIFError.truncatedData } |
62 | | - return Data(data[data.startIndex + tiffStart ..< data.startIndex + tiffStart + tiffLength]) |
63 | | - } |
| 42 | + // APP1 marker — check for EXIF signature |
| 43 | + if marker == 0xE1, payloadLength >= 6 { |
| 44 | + let sig = fileHandle.readData(ofLength: 6) |
| 45 | + guard sig.count == 6 else { throw EXIFError.truncatedData } |
| 46 | + // "Exif\0\0" |
| 47 | + if sig[0] == 0x45, sig[1] == 0x78, sig[2] == 0x69, sig[3] == 0x66, |
| 48 | + sig[4] == 0x00, sig[5] == 0x00 |
| 49 | + { |
| 50 | + let tiffLength = payloadLength - 6 |
| 51 | + let tiffData = fileHandle.readData(ofLength: tiffLength) |
| 52 | + guard tiffData.count == tiffLength else { throw EXIFError.truncatedData } |
| 53 | + return tiffData |
64 | 54 | } |
| 55 | + // Not EXIF APP1, skip the rest of this segment |
| 56 | + let remaining = payloadLength - 6 |
| 57 | + if remaining > 0 { |
| 58 | + fileHandle.seek(toFileOffset: fileHandle.offsetInFile + UInt64(remaining)) |
| 59 | + } |
| 60 | + } else { |
| 61 | + // Skip segment payload |
| 62 | + fileHandle.seek(toFileOffset: fileHandle.offsetInFile + UInt64(payloadLength)) |
65 | 63 | } |
66 | | - |
67 | | - // Move to next marker |
68 | | - offset += 2 + length |
69 | 64 | } |
70 | 65 |
|
71 | 66 | throw EXIFError.noEXIFData |
|
0 commit comments