// 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); }