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 = unix_to_exif_date(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 = unix_to_exif_date(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() {
tags.push(mk("Copyright", "Copyright", Value::String(s)));
}
}
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());
crate::encoding::decode_utf8_or_latin1(&data[..end]).to_string()
}
fn unix_to_exif_date(ts: i64) -> String {
let utc_offset = get_local_utc_offset();
let adjusted = ts + utc_offset;
let secs_per_day = 86400i64;
let days = adjusted / secs_per_day;
let time_of_day = adjusted.rem_euclid(secs_per_day);
let hour = time_of_day / 3600;
let minute = (time_of_day % 3600) / 60;
let second = time_of_day % 60;
let mut year = 1970i32;
let mut rem = days;
loop {
let dy = if is_leap(year) { 366i64 } else { 365i64 };
if rem < dy {
break;
}
rem -= dy;
year += 1;
}
let leap = is_leap(year);
let month_days = [
31i64,
if leap { 29 } else { 28 },
31,
30,
31,
30,
31,
31,
30,
31,
30,
31,
];
let mut month = 1i32;
for &dm in &month_days {
if rem < dm {
break;
}
rem -= dm;
month += 1;
}
let day = rem + 1;
let offset_hours = utc_offset / 3600;
let offset_mins = (utc_offset.abs() % 3600) / 60;
let sign = if utc_offset >= 0 { '+' } else { '-' };
format!(
"{:04}:{:02}:{:02} {:02}:{:02}:{:02}{}{:02}:{:02}",
year,
month,
day,
hour,
minute,
second,
sign,
offset_hours.abs(),
offset_mins
)
}
fn get_local_utc_offset() -> i64 {
if let Ok(tz) = std::env::var("TZ") {
let tz = tz.trim();
if let Some(sign_pos) = tz.rfind(['+', '-']) {
let sign: i64 = if &tz[sign_pos..sign_pos + 1] == "+" {
1
} else {
-1
};
if let Ok(h) = tz[sign_pos + 1..].parse::<i64>() {
return -sign * h * 3600;
}
}
}
0
}
fn is_leap(y: i32) -> bool {
(y % 4 == 0 && y % 100 != 0) || y % 400 == 0
}
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,
}
}