use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct KritaResource {
pub name: String,
pub filename: String,
pub resource_type: String,
pub md5sum: String,
pub byte_length: usize,
pub format: ResourceFormat,
#[serde(skip)]
pub bytes: Vec<u8>,
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ResourceFormat {
Png {
width: Option<u32>,
height: Option<u32>,
},
Jpeg,
Svg,
Gbr,
Gih,
Abr,
Unknown { magic_hex: String },
}
pub fn sniff_resource_format(bytes: &[u8]) -> ResourceFormat {
if bytes.starts_with(b"\x89PNG\r\n\x1a\n") {
let (w, h) = read_png_dimensions(bytes);
return ResourceFormat::Png {
width: w,
height: h,
};
}
if bytes.starts_with(b"\xff\xd8\xff") {
return ResourceFormat::Jpeg;
}
if looks_like_svg(bytes) {
return ResourceFormat::Svg;
}
if bytes.len() > 4 {
let head = &bytes[..bytes.len().min(256)];
if memchr_window(head, b"GIMP") {
return ResourceFormat::Gih;
}
}
if looks_like_gbr(bytes) {
return ResourceFormat::Gbr;
}
if looks_like_abr(bytes) {
return ResourceFormat::Abr;
}
let n = bytes.len().min(16);
let hex = bytes[..n]
.iter()
.map(|b| format!("{b:02x}"))
.collect::<Vec<_>>()
.join(" ");
ResourceFormat::Unknown { magic_hex: hex }
}
fn read_png_dimensions(bytes: &[u8]) -> (Option<u32>, Option<u32>) {
if bytes.len() < 24 || &bytes[12..16] != b"IHDR" {
return (None, None);
}
let w = u32::from_be_bytes([bytes[16], bytes[17], bytes[18], bytes[19]]);
let h = u32::from_be_bytes([bytes[20], bytes[21], bytes[22], bytes[23]]);
(Some(w), Some(h))
}
fn looks_like_svg(bytes: &[u8]) -> bool {
let head = &bytes[..bytes.len().min(512)];
let s = std::str::from_utf8(head).unwrap_or("");
let lower = s.to_ascii_lowercase();
lower.contains("<svg") || (lower.contains("<?xml") && lower.contains("svg"))
}
fn looks_like_gbr(bytes: &[u8]) -> bool {
if bytes.len() < 28 {
return false;
}
let header_size = u32::from_be_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
let version = u32::from_be_bytes([bytes[4], bytes[5], bytes[6], bytes[7]]);
if !(1..=3).contains(&version) || header_size < 20 || header_size as usize > bytes.len() {
return false;
}
if version >= 2 && &bytes[20..24] == b"GIMP" {
return true;
}
version == 1
}
fn looks_like_abr(bytes: &[u8]) -> bool {
if bytes.len() < 4 {
return false;
}
let v = u16::from_be_bytes([bytes[0], bytes[1]]);
if matches!(v, 1 | 2 | 6 | 10) {
return true;
}
bytes.starts_with(b"8BIM") || bytes.starts_with(b"8BPS")
}
fn memchr_window(haystack: &[u8], needle: &[u8]) -> bool {
if needle.is_empty() || haystack.len() < needle.len() {
return false;
}
haystack.windows(needle.len()).any(|w| w == needle)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_png_with_dimensions() {
let mut bytes = Vec::from(&b"\x89PNG\r\n\x1a\n"[..]);
bytes.extend_from_slice(&13u32.to_be_bytes()); bytes.extend_from_slice(b"IHDR");
bytes.extend_from_slice(&64u32.to_be_bytes()); bytes.extend_from_slice(&32u32.to_be_bytes()); bytes.extend_from_slice(&[8, 6, 0, 0, 0]); match sniff_resource_format(&bytes) {
ResourceFormat::Png { width, height } => {
assert_eq!(width, Some(64));
assert_eq!(height, Some(32));
}
other => panic!("expected png, got {other:?}"),
}
}
#[test]
fn detects_jpeg() {
assert!(matches!(
sniff_resource_format(&[0xff, 0xd8, 0xff, 0xe0, 0, 0]),
ResourceFormat::Jpeg
));
}
#[test]
fn detects_svg() {
let svg = b"<?xml version=\"1.0\"?><svg xmlns=\"...\"></svg>";
assert!(matches!(sniff_resource_format(svg), ResourceFormat::Svg));
}
#[test]
fn unknown_format_includes_hex() {
match sniff_resource_format(&[0xde, 0xad, 0xbe, 0xef]) {
ResourceFormat::Unknown { magic_hex } => {
assert!(magic_hex.contains("de"));
assert!(magic_hex.contains("ad"));
}
other => panic!("expected unknown, got {other:?}"),
}
}
}