// 容器层 · PNG。
//
// PNG 的 EXIF 住在 `eXIf` 块里(注册过的扩展块),载荷就是一整段 TIFF 块,
// 没有 JPEG 那个 "Exif\0\0" 头。XMP 则住在 `iTXt`/`tEXt` 里,
// 关键字是 "XML:com.adobe.xmp"。
//
// 每个 PNG 块自带 CRC,校验和不对的块会被解码器整块拒收,所以回写必须重算。
// 校验和只在动过的块上验:一个跟元数据无关的坏 `tEXt` 不该让人删不掉坐标,
// 但元数据块自己 CRC 对不上,就是必须停下来的事。
///|
let png_sig : Array[Byte] = [
b'\x89', b'P', b'N', b'G', b'\r', b'\n', b'\x1a', b'\n',
]
///|
let chunk_exif : Array[Byte] = [b'e', b'X', b'I', b'f']
///|
let chunk_iext : Array[Byte] = [b'i', b'T', b'X', b't']
///|
let chunk_text : Array[Byte] = [b't', b'E', b'X', b't']
///|
let chunk_iend : Array[Byte] = [b'I', b'E', b'N', b'D']
///|
/// iTXt/tEXt 里带 XMP 的那个关键字。
let xmp_keyword : Array[Byte] = [
b'X', b'M', b'L', b':', b'c', b'o', b'm', b'.', b'a', b'd', b'o', b'b', b'e', b'.',
b'x', b'm', b'p',
]
///|
/// 文本块:块里是人写的文字,不是图像参数。
///
/// 为什么单独列:真实语料里 `tEXt` 的关键字有 `date:create`、`date:modify`、
/// `icc:copyright` 这些——它们不归 IFD 管,逐条清单看不见,
/// 而"EXIF 删干净了"这句话对它们一句都没说。
let chunk_ztext : Array[Byte] = [b'z', b'T', b'X', b't']
///|
let text_chunk_kinds : Array[Array[Byte]] = [
chunk_text, chunk_iext, chunk_ztext,
]
///|
/// 公认"不装元数据"的块:结构与图像参数。它们不出声。
///
/// 和 JPEG 那份 `structural_heads` 同一条判据——白名单只收参数类,
/// 不收"看起来应该没事"。所以 `tIME`(文件最后修改时间)不在这里,
/// APNG 的 `acTL`/`fcTL` 也不在:那些我们没读,也没打算删。
let png_structural_kinds : Array[Array[Byte]] = [
[b'I', b'H', b'D', b'R'],
[b'P', b'L', b'T', b'E'],
[b'I', b'D', b'A', b'T'],
[b'I', b'E', b'N', b'D'],
[b'g', b'A', b'M', b'A'],
[b'c', b'H', b'R', b'M'],
[b's', b'R', b'G', b'B'],
[b'i', b'C', b'C', b'P'],
[b'p', b'H', b'Y', b's'],
[b'b', b'K', b'G', b'D'],
[b't', b'R', b'N', b'S'],
[b's', b'B', b'I', b'T'],
[b'h', b'I', b'S', b'T'],
[b'o', b'F', b'F', b's'],
[b'p', b'C', b'A', b'L'],
[b's', b'P', b'L', b'T'],
chunk_exif,
]
///|
/// 一个 PNG 块:类型、起始偏移、载荷字节数(不含长度域、类型和校验和)。
pub(all) struct PngChunk {
kind : String
offset : Int
length : Int
} derive(Debug, Eq)
///|
pub extend PngChunk with Eq::{not_equal, equal}
///|
pub extend PngChunk with @debug.Debug::{to_repr}
///|
/// 块表。`exif` 是 `eXIf` 块载荷的 `[起, 止)`,也就是那段 TIFF 块。
pub(all) struct PngMap {
chunks : Array[PngChunk]
exif : (Int, Int)?
has_xmp : Bool
/// 我们既没读、也不会删的块:逐块点名。
///
/// 与 JPEG 的 `unread` 同一条判据:文本块与非白名单块都算,
/// 白名单只收结构与图像参数。这一项为空的 PNG,才是
/// "除 eXIf 与 XMP 之外没有别的地方藏东西"。
unread : Array[PngChunk]
} derive(Debug, Eq)
///|
pub extend PngMap with Eq::{not_equal, equal}
///|
pub extend PngMap with @debug.Debug::{to_repr}
///|
/// 走一遍 PNG 的块表,顺手验一下 `eXIf` 的校验和。
pub fn png_inspect(data : Bytes) -> PngMap raise MetaError {
let n = data.length()
for i = 0; i < 8; i = i + 1 {
if byte_at(data, i) != png_sig[i] {
raise NotPng(i)
}
}
let chunks : Array[PngChunk] = []
let unread : Array[PngChunk] = []
let mut exif : (Int, Int)? = None
let mut has_xmp = false
let mut p = 8
let mut done = false
while p < n && !done {
let here = p
// 长度域(4) + 类型(4) + 载荷 + 校验和(4)
let avail = n - p - 12
if avail < 0 {
raise TruncatedAt(here)
}
let declared = u32_at(data, p, false)
if declared > 2147483647L || declared.to_int() > avail {
raise SegmentTooLarge(here, declared.to_int(), avail)
}
let len = declared.to_int()
let body = p + 4
let end = body + len + 8
let chunk = { kind: chunk_kind(data, body), offset: here, length: len, }
if kind_is(data, body, chunk_exif) {
verify_crc(data, here, body, len)
match exif {
Some(_) => raise ExtraMetadata(here)
None => exif = Some((body + 4, body + 4 + len))
}
} else if has_xmp_keyword(data, body, len) {
has_xmp = true
} else if unread_chunk_kind(data, body) {
unread.push(chunk)
}
chunks.push(chunk)
if kind_is(data, body, chunk_iend) {
done = true
}
p = end
}
{ chunks, exif, has_xmp, unread, }
}
///|
/// 这个块类型是不是"我们没读、又不敢担保不装元数据"的那一类。
///
/// 文本块一律算(带 XMP 关键字的那一支在上面已经被拦走了,走到这里的
/// 就是别的文字:`date:create`、`Copyright` 都在这支队伍里)。
/// 其余块只有白名单上的才算读过了——白名单外的一律点名。
fn unread_chunk_kind(data : Bytes, at : Int) -> Bool {
if one_of(data, at, text_chunk_kinds) {
true
} else {
!one_of(data, at, png_structural_kinds)
}
}
///|
fn one_of(data : Bytes, at : Int, kinds : Array[Array[Byte]]) -> Bool {
for kind in kinds {
if starts_with(data, at, kind) {
return true
}
}
false
}
///|
fn kind_is(data : Bytes, at : Int, kind : Array[Byte]) -> Bool {
starts_with(data, at, kind)
}
///|
/// `iTXt`/`tEXt` 里带 XMP 关键字的那一种。只在当前块的载荷里找。
fn has_xmp_keyword(data : Bytes, body : Int, len : Int) -> Bool {
let text = kind_is(data, body, chunk_iext) || kind_is(data, body, chunk_text)
text && find_bytes(data, body + 4, body + 4 + len, xmp_keyword) != None
}
///|
fn chunk_kind(data : Bytes, at : Int) -> String raise MetaError {
let c0 = byte_at(data, at).to_int().unsafe_to_char()
let c1 = byte_at(data, at + 1).to_int().unsafe_to_char()
let c2 = byte_at(data, at + 2).to_int().unsafe_to_char()
let c3 = byte_at(data, at + 3).to_int().unsafe_to_char()
"\{c0}\{c1}\{c2}\{c3}"
}
///|
/// `eXIf` 块的 CRC 必须和字节对得上。
///
/// 对不上就不回写:在一段已经坏了的元数据上报告"脱敏成功",
/// 等于把自己的正确性建立在一个没人能打开的文件上。
fn verify_crc(
data : Bytes,
here : Int,
body : Int,
len : Int,
) -> Unit raise MetaError {
let crc_at = body + 4 + len
let want = u32_at(data, crc_at, false)
let got = crc32(slice_bytes(data, body, crc_at))
if want != got {
raise BadChecksum(here, want, got)
}
}
///|
/// PNG 用的 CRC-32(IEEE 802.3 反射式,初值与末值取反)。
///
/// 结果会超过 31 位,所以一路带 Int64。
pub fn crc32(data : Bytes) -> Int64 {
let mut c = 4294967295L
for b in data {
c = c ^ b.to_int().to_int64()
let mut i = 0
while i < 8 {
let odd = (c & 1L) == 1L
c = c >> 1
if odd {
c = c ^ 3988292384L
}
i = i + 1
}
}
c ^ 4294967295L
}
///|
/// 取出 `eXIf` 块里的 TIFF 块。
pub fn png_exif(data : Bytes) -> Bytes? raise MetaError {
match png_inspect(data).exif {
None => None
Some((s, e)) => Some(slice_bytes(data, s, e))
}
}
///|
/// 读出 PNG 里的 EXIF。没有就是 `None`。
pub fn png_decode(data : Bytes) -> TiffBlock? raise MetaError {
match png_exif(data) {
None => None
Some(tiff) => Some(decode_tiff(tiff))
}
}
///|
/// 替换 `eXIf` 块;原本没有就插在 `IHDR` 之后。校验和重算。
pub fn png_replace_exif(data : Bytes, tiff : Bytes) -> Bytes raise MetaError {
let map = png_inspect(data)
let seg = exif_chunk(tiff)
let body = data.to_array()
let (from, to) = replace_range(map)
let out : Array[Byte] = []
push_all(out, body, 0, from)
push_all(out, seg, 0, seg.length())
push_all(out, body, to, body.length())
Bytes::from_array(out)
}
///|
/// 要被替换掉的字节区间:`eXIf` 整块,含长度域与校验和。
/// 没有该块时,退回插入点上的空区间。
fn replace_range(map : PngMap) -> (Int, Int) {
match map.exif {
Some((s, e)) => (s - 8, e + 4)
None => {
let at = after_ihdr(map)
(at, at)
}
}
}
///|
/// 没有 `eXIf` 时新块的落点:`IHDR` 之后。规范只要求它在 `IDAT` 之前,
/// 紧跟 `IHDR` 是最稳的位置。
fn after_ihdr(map : PngMap) -> Int {
match map.chunks.get(0) {
Some(c) => c.offset + 12 + c.length
None => 8
}
}
///|
/// 摘掉 `eXIf` 块,并报告原本有没有。
pub fn png_remove_exif(data : Bytes) -> (Bytes, Bool) raise MetaError {
let map = png_inspect(data)
match map.exif {
None => (data, false)
Some((s, e)) => {
let body = data.to_array()
let out : Array[Byte] = []
push_all(out, body, 0, s - 8)
push_all(out, body, e + 4, body.length())
(Bytes::from_array(out), true)
}
}
}
///|
/// 摘掉带 `XML:com.adobe.xmp` 关键字的文本块,并报告"原本确实有一块"。
///
/// 搬的是"这些块之外的字节",不是重建文件:IDAT 一个字节都不重编码,
/// IEND 之后可能有的尾部垃圾也照原样留着。
pub fn png_drop_xmp(data : Bytes) -> (Bytes, Bool) raise MetaError {
let map = png_inspect(data)
let body = data.to_array()
let out : Array[Byte] = []
let mut cursor = 0
let mut dropped = false
for c in map.chunks {
let end = c.offset + 12 + c.length
if has_xmp_keyword(data, c.offset + 4, c.length) {
dropped = true
push_all(out, body, cursor, c.offset)
cursor = end
}
}
if !dropped {
(data, false)
} else {
push_all(out, body, cursor, body.length())
(Bytes::from_array(out), true)
}
}
///|
/// 解析 → 按策略删 → 回写。
pub fn png_redact(
data : Bytes,
p : Policy,
) -> (Bytes, Redaction) raise MetaError {
let map = png_inspect(data)
let (out, report) = match map.exif {
None =>
(
data,
{
removed: [],
dropped_thumbnail: false,
dropped_xmp: false,
dropped_iptc: false,
},
)
Some((s, e)) => {
let block = decode_tiff(slice_bytes(data, s, e))
let (kept, report) = block.redact(p)
// 与 JPEG 侧同一条规矩:一条没删就不重写这个块。
// 空策略必须是恒等,不能顺手把文件规范化一遍。
if report.removed.is_empty() && !report.dropped_thumbnail {
(data, report)
} else {
(png_replace_exif(data, encode_tiff(kept)), report)
}
}
}
// 没有 eXIf 也照走这一趟:XMP 完全可以独立存在,那种文件正是"第二份副本"。
if !p.covers(Carrier) {
(out, report)
} else {
let (out2, dropped) = png_drop_xmp(out)
// dropped_iptc 在 PNG 这边永远是 false:那套 IPTC 住在 `Raw profile type iptc`
// 文本块里,本库不解析它,只把它点名进 `unread`。不解析的东西不许报"已摘除"。
(out2, { ..report, dropped_xmp: dropped, dropped_iptc: false, })
}
}
///|
/// 造一个 `eXIf` 块:长度(4) + 类型(4) + 载荷 + 校验和(4)。
fn exif_chunk(tiff : Bytes) -> Array[Byte] {
let out : Array[Byte] = []
put_u32(out, tiff.length().to_int64(), false)
push_all(out, chunk_exif, 0, 4)
let body = tiff.to_array()
push_all(out, body, 0, body.length())
let head : Array[Byte] = []
push_all(head, out, 4, out.length())
let crc = crc32(Bytes::from_array(head))
put_u32(out, crc, false)
out
}
///|
/// 一行块表,给 CLI 的审计输出用。
pub fn PngMap::describe(self : PngMap) -> String {
let mut head = "PNG"
for c in self.chunks {
head = head + " \{c.kind}@\{c.offset}(\{c.length}B)"
}
if self.has_xmp {
head = head + " ; 另含 XMP 文本块"
}
if !self.unread.is_empty() {
let names = self.unread.map(c => "\{c.kind}@\{c.offset}").join(" ")
head + " ; 未解析块 \{self.unread.length()} 块:\{names}"
} else {
head
}
}