// TIFF/IFD 读取器。
//
// 一个 EXIF APP1 段去掉 `Exif\0\0` 前缀之后就是一个完整的 TIFF 文件,
// 所以这里读的字节流从 TIFF 头开始:`II`/`MM` + 42 + IFD0 偏移。
//
// 与"读出 4 个认识的字段就走"的写法不同,这里把每一个能解的条目都留在
// IR 里——包括本库完全不懂的厂商私有 tag。脱敏工具只有在知道自己
// 略过了什么的时候才有意义。
///|
/// 一个 IFD 条目。
///
/// `type_code` 是原始 TIFF 类型码,回写时要照它重新编码;
/// `value` 的形状由 `type_code` 决定(见 `moonmeta_value.mbt` 的对应表)。
pub(all) struct IfdEntry {
tag : Int
type_code : Int
value : TagValue
} derive(Debug, Eq)
///|
pub extend IfdEntry with Eq::{not_equal, equal}
///|
pub extend IfdEntry with @debug.Debug::{to_repr}
///|
/// 解析结果:TIFF 头的字节序 + 四个目录。
///
/// `ifd0` 里**不含** `0x8769`/`0x8825` 这两个子目录指针——它们是结构而不是
/// 数据,解析时被消费掉,回写时按新布局重算,所以
/// `decode(encode(x)) == x` 才可能成立。
///
/// `has_ifd1` 记录原文件是否有缩略图目录。它只是"这里曾经有过一张缩略图"
/// 的记号:回写时缩略图链连同它的字节一起丢弃,这是**有意**的行为——
/// 留着缩略图的"脱敏"结果仍然可能泄露原始 GPS 与机身信息。
///
/// `interop` 是 Exif 子目录下的 Interoperability IFD。它曾经是"指针 tag,
/// 消费掉就完事",结果回写时整张表凭空消失——真实语料对拍里 44 个文件
/// 都少了这一张表,所以它现在和另三张一样进 IR、一样重发。
pub(all) struct TiffBlock {
little : Bool
ifd0 : Array[IfdEntry]
exif : Array[IfdEntry]
gps : Array[IfdEntry]
interop : Array[IfdEntry]
has_ifd1 : Bool
} derive(Debug, Eq)
///|
pub extend TiffBlock with Eq::{not_equal, equal}
///|
pub extend TiffBlock with @debug.Debug::{to_repr}
///|
/// Exif 子目录指针。
pub let tag_exif_ifd : Int = 0x8769
///|
/// GPS 子目录指针。
pub let tag_gps_ifd : Int = 0x8825
///|
/// 缩略图数据偏移(与 IFD1 配对出现)。
pub let tag_thumb_offset : Int = 0x0201
///|
/// 缩略图数据长度。
pub let tag_thumb_length : Int = 0x0202
///|
/// Interop 子目录指针。指向第四张表,解析时进 `TiffBlock.interop`。
pub let tag_interop_ifd : Int = 0xa005
///|
/// 从一个 TIFF 字节块解析。`data` 的第一字节必须是 `I`/`M`。
pub fn decode_tiff(data : Bytes) -> TiffBlock raise MetaError {
if data.length() < 8 {
raise TooShort(0, 8, data.length())
}
let b0 = byte_at(data, 0)
let b1 = byte_at(data, 1)
let little = if b0 == b'I' && b1 == b'I' {
true
} else if b0 == b'M' && b1 == b'M' {
false
} else {
raise BadByteOrder(0)
}
if u16_at(data, 2, little) != 42 {
raise BadTiffHeader(2)
}
let ifd0_off = check_ifd_offset(data, u32_at(data, 4, little))
let dir0 = parse_dir(data, ifd0_off, little)
let exif_dir = match sub_dir_offset(data, dir0.entries, tag_exif_ifd) {
Some(off) => parse_dir(data, off, little)
None => { entries: [], next: 0L, }
}
let gps_off = match sub_dir_offset(data, dir0.entries, tag_gps_ifd) {
Some(off) => Some(off)
None => sub_dir_offset(data, exif_dir.entries, tag_gps_ifd)
}
let gps_dir = match gps_off {
Some(off) => parse_dir(data, off, little)
None => { entries: [], next: 0L, }
}
let interop_dir = match
sub_dir_offset(data, exif_dir.entries, tag_interop_ifd) {
Some(off) => parse_dir(data, off, little)
None => { entries: [], next: 0L, }
}
// 指针 tag 是结构不是数据,不进 IR;各目录的后继(缩略图链)只留一个记号。
{
little,
ifd0: without(dir0.entries, [tag_exif_ifd, tag_gps_ifd]),
exif: without(exif_dir.entries, [tag_gps_ifd, tag_exif_ifd, tag_interop_ifd]),
gps: gps_dir.entries,
interop: interop_dir.entries,
has_ifd1: dir0.next > 0L ||
exif_dir.next > 0L ||
gps_dir.next > 0L ||
interop_dir.next > 0L,
}
}
///|
/// 一个目录末尾"下一目录指针"所在的偏移。
/// `n` 来自 u16,且 parse_dir 已保证表体落在块内,这里不会溢出。
fn dir_end(dir_off : Int, n : Int) -> Int {
dir_off + 2 + 12 * n
}
///|
fn parse_dir(
data : Bytes,
dir_off : Int,
little : Bool,
) -> RawDir raise MetaError {
let count = u16_at(data, dir_off, little)
let avail = data.length() - dir_off - 2
if count > avail / 12 {
// 声明的条目表比剩余字节还长:直接拒绝,不进入逐条读取。
raise EntryTooLarge(dir_off + 2, 0, count * 12, avail)
}
let entries : Array[IfdEntry] = []
for i = 0; i < count; i = i + 1 {
entries.push(parse_entry(data, dir_off + 2 + 12 * i, little))
}
let next = u32_at(data, dir_end(dir_off, count), little)
{ entries, next, }
}
///|
priv struct RawDir {
entries : Array[IfdEntry]
next : Int64
}
///|
fn parse_entry(
data : Bytes,
entry_off : Int,
little : Bool,
) -> IfdEntry raise MetaError {
let tag = u16_at(data, entry_off, little)
let type_code = u16_at(data, entry_off + 2, little)
let count = u32_at(data, entry_off + 4, little)
let size = match type_size(type_code) {
Some(s) => s
None => raise UnsupportedType(tag, type_code)
}
let total = count * size.to_int64()
// 先卡上限再看它放不放得进 4 字节:`total <= 4` 在 Int64 上判断,
// 避免 count 大到让 count*size 绕回 32 位。
let voff = if total <= 4L {
entry_off + 8
} else {
let off = u32_at(data, entry_off + 8, little)
let len = data.length().to_int64()
if off < 0L || off + total > len {
raise ValueOffsetOutOfRange(tag, off)
}
off.to_int()
}
let have = data.length() - voff
// 走到这里两处前置检查都已经保证 total <= have(内联值:parse_dir 保证表体
// 落在块内;外部值:上面那道指针检查)。这一道是重复的,留着是因为它是
// decode_value 逐字节读之前最后一道长度闸——没有测试能走到这里,
// 别把它当成"被测过的行为"。
if total > have.to_int64() {
raise EntryTooLarge(voff, tag, total.to_int(), have)
}
{
tag,
type_code,
value: decode_value(data, voff, tag, type_code, count.to_int(), little),
}
}
///|
fn decode_value(
data : Bytes,
voff : Int,
tag : Int,
type_code : Int,
count : Int,
little : Bool,
) -> TagValue raise MetaError {
match type_code {
2 => Text(read_latin1(data, voff, count))
1 | 6 | 7 => Raw(read_bytes(data, voff, count))
3 => Shorts(read_shorts(data, voff, count, little, false))
8 => Shorts(read_shorts(data, voff, count, little, true))
4 => Longs(read_longs(data, voff, count, little, false))
9 => Longs(read_longs(data, voff, count, little, true))
5 | 10 => Rationals(read_rationals(data, voff, type_code, count, little))
_ => raise UnsupportedType(tag, type_code)
}
}
///|
/// 子目录指针:只认 count=1 的 LONG。形状不对当作没有子目录,
/// 但指针落在块外是文件坏了,要报错而不是静默忽略。
fn sub_dir_offset(
data : Bytes,
entries : Array[IfdEntry],
tag : Int,
) -> Int? raise MetaError {
match find_entry(entries, tag) {
Some(e) =>
match e.value {
Longs(v) if v.length() == 1 => {
let off = v[0]
if off < 0L || off + 2L > data.length().to_int64() {
raise SubIfdOutOfRange(tag, off)
}
Some(off.to_int())
}
_ => None
}
None => None
}
}
///|
fn find_entry(entries : Array[IfdEntry], tag : Int) -> IfdEntry? {
for e in entries {
if e.tag == tag {
return Some(e)
}
}
None
}
///|
fn without(entries : Array[IfdEntry], tags : Array[Int]) -> Array[IfdEntry] {
let out : Array[IfdEntry] = []
for e in entries {
if !has_tag(tags, e.tag) {
out.push(e)
}
}
out
}
///|
fn has_tag(tags : Array[Int], tag : Int) -> Bool {
for t in tags {
if t == tag {
return true
}
}
false
}
// ---- 便捷访问 ----
///|
/// 按 tag 查值:IFD0 → Exif → GPS 依次找。
/// 同名 tag 在不同目录里含义不同,所以这是"够用就好"的捷径,
/// 需要精确时用 `ifd0_find`/`exif_find`/`gps_find`。
pub fn TiffBlock::get(self : TiffBlock, tag : Int) -> TagValue? {
match self.ifd0_find(tag) {
Some(e) => Some(e.value)
None =>
match self.exif_find(tag) {
Some(e) => Some(e.value)
None =>
match self.gps_find(tag) {
Some(e) => Some(e.value)
None => None
}
}
}
}
///|
pub fn TiffBlock::ifd0_find(self : TiffBlock, tag : Int) -> IfdEntry? {
find_entry(self.ifd0, tag)
}
///|
pub fn TiffBlock::exif_find(self : TiffBlock, tag : Int) -> IfdEntry? {
find_entry(self.exif, tag)
}
///|
pub fn TiffBlock::gps_find(self : TiffBlock, tag : Int) -> IfdEntry? {
find_entry(self.gps, tag)
}
///|
fn text_at(block : TiffBlock, tag : Int) -> String? {
match block.get(tag) {
Some(v) => v.text()
None => None
}
}
///|
/// 机身厂商(tag 0x010F)。
pub fn TiffBlock::make(self : TiffBlock) -> String? {
text_at(self, 0x010f)
}
///|
/// 机身型号(tag 0x0110)。
pub fn TiffBlock::model(self : TiffBlock) -> String? {
text_at(self, 0x0110)
}
///|
/// 生成软件(tag 0x0131)。
pub fn TiffBlock::software(self : TiffBlock) -> String? {
text_at(self, 0x0131)
}
///|
/// 拍摄时间(tag 0x0132)。
pub fn TiffBlock::date_time(self : TiffBlock) -> String? {
text_at(self, 0x0132)
}
///|
/// 原始拍摄时间(Exif tag 0x9003)。
pub fn TiffBlock::date_time_original(self : TiffBlock) -> String? {
text_at(self, 0x9003)
}
///|
/// 作者(tag 0x013B)。
pub fn TiffBlock::artist(self : TiffBlock) -> String? {
text_at(self, 0x013b)
}
///|
/// 版权声明(tag 0x8298)。
pub fn TiffBlock::copyright(self : TiffBlock) -> String? {
text_at(self, 0x8298)
}
///|
/// 方向(tag 0x0112),1..8;读不到或非 SHORT 时返回 None。
pub fn TiffBlock::orientation(self : TiffBlock) -> Int? {
match self.get(0x0112) {
Some(Shorts(v)) if v.length() > 0 => Some(v[0])
_ => None
}
}
///|
/// GPS 经纬度(度,东经/北纬为正)。缺任一半或形状不对都返回 None。
pub fn TiffBlock::gps_lat_lon(self : TiffBlock) -> (Double, Double)? {
match (dms(self, 0x0002, 0x0001), dms(self, 0x0004, 0x0003)) {
(Some(lat), Some(lon)) => Some((lat, lon))
_ => None
}
}
///|
/// 度分秒 → 十进制度,并按参考系(N/S、E/W)定符号。
fn dms(block : TiffBlock, value_tag : Int, ref_tag : Int) -> Double? {
let parts = match block.gps_find(value_tag) {
Some(e) =>
match e.value {
Rationals(v) if v.length() == 3 => v
_ => return None
}
None => return None
}
let deg = parts[0].to_double()
let min = parts[1].to_double()
let sec = parts[2].to_double()
let sign = match block.gps_find(ref_tag) {
Some(e) =>
match e.value {
Text(s) if s.length() > 0 =>
if s[0] == 'S' || s[0] == 'W' {
-1.0
} else {
1.0
}
_ => 1.0
}
None => 1.0
}
Some(sign * (deg + min / 60.0 + sec / 3600.0))
}
///|
/// 是否存在 GPS 目录(哪怕里面只有空壳)。脱敏审计的第一问。
pub fn TiffBlock::has_gps(self : TiffBlock) -> Bool {
self.gps.length() > 0
}
// ---- 字节读取原语:全部经检查过的 get,越界一律变成带偏移的错误 ----
///|
fn byte_at(data : Bytes, off : Int) -> Byte raise MetaError {
match data.get(off) {
Some(b) => b
None =>
raise TooShort(
off,
1,
if off < 0 || off >= data.length() {
0
} else {
data.length() - off
},
)
}
}
///|
fn u16_at(data : Bytes, off : Int, little : Bool) -> Int raise MetaError {
let a = byte_at(data, off).to_int()
let b = byte_at(data, off + 1).to_int()
if little {
a + (b << 8)
} else {
(a << 8) + b
}
}
///|
fn u32_at(data : Bytes, off : Int, little : Bool) -> Int64 raise MetaError {
let b0 = byte_at(data, off).to_int().to_int64()
let b1 = byte_at(data, off + 1).to_int().to_int64()
let b2 = byte_at(data, off + 2).to_int().to_int64()
let b3 = byte_at(data, off + 3).to_int().to_int64()
if little {
b0 + b1 * 256L + b2 * 65536L + b3 * 16777216L
} else {
b0 * 16777216L + b1 * 65536L + b2 * 256L + b3
}
}
///|
fn signed32(v : Int64, signed : Bool) -> Int64 {
if signed && v >= 2147483648L {
v - 4294967296L
} else {
v
}
}
///|
fn check_ifd_offset(data : Bytes, off : Int64) -> Int raise MetaError {
let len = data.length().to_int64()
if off < 0L || off + 2L > len {
raise IfdOffsetOutOfRange(off)
}
off.to_int()
}
///|
fn read_bytes(
data : Bytes,
voff : Int,
count : Int,
) -> Array[Byte] raise MetaError {
let out : Array[Byte] = []
for i = 0; i < count; i = i + 1 {
out.push(byte_at(data, voff + i))
}
out
}
///|
/// ISO-8859-1 逐字节映射:单字节 → 同名码点,无损且可逆。
///
/// **不**在 NUL 处截断:`count` 说什么就读多少字节。真实的坏文件里这一类的
/// 长度比看上去长——外来语料 `invalid-exif-without-x-resolution.jpg` 把
/// `XResolution`(本来是 RATIONAL)标成了 ASCII + count 10,负载第一个字节
/// 就是 NUL。截断读法得到空串,回写时只剩 1 字节,Pillow 随后拿
/// `x_resolution[1]` 越界,整个 JPEG 连"是不是 JPEG"都判不出来。
/// 结尾 NUL、填充 NUL 都是数据,读的时候丢不得;
/// "这段文字到哪儿为止"是显示层的事(见 `TagValue::text`)。
fn read_latin1(data : Bytes, voff : Int, count : Int) -> String raise MetaError {
let chars : Array[Char] = []
for i = 0; i < count; i = i + 1 {
let b = byte_at(data, voff + i)
match b.to_int().to_char() {
Some(c) => chars.push(c)
// 0..255 必是合法码点;这一臂取不到,取到也只是丢一个字符,
// 不值得为它 panic。
None => ()
}
}
String::from_array(chars)
}
///|
fn read_shorts(
data : Bytes,
voff : Int,
count : Int,
little : Bool,
signed : Bool,
) -> Array[Int] raise MetaError {
let out : Array[Int] = []
for i = 0; i < count; i = i + 1 {
let raw = u16_at(data, voff + 2 * i, little)
out.push(if signed && raw >= 32768 { raw - 65536 } else { raw })
}
out
}
///|
fn read_longs(
data : Bytes,
voff : Int,
count : Int,
little : Bool,
signed : Bool,
) -> Array[Int64] raise MetaError {
let out : Array[Int64] = []
for i = 0; i < count; i = i + 1 {
out.push(signed32(u32_at(data, voff + 4 * i, little), signed))
}
out
}
///|
fn read_rationals(
data : Bytes,
voff : Int,
type_code : Int,
count : Int,
little : Bool,
) -> Array[Rational] raise MetaError {
let signed = type_code == 10
let out : Array[Rational] = []
for i = 0; i < count; i = i + 1 {
let base = voff + 8 * i
out.push({
num: signed32(u32_at(data, base, little), signed),
den: signed32(u32_at(data, base + 4, little), signed),
})
}
out
}