use crate::reader::{
read_float32, read_signature, read_uint16, read_uint32, read_unicode_string_with_length,
PsdReader, ReadError, ReadResult,
};
use crate::writer::{
create_writer, get_writer_buffer, write_float32, write_signature, write_uint16, write_uint32,
PsdWriter,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AseColorType {
Global,
Spot,
Normal,
}
impl AseColorType {
fn from_index(index: u16) -> Option<AseColorType> {
match index {
0 => Some(AseColorType::Global),
1 => Some(AseColorType::Spot),
2 => Some(AseColorType::Normal),
_ => None,
}
}
fn to_index(self) -> u16 {
match self {
AseColorType::Global => 0,
AseColorType::Spot => 1,
AseColorType::Normal => 2,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum AseColorValue {
Rgb {
r: f32,
g: f32,
b: f32,
type_: AseColorType,
},
Cmyk {
c: f32,
m: f32,
y: f32,
k: f32,
type_: AseColorType,
},
Gray { k: f32, type_: AseColorType },
Lab {
l: f32,
a: f32,
b: f32,
type_: AseColorType,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct AseColor {
pub name: String,
pub color: AseColorValue,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AseGroup {
pub name: String,
pub colors: Vec<AseColor>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AseEntry {
Color(AseColor),
Group(AseGroup),
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Ase {
pub colors: Vec<AseEntry>,
}
pub fn read_ase(buffer: &[u8]) -> ReadResult<Ase> {
let reader = &mut PsdReader::new(buffer, None, None);
let signature = read_signature(reader)?; if signature != "ASEF" {
return Err(ReadError::StrictViolation("Invalid signature".to_string()));
}
let version_major = read_uint16(reader)?; let version_minor = read_uint16(reader)?; if version_major != 1 || version_minor != 0 {
return Err(ReadError::StrictViolation("Invalid version".to_string()));
}
let blocks_count = read_uint32(reader)?;
let mut ase = Ase { colors: Vec::new() };
let mut current_group: Option<usize> = None;
for _ in 0..blocks_count {
let type_ = read_uint16(reader)?;
let length = read_uint32(reader)? as usize;
let end = reader.offset + length;
match type_ {
0x0001 => {
let name_length = read_uint16(reader)? as usize;
let name = read_unicode_string_with_length(reader, name_length)?;
let color_mode = read_signature(reader)?;
let color = match color_mode.as_str() {
"RGB " => AseColorValue::Rgb {
r: read_float32(reader)?,
g: read_float32(reader)?,
b: read_float32(reader)?,
type_: read_color_type(reader)?,
},
"CMYK" => AseColorValue::Cmyk {
c: read_float32(reader)?,
m: read_float32(reader)?,
y: read_float32(reader)?,
k: read_float32(reader)?,
type_: read_color_type(reader)?,
},
"Gray" => AseColorValue::Gray {
k: read_float32(reader)?,
type_: read_color_type(reader)?,
},
"LAB " => AseColorValue::Lab {
l: read_float32(reader)?,
a: read_float32(reader)?,
b: read_float32(reader)?,
type_: read_color_type(reader)?,
},
_ => {
return Err(ReadError::StrictViolation("Invalid color mode".to_string()))
}
};
let entry = AseColor { name, color };
match current_group {
Some(gi) => {
if let AseEntry::Group(g) = &mut ase.colors[gi] {
g.colors.push(entry);
}
}
None => ase.colors.push(AseEntry::Color(entry)),
}
}
0xC001 => {
let name_length = read_uint16(reader)? as usize;
let name = read_unicode_string_with_length(reader, name_length)?;
ase.colors.push(AseEntry::Group(AseGroup {
name,
colors: Vec::new(),
}));
current_group = Some(ase.colors.len() - 1);
}
0xC002 => {
current_group = None;
}
_ => return Err(ReadError::StrictViolation("Invalid block type".to_string())),
}
reader.offset = end;
}
Ok(ase)
}
fn read_color_type(reader: &mut PsdReader) -> ReadResult<AseColorType> {
let index = read_uint16(reader)?;
AseColorType::from_index(index)
.ok_or_else(|| ReadError::StrictViolation(format!("Invalid color type: {}", index)))
}
pub fn write_ase(ase: &Ase) -> Vec<u8> {
let mut writer = create_writer(4096);
write_signature(&mut writer, "ASEF");
write_uint16(&mut writer, 1); write_uint16(&mut writer, 0);
let mut blocks_count: u32 = 0;
for entry in &ase.colors {
match entry {
AseEntry::Color(_) => blocks_count += 1,
AseEntry::Group(g) => blocks_count += 2 + g.colors.len() as u32,
}
}
write_uint32(&mut writer, blocks_count);
for entry in &ase.colors {
match entry {
AseEntry::Color(c) => write_color_block(&mut writer, c),
AseEntry::Group(g) => {
write_block(&mut writer, 0xC001, |w| write_name(w, &g.name));
for c in &g.colors {
write_color_block(&mut writer, c);
}
write_block(&mut writer, 0xC002, |_| {});
}
}
}
get_writer_buffer(&writer)
}
fn write_name(writer: &mut PsdWriter, name: &str) {
let units: Vec<u16> = name.encode_utf16().collect();
write_uint16(writer, (units.len() + 1) as u16);
for u in &units {
write_uint16(writer, *u);
}
write_uint16(writer, 0); }
fn write_color_block(writer: &mut PsdWriter, c: &AseColor) {
write_block(writer, 0x0001, |w| {
write_name(w, &c.name);
match &c.color {
AseColorValue::Rgb { r, g, b, type_ } => {
write_signature(w, "RGB ");
write_float32(w, *r);
write_float32(w, *g);
write_float32(w, *b);
write_uint16(w, type_.to_index());
}
AseColorValue::Cmyk { c, m, y, k, type_ } => {
write_signature(w, "CMYK");
write_float32(w, *c);
write_float32(w, *m);
write_float32(w, *y);
write_float32(w, *k);
write_uint16(w, type_.to_index());
}
AseColorValue::Gray { k, type_ } => {
write_signature(w, "Gray");
write_float32(w, *k);
write_uint16(w, type_.to_index());
}
AseColorValue::Lab { l, a, b, type_ } => {
write_signature(w, "LAB ");
write_float32(w, *l);
write_float32(w, *a);
write_float32(w, *b);
write_uint16(w, type_.to_index());
}
}
});
}
fn write_block<F: FnOnce(&mut PsdWriter)>(writer: &mut PsdWriter, type_: u16, func: F) {
write_uint16(writer, type_);
let length_offset = writer.offset;
write_uint32(writer, 0); let start = writer.offset;
func(writer);
let length = (writer.offset - start) as u32;
writer.buffer[length_offset..length_offset + 4].copy_from_slice(&length.to_be_bytes());
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Ase {
Ase {
colors: vec![
AseEntry::Color(AseColor {
name: "Red".to_string(),
color: AseColorValue::Rgb {
r: 1.0,
g: 0.0,
b: 0.0,
type_: AseColorType::Global,
},
}),
AseEntry::Group(AseGroup {
name: "Grays".to_string(),
colors: vec![
AseColor {
name: "Mid".to_string(),
color: AseColorValue::Gray {
k: 0.5,
type_: AseColorType::Normal,
},
},
AseColor {
name: "Cyanish".to_string(),
color: AseColorValue::Cmyk {
c: 1.0,
m: 0.0,
y: 0.0,
k: 0.0,
type_: AseColorType::Spot,
},
},
],
}),
AseEntry::Color(AseColor {
name: "Lab".to_string(),
color: AseColorValue::Lab {
l: 50.0,
a: 10.0,
b: -20.0,
type_: AseColorType::Normal,
},
}),
],
}
}
#[test]
fn ase_round_trip() {
let ase = sample();
let bytes = write_ase(&ase);
let decoded = read_ase(&bytes).expect("read_ase");
assert_eq!(ase, decoded);
}
#[test]
fn ase_rejects_bad_signature() {
let bytes = b"XXXX\x00\x01\x00\x00\x00\x00\x00\x00";
assert!(read_ase(bytes).is_err());
}
fn fixture(sub: &str) -> std::path::PathBuf {
let mut p = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
p.pop();
p.pop();
p.push("test/ag-psd/test/ase-read");
p.push(sub);
p.push("src.ase");
p
}
#[test]
fn ase_decodes_photoshop_fixture() {
let path = fixture("from-photoshop");
if !path.exists() {
eprintln!("ase fixture missing, skipping");
return;
}
let data = std::fs::read(&path).unwrap();
let ase = read_ase(&data).expect("decode ase fixture");
assert!(!ase.colors.is_empty());
match &ase.colors[0] {
AseEntry::Color(c) => {
assert_eq!(c.name, "#FFCCCC");
match &c.color {
AseColorValue::Rgb { r, g, b, type_ } => {
assert_eq!(*r, 1.0);
assert!((*g - 0.79998779).abs() < 1e-4);
assert!((*b - 0.79998779).abs() < 1e-4);
assert_eq!(*type_, AseColorType::Global);
}
other => panic!("expected rgb, got {:?}", other),
}
}
other => panic!("expected color entry, got {:?}", other),
}
}
#[test]
fn ase_fixture_round_trip() {
let path = fixture("piratetrousle-dusk");
if !path.exists() {
eprintln!("ase fixture missing, skipping");
return;
}
let data = std::fs::read(&path).unwrap();
let ase = read_ase(&data).expect("decode");
let bytes = write_ase(&ase);
let again = read_ase(&bytes).expect("re-decode");
assert_eq!(ase, again);
}
#[test]
fn ase_smoke_header() {
let bytes = write_ase(&Ase { colors: vec![] });
assert_eq!(&bytes[0..4], b"ASEF");
let decoded = read_ase(&bytes).unwrap();
assert!(decoded.colors.is_empty());
}
}