mcap/modules/Image_Decode/exif.jai

370 lines
14 KiB
Plaintext

// EXIF: where a photo was taken, when, on what, and which way up. Found in a JPEG's APP1 segment, a
// PNG's eXIf chunk, a WebP's EXIF chunk, a HEIF or AVIF's Exif item, and as the whole of a TIFF or a
// TIFF-based RAW file; read as TIFF image file directories.
Exif :: struct {
found: bool;
orientation: s64 = 1; // 1-8; 1 is upright
date_original: string; // DateTimeOriginal, "2024:05:12 10:15:30"; else DateTimeDigitized, else DateTime
offset_original: string; // OffsetTimeOriginal, "+02:00"; often missing
make, model, software: string;
has_gps: bool;
latitude, longitude: float64; // degrees, south and west negative
altitude: float64; // metres; 0 when missing
}
// What bytes' EXIF says; the strings are views into bytes. kind may be passed when known.
exif_read :: (bytes: [] u8, kind := Image_Kind.UNKNOWN) -> Exif {
if kind == .UNKNOWN kind = image_kind(bytes);
return exif_parse_tiff(exif_find(bytes, kind));
}
// The TIFF block holding the EXIF, or empty.
exif_find :: (bytes: [] u8, kind: Image_Kind) -> [] u8 {
if kind == {
case .JPEG;
// Segments: FF, marker, a big-endian length counting itself; the image data starts at SOS.
at := 2;
while at + 4 <= bytes.count {
if bytes[at] != 0xFF return .[];
marker := bytes[at + 1];
if marker == 0xFF { at += 1; continue; } // fill byte
if marker == 0xDA || marker == 0xD9 return .[];
if marker == 0x01 || (marker >= 0xD0 && marker <= 0xD7) { at += 2; continue; } // no length
length := cast(s64) be16(bytes, at + 2);
if length < 2 return .[];
if marker == 0xE1 && length >= 8 && at + 10 <= bytes.count && memcmp(bytes.data + at + 4, "Exif\0\0".data, 6) == 0 return slice_bytes(bytes, at + 10, length - 8);
at += 2 + length;
}
case .PNG;
at := 8;
while at + 12 <= bytes.count {
length := cast(s64) be32(bytes, at);
type := cast(string) slice_bytes(bytes, at + 4, 4);
if type == "eXIf" return slice_bytes(bytes, at + 8, length);
if type == "IDAT" || type == "IEND" return .[]; // eXIf comes before the image data
at += 12 + length;
}
case .WEBP;
at := 12;
while at + 8 <= bytes.count {
type := cast(string) slice_bytes(bytes, at, 4);
length := cast(s64) le32(bytes, at + 4);
if type == "EXIF" {
data := slice_bytes(bytes, at + 8, length);
if data.count >= 6 && memcmp(data.data, "Exif\0\0".data, 6) == 0 data = slice_bytes(data, 6, data.count - 6); // some writers keep the JPEG prefix
return data;
}
at += 8 + length + (length & 1);
}
case .TIFF; #through;
case .RAW;
if bytes.count >= 8 && (bytes[0] == #char "I" || bytes[0] == #char "M") return bytes;
case .HEIF; #through;
case .AVIF;
return heif_info(bytes).exif;
}
return .[];
}
exif_parse_tiff :: (tiff: [] u8) -> Exif {
exif: Exif;
if tiff.count < 8 return exif;
little: bool;
if tiff[0] == #char "I" && tiff[1] == #char "I" little = true;
else if tiff[0] == #char "M" && tiff[1] == #char "M" little = false;
else return exif;
r := Tiff_Reader.{data = tiff, little = little};
if tiff_u16(*r, 2) != 42 return exif;
exif.found = true;
date_time, date_digitized: string;
exif_ifd, gps_ifd: s64;
for tiff_entries(*r, tiff_u32(*r, 4)) {
if it.tag == {
case 0x010F; exif.make = tiff_text(*r, it);
case 0x0110; exif.model = tiff_text(*r, it);
case 0x0112; exif.orientation = tiff_number(*r, it, 0);
case 0x0131; exif.software = tiff_text(*r, it);
case 0x0132; date_time = tiff_text(*r, it);
case 0x8769; exif_ifd = tiff_number(*r, it, 0);
case 0x8825; gps_ifd = tiff_number(*r, it, 0);
}
}
if exif_ifd {
for tiff_entries(*r, exif_ifd) {
if it.tag == {
case 0x9003; exif.date_original = tiff_text(*r, it);
case 0x9004; date_digitized = tiff_text(*r, it);
case 0x9011; exif.offset_original = tiff_text(*r, it);
}
}
}
if !exif.date_original exif.date_original = ifx date_digitized then date_digitized else date_time;
if exif.orientation < 1 || exif.orientation > 8 exif.orientation = 1;
if gps_ifd {
lat_ref, lon_ref: string;
lat, lon: [3] float64;
has_lat, has_lon: bool;
altitude_below := false;
for e: tiff_entries(*r, gps_ifd) {
if e.tag == {
case 1; lat_ref = tiff_text(*r, e);
case 2; if e.count >= 3 { for 0..2 lat[it] = tiff_rational(*r, e, it); has_lat = true; }
case 3; lon_ref = tiff_text(*r, e);
case 4; if e.count >= 3 { for 0..2 lon[it] = tiff_rational(*r, e, it); has_lon = true; }
case 5; altitude_below = tiff_number(*r, e, 0) == 1;
case 6; exif.altitude = tiff_rational(*r, e, 0);
}
}
if has_lat && has_lon {
exif.latitude = lat[0] + lat[1] / 60 + lat[2] / 3600;
exif.longitude = lon[0] + lon[1] / 60 + lon[2] / 3600;
if lat_ref == "S" exif.latitude = -exif.latitude;
if lon_ref == "W" exif.longitude = -exif.longitude;
if altitude_below exif.altitude = -exif.altitude;
// 0, 0 is what some phones write with no fix.
exif.has_gps = (exif.latitude != 0 || exif.longitude != 0) && abs(exif.latitude) <= 90 && abs(exif.longitude) <= 180;
}
}
return exif;
}
// HEIF and AVIF: the primary item's size, rotation and mirroring, and its Exif item.
Heif_Info :: struct {
width, height: s64; // ispe: the coded size, before any rotation
rotation: s64; // irot, degrees anticlockwise: 0, 90, 180 or 270
mirror: s64 = -1; // imir's axis, -1 for none
exif: [] u8; // the TIFF block, a view into the bytes
}
heif_info :: (bytes: [] u8) -> Heif_Info {
info: Heif_Info;
meta := box_find(bytes, "meta");
if meta.count < 4 return info;
meta = slice_bytes(meta, 4, meta.count - 4); // a full box: version and flags first
// The primary item.
primary: u32;
pitm := box_find(meta, "pitm");
if pitm.count >= 6 primary = ifx pitm[0] == 0 then be16(pitm, 4) else be32(pitm, 4);
// The Exif item's ID, from the item infos.
exif_id: u32;
iinf := box_find(meta, "iinf");
if iinf.count >= 6 {
at := ifx iinf[0] == 0 then 6 else 8;
while at + 8 <= iinf.count {
size := cast(s64) be32(iinf, at);
if size < 8 || at + size > iinf.count break;
if cast(string) slice_bytes(iinf, at + 4, 4) == "infe" {
infe := slice_bytes(iinf, at + 8, size - 8);
version := infe[0];
if version >= 2 {
id: u32;
type_at: s64;
if version == 2 { id = be16(infe, 4); type_at = 8; }
else { id = be32(infe, 4); type_at = 10; }
if cast(string) slice_bytes(infe, type_at, 4) == "Exif" exif_id = id;
}
}
at += size;
}
}
// Where the Exif item's bytes are, from the item locations.
iloc := box_find(meta, "iloc");
if exif_id && iloc.count >= 8 {
version := iloc[0];
offset_size := iloc[4] >> 4;
length_size := iloc[4] & 0xF;
base_offset_size := iloc[5] >> 4;
index_size := ifx version == 1 || version == 2 then iloc[5] & 0xF else 0;
at: s64 = 6;
count: s64;
if version < 2 { count = be16(iloc, at); at += 2; }
else { count = be32(iloc, at); at += 4; }
sized :: (b: [] u8, at: *s64, size: u8) -> u64 {
value: u64;
if size == 4 value = be32(b, at.*);
else if size == 8 value = be64(b, at.*);
else if size == 2 value = be16(b, at.*);
at.* += size;
return value;
}
for 1..count {
if at >= iloc.count break;
id: u32;
if version < 2 { id = be16(iloc, at); at += 2; }
else { id = be32(iloc, at); at += 4; }
method: u16;
if version == 1 || version == 2 { method = be16(iloc, at) & 0xF; at += 2; }
at += 2; // data reference index
base := sized(iloc, *at, base_offset_size);
extents := be16(iloc, at);
at += 2;
first_offset, first_length: u64;
for e: 0..cast(s64) extents - 1 {
if index_size sized(iloc, *at, index_size);
offset := sized(iloc, *at, offset_size);
length := sized(iloc, *at, length_size);
if e == 0 { first_offset = offset; first_length = length; }
}
if id != exif_id || method != 0 continue; // only items in the file itself, in one piece
data := slice_bytes(bytes, cast(s64) (base + first_offset), cast(s64) first_length);
// An Exif item starts with where the TIFF header is, past itself.
if data.count >= 4 {
skip := cast(s64) be32(data, 0);
info.exif = slice_bytes(data, 4 + skip, data.count - 4 - skip);
}
}
}
// The primary item's properties: ispe, irot and imir, through ipma's list of each item's.
iprp := box_find(meta, "iprp");
ipco := box_find(iprp, "ipco");
ipma := box_find(iprp, "ipma");
if primary && ipco.count && ipma.count >= 8 {
properties: [..] [] u8; // the boxes in ipco, each with its type at 4..8
properties.allocator = temp;
at := 0;
while at + 8 <= ipco.count {
size := cast(s64) be32(ipco, at);
if size < 8 || at + size > ipco.count break;
array_add(*properties, slice_bytes(ipco, at, size));
at += size;
}
version := ipma[0];
wide_index := (ipma[3] & 1) != 0;
entries := be32(ipma, 4);
at = 8;
for 1..entries {
if at >= ipma.count break;
id: u32;
if version < 1 { id = be16(ipma, at); at += 2; }
else { id = be32(ipma, at); at += 4; }
associations := ipma[at];
at += 1;
for 1..associations {
index: s64;
if wide_index { index = be16(ipma, at) & 0x7FFF; at += 2; }
else { index = ipma[at] & 0x7F; at += 1; }
if id != primary || index < 1 || index > properties.count continue;
box := properties[index - 1];
type := cast(string) slice_bytes(box, 4, 4);
if type == "ispe" && box.count >= 20 { info.width = be32(box, 12); info.height = be32(box, 16); }
if type == "irot" && box.count >= 9 info.rotation = cast(s64) (box[8] & 3) * 90; // as a u8, 270 wrapped to 14
if type == "imir" && box.count >= 9 info.mirror = box[8] & 1;
}
}
}
return info;
}
#scope_file
// The contents of the first box of type in data (a run of boxes), past its 8-byte header; empty when
// there is none.
box_find :: (data: [] u8, type: string) -> [] u8 {
at := 0;
while at + 8 <= data.count {
size := cast(s64) be32(data, at);
header := 8;
if size == 1 { size = cast(s64) be64(data, at + 8); header = 16; } // a 64-bit size
else if size == 0 size = data.count - at; // to the end
if size < header || at + size > data.count return .[];
if cast(string) slice_bytes(data, at + 4, 4) == type return slice_bytes(data, at + header, size - header);
at += size;
}
return .[];
}
Tiff_Reader :: struct {
data: [] u8;
little: bool;
}
Tiff_Entry :: struct {
tag, type: u16;
count: u32;
value_at: s64; // where its value is: inside the entry when it fits in 4 bytes, else where the entry points
}
tiff_u16 :: (r: *Tiff_Reader, at: s64) -> u16 {
if at < 0 || at + 2 > r.data.count return 0;
a, b := cast(u16) r.data[at], cast(u16) r.data[at + 1];
return ifx r.little then a | (b << 8) else (a << 8) | b;
}
tiff_u32 :: (r: *Tiff_Reader, at: s64) -> u32 {
if at < 0 || at + 4 > r.data.count return 0;
a, b := cast(u32) tiff_u16(r, at), cast(u32) tiff_u16(r, at + 2);
return ifx r.little then a | (b << 16) else (a << 16) | b;
}
TIFF_TYPE_SIZES :: s64.[0, 1, 1, 2, 4, 8, 1, 1, 2, 4, 8, 4, 8];
// The entries of the directory at offset; none when it is out of the data or unreasonable.
tiff_entries :: (r: *Tiff_Reader, offset: s64) -> [] Tiff_Entry {
entries: [..] Tiff_Entry;
entries.allocator = temp;
if offset < 8 || offset + 2 > r.data.count return entries;
count := cast(s64) tiff_u16(r, offset);
if count > 1000 return entries;
for 0..count - 1 {
at := offset + 2 + it * 12;
if at + 12 > r.data.count break;
e: Tiff_Entry;
e.tag = tiff_u16(r, at);
e.type = tiff_u16(r, at + 2);
e.count = tiff_u32(r, at + 4);
if e.type == 0 || e.type >= TIFF_TYPE_SIZES.count continue;
size := TIFF_TYPE_SIZES[e.type] * e.count;
e.value_at = ifx size <= 4 then at + 8 else cast(s64) tiff_u32(r, at + 8);
if e.value_at + size > r.data.count continue;
array_add(*entries, e);
}
return entries;
}
// An ASCII value, without its trailing nulls and spaces.
tiff_text :: (r: *Tiff_Reader, e: Tiff_Entry) -> string {
if e.type != 2 && e.type != 7 return "";
text := cast(string) slice_bytes(r.data, e.value_at, e.count);
while text.count && (text[text.count - 1] == 0 || text[text.count - 1] == #char " ") text.count -= 1;
zero := find_index_from_left(text, #char "\0");
if zero >= 0 text.count = zero;
return text;
}
// The index-th of a BYTE, SHORT or LONG value.
tiff_number :: (r: *Tiff_Reader, e: Tiff_Entry, index: s64) -> s64 {
if index >= e.count return 0;
if e.type == {
case 1; return r.data[e.value_at + index];
case 3; return tiff_u16(r, e.value_at + index * 2);
case 4; return tiff_u32(r, e.value_at + index * 4);
case 9; return cast,no_check(s32) tiff_u32(r, e.value_at + index * 4);
}
return 0;
}
// The index-th of a RATIONAL value, as a float.
tiff_rational :: (r: *Tiff_Reader, e: Tiff_Entry, index: s64) -> float64 {
if index >= e.count return 0;
if e.type == 5 || e.type == 10 {
numerator := tiff_u32(r, e.value_at + index * 8);
denominator := tiff_u32(r, e.value_at + index * 8 + 4);
if !denominator return 0;
if e.type == 10 return (cast(float64) cast,no_check(s32) numerator) / (cast(float64) cast,no_check(s32) denominator);
return (cast(float64) numerator) / denominator;
}
return cast(float64) tiff_number(r, e, index);
}