use crate::error::{Error, Result};
use crate::tag::{Tag, TagGroup, TagId};
use crate::value::Value;
pub fn read_psp(data: &[u8]) -> Result<Vec<Tag>> {
if data.len() < 32 || !data.starts_with(b"Paint Shop Pro Image File\x0a\x1a\0\0\0\0\0") {
return Err(Error::InvalidData("not a PSP file".into()));
}
let mut tags = Vec::new();
if data.len() < 36 {
return Ok(tags);
}
let major = u16::from_le_bytes([data[32], data[33]]);
let minor = u16::from_le_bytes([data[34], data[35]]);
tags.push(mk(
"FileVersion",
"File Version",
Value::String(format!("{}.{}", major, minor)),
));
let hlen: usize = if major > 3 { 10 } else { 14 };
let mut pos = 36;
while pos + hlen <= data.len() {
if &data[pos..pos + 4] != b"~BK\0" {
break;
}
let block_type = u16::from_le_bytes([data[pos + 4], data[pos + 5]]);
let block_len = u32::from_le_bytes([
data[pos + hlen - 4],
data[pos + hlen - 3],
data[pos + hlen - 2],
data[pos + hlen - 1],
]) as usize;
pos += hlen;
if pos + block_len > data.len() {
break;
}
let block_data = &data[pos..pos + block_len];
pos += block_len;
match block_type {
0 => {
let start = if major > 3 { 4usize } else { 0usize };
parse_image_block(&block_data[start.min(block_data.len())..], &mut tags);
}
1 => {
parse_creator_block(block_data, &mut tags);
}
10 => {
parse_ext_block(block_data, &mut tags);
}
_ => {}
}
}
Ok(tags)
}
fn parse_image_block(data: &[u8], tags: &mut Vec<Tag>) {
if data.len() < 4 {
return;
}
let width = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
tags.push(mk("ImageWidth", "Image Width", Value::U32(width)));
if data.len() < 8 {
return;
}
let height = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
tags.push(mk("ImageHeight", "Image Height", Value::U32(height)));
if data.len() < 16 {
return;
}
let res_bytes: [u8; 8] = data[8..16].try_into().unwrap_or([0; 8]);
let resolution = f64::from_le_bytes(res_bytes);
if resolution > 0.0 {
tags.push(mk(
"ImageResolution",
"Image Resolution",
Value::String(format!("{}", resolution)),
));
}
if data.len() < 17 {
return;
}
let res_unit = data[16];
let unit_str = match res_unit {
0 => "None",
1 => "inches",
2 => "cm",
_ => "Unknown",
};
tags.push(mk(
"ResolutionUnit",
"Resolution Unit",
Value::String(unit_str.into()),
));
if data.len() < 19 {
return;
}
let compression = u16::from_le_bytes([data[17], data[18]]);
let comp_str = match compression {
0 => "None",
1 => "RLE",
2 => "LZ77",
3 => "JPEG",
_ => "Unknown",
};
tags.push(mk(
"Compression",
"Compression",
Value::String(comp_str.into()),
));
if data.len() < 21 {
return;
}
let bps = u16::from_le_bytes([data[19], data[20]]);
tags.push(mk("BitsPerSample", "Bits Per Sample", Value::U16(bps)));
if data.len() < 23 {
return;
}
let planes = u16::from_le_bytes([data[21], data[22]]);
tags.push(mk("Planes", "Planes", Value::U16(planes)));
if data.len() < 27 {
return;
}
let num_colors = u32::from_le_bytes([data[23], data[24], data[25], data[26]]);
tags.push(mk("NumColors", "Number of Colors", Value::U32(num_colors)));
if resolution > 0.0 {
tags.push(mk(
"XResolution",
"X Resolution",
Value::String(format!("{}", resolution)),
));
tags.push(mk(
"YResolution",
"Y Resolution",
Value::String(format!("{}", resolution)),
));
}
}
fn parse_creator_block(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 0;
while pos + 10 <= data.len() {
if &data[pos..pos + 4] != b"~FL\0" {
break;
}
let tag = u16::from_le_bytes([data[pos + 4], data[pos + 5]]);
let len = u32::from_le_bytes([data[pos + 6], data[pos + 7], data[pos + 8], data[pos + 9]])
as usize;
pos += 10;
if pos + len > data.len() {
break;
}
let val_data = &data[pos..pos + len];
pos += len;
match tag {
0 => {
let s = read_null_terminated_or_all(val_data);
if !s.is_empty() {
tags.push(mk("Title", "Title", Value::String(s)));
}
}
1 => {
if val_data.len() >= 4 {
let ts =
u32::from_le_bytes([val_data[0], val_data[1], val_data[2], val_data[3]])
as i64;
let dt = crate::formats::gzip::gzip_unix_to_datetime(ts);
tags.push(mk("CreateDate", "Create Date", Value::String(dt)));
}
}
2 => {
if val_data.len() >= 4 {
let ts =
u32::from_le_bytes([val_data[0], val_data[1], val_data[2], val_data[3]])
as i64;
let dt = crate::formats::gzip::gzip_unix_to_datetime(ts);
tags.push(mk("ModifyDate", "Modify Date", Value::String(dt)));
}
}
3 => {
let s = read_null_terminated_or_all(val_data);
if !s.is_empty() {
tags.push(mk("Artist", "Artist", Value::String(s)));
}
}
4 => {
let s = read_null_terminated_or_all(val_data);
if !s.is_empty() {
let mut t = mk("Copyright", "Copyright", Value::String(s));
t.priority = 2;
tags.push(t);
}
}
5 => {
let s = read_null_terminated_or_all(val_data);
if !s.is_empty() {
tags.push(mk("Description", "Description", Value::String(s)));
}
}
6 => {
if val_data.len() >= 4 {
let id =
u32::from_le_bytes([val_data[0], val_data[1], val_data[2], val_data[3]]);
let name = match id {
0 => "Unknown".to_string(),
1 => "Paint Shop Pro".to_string(),
n => format!("{}", n),
};
tags.push(mk("CreatorAppID", "Creator App ID", Value::String(name)));
}
}
7
if val_data.len() >= 4 => {
let v = format!(
"{}.{}.{}.{}",
val_data[3], val_data[2], val_data[1], val_data[0]
);
tags.push(mk(
"CreatorAppVersion",
"Creator App Version",
Value::String(v),
));
}
_ => {}
}
}
}
fn parse_ext_block(data: &[u8], tags: &mut Vec<Tag>) {
let mut pos = 0;
while pos + 10 <= data.len() {
if &data[pos..pos + 4] != b"~FL\0" {
break;
}
let tag = u16::from_le_bytes([data[pos + 4], data[pos + 5]]);
let len = u32::from_le_bytes([data[pos + 6], data[pos + 7], data[pos + 8], data[pos + 9]])
as usize;
pos += 10;
if pos + len > data.len() {
break;
}
let val_data = &data[pos..pos + len];
pos += len;
if tag == 3 && val_data.len() > 14 && &val_data[..6] == b"Exif\0\0" {
let exif_data = &val_data[6..];
if let Ok(exif_tags) = crate::metadata::exif::ExifReader::read(exif_data) {
tags.extend(exif_tags.into_iter().filter(|t| t.name != "ExifByteOrder"));
}
}
}
}
fn read_null_terminated_or_all(data: &[u8]) -> String {
let end = data.iter().position(|&b| b == 0).unwrap_or(data.len());
String::from_utf8_lossy(&data[..end]).into_owned()
}
fn mk(name: &str, description: &str, value: Value) -> Tag {
let pv = value.to_display_string();
Tag {
id: TagId::Text(name.to_string()),
name: name.to_string(),
description: description.to_string(),
group: TagGroup {
family0: "PSP".into(),
family1: "PSP".into(),
family2: "Image".into(),
},
raw_value: value,
print_value: pv,
priority: 0,
}
}