use std::fs;
use std::io::Read;
use std::path::Path;
use flate2::read::ZlibDecoder;
pub const SIGNATURE: [u8; 8] = [0x89, b'P', b'N', b'G', b'\r', b'\n', 0x1a, b'\n'];
const DISPLAY_CHUNKS: [&[u8; 4]; 15] = [
b"PLTE", b"tRNS", b"gAMA", b"cHRM", b"sRGB", b"iCCP", b"cICP", b"mDCv", b"cLLi", b"sBIT",
b"pHYs", b"bKGD", b"acTL", b"fcTL", b"fdAT",
];
const TEXT_CHUNKS: [&[u8; 4]; 3] = [b"tEXt", b"zTXt", b"iTXt"];
pub const GENERATOR_KEYWORDS: [&str; 9] = [
"parameters", "prompt", "workflow", "dream", "sd-metadata", "invokeai_metadata",
"invokeai_graph",
"aigenerated",
"generation_data",
];
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)]
pub struct TextChunk {
pub chunk: String,
pub keyword: String,
pub text: String,
#[serde(skip_serializing_if = "is_false", default)]
pub truncated: bool,
}
fn is_false(b: &bool) -> bool {
!*b
}
impl TextChunk {
pub fn is_generator(&self) -> bool {
let lower = self.keyword.to_ascii_lowercase();
GENERATOR_KEYWORDS.contains(&lower.as_str())
}
}
const fn crc_table() -> [u32; 256] {
let mut table = [0u32; 256];
let mut n = 0;
while n < 256 {
let mut c = n as u32;
let mut k = 0;
while k < 8 {
c = if c & 1 != 0 {
0xEDB8_8320 ^ (c >> 1)
} else {
c >> 1
};
k += 1;
}
table[n] = c;
n += 1;
}
table
}
static CRC_TABLE: [u32; 256] = crc_table();
pub fn crc32(bytes: &[u8]) -> u32 {
let mut c = 0xFFFF_FFFFu32;
for &b in bytes {
c = CRC_TABLE[((c ^ b as u32) & 0xFF) as usize] ^ (c >> 8);
}
c ^ 0xFFFF_FFFF
}
pub fn chunk(ctype: &[u8; 4], payload: &[u8]) -> Vec<u8> {
let mut out = Vec::with_capacity(12 + payload.len());
out.extend_from_slice(&(payload.len() as u32).to_be_bytes());
out.extend_from_slice(ctype);
out.extend_from_slice(payload);
let mut crc_input = Vec::with_capacity(4 + payload.len());
crc_input.extend_from_slice(ctype);
crc_input.extend_from_slice(payload);
out.extend_from_slice(&crc32(&crc_input).to_be_bytes());
out
}
fn be_u32(bytes: &[u8]) -> u32 {
u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
}
const MAX_CHUNK_TEXT: usize = 1 << 20; const MAX_TEXT_BUDGET: usize = 4 << 20; const MAX_TEXT_CHUNKS: usize = 256;
fn inflate(data: &[u8]) -> Option<(Vec<u8>, bool)> {
let mut out = Vec::new();
ZlibDecoder::new(data)
.take(MAX_CHUNK_TEXT as u64 + 1)
.read_to_end(&mut out)
.ok()?;
let truncated = out.len() > MAX_CHUNK_TEXT;
out.truncate(MAX_CHUNK_TEXT);
Some((out, truncated))
}
fn latin1(bytes: &[u8]) -> String {
bytes.iter().map(|&b| b as char).collect()
}
fn split_nul(data: &[u8]) -> Option<(&[u8], &[u8])> {
let at = data.iter().position(|&b| b == 0)?;
Some((&data[..at], &data[at + 1..]))
}
fn decode_text(ctype: &[u8; 4], payload: &[u8]) -> Option<(String, String, bool)> {
let (keyword, rest) = split_nul(payload)?;
let name = latin1(keyword);
match ctype {
b"tEXt" => {
let truncated = rest.len() > MAX_CHUNK_TEXT;
Some((
name,
latin1(&rest[..rest.len().min(MAX_CHUNK_TEXT)]),
truncated,
))
}
b"zTXt" => {
if rest.first() != Some(&0) {
return None;
}
let (bytes, truncated) = inflate(&rest[1..])?;
Some((name, latin1(&bytes), truncated))
}
b"iTXt" => {
if rest.len() < 2 {
return None;
}
let (compressed, method) = (rest[0], rest[1]);
let (_language, rest) = split_nul(&rest[2..])?;
let (_translated, text) = split_nul(rest)?;
let (bytes, truncated) = if compressed != 0 {
if method != 0 {
return None;
}
inflate(text)?
} else {
(
text[..text.len().min(MAX_CHUNK_TEXT)].to_vec(),
text.len() > MAX_CHUNK_TEXT,
)
};
Some((
name,
String::from_utf8_lossy(&bytes).into_owned(),
truncated,
))
}
_ => None,
}
}
pub fn read_text(path: &Path) -> Vec<TextChunk> {
let Ok(data) = fs::read(path) else {
return Vec::new();
};
if data.len() < 8 || data[..8] != SIGNATURE {
return Vec::new();
}
let mut found = Vec::new();
let mut spent = 0usize;
let mut i = 8usize;
while i + 12 <= data.len() {
let length = be_u32(&data[i..i + 4]) as usize;
let ctype: [u8; 4] = data[i + 4..i + 8].try_into().expect("four bytes");
let Some(total) = length.checked_add(12) else {
break;
};
if total > data.len() - i {
break;
}
if TEXT_CHUNKS.contains(&&ctype) && found.len() < MAX_TEXT_CHUNKS && spent < MAX_TEXT_BUDGET
{
if let Some((keyword, text, truncated)) =
decode_text(&ctype, &data[i + 8..i + 8 + length])
{
spent += text.len();
found.push(TextChunk {
chunk: String::from_utf8_lossy(&ctype).into_owned(),
keyword,
text,
truncated,
});
}
}
i += total;
if &ctype == b"IEND" {
break;
}
}
found
}
pub fn strip(src: &Path, dst: &Path, orientation: Option<&[u8]>) -> Result<u64, String> {
let data = fs::read(src).map_err(|e| format!("{}: {e}", src.display()))?;
if data.len() < 8 || data[..8] != SIGNATURE {
return Err("not a PNG".into());
}
let orientation_chunk = orientation.map(|payload| {
let body = payload.strip_prefix(b"Exif\x00\x00").unwrap_or(payload);
chunk(b"eXIf", body)
});
let mut out = Vec::with_capacity(data.len());
out.extend_from_slice(&SIGNATURE);
let mut i = 8usize;
let mut saw_header = false;
let mut saw_end = false;
let mut inserted_orientation = false;
while i < data.len() {
if i + 12 > data.len() {
return Err("truncated PNG chunk".into());
}
let length = be_u32(&data[i..i + 4]) as usize;
let ctype: [u8; 4] = data[i + 4..i + 8].try_into().expect("four bytes");
let Some(total) = length.checked_add(12) else {
return Err("PNG chunk length is not a length".into());
};
if total > data.len() - i {
return Err("truncated PNG chunk payload".into());
}
let payload = &data[i + 8..i + 8 + length];
let expected = be_u32(&data[i + 8 + length..i + total]);
let mut crc_input = Vec::with_capacity(4 + length);
crc_input.extend_from_slice(&ctype);
crc_input.extend_from_slice(payload);
if expected != crc32(&crc_input) {
return Err(format!(
"bad PNG CRC in {} chunk",
String::from_utf8_lossy(&ctype)
));
}
if !saw_header && &ctype != b"IHDR" {
return Err("PNG does not start with IHDR".into());
}
if &ctype == b"IHDR" {
if saw_header {
return Err("PNG has more than one IHDR chunk".into());
}
saw_header = true;
}
if &ctype == b"IDAT" && !inserted_orientation {
if let Some(ref oc) = orientation_chunk {
out.extend_from_slice(oc);
inserted_orientation = true;
}
}
let is_critical = ctype[0].is_ascii_uppercase();
if is_critical || DISPLAY_CHUNKS.contains(&&ctype) {
out.extend_from_slice(&data[i..i + total]);
}
i += total;
if &ctype == b"IEND" {
saw_end = true;
break;
}
}
if !saw_header || !saw_end {
return Err("PNG is missing a required terminal chunk".into());
}
if i != data.len() {
return Err("PNG has data after IEND".into());
}
fs::write(dst, &out).map_err(|e| format!("{}: {e}", dst.display()))?;
Ok((data.len() - out.len()) as u64)
}