use crate::sections::PsdCursor;
use thiserror::Error;
pub const EXPECTED_PSD_SIGNATURE: [u8; 4] = [56, 66, 80, 83];
const EXPECTED_VERSION: [u8; 2] = [0, 1];
const EXPECTED_RESERVED: [u8; 6] = [0; 6];
#[derive(Debug)]
pub struct FileHeaderSection {
pub(in crate) version: PsdVersion,
pub(in crate) channel_count: ChannelCount,
pub(in crate) width: PsdWidth,
pub(in crate) height: PsdHeight,
pub(in crate) depth: PsdDepth,
pub(in crate) color_mode: ColorMode,
}
#[derive(Debug, PartialEq, Error)]
pub enum FileHeaderSectionError {
#[error("A file section header is comprised of 26 bytes, you provided {length} bytes.")]
IncorrectLength { length: usize },
#[error(
r#"The first four bytes (indices 0-3) of a PSD must always equal [56, 66, 80, 83],
which in string form is '8BPS'."#
)]
InvalidSignature {},
#[error(
r#"Bytes 5 and 6 (indices 4-5) must always be [0, 1], Representing a PSD version of 1."#
)]
InvalidVersion {},
#[error(r#"Bytes 7-12 (indices 6-11) must be zeroes"#)]
InvalidReserved {},
#[error("Invalid channel count: {channel_count}. Must be 1 <= channel count <= 56")]
ChannelCountOutOfRange { channel_count: u8 },
#[error("Invalid width: {width}. Must be 1 <= width <= 30,000")]
WidthOutOfRange { width: u32 },
#[error("Invalid height: {height}. Must be 1 <= height <= 30,000")]
HeightOutOfRange { height: u32 },
#[error("Depth {depth} is invalid. Must be 1, 8, 16 or 32")]
InvalidDepth { depth: u8 },
#[error("Invalid color mode {color_mode}. Must be 0, 1, 2, 3, 4, 7, 8 or 9")]
InvalidColorMode { color_mode: u8 },
}
impl FileHeaderSection {
pub fn from_bytes(bytes: &[u8]) -> Result<FileHeaderSection, FileHeaderSectionError> {
let mut cursor = PsdCursor::new(bytes);
if bytes.len() != 26 {
return Err(FileHeaderSectionError::IncorrectLength {
length: bytes.len(),
}
);
}
let signature = cursor.read_4();
if signature != EXPECTED_PSD_SIGNATURE {
return Err(FileHeaderSectionError::InvalidSignature {});
}
let version = cursor.read_2();
if version != EXPECTED_VERSION {
return Err(FileHeaderSectionError::InvalidVersion {});
}
let reserved = cursor.read_6();
if reserved != EXPECTED_RESERVED {
return Err(FileHeaderSectionError::InvalidReserved {});
}
let channel_count = cursor.read_u16() as u8;
let channel_count = ChannelCount::new(channel_count)
.ok_or(FileHeaderSectionError::ChannelCountOutOfRange { channel_count })?;
let height = cursor.read_u32();
let height =
PsdHeight::new(height).ok_or(FileHeaderSectionError::HeightOutOfRange { height })?;
let width = cursor.read_u32();
let width =
PsdWidth::new(width).ok_or(FileHeaderSectionError::WidthOutOfRange { width })?;
let depth = cursor.read_2()[1];
let depth = PsdDepth::new(depth).ok_or(FileHeaderSectionError::InvalidDepth { depth })?;
let color_mode = cursor.read_2()[1];
let color_mode = ColorMode::new(color_mode)
.ok_or(FileHeaderSectionError::InvalidColorMode { color_mode })?;
let file_header_section = FileHeaderSection {
version: PsdVersion::One,
channel_count,
width,
height,
depth,
color_mode,
};
Ok(file_header_section)
}
}
#[derive(Debug)]
pub enum PsdVersion {
One,
}
#[derive(Debug)]
pub struct ChannelCount(u8);
impl ChannelCount {
pub fn new(channel_count: u8) -> Option<ChannelCount> {
if channel_count < 1 || channel_count > 56 {
return None;
}
Some(ChannelCount(channel_count))
}
pub fn count(&self) -> u8 {
self.0
}
}
#[derive(Debug)]
pub struct PsdHeight(pub(in crate) u32);
impl PsdHeight {
pub fn new(height: u32) -> Option<PsdHeight> {
if height < 1 || height > 30000 {
return None;
}
Some(PsdHeight(height))
}
}
#[derive(Debug, Clone, Copy)]
pub struct PsdWidth(pub(in crate) u32);
impl PsdWidth {
pub fn new(width: u32) -> Option<PsdWidth> {
if width < 1 || width > 30000 {
return None;
}
Some(PsdWidth(width))
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum PsdDepth {
One = 1,
Eight = 8,
Sixteen = 16,
ThirtyTwo = 32,
}
impl PsdDepth {
pub fn new(depth: u8) -> Option<PsdDepth> {
match depth {
1 => Some(PsdDepth::One),
8 => Some(PsdDepth::Eight),
16 => Some(PsdDepth::Sixteen),
32 => Some(PsdDepth::ThirtyTwo),
_ => None,
}
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
#[allow(missing_docs)]
pub enum ColorMode {
Bitmap = 0,
Grayscale = 1,
Indexed = 2,
Rgb = 3,
Cmyk = 4,
Multichannel = 7,
Duotone = 8,
Lab = 9,
}
impl ColorMode {
pub fn new(color_mode: u8) -> Option<ColorMode> {
match color_mode {
0 => Some(ColorMode::Bitmap),
1 => Some(ColorMode::Grayscale),
2 => Some(ColorMode::Indexed),
3 => Some(ColorMode::Rgb),
4 => Some(ColorMode::Cmyk),
7 => Some(ColorMode::Multichannel),
8 => Some(ColorMode::Duotone),
9 => Some(ColorMode::Lab),
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_channel_count() {
for channel_count in 1..=56 {
assert!(ChannelCount::new(channel_count).is_some());
}
}
#[test]
fn invalid_channel_count() {
assert!(ChannelCount::new(0).is_none());
assert!(ChannelCount::new(57).is_none());
}
#[test]
fn incorrect_file_header_section_length() {
let too_short = [0; 25];
match error_from_bytes(&too_short) {
FileHeaderSectionError::IncorrectLength { length } => assert_eq!(length, 25),
_ => panic!("Should have returned incorrect length error"),
};
}
#[test]
fn first_four_bytes_incorrect() {
let bytes = make_bytes();
let error = error_from_bytes(&bytes);
match error {
FileHeaderSectionError::InvalidSignature {} => {}
_ => panic!("Should have returned invalid signature error"),
};
}
#[test]
fn version_incorrect() {
let mut bytes = make_bytes();
bytes[0..4].copy_from_slice(&EXPECTED_PSD_SIGNATURE);
match error_from_bytes(&bytes) {
FileHeaderSectionError::InvalidVersion {} => {}
_ => panic!("Should have returned invalid version error"),
};
}
#[test]
fn invalid_reserved_section() {
let mut bytes = make_bytes();
bytes[0..4].copy_from_slice(&EXPECTED_PSD_SIGNATURE);
bytes[4..6].copy_from_slice(&EXPECTED_VERSION);
match error_from_bytes(&bytes) {
FileHeaderSectionError::InvalidReserved {} => {}
_ => panic!("Should have returned reserved section error"),
};
}
fn error_from_bytes(bytes: &[u8]) -> FileHeaderSectionError {
FileHeaderSection::from_bytes(&bytes).expect_err("error")
}
fn make_bytes() -> [u8; 26] {
let mut bytes = [0; 26];
for i in 0..26 {
bytes[i] = i as u8;
}
bytes
}
}