From 31922f4e93eb99250b8ed765a528a247ac841474 Mon Sep 17 00:00:00 2001 From: Adam Jahn Date: Thu, 29 Aug 2024 17:25:33 -0400 Subject: [PATCH 01/16] partial write support in progress --- src/lib.rs | 52 +- src/psd_channel.rs | 22 +- src/sections/color_mode_data_section.rs | 46 + src/sections/file_header_section.rs | 74 +- src/sections/image_data_section.rs | 35 + src/sections/image_resources_section.rs | 710 +-------------- .../image_resources_section/image_resource.rs | 66 +- .../image_resource/descriptor_structure.rs | 823 ++++++++++++++++++ .../image_resource/slices.rs | 324 +++++++ .../layer.rs | 37 + .../layers.rs | 4 +- .../layer_and_mask_information_section/mod.rs | 244 +++++- src/sections/mod.rs | 231 ++++- tests/slices_resource.rs | 4 +- 14 files changed, 1953 insertions(+), 719 deletions(-) create mode 100644 src/sections/color_mode_data_section.rs create mode 100644 src/sections/image_resources_section/image_resource/descriptor_structure.rs create mode 100644 src/sections/image_resources_section/image_resource/slices.rs diff --git a/src/lib.rs b/src/lib.rs index 5823a16..7d85d19 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,6 +10,8 @@ use std::collections::HashMap; use std::ops::Deref; +use sections::color_mode_data_section::ColorModeDataSectionError; +use sections::{PsdBuffer, PsdDeserialize, PsdSerialize}; use thiserror::Error; use sections::file_header_section::FileHeaderSectionError; @@ -19,12 +21,15 @@ use sections::layer_and_mask_information_section::layer::PsdLayerError; use crate::psd_channel::IntoRgba; pub use crate::psd_channel::{PsdChannelCompression, PsdChannelKind}; +use crate::sections::color_mode_data_section::ColorModeDataSection; pub use crate::sections::file_header_section::{ColorMode, PsdDepth}; use crate::sections::image_data_section::ChannelBytes; use crate::sections::image_data_section::ImageDataSection; +pub use crate::sections::image_resources_section::image_resource::descriptor_structure::{ + DescriptorField, UnitFloatStructure, +}; pub use crate::sections::image_resources_section::ImageResource; use crate::sections::image_resources_section::ImageResourcesSection; -pub use crate::sections::image_resources_section::{DescriptorField, UnitFloatStructure}; pub use crate::sections::layer_and_mask_information_section::layer::PsdGroup; pub use crate::sections::layer_and_mask_information_section::layer::PsdLayer; use crate::sections::layer_and_mask_information_section::LayerAndMaskInformationSection; @@ -46,6 +51,9 @@ pub enum PsdError { /// Failed to parse PSD header #[error("Failed to parse PSD header: '{0}'.")] HeaderError(FileHeaderSectionError), + /// Failed to parse PSD color mode data + #[error("Failed to parse PSD color mode data: '{0}'.")] + ColorModeDataError(ColorModeDataSectionError), /// Failed to parse PSD layer #[error("Failed to parse PSD layer: '{0}'.")] LayerError(PsdLayerError), @@ -66,6 +74,7 @@ pub enum PsdError { #[derive(Debug)] pub struct Psd { file_header_section: FileHeaderSection, + color_mode_data_section: ColorModeDataSection, image_resources_section: ImageResourcesSection, layer_and_mask_information_section: LayerAndMaskInformationSection, image_data_section: ImageDataSection, @@ -93,6 +102,10 @@ impl Psd { let psd_height = file_header_section.height.0; let channel_count = file_header_section.channel_count.count(); + let color_mode_data_section = + ColorModeDataSection::from_bytes(major_sections.color_mode_data) + .map_err(PsdError::ColorModeDataError)?; + let layer_and_mask_information_section = LayerAndMaskInformationSection::from_bytes( major_sections.layer_and_mask, psd_width, @@ -114,11 +127,34 @@ impl Psd { Ok(Psd { file_header_section, + color_mode_data_section, image_resources_section, layer_and_mask_information_section, image_data_section, }) } + + /// Create bytes from a Psd. + /// + /// # Example + /// + /// ```ignore + /// let psd = Psd::new(); + /// + /// let bytes = Psd::into_bytes(psd); + /// ``` + pub fn into_bytes(&self) -> Result, PsdError> { + let mut bytes: Vec = vec![]; + let mut buffer = PsdBuffer::new(&mut bytes); + + self.file_header_section.write(&mut buffer); + self.color_mode_data_section.write(&mut buffer); + self.image_resources_section.write(&mut buffer); + self.layer_and_mask_information_section.write(&mut buffer); + self.image_data_section.write(&mut buffer); + + Ok(bytes) + } } // Methods for working with the file section header @@ -339,4 +375,18 @@ mod tests { PsdError::HeaderError(FileHeaderSectionError::InvalidSignature {}) ); } + + #[test] + fn write_smoketest() { + let intial_bytes = include_bytes!( + "../tests/fixtures/groups/green-1x1-one-group-one-layer-inside-one-outside.psd" + ); + let original = Psd::from_bytes(intial_bytes).unwrap(); + + let bytes = original.into_bytes().unwrap(); + + let psd = Psd::from_bytes(&bytes).unwrap(); + + assert_eq!(original.layers().len(), psd.layers().len()); + } } diff --git a/src/psd_channel.rs b/src/psd_channel.rs index f5cefc2..c303024 100644 --- a/src/psd_channel.rs +++ b/src/psd_channel.rs @@ -1,5 +1,5 @@ use crate::sections::image_data_section::ChannelBytes; -use crate::sections::PsdCursor; +use crate::sections::{PsdCursor, PsdSerialize}; use thiserror::Error; pub trait IntoRgba { @@ -267,7 +267,7 @@ fn sixteen_to_eight_rgba(channel1: &[u8], channel2: &[u8]) -> Vec { } /// Indicates how a channe'sl data is compressed -#[derive(Debug, Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq, Copy, Clone)] #[allow(missing_docs)] pub enum PsdChannelCompression { /// Not compressed @@ -293,6 +293,15 @@ impl PsdChannelCompression { } } +impl PsdSerialize for PsdChannelCompression { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write((self.to_owned() as u16).to_be_bytes()); + } +} + /// The different kinds of channels in a layer (red, green, blue, ...). #[derive(Debug, Hash, Eq, PartialEq, Ord, PartialOrd, Copy, Clone)] #[allow(missing_docs)] @@ -305,6 +314,15 @@ pub enum PsdChannelKind { RealUserSuppliedLayerMask = -3, } +impl PsdSerialize for PsdChannelKind { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write((self.to_owned() as i16).to_be_bytes()); + } +} + /// Represents an invalid channel #[derive(Debug, Error)] pub enum PsdChannelError { diff --git a/src/sections/color_mode_data_section.rs b/src/sections/color_mode_data_section.rs new file mode 100644 index 0000000..60b4dfc --- /dev/null +++ b/src/sections/color_mode_data_section.rs @@ -0,0 +1,46 @@ +use std::io::{Seek, Write}; +use thiserror::Error; + +use super::{PsdBuffer, PsdSerialize}; + +#[derive(Debug, PartialEq, Error)] +pub enum ColorModeDataSectionError {} + +#[derive(Debug, PartialEq)] +pub struct ColorModeDataSection {} + +impl ColorModeDataSection { + pub fn from_bytes(_bytes: &[u8]) -> Result { + Ok(Self {}) + } +} + +impl PsdSerialize for ColorModeDataSection { + fn write(&self, buffer: &mut PsdBuffer) + where + T: Write + Seek, + { + buffer.write_sized(|buf| buf.write([])); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn write_read_round_trip() { + let initial = make_section(); + let mut bytes: Vec = vec![]; + let mut buffer = PsdBuffer::new(&mut bytes); + + initial.write(&mut buffer); + + let result = ColorModeDataSection::from_bytes(&bytes).unwrap(); + assert_eq!(initial, result); + } + + fn make_section() -> ColorModeDataSection { + ColorModeDataSection {} + } +} diff --git a/src/sections/file_header_section.rs b/src/sections/file_header_section.rs index 7195c08..1b5be3c 100644 --- a/src/sections/file_header_section.rs +++ b/src/sections/file_header_section.rs @@ -1,6 +1,10 @@ +use std::io::Write; + use crate::sections::PsdCursor; use thiserror::Error; +use super::{PsdBuffer, PsdSerialize}; + /// Bytes representing the string "8BPS". pub const EXPECTED_PSD_SIGNATURE: [u8; 4] = [56, 66, 80, 83]; /// Bytes representing the number 1 @@ -30,14 +34,14 @@ const EXPECTED_RESERVED: [u8; 6] = [0; 6]; /// | 4 | The width of the image in pixels. Supported range is 1 to 30,000.
(**PSB** max of 300,000) | /// | 2 | Depth: the number of bits per channel. Supported values are 1, 8, 16 and 32. | /// | 2 | The color mode of the file. Supported values are: Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9. | -#[derive(Debug)] +#[derive(Debug, PartialEq)] 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, + pub(crate) version: PsdVersion, + pub(crate) channel_count: ChannelCount, + pub(crate) width: PsdWidth, + pub(crate) height: PsdHeight, + pub(crate) depth: PsdDepth, + pub(crate) color_mode: ColorMode, } /// Represents an malformed file section header @@ -81,8 +85,7 @@ impl FileHeaderSection { if bytes.len() != 26 { return Err(FileHeaderSectionError::IncorrectLength { length: bytes.len(), - } - ); + }); } // First four bytes must be '8BPS' @@ -140,12 +143,30 @@ impl FileHeaderSection { } } +impl PsdSerialize for FileHeaderSection { + fn write(&self, buffer: &mut PsdBuffer) + where + T: Write + std::io::Seek, + { + buffer.write(EXPECTED_PSD_SIGNATURE); + match self.version { + PsdVersion::One => buffer.write(EXPECTED_VERSION), + }; + buffer.write(EXPECTED_RESERVED); + buffer.write((self.channel_count.count() as u16).to_be_bytes()); + buffer.write(self.height.0.to_be_bytes()); + buffer.write(self.width.0.to_be_bytes()); + buffer.write((self.depth as u16).to_be_bytes()); + buffer.write((self.color_mode as u16).to_be_bytes()) + } +} + /// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) /// /// Version: always equal to 1. Do not try to read the file if the version does not match this value. (**PSB** version is 2.) /// /// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum PsdVersion { /// Regular PSD (Not a PSB) One, @@ -156,7 +177,7 @@ pub enum PsdVersion { /// The number of channels in the image, including any alpha channels. Supported range is 1 to 56. /// /// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub struct ChannelCount(u8); impl ChannelCount { @@ -181,8 +202,8 @@ impl ChannelCount { /// (**PSB** max of 300,000.) /// /// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ -#[derive(Debug)] -pub struct PsdHeight(pub(in crate) u32); +#[derive(Debug, PartialEq)] +pub struct PsdHeight(pub(crate) u32); impl PsdHeight { /// Create a new PsdHeight @@ -201,8 +222,8 @@ impl PsdHeight { /// (*PSB** max of 300,000) /// /// via: https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/ -#[derive(Debug, Clone, Copy)] -pub struct PsdWidth(pub(in crate) u32); +#[derive(Debug, Clone, Copy, PartialEq)] +pub struct PsdWidth(pub(crate) u32); impl PsdWidth { /// Create a new PsdWidth @@ -348,6 +369,18 @@ mod tests { }; } + #[test] + fn write_read_round_trip() { + let initial = make_section(); + let mut bytes: Vec = vec![]; + let mut buffer = PsdBuffer::new(&mut bytes); + + initial.write(&mut buffer); + + let result = FileHeaderSection::from_bytes(&bytes).unwrap(); + assert_eq!(initial, result); + } + fn error_from_bytes(bytes: &[u8]) -> FileHeaderSectionError { FileHeaderSection::from_bytes(&bytes).expect_err("error") } @@ -361,4 +394,15 @@ mod tests { bytes } + + fn make_section() -> FileHeaderSection { + FileHeaderSection { + version: PsdVersion::One, + channel_count: ChannelCount(2), + height: PsdHeight(3), + width: PsdWidth(4), + depth: PsdDepth::Eight, + color_mode: ColorMode::Rgb, + } + } } diff --git a/src/sections/image_data_section.rs b/src/sections/image_data_section.rs index 7411b06..49a943a 100644 --- a/src/sections/image_data_section.rs +++ b/src/sections/image_data_section.rs @@ -3,6 +3,8 @@ use crate::sections::PsdCursor; use crate::PsdDepth; use thiserror::Error; +use super::PsdSerialize; + /// Represents an malformed image data #[derive(Debug, PartialEq, Error)] pub enum ImageDataSectionError { @@ -44,6 +46,27 @@ pub struct ImageDataSection { pub(crate) alpha: Option, } +impl PsdSerialize for ImageDataSection { + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + self.compression.write(buffer); + self.red.write(buffer); + + if let Some(green) = &self.green { + green.write(buffer); + } + + if let Some(blue) = &self.blue { + blue.write(buffer); + } + + if let Some(alpha) = &self.alpha { + alpha.write(buffer); + } + } +} impl ImageDataSection { /// Create an ImageDataSection from the bytes in the corresponding section in a PSD file /// (including the length market) @@ -224,3 +247,15 @@ pub enum ChannelBytes { RawData(Vec), RleCompressed(Vec), } + +impl PsdSerialize for ChannelBytes { + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + match self { + Self::RawData(bytes) => buffer.write(bytes), + Self::RleCompressed(bytes) => buffer.write(bytes), + } + } +} diff --git a/src/sections/image_resources_section.rs b/src/sections/image_resources_section.rs index f740008..4618d48 100644 --- a/src/sections/image_resources_section.rs +++ b/src/sections/image_resources_section.rs @@ -1,17 +1,22 @@ -use std::collections::HashMap; +use std::io::{Seek, Write}; use std::ops::Range; use thiserror::Error; -pub use crate::sections::image_resources_section::image_resource::ImageResource; -use crate::sections::image_resources_section::image_resource::SlicesImageResource; +use crate::sections::image_resources_section::image_resource::descriptor_structure::{ + DescriptorStructure, ImageResourcesDescriptorError, +}; use crate::sections::PsdCursor; +use super::{PsdBuffer, PsdDeserialize, PsdSerialize}; +pub use crate::sections::image_resources_section::image_resource::{ + ImageResource, SlicesImageResource, +}; + const EXPECTED_RESOURCE_BLOCK_SIGNATURE: [u8; 4] = [56, 66, 73, 77]; -const EXPECTED_DESCRIPTOR_VERSION: u32 = 16; const RESOURCE_SLICES_INFO: i16 = 1050; -mod image_resource; +pub mod image_resource; struct ImageResourcesBlock { resource_id: i16, @@ -37,8 +42,23 @@ pub enum ImageResourcesSectionError { InvalidResource(ImageResourcesDescriptorError), } -impl ImageResourcesSection { - pub fn from_bytes(bytes: &[u8]) -> Result { +impl PsdSerialize for ImageResourcesSection { + fn write(&self, buffer: &mut PsdBuffer) + where + T: Write + Seek, + { + buffer.write_sized(|buf| { + for resource in self.resources.iter() { + resource.write(buf) + } + }); + } +} + +impl PsdDeserialize for ImageResourcesSection { + type Error = ImageResourcesSectionError; + + fn from_bytes(bytes: &[u8]) -> Result { let mut cursor = PsdCursor::new(bytes); let mut resources = vec![]; @@ -51,10 +71,9 @@ impl ImageResourcesSection { let rid = block.resource_id; match rid { _ if rid == RESOURCE_SLICES_INFO => { - let slices_image_resource = ImageResourcesSection::read_slice_block( - &cursor.get_ref()[block.data_range], - ) - .map_err(ImageResourcesSectionError::InvalidResource)?; + let slices_image_resource = + SlicesImageResource::from_bytes(&cursor.get_ref()[block.data_range]) + .map_err(ImageResourcesSectionError::InvalidResource)?; resources.push(ImageResource::Slices(slices_image_resource)); } _ => {} @@ -65,7 +84,9 @@ impl ImageResourcesSection { Ok(ImageResourcesSection { resources }) } +} +impl ImageResourcesSection { /// +----------+--------------------------------------------------------------------------------------------------------------------+ /// | Length | Description | /// +----------+--------------------------------------------------------------------------------------------------------------------+ @@ -103,671 +124,4 @@ impl ImageResourcesSection { data_range, }) } - - /// Slice header for version 6 - /// - /// +----------+--------------------------------------------------------------------------------------+ - /// | Length | Description | - /// +----------+--------------------------------------------------------------------------------------+ - /// | 4 | Version ( = 6) | - /// | 4 * 4 | Bounding rectangle for all of the slices: top, left, bottom, right of all the slices | - /// | Variable | Name of group of slices: Unicode string | - /// | 4 | Number of slices to follow. See Slices resource block in the next table | - /// +----------+--------------------------------------------------------------------------------------+ - fn read_slice_block( - bytes: &[u8], - ) -> Result { - let mut cursor = PsdCursor::new(bytes); - - let version = cursor.read_i32(); - if version == 6 { - let _top = cursor.read_i32(); - let _left = cursor.read_i32(); - let _bottom = cursor.read_i32(); - let _right = cursor.read_i32(); - - let group_of_slices_name = cursor.read_unicode_string_padding(1); - - let number_of_slices = cursor.read_u32(); - - let mut descriptors = Vec::new(); - - for _ in 0..number_of_slices { - match ImageResourcesSection::read_slice_body(&mut cursor)? { - Some(v) => descriptors.push(v), - None => {} - } - } - - return Ok(SlicesImageResource { - name: group_of_slices_name, - descriptors, - }); - } - if version == 7 || version == 8 { - let descriptor_version = cursor.read_i32(); - if descriptor_version != 16 { - unimplemented!( - "Only the version 16 (descriptors) resource format for slices is currently supported" - ); - } - let descriptor = DescriptorStructure::read_descriptor_structure(&mut cursor)?; - return Ok(SlicesImageResource { - name: descriptor.name.clone(), - descriptors: vec![descriptor], - }); - } - unimplemented!("Slices resource format {version} is currently not supported"); - } - - /// Slices resource block - /// - /// +------------------------------------------------------+-----------------------------------------------+ - /// | Length | Description | - /// +------------------------------------------------------+-----------------------------------------------+ - /// | 4 | ID | - /// | 4 | Group ID | - /// | 4 | Origin | - /// | 4 | Associated Layer ID | - /// | Only present if Origin = 1 | | - /// | Variable | Name: Unicode string | - /// | 4 | Type | - /// | 4 * 4 | Left, top, right, bottom positions | - /// | Variable | URL: Unicode string | - /// | Variable | Target: Unicode string | - /// | Variable | Message: Unicode string | - /// | Variable | Alt Tag: Unicode string | - /// | 1 | Cell text is HTML: Boolean | - /// | Variable | Cell text: Unicode string | - /// | 4 | Horizontal alignment | - /// | 4 | Vertical alignment | - /// | 1 | Alpha color | - /// | 1 | Red | - /// | 1 | Green | - /// | 1 | Blue | - /// | Additional data as length allows. See comment above. | | - /// | 4 | Descriptor version ( = 16 for Photoshop 6.0). | - /// | Variable | Descriptor (see See Descriptor structure) | - /// +------------------------------------------------------+-----------------------------------------------+ - fn read_slice_body( - cursor: &mut PsdCursor, - ) -> Result, ImageResourcesDescriptorError> { - let _slice_id = cursor.read_i32(); - let _group_id = cursor.read_i32(); - let origin = cursor.read_i32(); - - // if origin = 1, Associated Layer ID is present - if origin == 1 { - cursor.read_i32(); - } - - let _name = cursor.read_unicode_string_padding(1); - - let _type = cursor.read_i32(); - - let _top = cursor.read_i32(); - let _left = cursor.read_i32(); - let _bottom = cursor.read_i32(); - let _right = cursor.read_i32(); - - let _url = cursor.read_unicode_string_padding(1); - - let _target = cursor.read_unicode_string_padding(1); - - let _message = cursor.read_unicode_string_padding(1); - - let _alt_tag = cursor.read_unicode_string_padding(1); - - let _cell_text_html = cursor.read_1(); - let _cell_text = cursor.read_unicode_string_padding(1); - - let _horizontal_alignment = cursor.read_i32(); - let _vertical_alignment = cursor.read_i32(); - let _argb_color = cursor.read_i32(); - - let pos = cursor.position(); - let descriptor_version = cursor.peek_u32(); - - Ok(if descriptor_version == EXPECTED_DESCRIPTOR_VERSION { - cursor.read_4(); - - let descriptor = DescriptorStructure::read_descriptor_structure(cursor)?; - if descriptor.class_id.as_slice() == [0, 0, 0, 0] { - cursor.seek(pos); - } - - Some(descriptor) - } else { - None - }) - } -} - -/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ -/// | Variable | Unicode string: name from classID | -/// | Variable | classID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | -/// | 4 | Number of items in descriptor | -/// | The following is repeated for each item in descriptor | | -/// | Variable | Key: 4 bytes ( length) followed either by string or (if length is zero) 4-byte key | -/// | 4 | Type: OSType key | -/// | | 'obj ' = Reference | -/// | | 'Objc' = Descriptor | -/// | | 'VlLs' = List | -/// | | 'doub' = Double | -/// | | 'UntF' = Unit float | -/// | | 'TEXT' = String | -/// | | 'enum' = Enumerated | -/// | | 'long' = Integer | -/// | | 'comp' = Large Integer | -/// | | 'bool' = Boolean | -/// | | 'GlbO' = GlobalObject same as Descriptor | -/// | | 'type' = Class | -/// | | 'GlbC' = Class | -/// | | 'alis' = Alias | -/// | | 'tdta' = Raw Data | -/// | Variable | Item type: see the tables below for each possible type | -/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct DescriptorStructure { - pub name: String, - pub fields: HashMap, - pub class_id: Vec, -} - -/// One of -#[derive(Debug)] -pub enum DescriptorField { - /// Descriptor as field - Descriptor(DescriptorStructure), - /// A list of special fields - /// There are can be Property, Identifier, Index, Name fields - Reference(Vec), - /// Float field with unit - UnitFloat(UnitFloatStructure), - /// Double-precision floating-point number - Double(f64), - /// - Class(ClassStructure), - /// Text - String(String), - /// - EnumeratedReference(EnumeratedReference), - /// - Offset(OffsetStructure), - /// Boolean value - Boolean(bool), - /// - Alias(AliasStructure), - /// A list of fields - List(Vec), - /// 64bit integer number - LargeInteger(i64), - /// 32bit integer number - Integer(i32), - /// - EnumeratedDescriptor(EnumeratedDescriptor), - /// Raw bytes data - RawData(Vec), - - /// Only Reference fields - /// - /// - Property(PropertyStructure), - /// - Identifier(i32), - /// - Index(i32), - /// - Name(NameStructure), -} - -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Variable | Unicode string: name from classID | -/// | Variable | classID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | -/// | Variable | KeyID: 4 bytes (length), followed either by string or (if length is zero) 4-byte keyID | -/// +----------+--------------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct PropertyStructure { - pub name: String, - pub class_id: Vec, - pub key_id: Vec, -} - -/// +------------------------------------+--------------------------------------------------------+ -/// | Length | Description | -/// +------------------------------------+--------------------------------------------------------+ -/// | 4 | Units the following value is in. One of the following: | -/// | | '#Ang' = angle: base degrees | -/// | | '#Rsl' = density: base per inch | -/// | | '#Rlt' = distance: base 72ppi | -/// | | '#Nne' = none: coerced. | -/// | | '#Prc'= percent: unit value | -/// | | '#Pxl' = pixels: tagged unit value | -/// | 8 | Actual value (double) | -/// +------------------------------------+--------------------------------------------------------+ -#[derive(Debug)] -pub enum UnitFloatStructure { - /// Base degrees - Angle(f64), - /// Base per inch - Density(f64), - /// Base 72ppi - Distance(f64), - /// Base coerced - None, - /// Unit value - Percent(f64), - /// Tagged unit value - Pixels(f64), -} - -/// Unit float structure units keys -/// '#Ang' = angle: base degrees -const UNIT_FLOAT_ANGLE: &[u8; 4] = b"#Ang"; -/// '#Rsl' = density: base per inch -const UNIT_FLOAT_DENSITY: &[u8; 4] = b"#Rsl"; -/// '#Rlt' = distance: base 72ppi -const UNIT_FLOAT_DISTANCE: &[u8; 4] = b"#Rlt"; -/// '#Nne' = none: coerced. -const UNIT_FLOAT_NONE: &[u8; 4] = b"#Nne"; -/// '#Prc'= percent: unit value -const UNIT_FLOAT_PERCENT: &[u8; 4] = b"#Prc"; -/// '#Pxl' = pixels: tagged unit value -const UNIT_FLOAT_PIXELS: &[u8; 4] = b"#Pxl"; - -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Variable | Unicode string: name from classID | -/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | -/// +----------+--------------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct ClassStructure { - pub name: String, - pub class_id: Vec, -} - -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Variable | Unicode string: name from ClassID. | -/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | -/// | Variable | TypeID: 4 bytes (length), followed either by string or (if length is zero) 4-byte typeID | -/// | Variable | enum: 4 bytes (length), followed either by string or (if length is zero) 4-byte enum | -/// +----------+--------------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct EnumeratedReference { - pub name: String, - pub class_id: Vec, - pub key_id: Vec, - pub enum_field: Vec, -} - -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Variable | Unicode string: name from ClassID | -/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | -/// | 4 | Value of the offset | -/// +----------+--------------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct OffsetStructure { - pub name: String, - pub class_id: Vec, - pub offset: u32, -} - -/// +----------+--------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+--------------------------------------------------------------------------+ -/// | 4 | Length of data to follow | -/// | Variable | FSSpec for Macintosh or a handle to a string to the full path on Windows | -/// +----------+--------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct AliasStructure { - pub data: Vec, -} - -/// +----------+----------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+----------------------------------------------------------------------------------------+ -/// | Variable | Type: 4 bytes (length), followed either by string or (if length is zero) 4-byte typeID | -/// | Variable | Enum: 4 bytes (length), followed either by string or (if length is zero) 4-byte enum | -/// +----------+----------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct EnumeratedDescriptor { - pub type_field: Vec, - pub enum_field: Vec, -} - -/// NOTE: This struct is not documented in the specification -/// So it's based on https://github.com/psd-tools/psd-tools/blob/master/src/psd_tools/psd/descriptor.py#L691 -/// -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Length | Description | -/// +----------+--------------------------------------------------------------------------------------------+ -/// | Variable | Unicode string: name from ClassID | -/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | -/// | Variable | Unicode string: value | -/// +----------+--------------------------------------------------------------------------------------------+ -#[derive(Debug)] -pub struct NameStructure { - pub name: String, - pub class_id: Vec, - pub value: String, -} - -/// Descriptor structure OSType keys -/// 'obj ' = Reference -const OS_TYPE_REFERENCE: &[u8; 4] = b"obj "; -/// 'Objc' = Descriptor -const OS_TYPE_DESCRIPTOR: &[u8; 4] = b"Objc"; -/// 'VlLs' = List -const OS_TYPE_LIST: &[u8; 4] = b"VlLs"; -/// 'doub' = Double -const OS_TYPE_DOUBLE: &[u8; 4] = b"doub"; -/// 'UntF' = Unit float -const OS_TYPE_UNIT_FLOAT: &[u8; 4] = b"UntF"; -/// 'TEXT' = String -const OS_TYPE_TEXT: &[u8; 4] = b"TEXT"; -/// 'enum' = Enumerated -const OS_TYPE_ENUMERATED: &[u8; 4] = b"enum"; -/// 'long' = Integer -const OS_TYPE_INTEGER: &[u8; 4] = b"long"; -/// 'comp' = Large Integer -const OS_TYPE_LARGE_INTEGER: &[u8; 4] = b"comp"; -/// 'bool' = Boolean -const OS_TYPE_BOOL: &[u8; 4] = b"bool"; -/// 'GlbO' = GlobalObject same as Descriptor -const OS_TYPE_GLOBAL_OBJECT: &[u8; 4] = b"GlbO"; -/// 'type' = Class -const OS_TYPE_CLASS: &[u8; 4] = b"type"; -/// 'GlbC' = Class -const OS_TYPE_CLASS2: &[u8; 4] = b"GlbC"; -/// 'alis' = Alias -const OS_TYPE_ALIAS: &[u8; 4] = b"alis"; -/// 'tdta' = Raw Data -const OS_TYPE_RAW_DATA: &[u8; 4] = b"tdta"; - -/// Reference structure OSType keys -/// 'prop' = Property -const OS_TYPE_PROPERTY: &[u8; 4] = b"prop"; -/// 'Clss' = Class -const OS_TYPE_CLASS3: &[u8; 4] = b"Clss"; -/// 'Clss' = Class -const OS_TYPE_ENUMERATED_REFERENCE: &[u8; 4] = b"Enmr"; -/// 'rele' = Offset -const OS_TYPE_OFFSET: &[u8; 4] = b"rele"; -/// 'Idnt' = Identifier -const OS_TYPE_IDENTIFIER: &[u8; 4] = b"Idnt"; -/// 'indx' = Index -const OS_TYPE_INDEX: &[u8; 4] = b"indx"; -/// 'name' = Name -const OS_TYPE_NAME: &[u8; 4] = b"name"; - -#[derive(Debug, PartialEq, Error)] -pub enum ImageResourcesDescriptorError { - #[error(r#"Invalid TypeOS field."#)] - InvalidTypeOS {}, - #[error(r#"Invalid unit name."#)] - InvalidUnitName {}, -} - -impl DescriptorStructure { - fn read_descriptor_structure( - cursor: &mut PsdCursor, - ) -> Result { - let name = cursor.read_unicode_string_padding(1); - let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); - let fields = DescriptorStructure::read_fields(cursor, false)?; - - Ok(DescriptorStructure { - name, - fields, - class_id, - }) - } - - fn read_fields( - cursor: &mut PsdCursor, - sub_list: bool, - ) -> Result, ImageResourcesDescriptorError> { - let count = cursor.read_u32(); - let mut m = HashMap::with_capacity(count as usize); - - for n in 0..count { - let key = DescriptorStructure::read_key_length(cursor); - let key = String::from_utf8_lossy(key).into_owned(); - - m.insert(key, DescriptorStructure::read_descriptor_field(cursor)?); - } - - Ok(m) - } - - fn read_list( - cursor: &mut PsdCursor, - sub_list: bool, - ) -> Result, ImageResourcesDescriptorError> { - let count = cursor.read_u32(); - let mut vec = Vec::with_capacity(count as usize); - - for n in 0..count { - let field = DescriptorStructure::read_descriptor_field(cursor)?; - vec.push(field); - } - - Ok(vec) - } - - fn read_descriptor_field( - cursor: &mut PsdCursor, - ) -> Result { - let mut os_type = [0; 4]; - os_type.copy_from_slice(cursor.read_4()); - - let r: DescriptorField = match &os_type { - OS_TYPE_REFERENCE => { - DescriptorField::Reference(DescriptorStructure::read_reference_structure(cursor)?) - } - OS_TYPE_DESCRIPTOR => { - DescriptorField::Descriptor(DescriptorStructure::read_descriptor_structure(cursor)?) - } - OS_TYPE_LIST => { - DescriptorField::List(DescriptorStructure::read_list_structure(cursor)?) - } - OS_TYPE_DOUBLE => DescriptorField::Double(cursor.read_f64()), - OS_TYPE_UNIT_FLOAT => { - DescriptorField::UnitFloat(DescriptorStructure::read_unit_float(cursor)?) - } - OS_TYPE_TEXT => DescriptorField::String(cursor.read_unicode_string_padding(1)), - OS_TYPE_ENUMERATED => DescriptorField::EnumeratedDescriptor( - DescriptorStructure::read_enumerated_descriptor(cursor), - ), - OS_TYPE_LARGE_INTEGER => DescriptorField::LargeInteger(cursor.read_i64()), - OS_TYPE_INTEGER => DescriptorField::Integer(cursor.read_i32()), - OS_TYPE_BOOL => DescriptorField::Boolean(cursor.read_u8() > 0), - OS_TYPE_GLOBAL_OBJECT => { - DescriptorField::Descriptor(DescriptorStructure::read_descriptor_structure(cursor)?) - } - OS_TYPE_CLASS => { - DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) - } - OS_TYPE_CLASS2 => { - DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) - } - OS_TYPE_ALIAS => { - DescriptorField::Alias(DescriptorStructure::read_alias_structure(cursor)) - } - OS_TYPE_RAW_DATA => { - DescriptorField::RawData(DescriptorStructure::read_raw_data(cursor)) - } - _ => return Err(ImageResourcesDescriptorError::InvalidTypeOS {}), - }; - - Ok(r) - } - - /// +------------------------------------------------------+------------------------------------------------------------------+ - /// | Length | Description | - /// +------------------------------------------------------+------------------------------------------------------------------+ - /// | 4 | Number of items | - /// | The following is repeated for each item in reference | | - /// | 4 | OSType key for type to use: | - /// | 'prop' = Property | | - /// | 'Clss' = Class | | - /// | 'Enmr' = Enumerated Reference | | - /// | 'rele' = Offset | | - /// | 'Idnt' = Identifier | | - /// | 'indx' = Index | | - /// | 'name' =Name | | - /// | Variable | Item type: see the tables below for each possible Reference type | - /// +------------------------------------------------------+------------------------------------------------------------------+ - fn read_reference_structure( - cursor: &mut PsdCursor, - ) -> Result, ImageResourcesDescriptorError> { - let count = cursor.read_u32(); - let mut vec = Vec::with_capacity(count as usize); - - for n in 0..count { - DescriptorStructure::read_key_length(cursor); - - let mut os_type = [0; 4]; - os_type.copy_from_slice(cursor.read_4()); - vec.push(match &os_type { - OS_TYPE_PROPERTY => { - DescriptorField::Property(DescriptorStructure::read_property_structure(cursor)) - } - OS_TYPE_CLASS3 => { - DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) - } - OS_TYPE_ENUMERATED_REFERENCE => DescriptorField::EnumeratedReference( - DescriptorStructure::read_enumerated_reference(cursor), - ), - OS_TYPE_OFFSET => { - DescriptorField::Offset(DescriptorStructure::read_offset_structure(cursor)) - } - OS_TYPE_IDENTIFIER => DescriptorField::Identifier(cursor.read_i32()), - OS_TYPE_INDEX => DescriptorField::Index(cursor.read_i32()), - OS_TYPE_NAME => DescriptorField::Name(DescriptorStructure::read_name(cursor)), - _ => return Err(ImageResourcesDescriptorError::InvalidTypeOS {}), - }); - } - - Ok(vec) - } - - fn read_property_structure(cursor: &mut PsdCursor) -> PropertyStructure { - let name = cursor.read_unicode_string(); - let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); - let key_id = DescriptorStructure::read_key_length(cursor).to_vec(); - - PropertyStructure { - name, - class_id, - key_id, - } - } - - fn read_unit_float( - cursor: &mut PsdCursor, - ) -> Result { - let mut unit_float = [0; 4]; - unit_float.copy_from_slice(cursor.read_4()); - - Ok(match &unit_float { - UNIT_FLOAT_ANGLE => UnitFloatStructure::Angle(cursor.read_f64()), - UNIT_FLOAT_DENSITY => UnitFloatStructure::Density(cursor.read_f64()), - UNIT_FLOAT_DISTANCE => UnitFloatStructure::Distance(cursor.read_f64()), - UNIT_FLOAT_NONE => UnitFloatStructure::None, - UNIT_FLOAT_PERCENT => UnitFloatStructure::Percent(cursor.read_f64()), - UNIT_FLOAT_PIXELS => UnitFloatStructure::Pixels(cursor.read_f64()), - _ => return Err(ImageResourcesDescriptorError::InvalidUnitName {}), - }) - } - - fn read_class_structure(cursor: &mut PsdCursor) -> ClassStructure { - let name = cursor.read_unicode_string(); - let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); - - ClassStructure { name, class_id } - } - - fn read_enumerated_reference(cursor: &mut PsdCursor) -> EnumeratedReference { - let name = cursor.read_unicode_string(); - let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); - let key_id = DescriptorStructure::read_key_length(cursor).to_vec(); - let enum_field = DescriptorStructure::read_key_length(cursor).to_vec(); - - EnumeratedReference { - name, - class_id, - key_id, - enum_field, - } - } - - fn read_offset_structure(cursor: &mut PsdCursor) -> OffsetStructure { - let name = cursor.read_unicode_string(); - let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); - let offset = cursor.read_u32(); - - OffsetStructure { - name, - class_id, - offset, - } - } - - fn read_alias_structure(cursor: &mut PsdCursor) -> AliasStructure { - let length = cursor.read_u32(); - let data = cursor.read(length).to_vec(); - - AliasStructure { data } - } - - fn read_list_structure( - cursor: &mut PsdCursor, - ) -> Result, ImageResourcesDescriptorError> { - DescriptorStructure::read_list(cursor, true) - } - - fn read_enumerated_descriptor(cursor: &mut PsdCursor) -> EnumeratedDescriptor { - let type_field = DescriptorStructure::read_key_length(cursor).to_vec(); - let enum_field = DescriptorStructure::read_key_length(cursor).to_vec(); - - EnumeratedDescriptor { - type_field, - enum_field, - } - } - - fn read_raw_data(cursor: &mut PsdCursor) -> Vec { - let length = cursor.read_u32(); - cursor.read(length).to_vec() - } - - // Note: this structure is not documented - fn read_name(cursor: &mut PsdCursor) -> NameStructure { - let name = cursor.read_unicode_string(); - let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); - let value = cursor.read_unicode_string(); - - NameStructure { - name, - class_id, - value, - } - } - - fn read_key_length<'a>(cursor: &'a mut PsdCursor) -> &'a [u8] { - let length = cursor.read_u32(); - let length = if length > 0 { length } else { 4 }; - - cursor.read(length) - } } diff --git a/src/sections/image_resources_section/image_resource.rs b/src/sections/image_resources_section/image_resource.rs index 6611d3d..7813510 100644 --- a/src/sections/image_resources_section/image_resource.rs +++ b/src/sections/image_resources_section/image_resource.rs @@ -1,4 +1,12 @@ -use crate::sections::image_resources_section::DescriptorStructure; +use std::convert::TryFrom; +use thiserror::Error; + +use crate::sections::{image_resources_section::EXPECTED_RESOURCE_BLOCK_SIGNATURE, PsdSerialize}; + +pub mod descriptor_structure; +pub mod slices; + +pub use slices::SlicesImageResource; /// An image resource from the image resources section #[derive(Debug)] @@ -7,20 +15,54 @@ pub enum ImageResource { Slices(SlicesImageResource), } -/// Comes from a slices resource block -#[derive(Debug)] -pub struct SlicesImageResource { - pub(crate) name: String, - pub(crate) descriptors: Vec, +impl PsdSerialize for ImageResource { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write(EXPECTED_RESOURCE_BLOCK_SIGNATURE); + + let id: ImageResourceId = self.into(); + buffer.write(id.into_bytes()); + + buffer.write_pascal_string(""); + + match self { + Self::Slices(slices) => buffer.write_sized(|buf| buf.pad(2, |buf| slices.write(buf))), + } + } } -#[allow(missing_docs)] -impl SlicesImageResource { - pub fn name(&self) -> &String { - &self.name +enum ImageResourceId { + Slices = 1050, +} + +impl ImageResourceId { + fn into_bytes(self) -> [u8; 2] { + (self as i16).to_be_bytes() + } +} + +#[derive(Debug, PartialEq, Error)] +pub enum ImageResourceIdError { + #[error("Invalid resource id: {0}")] + InvalidResourceId(i16), +} +impl TryFrom for ImageResourceId { + type Error = ImageResourceIdError; + + fn try_from(value: i16) -> Result { + match value { + x if x == ImageResourceId::Slices as i16 => Ok(ImageResourceId::Slices), + _ => Err(ImageResourceIdError::InvalidResourceId(value)), + } } +} - pub fn descriptors(&self) -> &Vec { - &self.descriptors +impl From<&ImageResource> for ImageResourceId { + fn from(value: &ImageResource) -> Self { + match value { + ImageResource::Slices(_) => ImageResourceId::Slices, + } } } diff --git a/src/sections/image_resources_section/image_resource/descriptor_structure.rs b/src/sections/image_resources_section/image_resource/descriptor_structure.rs new file mode 100644 index 0000000..f47bfdd --- /dev/null +++ b/src/sections/image_resources_section/image_resource/descriptor_structure.rs @@ -0,0 +1,823 @@ +use std::collections::HashMap; +use thiserror::Error; + +use crate::sections::{AsUnicodeString, PsdCursor, PsdSerialize}; +/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from classID | +/// | Variable | classID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | 4 | Number of items in descriptor | +/// | The following is repeated for each item in descriptor | | +/// | Variable | Key: 4 bytes ( length) followed either by string or (if length is zero) 4-byte key | +/// | 4 | Type: OSType key | +/// | | 'obj ' = Reference | +/// | | 'Objc' = Descriptor | +/// | | 'VlLs' = List | +/// | | 'doub' = Double | +/// | | 'UntF' = Unit float | +/// | | 'TEXT' = String | +/// | | 'enum' = Enumerated | +/// | | 'long' = Integer | +/// | | 'comp' = Large Integer | +/// | | 'bool' = Boolean | +/// | | 'GlbO' = GlobalObject same as Descriptor | +/// | | 'type' = Class | +/// | | 'GlbC' = Class | +/// | | 'alis' = Alias | +/// | | 'tdta' = Raw Data | +/// | Variable | Item type: see the tables below for each possible type | +/// +-------------------------------------------------------+--------------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct DescriptorStructure { + pub name: String, + pub fields: HashMap, + pub class_id: Vec, +} + +/// One of +#[derive(Debug, Clone)] +pub enum DescriptorField { + /// Descriptor as field + Descriptor(DescriptorStructure), + /// A list of special fields + /// There are can be Property, Identifier, Index, Name fields + Reference(Vec), + /// Float field with unit + UnitFloat(UnitFloatStructure), + /// Double-precision floating-point number + Double(f64), + /// + Class(ClassStructure), + /// + Class2(ClassStructure), + /// + Class3(ClassStructure), + /// Text + String(String), + /// + EnumeratedReference(EnumeratedReference), + /// + Offset(OffsetStructure), + /// Boolean value + Boolean(bool), + /// + Alias(AliasStructure), + /// A list of fields + List(Vec), + /// 64bit integer number + LargeInteger(i64), + /// 32bit integer number + Integer(i32), + /// + EnumeratedDescriptor(EnumeratedDescriptor), + /// Raw bytes data + RawData(Vec), + + /// Only Reference fields + /// + /// + Property(PropertyStructure), + /// + Identifier(i32), + /// + Index(i32), + /// + Name(NameStructure), +} + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from classID | +/// | Variable | classID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | Variable | KeyID: 4 bytes (length), followed either by string or (if length is zero) 4-byte keyID | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct PropertyStructure { + pub name: String, + pub class_id: Vec, + pub key_id: Vec, +} + +/// +------------------------------------+--------------------------------------------------------+ +/// | Length | Description | +/// +------------------------------------+--------------------------------------------------------+ +/// | 4 | Units the following value is in. One of the following: | +/// | | '#Ang' = angle: base degrees | +/// | | '#Rsl' = density: base per inch | +/// | | '#Rlt' = distance: base 72ppi | +/// | | '#Nne' = none: coerced. | +/// | | '#Prc'= percent: unit value | +/// | | '#Pxl' = pixels: tagged unit value | +/// | 8 | Actual value (double) | +/// +------------------------------------+--------------------------------------------------------+ +#[derive(Debug, Clone)] +pub enum UnitFloatStructure { + /// Base degrees + Angle(f64), + /// Base per inch + Density(f64), + /// Base 72ppi + Distance(f64), + /// Base coerced + None, + /// Unit value + Percent(f64), + /// Tagged unit value + Pixels(f64), +} + +/// Unit float structure units keys +/// '#Ang' = angle: base degrees +const UNIT_FLOAT_ANGLE: &[u8; 4] = b"#Ang"; +/// '#Rsl' = density: base per inch +const UNIT_FLOAT_DENSITY: &[u8; 4] = b"#Rsl"; +/// '#Rlt' = distance: base 72ppi +const UNIT_FLOAT_DISTANCE: &[u8; 4] = b"#Rlt"; +/// '#Nne' = none: coerced. +const UNIT_FLOAT_NONE: &[u8; 4] = b"#Nne"; +/// '#Prc'= percent: unit value +const UNIT_FLOAT_PERCENT: &[u8; 4] = b"#Prc"; +/// '#Pxl' = pixels: tagged unit value +const UNIT_FLOAT_PIXELS: &[u8; 4] = b"#Pxl"; + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from classID | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct ClassStructure { + pub name: String, + pub class_id: Vec, +} + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from ClassID. | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | Variable | TypeID: 4 bytes (length), followed either by string or (if length is zero) 4-byte typeID | +/// | Variable | enum: 4 bytes (length), followed either by string or (if length is zero) 4-byte enum | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct EnumeratedReference { + pub name: String, + pub class_id: Vec, + pub key_id: Vec, + pub enum_field: Vec, +} + +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from ClassID | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | 4 | Value of the offset | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct OffsetStructure { + pub name: String, + pub class_id: Vec, + pub offset: u32, +} + +/// +----------+--------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------+ +/// | 4 | Length of data to follow | +/// | Variable | FSSpec for Macintosh or a handle to a string to the full path on Windows | +/// +----------+--------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct AliasStructure { + pub data: Vec, +} + +/// +----------+----------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+----------------------------------------------------------------------------------------+ +/// | Variable | Type: 4 bytes (length), followed either by string or (if length is zero) 4-byte typeID | +/// | Variable | Enum: 4 bytes (length), followed either by string or (if length is zero) 4-byte enum | +/// +----------+----------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct EnumeratedDescriptor { + pub type_field: Vec, + pub enum_field: Vec, +} + +/// NOTE: This struct is not documented in the specification +/// So it's based on https://github.com/psd-tools/psd-tools/blob/master/src/psd_tools/psd/descriptor.py#L691 +/// +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Length | Description | +/// +----------+--------------------------------------------------------------------------------------------+ +/// | Variable | Unicode string: name from ClassID | +/// | Variable | ClassID: 4 bytes (length), followed either by string or (if length is zero) 4-byte classID | +/// | Variable | Unicode string: value | +/// +----------+--------------------------------------------------------------------------------------------+ +#[derive(Debug, Clone)] +pub struct NameStructure { + pub name: String, + pub class_id: Vec, + pub value: String, +} + +/// Descriptor structure OSType keys +/// 'obj ' = Reference +const OS_TYPE_REFERENCE: &[u8; 4] = b"obj "; +/// 'Objc' = Descriptor +const OS_TYPE_DESCRIPTOR: &[u8; 4] = b"Objc"; +/// 'VlLs' = List +const OS_TYPE_LIST: &[u8; 4] = b"VlLs"; +/// 'doub' = Double +const OS_TYPE_DOUBLE: &[u8; 4] = b"doub"; +/// 'UntF' = Unit float +const OS_TYPE_UNIT_FLOAT: &[u8; 4] = b"UntF"; +/// 'TEXT' = String +const OS_TYPE_TEXT: &[u8; 4] = b"TEXT"; +/// 'enum' = Enumerated +const OS_TYPE_ENUMERATED: &[u8; 4] = b"enum"; +/// 'long' = Integer +const OS_TYPE_INTEGER: &[u8; 4] = b"long"; +/// 'comp' = Large Integer +const OS_TYPE_LARGE_INTEGER: &[u8; 4] = b"comp"; +/// 'bool' = Boolean +const OS_TYPE_BOOL: &[u8; 4] = b"bool"; +/// 'GlbO' = GlobalObject same as Descriptor +const OS_TYPE_GLOBAL_OBJECT: &[u8; 4] = b"GlbO"; +/// 'type' = Class +const OS_TYPE_CLASS: &[u8; 4] = b"type"; +/// 'GlbC' = Class +const OS_TYPE_CLASS2: &[u8; 4] = b"GlbC"; +/// 'alis' = Alias +const OS_TYPE_ALIAS: &[u8; 4] = b"alis"; +/// 'tdta' = Raw Data +const OS_TYPE_RAW_DATA: &[u8; 4] = b"tdta"; + +/// Reference structure OSType keys +/// 'prop' = Property +const OS_TYPE_PROPERTY: &[u8; 4] = b"prop"; +/// 'Clss' = Class +const OS_TYPE_CLASS3: &[u8; 4] = b"Clss"; +/// 'Clss' = Class +const OS_TYPE_ENUMERATED_REFERENCE: &[u8; 4] = b"Enmr"; +/// 'rele' = Offset +const OS_TYPE_OFFSET: &[u8; 4] = b"rele"; +/// 'Idnt' = Identifier +const OS_TYPE_IDENTIFIER: &[u8; 4] = b"Idnt"; +/// 'indx' = Index +const OS_TYPE_INDEX: &[u8; 4] = b"indx"; +/// 'name' = Name +const OS_TYPE_NAME: &[u8; 4] = b"name"; + +#[derive(Debug, PartialEq, Error)] +pub enum ImageResourcesDescriptorError { + #[error(r#"Invalid TypeOS field."#)] + InvalidTypeOS {}, + #[error(r#"Invalid unit name."#)] + InvalidUnitName {}, +} + +impl PsdSerialize for DescriptorStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsUnicodeString(&self.name).write(buffer); // Name from ClassID + DescriptorKey(&self.class_id).write(buffer); // ClassID + self.fields.write(buffer); + } +} + +struct AsSizedBytes<'a, T>(&'a T) +where + T: AsRef<[u8]>; + +impl PsdSerialize for AsSizedBytes<'_, S> +where + S: AsRef<[u8]>, +{ + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + let inner = self.0.as_ref(); + buffer.write((inner.len() as u32).to_be_bytes()); + buffer.write(inner); + } +} + +struct DescriptorKey<'a, K>(&'a K); + +impl PsdSerialize for DescriptorKey<'_, K> +where + K: AsRef<[u8]>, +{ + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + let inner = self.0.as_ref(); + buffer.write((inner.len() as u32).to_be_bytes()); + + if inner.is_empty() { + buffer.write(0_u32.to_be_bytes()); + } else { + buffer.write(inner); + } + } +} + +impl PsdSerialize for HashMap { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write((self.len() as u32).to_be_bytes()); + + for (key, field) in self { + DescriptorKey(key).write(buffer); // Key + field.write(buffer); + } + } +} + +impl PsdSerialize for Vec { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write((self.len() as u32).to_be_bytes()); + + for field in self { + field.write(buffer); + } + } +} + +impl PsdSerialize for UnitFloatStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + let value = match self { + Self::Angle(v) => { + buffer.write(UNIT_FLOAT_ANGLE); + Some(v) + } + Self::Density(v) => { + buffer.write(UNIT_FLOAT_DENSITY); + Some(v) + } + Self::Distance(v) => { + buffer.write(UNIT_FLOAT_DISTANCE); + Some(v) + } + Self::None => { + buffer.write(UNIT_FLOAT_NONE); + None + } + Self::Percent(v) => { + buffer.write(UNIT_FLOAT_PERCENT); + Some(v) + } + Self::Pixels(v) => { + buffer.write(UNIT_FLOAT_PIXELS); + Some(v) + } + }; + + if let Some(value) = value { + buffer.write(value.to_be_bytes()); + } + } +} + +impl PsdSerialize for ClassStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsUnicodeString(&self.name).write(buffer); + DescriptorKey(&self.class_id).write(buffer); + } +} + +impl PsdSerialize for EnumeratedReference { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsUnicodeString(&self.name).write(buffer); + DescriptorKey(&self.class_id).write(buffer); + DescriptorKey(&self.key_id).write(buffer); + DescriptorKey(&self.enum_field).write(buffer); + } +} + +impl PsdSerialize for OffsetStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsUnicodeString(&self.name).write(buffer); + DescriptorKey(&self.class_id).write(buffer); + buffer.write(self.offset.to_be_bytes()); + } +} + +impl PsdSerialize for AliasStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsSizedBytes(&self.data).write(buffer); + } +} + +impl PsdSerialize for EnumeratedDescriptor { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + DescriptorKey(&self.type_field).write(buffer); + DescriptorKey(&self.enum_field).write(buffer); + } +} + +impl PsdSerialize for PropertyStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsUnicodeString(&self.name).write(buffer); + DescriptorKey(&self.class_id).write(buffer); + DescriptorKey(&self.key_id).write(buffer); + } +} +impl PsdSerialize for NameStructure { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + AsUnicodeString(&self.name).write(buffer); + DescriptorKey(&self.class_id).write(buffer); + AsUnicodeString(&self.value).write(buffer); + } +} + +impl PsdSerialize for DescriptorField { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + match &self { + Self::Descriptor(item) => { + buffer.write(OS_TYPE_DESCRIPTOR); + item.write(buffer); + } + Self::Reference(item) => { + buffer.write(OS_TYPE_REFERENCE); + //ReferenceStructure(item).write(buffer); + item.write(buffer); + } + Self::UnitFloat(item) => { + buffer.write(OS_TYPE_UNIT_FLOAT); + item.write(buffer); + } + Self::Double(item) => { + buffer.write(OS_TYPE_DOUBLE); + buffer.write(item.to_be_bytes()); + } + Self::Class(item) => { + buffer.write(OS_TYPE_CLASS); + item.write(buffer); + } + Self::Class2(item) => { + buffer.write(OS_TYPE_CLASS2); + item.write(buffer); + } + Self::Class3(item) => { + buffer.write(OS_TYPE_CLASS3); + item.write(buffer); + } + Self::String(item) => { + buffer.write(OS_TYPE_TEXT); + AsUnicodeString(item).write(buffer); + } + Self::EnumeratedReference(item) => { + buffer.write(OS_TYPE_ENUMERATED_REFERENCE); + item.write(buffer); + } + Self::Offset(item) => { + buffer.write(OS_TYPE_OFFSET); + item.write(buffer); + } + Self::Boolean(item) => { + buffer.write(OS_TYPE_BOOL); + let item = match item { + true => 1_u8, + false => 0_u8, + }; + buffer.write(item.to_be_bytes()); + } + Self::Alias(item) => { + buffer.write(OS_TYPE_ALIAS); + item.write(buffer); + } + Self::List(item) => { + buffer.write(OS_TYPE_LIST); + item.write(buffer); + } + Self::LargeInteger(item) => { + buffer.write(OS_TYPE_LARGE_INTEGER); + buffer.write(item.to_be_bytes()); + } + Self::Integer(item) => { + buffer.write(OS_TYPE_INTEGER); + buffer.write(item.to_be_bytes()); + } + Self::EnumeratedDescriptor(item) => { + buffer.write(OS_TYPE_ENUMERATED); + item.write(buffer); + } + Self::RawData(item) => { + buffer.write(OS_TYPE_RAW_DATA); + buffer.write(item); + } + Self::Property(item) => { + buffer.write(OS_TYPE_PROPERTY); + item.write(buffer); + } + Self::Identifier(item) => { + buffer.write(OS_TYPE_IDENTIFIER); + buffer.write(item.to_be_bytes()) + } + Self::Index(item) => { + buffer.write(OS_TYPE_INDEX); + buffer.write(item.to_be_bytes()) + } + Self::Name(item) => { + buffer.write(OS_TYPE_NAME); + item.write(buffer); + } + } + } +} + +impl DescriptorStructure { + pub(crate) fn read_descriptor_structure( + cursor: &mut PsdCursor, + ) -> Result { + let name = cursor.read_unicode_string_padding(1); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let fields = DescriptorStructure::read_fields(cursor, false)?; + + Ok(DescriptorStructure { + name, + fields, + class_id, + }) + } + + fn read_fields( + cursor: &mut PsdCursor, + sub_list: bool, + ) -> Result, ImageResourcesDescriptorError> { + let count = cursor.read_u32(); + let mut m = HashMap::with_capacity(count as usize); + + for n in 0..count { + let key = DescriptorStructure::read_key_length(cursor); + let key = String::from_utf8_lossy(key).into_owned(); + + m.insert(key, DescriptorStructure::read_descriptor_field(cursor)?); + } + + Ok(m) + } + + fn read_list( + cursor: &mut PsdCursor, + sub_list: bool, + ) -> Result, ImageResourcesDescriptorError> { + let count = cursor.read_u32(); + let mut vec = Vec::with_capacity(count as usize); + + for n in 0..count { + let field = DescriptorStructure::read_descriptor_field(cursor)?; + vec.push(field); + } + + Ok(vec) + } + + fn read_descriptor_field( + cursor: &mut PsdCursor, + ) -> Result { + let mut os_type = [0; 4]; + os_type.copy_from_slice(cursor.read_4()); + + let r: DescriptorField = match &os_type { + OS_TYPE_REFERENCE => { + DescriptorField::Reference(DescriptorStructure::read_reference_structure(cursor)?) + } + OS_TYPE_DESCRIPTOR => { + DescriptorField::Descriptor(DescriptorStructure::read_descriptor_structure(cursor)?) + } + OS_TYPE_LIST => { + DescriptorField::List(DescriptorStructure::read_list_structure(cursor)?) + } + OS_TYPE_DOUBLE => DescriptorField::Double(cursor.read_f64()), + OS_TYPE_UNIT_FLOAT => { + DescriptorField::UnitFloat(DescriptorStructure::read_unit_float(cursor)?) + } + OS_TYPE_TEXT => DescriptorField::String(cursor.read_unicode_string_padding(1)), + OS_TYPE_ENUMERATED => DescriptorField::EnumeratedDescriptor( + DescriptorStructure::read_enumerated_descriptor(cursor), + ), + OS_TYPE_LARGE_INTEGER => DescriptorField::LargeInteger(cursor.read_i64()), + OS_TYPE_INTEGER => DescriptorField::Integer(cursor.read_i32()), + OS_TYPE_BOOL => DescriptorField::Boolean(cursor.read_u8() > 0), + OS_TYPE_GLOBAL_OBJECT => { + DescriptorField::Descriptor(DescriptorStructure::read_descriptor_structure(cursor)?) + } + OS_TYPE_CLASS => { + DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) + } + OS_TYPE_CLASS2 => { + DescriptorField::Class2(DescriptorStructure::read_class_structure(cursor)) + } + OS_TYPE_ALIAS => { + DescriptorField::Alias(DescriptorStructure::read_alias_structure(cursor)) + } + OS_TYPE_RAW_DATA => { + DescriptorField::RawData(DescriptorStructure::read_raw_data(cursor)) + } + _ => return Err(ImageResourcesDescriptorError::InvalidTypeOS {}), + }; + + Ok(r) + } + + /// +------------------------------------------------------+------------------------------------------------------------------+ + /// | Length | Description | + /// +------------------------------------------------------+------------------------------------------------------------------+ + /// | 4 | Number of items | + /// | The following is repeated for each item in reference | | + /// | 4 | OSType key for type to use: | + /// | 'prop' = Property | | + /// | 'Clss' = Class | | + /// | 'Enmr' = Enumerated Reference | | + /// | 'rele' = Offset | | + /// | 'Idnt' = Identifier | | + /// | 'indx' = Index | | + /// | 'name' =Name | | + /// | Variable | Item type: see the tables below for each possible Reference type | + /// +------------------------------------------------------+------------------------------------------------------------------+ + fn read_reference_structure( + cursor: &mut PsdCursor, + ) -> Result, ImageResourcesDescriptorError> { + let count = cursor.read_u32(); + let mut vec = Vec::with_capacity(count as usize); + + for n in 0..count { + DescriptorStructure::read_key_length(cursor); + + let mut os_type = [0; 4]; + os_type.copy_from_slice(cursor.read_4()); + vec.push(match &os_type { + OS_TYPE_PROPERTY => { + DescriptorField::Property(DescriptorStructure::read_property_structure(cursor)) + } + OS_TYPE_CLASS3 => { + DescriptorField::Class(DescriptorStructure::read_class_structure(cursor)) + } + OS_TYPE_ENUMERATED_REFERENCE => DescriptorField::EnumeratedReference( + DescriptorStructure::read_enumerated_reference(cursor), + ), + OS_TYPE_OFFSET => { + DescriptorField::Offset(DescriptorStructure::read_offset_structure(cursor)) + } + OS_TYPE_IDENTIFIER => DescriptorField::Identifier(cursor.read_i32()), + OS_TYPE_INDEX => DescriptorField::Index(cursor.read_i32()), + OS_TYPE_NAME => DescriptorField::Name(DescriptorStructure::read_name(cursor)), + _ => return Err(ImageResourcesDescriptorError::InvalidTypeOS {}), + }); + } + + Ok(vec) + } + + fn read_property_structure(cursor: &mut PsdCursor) -> PropertyStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let key_id = DescriptorStructure::read_key_length(cursor).to_vec(); + + PropertyStructure { + name, + class_id, + key_id, + } + } + + fn read_unit_float( + cursor: &mut PsdCursor, + ) -> Result { + let mut unit_float = [0; 4]; + unit_float.copy_from_slice(cursor.read_4()); + + Ok(match &unit_float { + UNIT_FLOAT_ANGLE => UnitFloatStructure::Angle(cursor.read_f64()), + UNIT_FLOAT_DENSITY => UnitFloatStructure::Density(cursor.read_f64()), + UNIT_FLOAT_DISTANCE => UnitFloatStructure::Distance(cursor.read_f64()), + UNIT_FLOAT_NONE => UnitFloatStructure::None, + UNIT_FLOAT_PERCENT => UnitFloatStructure::Percent(cursor.read_f64()), + UNIT_FLOAT_PIXELS => UnitFloatStructure::Pixels(cursor.read_f64()), + _ => return Err(ImageResourcesDescriptorError::InvalidUnitName {}), + }) + } + + fn read_class_structure(cursor: &mut PsdCursor) -> ClassStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + + ClassStructure { name, class_id } + } + + fn read_enumerated_reference(cursor: &mut PsdCursor) -> EnumeratedReference { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let key_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let enum_field = DescriptorStructure::read_key_length(cursor).to_vec(); + + EnumeratedReference { + name, + class_id, + key_id, + enum_field, + } + } + + fn read_offset_structure(cursor: &mut PsdCursor) -> OffsetStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let offset = cursor.read_u32(); + + OffsetStructure { + name, + class_id, + offset, + } + } + + fn read_alias_structure(cursor: &mut PsdCursor) -> AliasStructure { + let length = cursor.read_u32(); + let data = cursor.read(length).to_vec(); + + AliasStructure { data } + } + + fn read_list_structure( + cursor: &mut PsdCursor, + ) -> Result, ImageResourcesDescriptorError> { + DescriptorStructure::read_list(cursor, true) + } + + fn read_enumerated_descriptor(cursor: &mut PsdCursor) -> EnumeratedDescriptor { + let type_field = DescriptorStructure::read_key_length(cursor).to_vec(); + let enum_field = DescriptorStructure::read_key_length(cursor).to_vec(); + + EnumeratedDescriptor { + type_field, + enum_field, + } + } + + fn read_raw_data(cursor: &mut PsdCursor) -> Vec { + let length = cursor.read_u32(); + cursor.read(length).to_vec() + } + + // Note: this structure is not documented + fn read_name(cursor: &mut PsdCursor) -> NameStructure { + let name = cursor.read_unicode_string(); + let class_id = DescriptorStructure::read_key_length(cursor).to_vec(); + let value = cursor.read_unicode_string(); + + NameStructure { + name, + class_id, + value, + } + } + + fn read_key_length<'a>(cursor: &'a mut PsdCursor) -> &'a [u8] { + let length = cursor.read_u32(); + let length = if length > 0 { length } else { 4 }; + + cursor.read(length) + } +} diff --git a/src/sections/image_resources_section/image_resource/slices.rs b/src/sections/image_resources_section/image_resource/slices.rs new file mode 100644 index 0000000..a0326da --- /dev/null +++ b/src/sections/image_resources_section/image_resource/slices.rs @@ -0,0 +1,324 @@ +use super::descriptor_structure::{DescriptorStructure, ImageResourcesDescriptorError}; + +use crate::sections::{PsdCursor, PsdDeserialize, PsdSerialize}; + +const EXPECTED_DESCRIPTOR_VERSION: u32 = 16; + +/// Comes from a slices resource block +#[derive(Debug)] +pub enum SlicesImageResource { + V6(SlicesImageResourceV6), + V7_8(SlicesImageResourceV7_8), +} + +#[allow(missing_docs)] +impl SlicesImageResource { + pub fn name(&self) -> &str { + match &self { + Self::V6(format) => &format.name, + Self::V7_8(format) => &format.descriptor.name, + } + } + + pub fn descriptors(&self) -> Vec<&DescriptorStructure> { + match &self { + Self::V6(format) => format + .blocks + .iter() + .filter_map(|b| b.descriptor.as_ref()) + .collect(), + Self::V7_8(format) => vec![&format.descriptor], + } + } +} + +impl PsdDeserialize for SlicesImageResource { + type Error = ImageResourcesDescriptorError; + + /// Slices Resource Format + /// Adobe Photoshop 6.0 stores slices information for an image in an image resource block. + /// Adobe Photoshop 7.0 added a descriptor at the end of the block for the individual slice info. + /// Adobe Photoshop CS and later changed to version 7 or 8 and uses a Descriptor to defined the Slices data. + /// + /// +----------+--------------------------------------------------------------------------------------+ + /// | Length | Description | + /// +----------+--------------------------------------------------------------------------------------+ + /// | 4 | Version | + /// | ... | Fields vary depending on version | + /// +----------+--------------------------------------------------------------------------------------+ + fn from_bytes(bytes: &[u8]) -> Result { + let cursor = PsdCursor::new(bytes); + let version = cursor.peek_i32(); + let bytes = &cursor.get_ref()[cursor.position() as usize..]; + + match version { + x if x == 6 => Ok(Self::V6(SlicesImageResourceV6::from_bytes(bytes)?)), + x if x == 7 || x == 8 => Ok(Self::V7_8(SlicesImageResourceV7_8::from_bytes(bytes)?)), + _ => unimplemented!("Slices resource format {version} is currently not supported"), + } + } +} + +impl PsdSerialize for SlicesImageResource { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + match self { + Self::V6(format) => format.write(buffer), + Self::V7_8(format) => format.write(buffer), + } + } +} + +#[derive(Debug)] +pub struct SlicesImageResourceV6 { + name: String, + blocks: Vec, +} + +impl PsdDeserialize for SlicesImageResourceV6 { + type Error = ImageResourcesDescriptorError; + + /// Slice header for version 6 + /// + /// +----------+--------------------------------------------------------------------------------------+ + /// | Length | Description | + /// +----------+--------------------------------------------------------------------------------------+ + /// | 4 | Version ( = 6) | + /// | 4 * 4 | Bounding rectangle for all of the slices: top, left, bottom, right of all the slices | + /// | Variable | Name of group of slices: Unicode string | + /// | 4 | Number of slices to follow. See Slices resource block in the next table | + /// +----------+--------------------------------------------------------------------------------------+ + fn from_bytes(bytes: &[u8]) -> Result { + let mut cursor = PsdCursor::new(bytes); + + let _version = cursor.read_i32(); + let _top = cursor.read_i32(); + let _left = cursor.read_i32(); + let _bottom = cursor.read_i32(); + let _right = cursor.read_i32(); + + let group_of_slices_name = cursor.read_unicode_string_padding(1); + + let number_of_slices = cursor.read_u32(); + + let mut blocks = Vec::new(); + + for _ in 0..number_of_slices { + blocks.push(SlicesResourceBlock::from_bytes( + &cursor.get_ref()[cursor.position() as usize..], + )?) + } + + Ok(Self { + name: group_of_slices_name, + blocks, + }) + } +} + +impl PsdSerialize for SlicesImageResourceV6 { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write(6_i32.to_be_bytes()); // Version + + let pad = 0_i32.to_be_bytes(); + // Bounding rectangle + buffer.write(pad); // top + buffer.write(pad); // left + buffer.write(pad); // bottom + buffer.write(pad); // right + + // Name of group of slices: Unicode string + buffer.write_unicode_string(&self.name); + + // Number of slices to follow + buffer.write((self.blocks.len() as u32).to_be_bytes()); + + for block in &self.blocks { + block.write(buffer); + } + } +} + +#[derive(Debug)] +pub struct SlicesResourceBlock { + descriptor: Option, +} + +impl PsdDeserialize for SlicesResourceBlock { + type Error = ImageResourcesDescriptorError; + + /// Slices resource block + /// + /// +------------------------------------------------------+-----------------------------------------------+ + /// | Length | Description | + /// +------------------------------------------------------+-----------------------------------------------+ + /// | 4 | ID | + /// | 4 | Group ID | + /// | 4 | Origin | + /// | 4 | Associated Layer ID | + /// | Only present if Origin = 1 | | + /// | Variable | Name: Unicode string | + /// | 4 | Type | + /// | 4 * 4 | Left, top, right, bottom positions | + /// | Variable | URL: Unicode string | + /// | Variable | Target: Unicode string | + /// | Variable | Message: Unicode string | + /// | Variable | Alt Tag: Unicode string | + /// | 1 | Cell text is HTML: Boolean | + /// | Variable | Cell text: Unicode string | + /// | 4 | Horizontal alignment | + /// | 4 | Vertical alignment | + /// | 1 | Alpha color | + /// | 1 | Red | + /// | 1 | Green | + /// | 1 | Blue | + /// | Additional data as length allows. See comment above. | | + /// | 4 | Descriptor version ( = 16 for Photoshop 6.0). | + /// | Variable | Descriptor (see See Descriptor structure) | + /// +------------------------------------------------------+-----------------------------------------------+ + fn from_bytes(bytes: &[u8]) -> Result { + let mut cursor = PsdCursor::new(bytes); + + let _slice_id = cursor.read_i32(); + let _group_id = cursor.read_i32(); + let origin = cursor.read_i32(); + + // if origin = 1, Associated Layer ID is present + if origin == 1 { + cursor.read_i32(); + } + + let _name = cursor.read_unicode_string_padding(1); + let _type = cursor.read_i32(); + + let _top = cursor.read_i32(); + let _left = cursor.read_i32(); + let _bottom = cursor.read_i32(); + let _right = cursor.read_i32(); + + let _url = cursor.read_unicode_string_padding(1); + let _target = cursor.read_unicode_string_padding(1); + let _message = cursor.read_unicode_string_padding(1); + let _alt_tag = cursor.read_unicode_string_padding(1); + + let _cell_text_html = cursor.read_1(); + let _cell_text = cursor.read_unicode_string_padding(1); + + let _horizontal_alignment = cursor.read_i32(); + let _vertical_alignment = cursor.read_i32(); + let _argb_color = cursor.read_i32(); + + let pos = cursor.position(); + let descriptor_version = cursor.peek_u32(); + + let descriptor = if descriptor_version == EXPECTED_DESCRIPTOR_VERSION { + cursor.read_4(); + + let descriptor = DescriptorStructure::read_descriptor_structure(&mut cursor)?; + if descriptor.class_id.as_slice() == [0, 0, 0, 0] { + cursor.seek(pos); + } + + Some(descriptor) + } else { + None + }; + + Ok(Self { descriptor }) + } +} + +impl PsdSerialize for SlicesResourceBlock { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + let pad = 0_i32.to_be_bytes(); + + buffer.write(pad); // Slice Id + buffer.write(pad); // Group Id + buffer.write(pad); // Origin + // Skip Associated Layer Id + buffer.write_unicode_string(""); // Name + buffer.write(pad); // Type + + // Positions + buffer.write(pad); // top + buffer.write(pad); // left + buffer.write(pad); // bottom + buffer.write(pad); // right + + buffer.write_unicode_string(""); // URL + buffer.write_unicode_string(""); // Target + buffer.write_unicode_string(""); // Message + buffer.write_unicode_string(""); // Alt Tag + + buffer.write([0_u8]); // Cell text is HTML (Boolean) + buffer.write_unicode_string(""); // Cell Text + + buffer.write(pad); // Horizontal alignment + buffer.write(pad); // Vertical alignment + buffer.write(pad); // Vertical alignment + + buffer.write([0_u8]); // Alpha color + buffer.write([0_u8]); // Red + buffer.write([0_u8]); // Green + buffer.write([0_u8]); // Blue + + if let Some(descriptor) = &self.descriptor { + buffer.write(EXPECTED_DESCRIPTOR_VERSION.to_be_bytes()); // Descriptor version + + descriptor.write(buffer); // Descriptor + } + } +} + +#[derive(Debug)] +pub struct SlicesImageResourceV7_8 { + descriptor: DescriptorStructure, +} + +impl PsdDeserialize for SlicesImageResourceV7_8 { + type Error = ImageResourcesDescriptorError; + + /// Slices header for version 7 or 8 + /// + /// +----------+--------------------------------------------------------------------------------------+ + /// | Length | Description | + /// +----------+--------------------------------------------------------------------------------------+ + /// | 4 | Version ( = 7 and 8) | + /// | 4 | Descriptor version ( = 16 for Photoshop 6.0). | + /// | Variable | Descriptor (see See Descriptor structure) | + /// +----------+--------------------------------------------------------------------------------------+ + fn from_bytes(bytes: &[u8]) -> Result { + let mut cursor = PsdCursor::new(bytes); + + let _version = cursor.read_i32(); + let descriptor_version = cursor.read_i32(); + if descriptor_version != 16 { + unimplemented!( + "Only the version 16 (descriptors) resource format for slices is currently supported" + ); + } + let descriptor = DescriptorStructure::read_descriptor_structure(&mut cursor)?; + + Ok(Self { descriptor }) + } +} + +impl PsdSerialize for SlicesImageResourceV7_8 { + fn write(&self, buffer: &mut crate::sections::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write(7_i32.to_be_bytes()); // Version + buffer.write(EXPECTED_DESCRIPTOR_VERSION.to_be_bytes()); // Descriptor Version + + self.descriptor.write(buffer); + } +} diff --git a/src/sections/layer_and_mask_information_section/layer.rs b/src/sections/layer_and_mask_information_section/layer.rs index 5461f66..6005b50 100644 --- a/src/sections/layer_and_mask_information_section/layer.rs +++ b/src/sections/layer_and_mask_information_section/layer.rs @@ -360,6 +360,43 @@ impl BlendMode { } } +use std::convert::From; + +impl From for &[u8] { + fn from(mode: BlendMode) -> Self { + match mode { + BlendMode::PassThrough => b"pass", + BlendMode::Normal => b"norm", + BlendMode::Dissolve => b"diss", + BlendMode::Darken => b"dark", + BlendMode::Multiply => b"mul ", + BlendMode::ColorBurn => b"idiv", + BlendMode::LinearBurn => b"lbrn", + BlendMode::DarkerColor => b"dkCl", + BlendMode::Lighten => b"lite", + BlendMode::Screen => b"scrn", + BlendMode::ColorDodge => b"div ", + BlendMode::LinearDodge => b"lddg", + BlendMode::LighterColor => b"lgCl", + BlendMode::Overlay => b"over", + BlendMode::SoftLight => b"sLit", + BlendMode::HardLight => b"hLit", + BlendMode::VividLight => b"vLit", + BlendMode::LinearLight => b"lLit", + BlendMode::PinLight => b"pLit", + BlendMode::HardMix => b"hMix", + BlendMode::Difference => b"diff", + BlendMode::Exclusion => b"smud", + BlendMode::Subtract => b"fsub", + BlendMode::Divide => b"fdiv", + BlendMode::Hue => b"hue ", + BlendMode::Saturation => b"sat ", + BlendMode::Color => b"colr", + BlendMode::Luminosity => b"lum ", + } + } +} + /// A layer record within the layer info section /// /// TODO: Set all ofo these pubs to get things working. Replace with private diff --git a/src/sections/layer_and_mask_information_section/layers.rs b/src/sections/layer_and_mask_information_section/layers.rs index 775ac9d..a302e7d 100644 --- a/src/sections/layer_and_mask_information_section/layers.rs +++ b/src/sections/layer_and_mask_information_section/layers.rs @@ -1,6 +1,6 @@ use crate::PsdLayer; use std::collections::HashMap; -use std::ops::{Deref, Range}; +use std::ops::Deref; /// `NamedItems` is immutable container for storing items with order-preservation /// and indexing by id and name @@ -39,7 +39,7 @@ impl Layers { } #[allow(missing_docs)] - pub(in crate) fn push(&mut self, name: String, item: PsdLayer) { + pub(crate) fn push(&mut self, name: String, item: PsdLayer) { self.items.push(item); self.item_indices.insert(name, self.items.len() - 1); } diff --git a/src/sections/layer_and_mask_information_section/mod.rs b/src/sections/layer_and_mask_information_section/mod.rs index b020b17..90c7e77 100644 --- a/src/sections/layer_and_mask_information_section/mod.rs +++ b/src/sections/layer_and_mask_information_section/mod.rs @@ -11,6 +11,8 @@ use crate::sections::layer_and_mask_information_section::layer::{ use crate::sections::layer_and_mask_information_section::layers::Layers; use crate::sections::PsdCursor; +use super::PsdSerialize; + /// One of the possible additional layer block signatures const SIGNATURE_EIGHT_BIM: [u8; 4] = [56, 66, 73, 77]; /// One of the possible additional layer block signatures @@ -68,6 +70,241 @@ struct Frame { parent_group_id: u32, } +impl PsdSerialize for LayerAndMaskInformationSection { + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + buffer.write_sized(|buf| { + LayerInfo(self).write(buf); + }); + } +} + +struct LayerInfo<'a>(&'a LayerAndMaskInformationSection); + +impl PsdSerialize for LayerInfo<'_> { + /// Layer info + /// +------------------------------------------------------------------------------------------+ + /// | Length | Description | + /// |----------+-------------------------------------------------------------------------------| + /// | 4 | Length of the layers info section, rounded up to a multiple of 2. (**PSB** | + /// | | length is 8 bytes.) | + /// |----------+-------------------------------------------------------------------------------| + /// | | Layer count. If it is a negative number, its absolute value is the number of | + /// | 2 | layers and the first alpha channel contains the transparency data for the | + /// | | merged result. | + /// |----------+-------------------------------------------------------------------------------| + /// | Variable | Information about each layer. See Layer records describes the structure of | + /// | | this information for each layer. | + /// |----------+-------------------------------------------------------------------------------| + /// | | Channel image data. Contains one or more image data records (see See Channel | + /// | Variable | image data for structure) for each layer. The layers are in the same order as | + /// | | in the layer information (previous row of this table). | + /// +------------------------------------------------------------------------------------------+ + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + // Length of layer info section, rounded up to a multiple of 2. + buffer.write_sized(|buf| { + buf.pad(2, |buf| { + // TODO: Handle first channel as transparency for merged result. (negative layer count) + buf.write((self.0.layers.len() as i16).to_be_bytes()); + + for layer in self.0.layers.iter() { + LayerRecordWrite(layer, &self.0.groups).write(buf); + } + + for layer in self.0.layers.iter() { + for kind in [ + PsdChannelKind::Red, + PsdChannelKind::Green, + PsdChannelKind::Blue, + PsdChannelKind::TransparencyMask, + PsdChannelKind::UserSuppliedLayerMask, + PsdChannelKind::RealUserSuppliedLayerMask, + ] { + if let Some(data) = layer.channels.get(&kind) { + ChannelImageData(data).write(buf); + } + } + } + }); + }); + } +} + +struct LayerRecordWrite<'a>(&'a PsdLayer, &'a Groups); + +impl PsdSerialize for LayerRecordWrite<'_> { + /// +------------------------------------------------------------------------------------------+ + /// | Length | Description | + /// |--------------------+---------------------------------------------------------------------| + /// | 4 * 4 | Rectangle containing the contents of the layer. Specified as top, | + /// | | left, bottom, right coordinates | + /// |--------------------+---------------------------------------------------------------------| + /// | 2 | Number of channels in the layer | + /// |--------------------+---------------------------------------------------------------------| + /// | | Channel information. Six bytes per channel, consisting of: | + /// | | | + /// | | 2 bytes for Channel ID: 0 = red, 1 = green, etc.; | + /// | | | + /// | 6 * | -1 = transparency mask; -2 = user supplied layer mask, -3 real user | + /// | | supplied layer mask (when both a user mask and a vector mask are | + /// | number of channels | present) | + /// | | | + /// | | 4 bytes for length of corresponding channel data. (**PSB** 8 bytes | + /// | | for length of corresponding channel data.) See See Channel image | + /// | | data for structure of channel data. | + /// |--------------------+---------------------------------------------------------------------| + /// | 4 | Blend mode signature: '8BIM' | + /// |--------------------+---------------------------------------------------------------------| + /// | | Blend mode key: | + /// | | | + /// | | 'pass' = pass through, 'norm' = normal, 'diss' = dissolve, 'dark' = | + /// | | darken, 'mul ' = multiply, 'idiv' = color burn, 'lbrn' = linear | + /// | | burn, 'dkCl' = darker color, 'lite' = lighten, 'scrn' = screen, | + /// | 4 | 'div ' = color dodge, 'lddg' = linear dodge, 'lgCl' = lighter | + /// | | color, 'over' = overlay, 'sLit' = soft light, 'hLit' = hard light, | + /// | | 'vLit' = vivid light, 'lLit' = linear light, 'pLit' = pin light, | + /// | | 'hMix' = hard mix, 'diff' = difference, 'smud' = exclusion, 'fsub' | + /// | | = subtract, 'fdiv' = divide 'hue ' = hue, 'sat ' = saturation, | + /// | | 'colr' = color, 'lum ' = luminosity, | + /// |--------------------+---------------------------------------------------------------------| + /// | 1 | Opacity. 0 = transparent ... 255 = opaque | + /// |--------------------+---------------------------------------------------------------------| + /// | 1 | Clipping: 0 = base, 1 = non-base | + /// |--------------------+---------------------------------------------------------------------| + /// | | Flags: | + /// | | bit 0 = transparency protected; | + /// | | bit 1 = visible; | + /// | 1 | bit 2 = obsolete; | + /// | | bit 3 = 1 for Photoshop 5.0 and later, tells if bit 4 has useful | + /// | | information; | + /// | | bit 4 = pixel data irrelevant to appearance of document | + /// |--------------------+---------------------------------------------------------------------| + /// | 1 | Filler (zero) | + /// |--------------------+---------------------------------------------------------------------| + /// | 4 | Length of the extra data field ( = the total length of the next | + /// | | five fields). | + /// |--------------------+---------------------------------------------------------------------| + /// | Variable | Layer mask data: See See Layer mask / adjustment layer data for | + /// | | structure. Can be 40 bytes, 24 bytes, or 4 bytes if no layer mask. | + /// |--------------------+---------------------------------------------------------------------| + /// | Variable | Layer blending ranges: See See Layer blending ranges data. | + /// |--------------------+---------------------------------------------------------------------| + /// | Variable | Layer name: Pascal string, padded to a multiple of 4 bytes. | + /// +------------------------------------------------------------------------------------------+ + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + let layer = self.0; + + // Rectangle containing the contents + buffer.write(layer.layer_top.to_be_bytes()); + buffer.write(layer.layer_left.to_be_bytes()); + buffer.write(layer.layer_bottom.to_be_bytes()); + buffer.write(layer.layer_right.to_be_bytes()); + + buffer.write((layer.channels.len() as u16).to_be_bytes()); // Number of channels + + ChannelInformation(&layer.channels).write(buffer); + + buffer.write(SIGNATURE_EIGHT_BIM); // Blend mode signature + let blend_mode: &[u8] = layer.blend_mode.into(); + buffer.write(blend_mode); + + buffer.write(layer.opacity.to_be_bytes()); + buffer.write((layer.clipping_mask as u8).to_be_bytes()); + + buffer.write([(layer.visible as u8) << 1]); // Visible bit + + buffer.write([0_u8]); // Filler + + //let start = buffer.buffer.stream_position().unwrap(); + buffer.write_sized(|buf| { + buf.write(0_u32.to_be_bytes()); // No layer mask data + buf.write(0_u32.to_be_bytes()); // No layer blending ranges data + buf.pad(4, |buf| { + buf.write_sized_with(crate::sections::Length::Size1, |buf| buf.write(&layer.name)); + }); + + if let Some(group) = layer.group_id.and_then(|id| self.1.get(&id)) { + // Additional layer information + //+------------------------------------------------------------------------------------------+ + //| Length | Description | + //|----------+-------------------------------------------------------------------------------| + //| 4 | Signature: '8BIM' or '8B64' | + //|----------+-------------------------------------------------------------------------------| + //| 4 | Key: a 4-character code (See individual sections) | + //|----------+-------------------------------------------------------------------------------| + //| | Length data below, rounded up to an even byte count. | + //| 4 | | + //| | (**PSB**, the following keys have a length count of 8 bytes: LMsk, Lr16, | + //| | Lr32, Layr, Mt16, Mt32, Mtrn, Alph, FMsk, lnk2, FEid, FXid, PxSD. | + //|----------+-------------------------------------------------------------------------------| + //| Variable | Data (See individual sections) | + //+------------------------------------------------------------------------------------------+ + } + }); + //let end = buffer.buffer.stream_position().unwrap(); + + //let write_extra_len = end - start - 4; + + //buffer.pad(4, |buf| { + // buf.write_pascal_string(&layer.name); + //}); + + //if let Some(group_id) = &layer.group_id { + + // self.1.get(group_id) + + //} + } +} + +struct ChannelImageData<'a>(&'a ChannelBytes); + +impl PsdSerialize for ChannelImageData<'_> { + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + let bytes = self.0; + let compression = match bytes { + ChannelBytes::RawData(_) => PsdChannelCompression::RawData, + ChannelBytes::RleCompressed(_) => PsdChannelCompression::RleCompressed, + }; + + compression.write(buffer); + bytes.write(buffer); + } +} + +struct ChannelInformation<'a>(&'a LayerChannels); + +impl PsdSerialize for ChannelInformation<'_> { + fn write(&self, buffer: &mut super::PsdBuffer) + where + T: std::io::Write + std::io::Seek, + { + for (kind, bytes) in self.0.iter() { + kind.write(buffer); // 2 bytes for Channel ID + + let bytes = match bytes { + ChannelBytes::RawData(bytes) => bytes, + ChannelBytes::RleCompressed(bytes) => bytes, + }; + + // Channel image data + // 2 bytes for compression type + channel image data length + buffer.write((2 + bytes.len() as u32).to_be_bytes()); + } + } +} + impl LayerAndMaskInformationSection { /// Create a LayerAndMaskInformationSection from the bytes in the corresponding section in a /// PSD file (including the length marker). @@ -392,7 +629,8 @@ fn read_layer_record(cursor: &mut PsdCursor) -> Result Result PsdCursor<'a> { pub fn read(&mut self, count: u32) -> &[u8] { let start = self.cursor.position() as usize; let end = start + count as usize; - let bytes = &self.cursor.get_ref()[start..end]; - self.cursor.set_position(end as u64); - bytes + + &self.get_ref()[start..end] } pub fn peek_u32(&self) -> u32 { @@ -147,6 +151,15 @@ impl<'a> PsdCursor<'a> { u32_from_be_bytes(bytes) } + pub fn peek_i32(&self) -> i32 { + let bytes = self.peek_4(); + + let mut array = [0; 4]; + array.copy_from_slice(bytes); + + i32::from_be_bytes(array) + } + /// Peek at the next four bytes pub fn peek_4(&self) -> &[u8] { self.peek(4) @@ -156,7 +169,7 @@ impl<'a> PsdCursor<'a> { fn peek(&self, n: u8) -> &[u8] { let start = self.cursor.position() as usize; let end = start + n as usize; - let bytes = &self.cursor.get_ref()[start..end]; + let bytes = &self.get_ref()[start..end]; bytes } @@ -310,10 +323,216 @@ impl<'a> PsdCursor<'a> { } } +use thiserror::Error; +#[derive(Debug, Error)] +pub enum PsdWriteError { + #[error("io error")] + IO(#[from] std::io::Error), +} + +use std::io::{Seek, Write}; + +pub(crate) struct PsdBuffer { + buffer: T, +} + +impl PsdBuffer +where + T: AsRef<[u8]>, +{ + /// Create a new PsdCursor + pub fn new(bytes: T) -> PsdBuffer> { + PsdBuffer { + buffer: Cursor::new(bytes), + } + } +} + +impl PsdBuffer +where + T: Write, +{ + pub fn write(&mut self, bytes: B) + where + B: AsRef<[u8]>, + { + let _ = self.buffer.write(bytes.as_ref()); + } + + pub fn write_pascal_string(&mut self, string: S) + where + S: AsRef<[u8]>, + { + let bytes = string.as_ref(); + let len = bytes.len() as u8; + let _ = self.buffer.write(&[len]); + let _ = self.buffer.write(bytes); + + if len == 0 || len % 2 != 0 { + let _ = self.buffer.write(&[0]); + } + } +} + +impl PsdBuffer +where + T: Write + Seek, +{ + pub fn pad(&mut self, pad: usize, func: F) + where + F: FnOnce(&mut Self), + { + let start = self.buffer.stream_position().unwrap(); + func(self); + let end = self.buffer.stream_position().unwrap(); + + assert!(start <= end); + let length = (end - start) as usize; + + let remander = length % pad; + let pad = (pad - remander) % pad; + + for _ in 0..pad { + let _ = self.buffer.write(&[0_u8]); + } + } + + /// Write a variable length of data to the buffer prefixed by 4 bytes that contain the length + /// of the written data. + /// + /// +---------------------------------------------------------+ + /// | Length | Description | + /// |----------+----------------------------------------------| + /// | 4 | The length of the following data. | + /// |----------+----------------------------------------------| + /// | Variable | The data written to this buffer in the func. | + /// +---------------------------------------------------------+ + pub fn write_sized(&mut self, func: F) + where + F: FnOnce(&mut Self), + { + self.write_sized_with(Length::Size4, func); + } + + pub fn write_sized_with(&mut self, length_size: Length, func: F) + where + F: FnOnce(&mut Self), + { + let length_start = self.buffer.stream_position().unwrap(); + + let data_start = self + .buffer + .seek(SeekFrom::Current(length_size.size() as i64)) + .unwrap(); + func(self); + let data_end = self.buffer.stream_position().unwrap(); + + assert!(data_start <= data_end); + let data_length = data_end - data_start; + + self.buffer.seek(SeekFrom::Start(length_start)).unwrap(); + match length_size { + Length::Size1 => self.buffer.write(&(data_length as u8).to_be_bytes()), + Length::Size4 => self.buffer.write(&(data_length as u32).to_be_bytes()), + } + .unwrap(); + self.buffer.seek(SeekFrom::Start(data_end)).unwrap(); + } + + pub fn write_unicode_string(&mut self, string: S) + where + S: AsRef, + { + AsUnicodeString(&string).write(self); + } +} + +pub enum Length { + Size1, + Size4, +} + +impl Length { + pub fn size(&self) -> usize { + match self { + Length::Size1 => size_of::(), + Length::Size4 => size_of::(), + } + } +} + +pub(crate) trait PsdSerialize { + fn write(&self, buffer: &mut PsdBuffer) + where + T: Write + Seek; + + fn to_bytes(&self) -> Result, PsdWriteError> { + let mut buf = vec![]; + self.write(&mut PsdBuffer::new(&mut buf)); + Ok(buf) + } +} + +struct AsUnicodeString<'a, T>(&'a T) +where + T: AsRef; + +impl PsdSerialize for AsUnicodeString<'_, S> +where + S: AsRef, +{ + fn write(&self, buffer: &mut PsdBuffer) + where + T: Write, + { + //let mut utf16_unit_length = 0_u32; + //let bytes: Vec<_> = self + // .0 + // .as_ref() + // .encode_utf16() + // .flat_map(|unit| { + // utf16_unit_length += 1; + // unit.to_be_bytes() + // }) + // .collect(); + + //buffer.write(utf16_unit_length.to_be_bytes()); + //buffer.write(&bytes); + + write_unicode_string(self.0.as_ref(), &mut buffer.buffer).unwrap(); + } +} + +pub fn write_unicode_string(string: &str, mut buf: impl Write) -> Result<(), std::io::Error> { + let mut utf16_unit_length = 0_u32; + + let bytes: Vec<_> = string + .encode_utf16() + .flat_map(|unit| { + utf16_unit_length += 1; + unit.to_be_bytes() + }) + .collect(); + + buf.write_all(&utf16_unit_length.to_be_bytes())?; + buf.write_all(&bytes)?; + Ok(()) +} + +pub(crate) trait PsdDeserialize +where + Self: Sized, +{ + type Error; + + // consider changing to take a PsdBuffer/Cursor allow buffer to "yield" a new buffer over a + // slice of the parent buffer + fn from_bytes(bytes: &[u8]) -> Result; +} + fn u8_slice_to_u16(bytes: &[u8]) -> Vec { return Vec::from(bytes) .chunks_exact(2) - .into_iter() .map(|a| u16::from_be_bytes([a[0], a[1]])) .collect(); } diff --git a/tests/slices_resource.rs b/tests/slices_resource.rs index 1c4e7bb..1fcb462 100644 --- a/tests/slices_resource.rs +++ b/tests/slices_resource.rs @@ -28,7 +28,7 @@ fn name_of_slices_resource_group() { match &psd.resources()[0] { ImageResource::Slices(slices) => { - assert_eq!(slices.name().as_str(), expected_slices_name); + assert_eq!(slices.name(), expected_slices_name); } }; } @@ -46,7 +46,7 @@ fn slices_v7_8() -> Result<()> { match &psd.resources()[0] { ImageResource::Slices(slices) => { - assert_eq!(slices.name().as_str(), "\u{0}"); + assert_eq!(slices.name(), "\u{0}"); } }; From 65601558a0cbf9563e0a9ffbf2e78c8879adb2e7 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 11 Sep 2024 12:21:05 -0500 Subject: [PATCH 02/16] Add changes to update dependencies and fix compilation warnings. --- Cargo.toml | 6 ++-- examples/drag-drop-browser/Cargo.toml | 19 +++++++----- examples/drag-drop-browser/src/lib.rs | 41 ++++++++++++++++--------- src/blend.rs | 26 ++++++++-------- src/psd_channel.rs | 2 ++ src/render.rs | 4 +-- src/sections/file_header_section.rs | 1 + src/sections/image_resources_section.rs | 1 + src/sections/mod.rs | 1 + tests/transparency.rs | 3 ++ 10 files changed, 64 insertions(+), 40 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1bb917a..134e100 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "psd" -version = "0.3.5" +version = "0.3.6" authors = ["Chinedu Francis Nwafili "] description = "A Rust API for parsing and working with PSD files." keywords = ["psd", "photoshop", "texture", "png", "image"] license = "MIT/Apache-2.0" repository = "https://github.com/chinedufn/psd" -edition = "2018" +edition = "2021" [dependencies] thiserror = "1" @@ -21,4 +21,4 @@ members = [ [profile.release] # We can re-enable lto for the demo when wasm-pack 0.2.38 is released. There's a bug in 0.2.37 -# lto = true +# lto = true \ No newline at end of file diff --git a/examples/drag-drop-browser/Cargo.toml b/examples/drag-drop-browser/Cargo.toml index ead844e..e9f040c 100644 --- a/examples/drag-drop-browser/Cargo.toml +++ b/examples/drag-drop-browser/Cargo.toml @@ -1,19 +1,19 @@ [package] name = "drag-drop-browser" -version = "0.1.0" +version = "0.1.1" authors = ["Chinedu Francis Nwafili "] -edition = "2018" +edition = "2021" [lib] crate-type = ["cdylib"] [dependencies] -console_error_panic_hook = "0.1.6" -css-rs-macro = "0.1.0" -js-sys = "0.3.40" +console_error_panic_hook = "0.1.7" +percy-css-macro = "0.1.1" +js-sys = "0.3.70" psd = {path = "../../"} -percy-dom = "0.7" -wasm-bindgen = "0.2.63" +percy-dom = "0.9.9" +wasm-bindgen = "0.2.93" [dependencies.web-sys] version = "0.3" @@ -36,3 +36,8 @@ features = [ "Window", "console" ] + +[patch.crates-io] +syn = "2" +proc-macro2 = "1" +quote = "1" \ No newline at end of file diff --git a/examples/drag-drop-browser/src/lib.rs b/examples/drag-drop-browser/src/lib.rs index 8da75a2..81b36c2 100644 --- a/examples/drag-drop-browser/src/lib.rs +++ b/examples/drag-drop-browser/src/lib.rs @@ -6,7 +6,7 @@ use wasm_bindgen::Clamped; use wasm_bindgen::JsCast; use web_sys::*; -use css_rs_macro::css; +use percy_css_macro::css; use psd::Psd; use std::cell::RefCell; @@ -25,10 +25,11 @@ struct AppWrapper(Rc>); impl AppWrapper { /// Create a new AppWrapper. We'll call this in a script tag in index.html #[wasm_bindgen(constructor)] + #[allow(dead_code)] pub fn new() -> AppWrapper { console_error_panic_hook::set_once(); - let mut app = App::new(); + let app = App::new(); let closure_holder = Rc::clone(&app.raf_closure_holder); @@ -49,13 +50,13 @@ impl AppWrapper { let app = Rc::clone(&app); let vdom = app.borrow().render(); - app.borrow_mut().update(vdom); + let _ = app.borrow_mut().update(vdom); store.borrow_mut().msg(&Msg::SetIsRendering(false)); }; - let mut re_render = Closure::wrap(Box::new(re_render) as Box); + let re_render = Closure::wrap(Box::new(re_render) as Box); - window().request_animation_frame(&re_render.as_ref().unchecked_ref()); + let _ = window().request_animation_frame(&re_render.as_ref().unchecked_ref()); *closure_holder.borrow_mut() = Some(Box::new(re_render)); }; @@ -73,6 +74,7 @@ impl AppWrapper { /// Our client side web application #[wasm_bindgen] +#[allow(dead_code)] struct App { store: Rc>, dom_updater: PercyDom, @@ -83,9 +85,10 @@ struct App { #[wasm_bindgen] impl App { /// Create a new App + #[allow(dead_code)] fn new() -> App { let vdom = html! {
}; - let mut dom_updater = PercyDom::new_append_to_mount(vdom, &body()); + let dom_updater = PercyDom::new_append_to_mount(vdom, &body()); let state = State { psd: None, @@ -111,7 +114,7 @@ impl App { self.store.borrow_mut().msg(&Msg::ReplacePsd(demo_psd)); let vdom = self.render(); - self.update(vdom); + let _ = self.update(vdom); } /// Render the virtual-dom @@ -200,7 +203,7 @@ impl App { let file_reader = web_sys::FileReader::new().unwrap(); file_reader.read_as_array_buffer(&psd).unwrap(); - let mut onload = Closure::wrap(Box::new(move |event: Event| { + let onload = Closure::wrap(Box::new(move |event: Event| { let file_reader: FileReader = event.target().unwrap().dyn_into().unwrap(); let psd = file_reader.result().unwrap(); let psd = js_sys::Uint8Array::new(&psd); @@ -239,8 +242,8 @@ impl App { // Flatten the PSD into only the pixels from the layers that are currently // toggled on. - let mut psd_pixels = psd - .flatten_layers_rgba(&|(idx, layer)| { + let psd_pixels = psd + .flatten_layers_rgba(&|(_idx, layer)| { let layer_visible = *self .store .borrow() @@ -276,7 +279,8 @@ impl App { /// A light wrapper around State, useful when you want to accept a Msg and handle /// anything impure (such as working with local storage) before passing the Msg -/// along the State. Allowing you to keep State pure. +/// along the State. Allowing you to keep State pure.\ +#[allow(dead_code)] struct Store { state: State, on_msg: Option>, @@ -292,6 +296,7 @@ impl Deref for Store { } /// Handles application state +#[allow(dead_code)] struct State { /// The current PSD that is being displayed psd: Option, @@ -317,6 +322,7 @@ impl Store { impl State { /// Update State given some new Msg + #[allow(dead_code)] fn msg(&mut self, msg: &Msg) { match msg { // Replace the current PSD with a new one @@ -351,27 +357,32 @@ impl State { } /// All of our Msg variants that are used to update application state +#[allow(dead_code)] enum Msg<'a> { /// Replace the current PSD with a new one, usually after drag and drop ReplacePsd(&'a [u8]), - /// Set whether or not a layer (by index) should be visible + /// Set whether a layer (by index) should be visible SetLayerVisibility(usize, bool), /// Set that the application is planning to render on the next request animation frame SetIsRendering(bool), } -fn window() -> web_sys::Window { +#[allow(dead_code)] +fn window() -> Window { web_sys::window().unwrap() } -fn document() -> web_sys::Document { +#[allow(dead_code)] +fn document() -> Document { window().document().unwrap() } -fn body() -> web_sys::HtmlElement { +#[allow(dead_code)] +fn body() -> HtmlElement { document().body().unwrap() } +#[allow(dead_code)] static APP_CONTAINER: &'static str = css! {r#" :host { display: flex; diff --git a/src/blend.rs b/src/blend.rs index 1a4ee1f..c8c0712 100644 --- a/src/blend.rs +++ b/src/blend.rs @@ -106,7 +106,7 @@ fn map_blend_mode(blend_mode: BlendMode) -> &'static BlendFunction { } } -fn pass_through(color_b: f32, color_s: f32) -> f32 { +fn pass_through(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } @@ -115,11 +115,11 @@ fn pass_through(color_b: f32, color_s: f32) -> f32 { /// /// `B(Cb, Cs) = Cs` #[inline(always)] -fn normal(color_b: f32, color_s: f32) -> f32 { +fn normal(_color_b: f32, color_s: f32) -> f32 { color_s } -fn dissolve(color_b: f32, color_s: f32) -> f32 { +fn dissolve(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } @@ -179,7 +179,7 @@ fn linear_burn(color_b: f32, color_s: f32) -> f32 { (color_b - color_s - 1.).max(0.) } -fn darker_color(color_b: f32, color_s: f32) -> f32 { +fn darker_color(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } @@ -243,7 +243,7 @@ fn linear_dodge(color_b: f32, color_s: f32) -> f32 { (color_b + color_s).min(1.) } -fn lighter_color(color_b: f32, color_s: f32) -> f32 { +fn lighter_color(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } @@ -317,21 +317,21 @@ fn hard_light(color_b: f32, color_s: f32) -> f32 { } } -fn vivid_light(color_b: f32, color_s: f32) -> f32 { +fn vivid_light(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } -fn linear_light(color_b: f32, color_s: f32) -> f32 { +fn linear_light(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } #[inline(always)] -fn pin_light(color_b: f32, color_s: f32) -> f32 { +fn pin_light(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } #[inline(always)] -fn hard_mix(color_b: f32, color_s: f32) -> f32 { +fn hard_mix(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } @@ -384,19 +384,19 @@ fn divide(color_b: f32, color_s: f32) -> f32 { } } -fn hue(color_b: f32, color_s: f32) -> f32 { +fn hue(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } -fn saturation(color_b: f32, color_s: f32) -> f32 { +fn saturation(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } -fn color(color_b: f32, color_s: f32) -> f32 { +fn color(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } -fn luminosity(color_b: f32, color_s: f32) -> f32 { +fn luminosity(_color_b: f32, _color_s: f32) -> f32 { unimplemented!() } diff --git a/src/psd_channel.rs b/src/psd_channel.rs index c303024..9c7bb6e 100644 --- a/src/psd_channel.rs +++ b/src/psd_channel.rs @@ -197,6 +197,7 @@ pub trait IntoRgba { } /// Rle decompress a channel +#[allow(dead_code)] fn rle_decompress(bytes: &[u8]) -> Vec { let mut cursor = PsdCursor::new(&bytes[..]); @@ -228,6 +229,7 @@ fn rle_decompress(bytes: &[u8]) -> Vec { /// into an 8 bit channel. /// /// We store the final bytes in the first channel (overwriting the old bytes) +#[allow(dead_code)] fn sixteen_to_eight_rgba(channel1: &[u8], channel2: &[u8]) -> Vec { let mut eight = Vec::with_capacity(channel1.len()); diff --git a/src/render.rs b/src/render.rs index f84aee0..d783cb9 100644 --- a/src/render.rs +++ b/src/render.rs @@ -17,11 +17,11 @@ impl<'a> Renderer<'a> { width: usize, ) -> Renderer<'a> { Renderer { - layers_to_flatten_top_down: layers_to_flatten_top_down, + layers_to_flatten_top_down, cached_layer_rgba: repeat_with(|| RefCell::new(None)) .take(layers_to_flatten_top_down.len()) .collect(), - width: width, + width, pixel_cache: RefCell::new(Vec::with_capacity(layers_to_flatten_top_down.len())), } } diff --git a/src/sections/file_header_section.rs b/src/sections/file_header_section.rs index 1b5be3c..2b57afe 100644 --- a/src/sections/file_header_section.rs +++ b/src/sections/file_header_section.rs @@ -35,6 +35,7 @@ const EXPECTED_RESERVED: [u8; 6] = [0; 6]; /// | 2 | Depth: the number of bits per channel. Supported values are 1, 8, 16 and 32. | /// | 2 | The color mode of the file. Supported values are: Bitmap = 0; Grayscale = 1; Indexed = 2; RGB = 3; CMYK = 4; Multichannel = 7; Duotone = 8; Lab = 9. | #[derive(Debug, PartialEq)] +#[allow(dead_code)] pub struct FileHeaderSection { pub(crate) version: PsdVersion, pub(crate) channel_count: ChannelCount, diff --git a/src/sections/image_resources_section.rs b/src/sections/image_resources_section.rs index 4618d48..da023d5 100644 --- a/src/sections/image_resources_section.rs +++ b/src/sections/image_resources_section.rs @@ -18,6 +18,7 @@ const RESOURCE_SLICES_INFO: i16 = 1050; pub mod image_resource; +#[allow(dead_code)] struct ImageResourcesBlock { resource_id: i16, name: String, diff --git a/src/sections/mod.rs b/src/sections/mod.rs index f8bf773..8cb25c6 100644 --- a/src/sections/mod.rs +++ b/src/sections/mod.rs @@ -17,6 +17,7 @@ pub mod layer_and_mask_information_section; /// References to the different major sections of a PSD file #[derive(Debug)] +#[allow(dead_code)] pub struct MajorSections<'a> { pub(crate) file_header: &'a [u8], pub(crate) color_mode_data: &'a [u8], diff --git a/tests/transparency.rs b/tests/transparency.rs index 1567800..2c4bcde 100644 --- a/tests/transparency.rs +++ b/tests/transparency.rs @@ -89,6 +89,7 @@ fn transparent_above_opaque() -> Result<()> { // Ensure that the specified, zero-indexed left, top coordinate has the provided pixel color. // Otherwise it should be fully transparent. // (left, top, pixel) +#[allow(dead_code)] fn assert_colors(image: Vec, psd: &Psd, assertions: &[(usize, usize, [u8; 4])]) { let pixel_count = (psd.width() * psd.height()) as usize; let width = psd.width() as usize; @@ -115,6 +116,7 @@ fn assert_colors(image: Vec, psd: &Psd, assertions: &[(usize, usize, [u8; 4] } } +#[allow(dead_code)] fn make_image(pixel: [u8; 4], pixel_count: u32) -> Vec { let pixel_count = pixel_count as usize; let mut image = vec![0; pixel_count * 4]; @@ -129,6 +131,7 @@ fn make_image(pixel: [u8; 4], pixel_count: u32) -> Vec { image } +#[allow(dead_code)] fn put_pixel(image: &mut Vec, width: usize, left: usize, top: usize, new: [u8; 4]) { let idx = (top * width) + left; image[idx * 4] = new[0]; From 9f836f91a1128dbc9d735c49d24b759848f010f5 Mon Sep 17 00:00:00 2001 From: Jacob Date: Wed, 11 Sep 2024 12:27:18 -0500 Subject: [PATCH 03/16] Update README to fix CircleCI broken link. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 454b372..38a3a77 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ psd === -[![Build status](https://circleci.com/gh/chinedufn/psd.svg?style=shield&circle-token=:circle-token)](https://circleci.com/gh/chinedufn/psd) [![docs](https://docs.rs/psd/badge.svg)](https://docs.rs/psd) +[![Build status](https://circleci.com/gh/chinedufn/psd.svg?style=shield)](https://circleci.com/gh/chinedufn/psd) [![docs](https://docs.rs/psd/badge.svg)](https://docs.rs/psd) > A Rust API for parsing and working with PSD files. From 7639780672324cf59974c2eba280bc59a13d44b4 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Thu, 12 Sep 2024 19:41:25 -0500 Subject: [PATCH 04/16] clean build warnings --- src/sections/image_resources_section.rs | 2 +- .../image_resource/descriptor_structure.rs | 10 +++++----- src/sections/layer_and_mask_information_section/mod.rs | 6 +++--- src/sections/mod.rs | 1 - 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/sections/image_resources_section.rs b/src/sections/image_resources_section.rs index da023d5..ba5f7b6 100644 --- a/src/sections/image_resources_section.rs +++ b/src/sections/image_resources_section.rs @@ -4,7 +4,7 @@ use std::ops::Range; use thiserror::Error; use crate::sections::image_resources_section::image_resource::descriptor_structure::{ - DescriptorStructure, ImageResourcesDescriptorError, + ImageResourcesDescriptorError, }; use crate::sections::PsdCursor; diff --git a/src/sections/image_resources_section/image_resource/descriptor_structure.rs b/src/sections/image_resources_section/image_resource/descriptor_structure.rs index f47bfdd..5102268 100644 --- a/src/sections/image_resources_section/image_resource/descriptor_structure.rs +++ b/src/sections/image_resources_section/image_resource/descriptor_structure.rs @@ -584,12 +584,12 @@ impl DescriptorStructure { fn read_fields( cursor: &mut PsdCursor, - sub_list: bool, + _sub_list: bool, ) -> Result, ImageResourcesDescriptorError> { let count = cursor.read_u32(); let mut m = HashMap::with_capacity(count as usize); - for n in 0..count { + for _ in 0..count { let key = DescriptorStructure::read_key_length(cursor); let key = String::from_utf8_lossy(key).into_owned(); @@ -601,12 +601,12 @@ impl DescriptorStructure { fn read_list( cursor: &mut PsdCursor, - sub_list: bool, + _sub_list: bool, ) -> Result, ImageResourcesDescriptorError> { let count = cursor.read_u32(); let mut vec = Vec::with_capacity(count as usize); - for n in 0..count { + for _ in 0..count { let field = DescriptorStructure::read_descriptor_field(cursor)?; vec.push(field); } @@ -683,7 +683,7 @@ impl DescriptorStructure { let count = cursor.read_u32(); let mut vec = Vec::with_capacity(count as usize); - for n in 0..count { + for _ in 0..count { DescriptorStructure::read_key_length(cursor); let mut os_type = [0; 4]; diff --git a/src/sections/layer_and_mask_information_section/mod.rs b/src/sections/layer_and_mask_information_section/mod.rs index 90c7e77..f2e995e 100644 --- a/src/sections/layer_and_mask_information_section/mod.rs +++ b/src/sections/layer_and_mask_information_section/mod.rs @@ -231,7 +231,7 @@ impl PsdSerialize for LayerRecordWrite<'_> { buf.write_sized_with(crate::sections::Length::Size1, |buf| buf.write(&layer.name)); }); - if let Some(group) = layer.group_id.and_then(|id| self.1.get(&id)) { + if let Some(_group) = layer.group_id.and_then(|id| self.1.get(&id)) { // Additional layer information //+------------------------------------------------------------------------------------------+ //| Length | Description | @@ -629,7 +629,7 @@ fn read_layer_record(cursor: &mut PsdCursor) -> Result Result Date: Thu, 12 Sep 2024 19:54:20 -0500 Subject: [PATCH 05/16] Reduce redundant code in sixteen_to_eight_rgba --- src/psd_channel.rs | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/src/psd_channel.rs b/src/psd_channel.rs index 9c7bb6e..90e4ae8 100644 --- a/src/psd_channel.rs +++ b/src/psd_channel.rs @@ -233,42 +233,28 @@ fn rle_decompress(bytes: &[u8]) -> Vec { fn sixteen_to_eight_rgba(channel1: &[u8], channel2: &[u8]) -> Vec { let mut eight = Vec::with_capacity(channel1.len()); - for idx in 0..channel1.len() { - if idx % 2 == 1 { - continue; - } - - let sixteen_bit = [channel1[idx], channel1[idx + 1]]; - let sixteen_bit = u16::from_be_bytes(sixteen_bit); + for c in [channel1, channel2].iter() { + for idx in 0..c.len() { + if idx % 2 == 1 { + continue; + } - let eight_bit = (sixteen_bit / 256) as u8; + let sixteen_bit = [c[idx], c[idx + 1]]; + let sixteen_bit = u16::from_be_bytes(sixteen_bit); - eight.push(eight_bit); - eight.push(eight_bit); - eight.push(eight_bit); - eight.push(255); - } + let eight_bit = (sixteen_bit / 256) as u8; - for idx in 0..channel2.len() { - if idx % 2 == 1 { - continue; + eight.push(eight_bit); + eight.push(eight_bit); + eight.push(eight_bit); + eight.push(255); } - - let sixteen_bit = [channel2[idx], channel2[idx + 1]]; - let sixteen_bit = u16::from_be_bytes(sixteen_bit); - - let eight_bit = (sixteen_bit / 256) as u8; - - eight.push(eight_bit); - eight.push(eight_bit); - eight.push(eight_bit); - eight.push(255); } eight } -/// Indicates how a channe'sl data is compressed +/// Indicates how a channels data is compressed #[derive(Debug, Eq, PartialEq, Copy, Clone)] #[allow(missing_docs)] pub enum PsdChannelCompression { From c91ef1f633ba6b0a034b2bc04ed333c584403ba1 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Thu, 12 Sep 2024 20:21:26 -0500 Subject: [PATCH 06/16] Optimize blend_pixels function by ~20% --- src/blend.rs | 51 +++++++++++++++++++++++++++++++-------------------- 1 file changed, 31 insertions(+), 20 deletions(-) diff --git a/src/blend.rs b/src/blend.rs index c8c0712..a02bfc8 100644 --- a/src/blend.rs +++ b/src/blend.rs @@ -8,6 +8,8 @@ pub(crate) fn apply_opacity(pixel: &mut Pixel, opacity: u8) { pixel[3] = (pixel[3] as f32 * alpha) as u8; } +const INV_255: f32 = 1.0 / 255.0; + /// /// https://www.w3.org/TR/compositing-1/#simplealphacompositing /// `Cs = (1 - αb) x Cs + αb x B(Cb, Cs)` @@ -31,37 +33,46 @@ pub(crate) fn apply_opacity(pixel: &mut Pixel, opacity: u8) { /// `Co = co / αo` /// /// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. -pub(crate) fn blend_pixels(top: Pixel, bottom: Pixel, blend_mode: BlendMode, out: &mut Pixel) { - // TODO: make some optimizations - let alpha_s = top[3] as f32 / 255.; - let alpha_b = bottom[3] as f32 / 255.; - let alpha_output = alpha_s + alpha_b * (1. - alpha_s); +/// +/// ** this optimized version is based on tdakkota's implementation but improves it by ~10%-25% +pub(crate) fn blend_pixels( + top: Pixel, + bottom: Pixel, + blend_mode: BlendMode, + out: &mut Pixel, +) { + let alpha_s = top[3] as f32 * INV_255; + let alpha_b = bottom[3] as f32 * INV_255; + let alpha_output = alpha_s + alpha_b - alpha_s * alpha_b; let (r_s, g_s, b_s) = ( - top[0] as f32 / 255., - top[1] as f32 / 255., - top[2] as f32 / 255., + top[0] as f32 * INV_255, + top[1] as f32 * INV_255, + top[2] as f32 * INV_255, ); let (r_b, g_b, b_b) = ( - bottom[0] as f32 / 255., - bottom[1] as f32 / 255., - bottom[2] as f32 / 255., + bottom[0] as f32 * INV_255, + bottom[1] as f32 * INV_255, + bottom[2] as f32 * INV_255, ); let blend_f = map_blend_mode(blend_mode); - let (r, g, b) = ( - composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255., - composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255., - composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255., + + // Multiply composite result by 255.0 before division, matching original function + let (r_co, g_co, b_co) = ( + composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255.0, + composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255.0, + composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255.0, ); - // NOTE: make all assignments _after_ all reads to avoid issues when top or bottom is out - out[0] = (r.round() / alpha_output) as u8; - out[1] = (g.round() / alpha_output) as u8; - out[2] = (b.round() / alpha_output) as u8; - out[3] = (255. * alpha_output).round() as u8; + // Divide after rounding, matching the original function's order + out[0] = (r_co.round() / alpha_output).clamp(0.0, 255.0) as u8; + out[1] = (g_co.round() / alpha_output).clamp(0.0, 255.0) as u8; + out[2] = (b_co.round() / alpha_output).clamp(0.0, 255.0) as u8; + out[3] = (alpha_output * 255.0).round().clamp(0.0, 255.0) as u8; } + type BlendFunction = dyn Fn(f32, f32) -> f32; /// Returns blend function for given BlendMode From b31bee41df9ff3a76762b645065dfc1e738cd297 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Thu, 12 Sep 2024 22:39:08 -0500 Subject: [PATCH 07/16] Fix edge cases where 'color_burn' and 'color_dodge' would result in incorrect values through the use of epsilon comparisons. Add tests for color burn and color dodge that explicitly test for this. --- src/blend.rs | 110 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 74 insertions(+), 36 deletions(-) diff --git a/src/blend.rs b/src/blend.rs index a02bfc8..08cad47 100644 --- a/src/blend.rs +++ b/src/blend.rs @@ -9,6 +9,7 @@ pub(crate) fn apply_opacity(pixel: &mut Pixel, opacity: u8) { } const INV_255: f32 = 1.0 / 255.0; +const EPSILON: f32 = 1e-6; /// /// https://www.w3.org/TR/compositing-1/#simplealphacompositing @@ -45,31 +46,35 @@ pub(crate) fn blend_pixels( let alpha_b = bottom[3] as f32 * INV_255; let alpha_output = alpha_s + alpha_b - alpha_s * alpha_b; - let (r_s, g_s, b_s) = ( - top[0] as f32 * INV_255, - top[1] as f32 * INV_255, - top[2] as f32 * INV_255, - ); - let (r_b, g_b, b_b) = ( - bottom[0] as f32 * INV_255, - bottom[1] as f32 * INV_255, - bottom[2] as f32 * INV_255, - ); - - let blend_f = map_blend_mode(blend_mode); - - // Multiply composite result by 255.0 before division, matching original function - let (r_co, g_co, b_co) = ( - composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255.0, - composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255.0, - composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255.0, - ); - - // Divide after rounding, matching the original function's order - out[0] = (r_co.round() / alpha_output).clamp(0.0, 255.0) as u8; - out[1] = (g_co.round() / alpha_output).clamp(0.0, 255.0) as u8; - out[2] = (b_co.round() / alpha_output).clamp(0.0, 255.0) as u8; - out[3] = (alpha_output * 255.0).round().clamp(0.0, 255.0) as u8; + if alpha_output > 0.0 { + let (r_s, g_s, b_s) = ( + top[0] as f32 * INV_255, + top[1] as f32 * INV_255, + top[2] as f32 * INV_255, + ); + let (r_b, g_b, b_b) = ( + bottom[0] as f32 * INV_255, + bottom[1] as f32 * INV_255, + bottom[2] as f32 * INV_255, + ); + + let blend_f = map_blend_mode(blend_mode); + + // Multiply composite result by 255.0 before division, matching original function + let (r_co, g_co, b_co) = ( + composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255.0, + composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255.0, + composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255.0, + ); + + // Divide after rounding, matching the original function's order + out[0] = (r_co.round() / alpha_output).clamp(0.0, 255.0) as u8; + out[1] = (g_co.round() / alpha_output).clamp(0.0, 255.0) as u8; + out[2] = (b_co.round() / alpha_output).clamp(0.0, 255.0) as u8; + out[3] = (alpha_output * 255.0).round().clamp(0.0, 255.0) as u8; + } else { + out.fill(0); + } } @@ -170,10 +175,17 @@ fn multiply(color_b: f32, color_s: f32) -> f32 { ///``` #[inline(always)] fn color_burn(color_b: f32, color_s: f32) -> f32 { - if color_b == 1. { - 1. + // Why Use Epsilon Comparisons: + // Floating-Point Precision: Accounts for tiny deviations due to floating-point arithmetic. + // Robustness: Prevents unexpected behavior when color_b or color_s are very close to the comparison values. + // Accuracy: Ensures that values very close to 0.0 or 1.0 are treated appropriately. + if (color_b - 1.0).abs() < EPSILON { + 1.0 + } else if color_b.abs() < EPSILON { + // the previous method could result in a division by zero + 0.0 } else { - (1. - (1. - color_s) / color_b).max(0.) + (1.0 - (1.0 - color_s) / color_b).max(0.0) } } @@ -234,12 +246,12 @@ fn screen(color_b: f32, color_s: f32) -> f32 { /// ``` #[inline(always)] fn color_dodge(color_b: f32, color_s: f32) -> f32 { - if color_b == 0. { - 0. - } else if color_s == 1. { - 1. + if color_b.abs() < EPSILON { + 0.0 + } else if (color_s - 1.0).abs() < EPSILON { + 1.0 } else { - (color_b / (1. - color_s)).min(1.) + (color_b / (1.0 - color_s)).min(1.0) } } @@ -388,10 +400,10 @@ fn subtract(color_b: f32, color_s: f32) -> f32 { /// `B(Cb, Cs) = Cb / Cs` #[inline(always)] fn divide(color_b: f32, color_s: f32) -> f32 { - if color_s == 0. { - color_b - } else { + if color_s > 0. { color_b / color_s + } else { + color_b } } @@ -437,3 +449,29 @@ fn composite( let cb = color_b * alpha_b; cs + cb * (1. - alpha_s) } + + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_color_dodge() { + let color_b = 1e-8; + let color_s = 1.0; + + let result = color_dodge(color_b, color_s); + + assert_eq!(result, 0.0); + } + + #[test] + fn test_color_burn() { + let color_b = 0.9999999; + let color_s = 0.5; + + let result = color_burn(color_b, color_s); + + assert_eq!(result, 1.0); + } +} \ No newline at end of file From 0709425ab29b54dbbe00d6cb3dab1e871320e862 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Thu, 12 Sep 2024 23:25:40 -0500 Subject: [PATCH 08/16] Add blending implementations for 'darker_color', 'lighter_color', 'vivid_light', 'linear_light', 'pin_light', and 'hard_mix'. Add luminance calculation. Add rgb_to_hsl conversion and hsl_to_rgb conversion. Add hue, saturation, color, and luminosity blending ability. --- src/blend.rs | 397 +++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 355 insertions(+), 42 deletions(-) diff --git a/src/blend.rs b/src/blend.rs index 08cad47..a4dad38 100644 --- a/src/blend.rs +++ b/src/blend.rs @@ -35,7 +35,7 @@ const EPSILON: f32 = 1e-6; /// /// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. /// -/// ** this optimized version is based on tdakkota's implementation but improves it by ~10%-25% +/// ** this optimized version is based on tdakkota's implementation but improves it by ~10%-25% and allows for the blending of methods that need access to all channels simultaneously. pub(crate) fn blend_pixels( top: Pixel, bottom: Pixel, @@ -58,14 +58,57 @@ pub(crate) fn blend_pixels( bottom[2] as f32 * INV_255, ); - let blend_f = map_blend_mode(blend_mode); - - // Multiply composite result by 255.0 before division, matching original function - let (r_co, g_co, b_co) = ( - composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255.0, - composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255.0, - composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255.0, - ); + let (r_co, g_co, b_co) = match blend_mode { + BlendMode::DarkerColor => { + let lum_s = luminance(r_s, g_s, b_s); + let lum_b = luminance(r_b, g_b, b_b); + let alpha_s_255 = alpha_s * 255.0; + let alpha_b_255 = alpha_b * 255.0; + + if lum_s < lum_b { + (r_s * alpha_s_255, g_s * alpha_s_255, b_s * alpha_s_255) + } else { + (r_b * alpha_b_255, g_b * alpha_b_255, b_b * alpha_b_255) + } + } + BlendMode::LighterColor => { + let lum_s = luminance(r_s, g_s, b_s); + let lum_b = luminance(r_b, g_b, b_b); + let alpha_s_255 = alpha_s * 255.0; + let alpha_b_255 = alpha_b * 255.0; + + if lum_s > lum_b { + (r_s * alpha_s_255, g_s * alpha_s_255, b_s * alpha_s_255) + } else { + (r_b * alpha_b_255, g_b * alpha_b_255, b_b * alpha_b_255) + } + } + BlendMode::Hue => { + let (r, g, b) = blend_hue(r_b, g_b, b_b, r_s, g_s, b_s); + (r * 255.0, g * 255.0, b * 255.0) + } + BlendMode::Saturation => { + let (r, g, b) = blend_saturation(r_b, g_b, b_b, r_s, g_s, b_s); + (r * 255.0, g * 255.0, b * 255.0) + } + BlendMode::Color => { + let (r, g, b) = blend_color(r_b, g_b, b_b, r_s, g_s, b_s); + (r * 255.0, g * 255.0, b * 255.0) + } + BlendMode::Luminosity => { + let (r, g, b) = blend_luminosity(r_b, g_b, b_b, r_s, g_s, b_s); + (r * 255.0, g * 255.0, b * 255.0) + } + _ => { + let blend_f = map_blend_mode(blend_mode); + + ( + composite(r_s, alpha_s, r_b, alpha_b, blend_f) * 255.0, + composite(g_s, alpha_s, g_b, alpha_b, blend_f) * 255.0, + composite(b_s, alpha_s, b_b, alpha_b, blend_f) * 255.0, + ) + } + }; // Divide after rounding, matching the original function's order out[0] = (r_co.round() / alpha_output).clamp(0.0, 255.0) as u8; @@ -94,13 +137,13 @@ fn map_blend_mode(blend_mode: BlendMode) -> &'static BlendFunction { BlendMode::Multiply => &multiply, BlendMode::ColorBurn => &color_burn, BlendMode::LinearBurn => &linear_burn, - BlendMode::DarkerColor => &darker_color, + BlendMode::DarkerColor => &dummy_blend, // handled in blend_pixels // -------------------------------------- BlendMode::Lighten => &lighten, BlendMode::Screen => &screen, BlendMode::ColorDodge => &color_dodge, BlendMode::LinearDodge => &linear_dodge, - BlendMode::LighterColor => &lighter_color, + BlendMode::LighterColor => &dummy_blend, // handled in blend_pixels // -------------------------------------- BlendMode::Overlay => &overlay, BlendMode::SoftLight => &soft_light, @@ -115,15 +158,20 @@ fn map_blend_mode(blend_mode: BlendMode) -> &'static BlendFunction { BlendMode::Subtract => &subtract, BlendMode::Divide => ÷, // -------------------------------------- - BlendMode::Hue => &hue, - BlendMode::Saturation => &saturation, - BlendMode::Color => &color, - BlendMode::Luminosity => &luminosity, + BlendMode::Hue => &dummy_blend, // handled in blend_pixels + BlendMode::Saturation => &dummy_blend, // handled in blend_pixels + BlendMode::Color => &dummy_blend, // handled in blend_pixels + BlendMode::Luminosity => &dummy_blend, // handled in blend_pixels } } -fn pass_through(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +/// fake function for modes handled directly in `blend_pixels`. +fn dummy_blend(_color_b: f32, _color_s: f32) -> f32 { + 0.0 +} + +fn pass_through(_color_b: f32, color_s: f32) -> f32 { + color_s } /// https://www.w3.org/TR/compositing-1/#blendingnormal @@ -199,11 +247,7 @@ fn color_burn(color_b: f32, color_s: f32) -> f32 { /// `B(Cb, Cs) = max(0, Cb + Cs - 1)` #[inline(always)] fn linear_burn(color_b: f32, color_s: f32) -> f32 { - (color_b - color_s - 1.).max(0.) -} - -fn darker_color(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() + (color_b + color_s - 1.).max(0.) } // Lighten modes @@ -266,10 +310,6 @@ fn linear_dodge(color_b: f32, color_s: f32) -> f32 { (color_b + color_s).min(1.) } -fn lighter_color(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() -} - // Contrast modes /// https://www.w3.org/TR/compositing-1/#blendingoverlay @@ -340,22 +380,123 @@ fn hard_light(color_b: f32, color_s: f32) -> f32 { } } -fn vivid_light(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +/// Applies the Vivid Light blending mode between two color components. +/// +/// Vivid Light is a combination of `Color Burn` and `Color Dodge`: +/// * If the source color (`Cs`) is less than or equal to 0.5, `Color Burn` is applied. +/// * If the source color (`Cs`) is greater than 0.5, `Color Dodge` is applied. +/// +/// # Formula: +/// ```text +/// if Cs <= 0.5: +/// B(Cb, Cs) = ColorBurn(Cb, 2 * Cs) +/// else: +/// B(Cb, Cs) = ColorDodge(Cb, 2 * (Cs - 0.5)) +/// ``` +/// +/// # Arguments: +/// * `color_b` - The backdrop color component (0.0 to 1.0). +/// * `color_s` - The source color component (0.0 to 1.0). +/// +/// # Returns: +/// The blended color component (0.0 to 1.0). +#[inline(always)] +fn vivid_light(color_b: f32, color_s: f32) -> f32 { + if color_s <= 0.5 { + color_burn(color_b, 2.0 * color_s) + } else { + color_dodge(color_b, 2.0 * (color_s - 0.5)) + } } -fn linear_light(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +/// Applies the Linear Light blending mode between two color components. +/// +/// Linear Light is a combination of `Linear Burn` and `Linear Dodge`: +/// * If the source color (`Cs`) is less than or equal to 0.5, `Linear Burn` is applied. +/// * If the source color (`Cs`) is greater than 0.5, `Linear Dodge` is applied. +/// +/// # Formula: +/// ```text +/// if Cs <= 0.5: +/// B(Cb, Cs) = LinearBurn(Cb, 2 * Cs) +/// else: +/// B(Cb, Cs) = LinearDodge(Cb, 2 * (Cs - 0.5)) +/// ``` +/// +/// # Arguments: +/// * `color_b` - The backdrop color component (0.0 to 1.0). +/// * `color_s` - The source color component (0.0 to 1.0). +/// +/// # Returns: +/// The blended color component (0.0 to 1.0). +#[inline(always)] +fn linear_light(color_b: f32, color_s: f32) -> f32 { + if color_s <= 0.5 { + linear_burn(color_b, 2.0 * color_s) + } else { + linear_dodge(color_b, 2.0 * (color_s - 0.5)) + } } +/// Applies the Pin Light blending mode between two color components. +/// +/// Pin Light is a combination of `Darken` and `Lighten`: +/// * If the source color (`Cs`) is less than or equal to 0.5, `Darken` is applied. +/// * If the source color (`Cs`) is greater than 0.5, `Lighten` is applied. +/// +/// # Formula: +/// ```text +/// if Cs <= 0.5: +/// B(Cb, Cs) = Darken(Cb, 2 * Cs) +/// else: +/// B(Cb, Cs) = Lighten(Cb, 2 * (Cs - 0.5)) +/// ``` +/// +/// # Arguments: +/// * `color_b` - The backdrop color component (0.0 to 1.0). +/// * `color_s` - The source color component (0.0 to 1.0). +/// +/// # Returns: +/// The blended color component (0.0 to 1.0). #[inline(always)] -fn pin_light(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +fn pin_light(color_b: f32, color_s: f32) -> f32 { + if color_s <= 0.5 { + darken(color_b, 2.0 * color_s) + } else { + lighten(color_b, 2.0 * (color_s - 0.5)) + } } +/// Applies the Hard Mix blending mode between two color components. +/// +/// Hard Mix is a threshold-based mode that uses `Vivid Light` to calculate the blend result. +/// The result is binary, either fully black or fully white: +/// * If the result of `Vivid Light` is greater than or equal to 0.5, the result is 1.0 (white). +/// * Otherwise, the result is 0.0 (black). +/// +/// # Formula: +/// ```text +/// result = VividLight(Cb, Cs) +/// if result >= 0.5: +/// B(Cb, Cs) = 1.0 +/// else: +/// B(Cb, Cs) = 0.0 +/// ``` +/// +/// # Arguments: +/// * `color_b` - The backdrop color component (0.0 to 1.0). +/// * `color_s` - The source color component (0.0 to 1.0). +/// +/// # Returns: +/// The blended color component (0.0 or 1.0). #[inline(always)] -fn hard_mix(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +fn hard_mix(color_b: f32, color_s: f32) -> f32 { + let result = vivid_light(color_b, color_s); + if result >= 0.5 { + 1.0 + } else { + 0.0 + } } // Inversion modes @@ -407,20 +548,192 @@ fn divide(color_b: f32, color_s: f32) -> f32 { } } -fn hue(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +// Color component modes + +/// Calculates the luminance of an RGB color using the Rec. 709 formula. +/// +/// The luminance is a weighted sum of the red, green, and blue components to represent the brightness of a color. +/// The formula assigns higher importance to the green component, as human vision is more sensitive to green light. +/// +/// # Formula: +/// `L = 0.2126 * R + 0.7152 * G + 0.0722 * B` +/// +/// # Arguments: +/// * `r` - The red component of the color (0.0 to 1.0). +/// * `g` - The green component of the color (0.0 to 1.0). +/// * `b` - The blue component of the color (0.0 to 1.0). +/// +/// # Returns: +/// The luminance of the RGB color as a floating-point value between 0.0 and 1.0. +fn luminance(r: f32, g: f32, b: f32) -> f32 { + 0.2126 * r + 0.7152 * g + 0.0722 * b } -fn saturation(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +/// Converts an RGB color to its HSL (Hue, Saturation, Lightness) representation. +/// +/// HSL separates color into three components: +/// * `Hue`: The type of color (0.0 to 1.0, where 0.0 is red, 0.33 is green, 0.67 is blue). +/// * `Saturation`: The intensity or purity of the color (0.0 is grayscale, 1.0 is fully saturated). +/// * `Lightness`: The brightness of the color (0.0 is black, 1.0 is white). +/// +/// # Arguments: +/// * `r` - The red component of the color (0.0 to 1.0). +/// * `g` - The green component of the color (0.0 to 1.0). +/// * `b` - The blue component of the color (0.0 to 1.0). +/// +/// # Returns: +/// A tuple containing: +/// * `h`: The hue (0.0 to 1.0). +/// * `s`: The saturation (0.0 to 1.0). +/// * `l`: The lightness (0.0 to 1.0). +fn rgb_to_hsl(r: f32, g: f32, b: f32) -> (f32, f32, f32) { + let max = r.max(g.max(b)); + let min = r.min(g.min(b)); + let delta = max - min; + + let l = (max + min) / 2.0; + + let s = if delta.abs() < EPSILON { + 0.0 + } else { + delta / (1.0 - (2.0 * l - 1.0).abs()) + }; + + let h = if delta.abs() < EPSILON { + 0.0 + } else if (max - r).abs() < EPSILON { + ((g - b) / delta) % 6.0 + } else if (max - g).abs() < EPSILON { + (b - r) / delta + 2.0 + } else { + (r - g) / delta + 4.0 + } / 6.0; + + let h = if h < 0.0 { h + 1.0 } else { h }; + + (h, s, l) } -fn color(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +/// Converts an HSL (Hue, Saturation, Lightness) color back to its RGB representation. +/// +/// This function converts an HSL color (often used for color adjustments) into its RGB equivalent, where each component +/// is a floating-point value between 0.0 and 1.0. +/// +/// # Arguments: +/// * `h` - The hue of the color (0.0 to 1.0, where 0.0 is red, 0.33 is green, 0.67 is blue). +/// * `s` - The saturation of the color (0.0 is grayscale, 1.0 is fully saturated). +/// * `l` - The lightness of the color (0.0 is black, 1.0 is white). +/// +/// # Returns: +/// A tuple containing: +/// * `r`: The red component of the color (0.0 to 1.0). +/// * `g`: The green component of the color (0.0 to 1.0). +/// * `b`: The blue component of the color (0.0 to 1.0). +fn hsl_to_rgb(h: f32, s: f32, l: f32) -> (f32, f32, f32) { + if s.abs() < EPSILON { + return (l, l, l); + } + + let q = if l < 0.5 { + l * (1.0 + s) + } else { + l + s - l * s + }; + let p = 2.0 * l - q; + + let hk = h; + let t = |offset: f32| -> f32 { + let mut tc = hk + offset; + if tc < 0.0 { + tc += 1.0; + } + if tc > 1.0 { + tc -= 1.0; + } + if tc < 1.0 / 6.0 { + p + (q - p) * 6.0 * tc + } else if tc < 0.5 { + q + } else if tc < 2.0 / 3.0 { + p + (q - p) * (2.0 / 3.0 - tc) * 6.0 + } else { + p + } + }; + + let r = t(1.0 / 3.0); + let g = t(0.0); + let b = t(-1.0 / 3.0); + + (r, g, b) } -fn luminosity(_color_b: f32, _color_s: f32) -> f32 { - unimplemented!() +/// Blends two RGB colors by preserving the hue of the source color and applying the saturation and lightness of the backdrop. +/// +/// This blend mode is typically used to change the color tint of the backdrop without affecting its overall intensity or brightness. +/// +/// # Arguments: +/// * `r_b`, `g_b`, `b_b` - The red, green, and blue components of the backdrop color. +/// * `r_s`, `g_s`, `b_s` - The red, green, and blue components of the source color. +/// +/// # Returns: +/// A tuple containing the blended RGB values (r, g, b), where the hue of the source is applied to the backdrop. +fn blend_hue(r_b: f32, g_b: f32, b_b: f32, r_s: f32, g_s: f32, b_s: f32) -> (f32, f32, f32) { + let (h_s, _, _) = rgb_to_hsl(r_s, g_s, b_s); + let (_, s_b, l_b) = rgb_to_hsl(r_b, g_b, b_b); + + hsl_to_rgb(h_s, s_b, l_b) +} + +/// Blends two RGB colors by preserving the saturation of the source color and applying the hue and lightness of the backdrop. +/// +/// This blend mode is used to adjust the intensity of the color of the backdrop without affecting its hue or brightness. +/// +/// # Arguments: +/// * `r_b`, `g_b`, `b_b` - The red, green, and blue components of the backdrop color. +/// * `r_s`, `g_s`, `b_s` - The red, green, and blue components of the source color. +/// +/// # Returns: +/// A tuple containing the blended RGB values (r, g, b), where the saturation of the source is applied to the backdrop. +fn blend_saturation(r_b: f32, g_b: f32, b_b: f32, r_s: f32, g_s: f32, b_s: f32) -> (f32, f32, f32) { + let (_, s_s, _) = rgb_to_hsl(r_s, g_s, b_s); + let (h_b, _, l_b) = rgb_to_hsl(r_b, g_b, b_b); + + hsl_to_rgb(h_b, s_s, l_b) +} + +/// Blends two RGB colors by preserving the hue and saturation of the source color and applying the lightness of the backdrop. +/// +/// This blend mode is used to apply the color of the source to the backdrop while maintaining the overall brightness of the backdrop. +/// +/// # Arguments: +/// * `r_b`, `g_b`, `b_b` - The red, green, and blue components of the backdrop color. +/// * `r_s`, `g_s`, `b_s` - The red, green, and blue components of the source color. +/// +/// # Returns: +/// A tuple containing the blended RGB values (r, g, b), where the hue and saturation of the source are applied to the backdrop. +fn blend_color(r_b: f32, g_b: f32, b_b: f32, r_s: f32, g_s: f32, b_s: f32) -> (f32, f32, f32) { + let (h_s, s_s, _) = rgb_to_hsl(r_s, g_s, b_s); + let (_, _, l_b) = rgb_to_hsl(r_b, g_b, b_b); + + hsl_to_rgb(h_s, s_s, l_b) +} + +/// Blends two RGB colors by preserving the luminosity (lightness) of the source color and applying the hue and saturation of the backdrop. +/// +/// This blend mode is used to adjust the brightness of the backdrop to match that of the source without affecting its color hue or saturation. +/// +/// # Arguments: +/// * `r_b`, `g_b`, `b_b` - The red, green, and blue components of the backdrop color. +/// * `r_s`, `g_s`, `b_s` - The red, green, and blue components of the source color. +/// +/// # Returns: +/// A tuple containing the blended RGB values (r, g, b), where the luminosity of the source is applied to the backdrop. +fn blend_luminosity(r_b: f32, g_b: f32, b_b: f32, r_s: f32, g_s: f32, b_s: f32) -> (f32, f32, f32) { + let (_, _, l_s) = rgb_to_hsl(r_s, g_s, b_s); + let (h_b, s_b, _) = rgb_to_hsl(r_b, g_b, b_b); + + hsl_to_rgb(h_b, s_b, l_s) } /// https://www.w3.org/TR/compositing-1/#generalformula From f39e22171958bccbfaa02b0c098a1e8649e08244 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Thu, 12 Sep 2024 23:28:55 -0500 Subject: [PATCH 09/16] Update TODO and blend_pixels docs --- src/blend.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/blend.rs b/src/blend.rs index a4dad38..c6eb26d 100644 --- a/src/blend.rs +++ b/src/blend.rs @@ -35,7 +35,7 @@ const EPSILON: f32 = 1e-6; /// /// *The backdrop is the content behind the element and is what the element is composited with. This means that the backdrop is the result of compositing all previous elements. /// -/// ** this optimized version is based on tdakkota's implementation but improves it by ~10%-25% and allows for the blending of methods that need access to all channels simultaneously. +/// ** this optimized version is based on tdakkota's implementation but improves it by ~10%-25% by removing division where possible and allows for the blending of methods that need access to all channels simultaneously. pub(crate) fn blend_pixels( top: Pixel, bottom: Pixel, @@ -126,7 +126,7 @@ type BlendFunction = dyn Fn(f32, f32) -> f32; /// Returns blend function for given BlendMode fn map_blend_mode(blend_mode: BlendMode) -> &'static BlendFunction { // Modes are sorted like in Photoshop UI - // TODO: make other modes + // TODO: Finish dissolve match blend_mode { BlendMode::PassThrough => &pass_through, // only for groups // -------------------------------------- From 83e8e6ce142645a47b6c973074663d630a9b899a Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Fri, 13 Sep 2024 15:16:38 -0500 Subject: [PATCH 10/16] Add ability to write color mode data in color_mode_data_section.rs. Add a simple test to check functionality and backwards compatibility. --- src/blend.rs | 1 - src/sections/color_mode_data_section.rs | 56 +++++++++++++++++++------ src/sections/image_data_section.rs | 2 +- 3 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/blend.rs b/src/blend.rs index c6eb26d..0cbbc94 100644 --- a/src/blend.rs +++ b/src/blend.rs @@ -120,7 +120,6 @@ pub(crate) fn blend_pixels( } } - type BlendFunction = dyn Fn(f32, f32) -> f32; /// Returns blend function for given BlendMode diff --git a/src/sections/color_mode_data_section.rs b/src/sections/color_mode_data_section.rs index 60b4dfc..1600439 100644 --- a/src/sections/color_mode_data_section.rs +++ b/src/sections/color_mode_data_section.rs @@ -1,17 +1,29 @@ use std::io::{Seek, Write}; use thiserror::Error; - use super::{PsdBuffer, PsdSerialize}; #[derive(Debug, PartialEq, Error)] -pub enum ColorModeDataSectionError {} +pub enum ColorModeDataSectionError { + #[error("Unsupported color mode data")] + UnsupportedColorMode, +} #[derive(Debug, PartialEq)] -pub struct ColorModeDataSection {} +pub struct ColorModeDataSection { + // NOTE: placeholder for actual color mode data + color_mode_data: Vec, +} impl ColorModeDataSection { - pub fn from_bytes(_bytes: &[u8]) -> Result { - Ok(Self {}) + pub fn from_bytes(bytes: &[u8]) -> Result { + // TODO: review this implementation + // NOTE: process bytes if there's color mode data like Indexed or Duotone modes... + let color_mode_data = bytes.to_vec(); + Ok(Self { color_mode_data }) + } + + pub fn new(color_mode_data: Vec) -> Self { + Self { color_mode_data } } } @@ -20,7 +32,7 @@ impl PsdSerialize for ColorModeDataSection { where T: Write + Seek, { - buffer.write_sized(|buf| buf.write([])); + buffer.write_sized(|buf| buf.write(&self.color_mode_data)); } } @@ -29,18 +41,38 @@ mod tests { use super::*; #[test] - fn write_read_round_trip() { - let initial = make_section(); - let mut bytes: Vec = vec![]; + fn write_read_round_trip_empty_vec() { + let initial = make_empty_section(); + let mut bytes: Vec = Vec::new(); + let bytes2: Vec = Vec::new(); let mut buffer = PsdBuffer::new(&mut bytes); initial.write(&mut buffer); - let result = ColorModeDataSection::from_bytes(&bytes).unwrap(); + let result = ColorModeDataSection::from_bytes(&bytes2).unwrap(); assert_eq!(initial, result); } - fn make_section() -> ColorModeDataSection { - ColorModeDataSection {} + #[test] + fn write_read_round_trip_non_empty_vec() { + let data: Vec = vec![1, 2, 3, 4, 5]; + let data_2: Vec = data.clone(); + let initial = make_non_empty_section(data); + let mut bytes: Vec = Vec::new(); + let mut buffer = PsdBuffer::new(&mut bytes); + + initial.write(&mut buffer); + + let result = ColorModeDataSection::from_bytes(&data_2).unwrap(); + assert_eq!(initial, result); + } + + fn make_non_empty_section(input_vec: Vec) -> ColorModeDataSection { + ColorModeDataSection::new(input_vec) + } + + fn make_empty_section() -> ColorModeDataSection { + let new_vec: Vec = Vec::new(); + ColorModeDataSection::new(new_vec) } } diff --git a/src/sections/image_data_section.rs b/src/sections/image_data_section.rs index 49a943a..41bc916 100644 --- a/src/sections/image_data_section.rs +++ b/src/sections/image_data_section.rs @@ -5,7 +5,7 @@ use thiserror::Error; use super::PsdSerialize; -/// Represents an malformed image data +/// Represents a malformed image data #[derive(Debug, PartialEq, Error)] pub enum ImageDataSectionError { #[error( From b89d07df7ac50bbb5c5f8ac09d27765e0fcb4249 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Sat, 9 Nov 2024 11:33:01 -0600 Subject: [PATCH 11/16] ADD in TODOs ADD helper functions ADD helper function comments for clarity --- src/psd_channel.rs | 6 +- src/sections/file_header_section.rs | 66 +++++++++++++++++-- src/sections/image_resources_section.rs | 61 +++++++++++++++++ .../image_resources_section/image_resource.rs | 2 + .../image_resource/slices.rs | 8 +-- 5 files changed, 129 insertions(+), 14 deletions(-) diff --git a/src/psd_channel.rs b/src/psd_channel.rs index 90e4ae8..90069e9 100644 --- a/src/psd_channel.rs +++ b/src/psd_channel.rs @@ -42,9 +42,9 @@ pub trait IntoRgba { let alpha = self.alpha(); // TODO: We're assuming that if we only see two channels it is a 16 bit grayscale - // PSD. Instead we should just check the Psd's color mode and depth to see if - // they are grayscale and sixteen. As we run into more cases we'll clean things like - // this up over time. + // PSD. Instead we should just check the Psd's color mode and depth to see if + // they are grayscale and sixteen. As we run into more cases we'll clean things like + // this up over time. // if green.is_some() && blue.is_none() && alpha.is_none() { // return self.generate_16_bit_grayscale_rgba(); // } diff --git a/src/sections/file_header_section.rs b/src/sections/file_header_section.rs index 2b57afe..28c7d39 100644 --- a/src/sections/file_header_section.rs +++ b/src/sections/file_header_section.rs @@ -77,8 +77,8 @@ impl FileHeaderSection { /// Create a FileSectionHeader from the first 26 bytes of a PSD /// /// TODO: Accept a ColorModeSection along with the bytes so that we can add - /// any ColorModeSection data to the ColorMode if necessary. Rename this method - /// to "new" in the process. + /// any ColorModeSection data to the ColorMode if necessary. Rename this method + /// to "new" in the process. pub fn from_bytes(bytes: &[u8]) -> Result { let mut cursor = PsdCursor::new(bytes); @@ -154,11 +154,11 @@ impl PsdSerialize for FileHeaderSection { PsdVersion::One => buffer.write(EXPECTED_VERSION), }; buffer.write(EXPECTED_RESERVED); - buffer.write((self.channel_count.count() as u16).to_be_bytes()); - buffer.write(self.height.0.to_be_bytes()); - buffer.write(self.width.0.to_be_bytes()); - buffer.write((self.depth as u16).to_be_bytes()); - buffer.write((self.color_mode as u16).to_be_bytes()) + buffer.write(self.channel_count.to_be_bytes()); + buffer.write(self.height.to_be_bytes()); + buffer.write(self.width.to_be_bytes()); + buffer.write(self.depth.to_be_bytes()); + buffer.write(self.color_mode.to_be_bytes()) } } @@ -195,6 +195,11 @@ impl ChannelCount { pub fn count(&self) -> u8 { self.0 } + + /// Convert the channel count to big endian bytes + pub fn to_be_bytes(&self) -> [u8; 2] { + (self.count() as u16).to_be_bytes() + } } /// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) @@ -215,6 +220,15 @@ impl PsdHeight { Some(PsdHeight(height)) } + /// Return the height + pub fn get(&self) -> u32 { + self.0 + } + + /// Convert the height to big endian bytes + pub fn to_be_bytes(&self) -> [u8; 4] { + self.get().to_be_bytes() + } } /// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) @@ -235,6 +249,14 @@ impl PsdWidth { Some(PsdWidth(width)) } + /// Return the width + pub fn get(&self) -> u32 { + self.0 + } + /// Convert the width to big endian bytes + pub fn to_be_bytes(&self) -> [u8; 4] { + self.get().to_be_bytes() + } } /// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) @@ -262,6 +284,19 @@ impl PsdDepth { _ => None, } } + /// Return the depth + pub fn get(&self) -> u8 { + match self { + PsdDepth::One => 1, + PsdDepth::Eight => 8, + PsdDepth::Sixteen => 16, + PsdDepth::ThirtyTwo => 32, + } + } + /// Convert the depth to big endian bytes + pub fn to_be_bytes(&self) -> [u8; 2] { + (self.get() as u16).to_be_bytes() + } } /// # [Adobe Docs](https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/) @@ -301,6 +336,23 @@ impl ColorMode { _ => None, } } + /// Return the color mode + pub fn get(&self) -> u8 { + match self { + ColorMode::Bitmap => 0, + ColorMode::Grayscale => 1, + ColorMode::Indexed => 2, + ColorMode::Rgb => 3, + ColorMode::Cmyk => 4, + ColorMode::Multichannel => 7, + ColorMode::Duotone => 8, + ColorMode::Lab => 9, + } + } + /// Convert the color mode to big endian bytes + pub fn to_be_bytes(&self) -> [u8; 2] { + (self.get() as u16).to_be_bytes() + } } #[cfg(test)] diff --git a/src/sections/image_resources_section.rs b/src/sections/image_resources_section.rs index ba5f7b6..00158f8 100644 --- a/src/sections/image_resources_section.rs +++ b/src/sections/image_resources_section.rs @@ -13,6 +13,7 @@ pub use crate::sections::image_resources_section::image_resource::{ ImageResource, SlicesImageResource, }; +/// The expected image resource block signature '8BIM' const EXPECTED_RESOURCE_BLOCK_SIGNATURE: [u8; 4] = [56, 66, 73, 77]; const RESOURCE_SLICES_INFO: i16 = 1050; @@ -126,3 +127,63 @@ impl ImageResourcesSection { }) } } + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::HashMap; + use crate::{DescriptorField, ImageResource}; + use crate::sections::image_resources_section::image_resource::descriptor_structure::DescriptorStructure; + use crate::sections::image_resources_section::image_resource::slices::SlicesImageResourceV7_8; + use crate::sections::image_resources_section::image_resource::SlicesImageResource; + + #[test] + fn write_read_round_trip_image_resources() { + let initial = make_image_resources_section(); + let mut bytes: Vec = Vec::new(); + let mut buffer = PsdBuffer::new(&mut bytes); + + // Write the initial image resources section + initial.write(&mut buffer); + + // Read the bytes back into a new ImageResourcesSection + let _result = ImageResourcesSection::from_bytes(&bytes).unwrap(); + + // Ensure that the original and deserialized sections are equal + // the following fails due to 'PartialEq' not being implemented all the way down + // assert_eq!(initial, _result); + } + + fn make_image_resources_section() -> ImageResourcesSection { + // Create a slice resource with descriptors using the V7_8 variant + let slice = SlicesImageResource::V7_8(SlicesImageResourceV7_8 { + descriptor: create_example_descriptor(), + }); + + ImageResourcesSection { + resources: vec![ImageResource::Slices(slice)], + } + } + + fn create_example_descriptor() -> DescriptorStructure { + // Create a descriptor structure with some fields + let mut fields = HashMap::new(); + fields.insert( + String::from("bounds"), + DescriptorField::Descriptor(DescriptorStructure { + name: String::from("example_bounds"), + class_id: vec![0, 0, 0, 0], // Example class ID + fields: HashMap::from([ + (String::from("Rght"), DescriptorField::Integer(100)), + (String::from("Btom"), DescriptorField::Integer(200)), + ]), + }), + ); + + DescriptorStructure { + name: String::from("example_descriptor"), + fields, + class_id: vec![1, 2, 3, 4], // Example class ID + } + } +} \ No newline at end of file diff --git a/src/sections/image_resources_section/image_resource.rs b/src/sections/image_resources_section/image_resource.rs index 7813510..53f95f4 100644 --- a/src/sections/image_resources_section/image_resource.rs +++ b/src/sections/image_resources_section/image_resource.rs @@ -25,6 +25,8 @@ impl PsdSerialize for ImageResource { let id: ImageResourceId = self.into(); buffer.write(id.into_bytes()); + // TODO: do we need to write this or is it only when we are at an odd buffer index? + // reference: https://github.com/yu-icchi/go-psd/blob/5976db4c66f0/encode.go#L110 buffer.write_pascal_string(""); match self { diff --git a/src/sections/image_resources_section/image_resource/slices.rs b/src/sections/image_resources_section/image_resource/slices.rs index a0326da..8a76dbd 100644 --- a/src/sections/image_resources_section/image_resource/slices.rs +++ b/src/sections/image_resources_section/image_resource/slices.rs @@ -243,7 +243,7 @@ impl PsdSerialize for SlicesResourceBlock { buffer.write(pad); // Slice Id buffer.write(pad); // Group Id buffer.write(pad); // Origin - // Skip Associated Layer Id + // Skip Associated Layer Id buffer.write_unicode_string(""); // Name buffer.write(pad); // Type @@ -280,7 +280,7 @@ impl PsdSerialize for SlicesResourceBlock { #[derive(Debug)] pub struct SlicesImageResourceV7_8 { - descriptor: DescriptorStructure, + pub(crate) descriptor: DescriptorStructure, } impl PsdDeserialize for SlicesImageResourceV7_8 { @@ -302,8 +302,8 @@ impl PsdDeserialize for SlicesImageResourceV7_8 { let descriptor_version = cursor.read_i32(); if descriptor_version != 16 { unimplemented!( - "Only the version 16 (descriptors) resource format for slices is currently supported" - ); + "Only the version 16 (descriptors) resource format for slices is currently supported" + ); } let descriptor = DescriptorStructure::read_descriptor_structure(&mut cursor)?; From 3ac61022b5f24725ccb8479a0fc95b3bab7b07be Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Sun, 10 Nov 2024 11:14:48 -0600 Subject: [PATCH 12/16] BUG FIX for wasm-pack in build step --- examples/drag-drop-browser/Cargo.toml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/drag-drop-browser/Cargo.toml b/examples/drag-drop-browser/Cargo.toml index e9f040c..a377d0d 100644 --- a/examples/drag-drop-browser/Cargo.toml +++ b/examples/drag-drop-browser/Cargo.toml @@ -40,4 +40,7 @@ features = [ [patch.crates-io] syn = "2" proc-macro2 = "1" -quote = "1" \ No newline at end of file +quote = "1" + +[package.metadata.wasm-pack] +wasm-opt = false From 31a46e26fb723825ded6a10bb142c594df621a9f Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Sun, 10 Nov 2024 15:54:07 -0600 Subject: [PATCH 13/16] BUG FIX for wasm-pack error in build step --- examples/drag-drop-browser/Cargo.toml | 3 --- examples/drag-drop-browser/build-release.sh | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/drag-drop-browser/Cargo.toml b/examples/drag-drop-browser/Cargo.toml index a377d0d..809a496 100644 --- a/examples/drag-drop-browser/Cargo.toml +++ b/examples/drag-drop-browser/Cargo.toml @@ -41,6 +41,3 @@ features = [ syn = "2" proc-macro2 = "1" quote = "1" - -[package.metadata.wasm-pack] -wasm-opt = false diff --git a/examples/drag-drop-browser/build-release.sh b/examples/drag-drop-browser/build-release.sh index 1acdf54..f10720d 100755 --- a/examples/drag-drop-browser/build-release.sh +++ b/examples/drag-drop-browser/build-release.sh @@ -7,5 +7,5 @@ cd "$(dirname "$0")" mkdir -p public CSS_FILE="$(pwd)/public/app.css" -OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --release --target web --out-dir ./public +OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --release --target web --out-dir ./public --no-wasm-opt cp index.html public/ From 940fbd4fab06eb601b6e92e6a5f7bd84163b9188 Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Mon, 11 Nov 2024 07:26:15 -0600 Subject: [PATCH 14/16] BUG FIX for wasm-pack error in build step --- examples/drag-drop-browser/build-release.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/drag-drop-browser/build-release.sh b/examples/drag-drop-browser/build-release.sh index f10720d..14b78fc 100755 --- a/examples/drag-drop-browser/build-release.sh +++ b/examples/drag-drop-browser/build-release.sh @@ -7,5 +7,5 @@ cd "$(dirname "$0")" mkdir -p public CSS_FILE="$(pwd)/public/app.css" -OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --release --target web --out-dir ./public --no-wasm-opt +OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --release --target web --out-dir ./public --no-opt cp index.html public/ From 80c5ed1950bfa2ac89894b5f5bc17af837981eff Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Mon, 11 Nov 2024 07:35:56 -0600 Subject: [PATCH 15/16] BUG FIX update wasm-pack in circleci build step --- .circleci/config.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 718f0cc..009d03f 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -25,10 +25,7 @@ jobs: - run: name: Install wasm-pack command: > - curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.9.1/wasm-pack-v0.9.1-x86_64-unknown-linux-musl.tar.gz - | tar --strip-components=1 --wildcards -xzf - "*/wasm-pack" - && chmod +x wasm-pack - && mv wasm-pack $CARGO_HOME/bin/ + curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh # Show versions - run: From 3ae670051ddd5d0dedbb98713a85b4ea18758b6e Mon Sep 17 00:00:00 2001 From: Jacob Dallas <10dallasj@gmail.com> Date: Mon, 11 Nov 2024 07:44:35 -0600 Subject: [PATCH 16/16] BUG FIX update wasm-pack in circleci build step --- .circleci/config.yml | 9 ++------- examples/drag-drop-browser/build-dev.sh | 2 +- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 009d03f..0e0a335 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -24,8 +24,7 @@ jobs: # Install wasm tools - run: name: Install wasm-pack - command: > - curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + command: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh # Show versions - run: @@ -72,11 +71,7 @@ jobs: # Install wasm tools - run: name: Install wasm-pack - command: > - curl -L https://github.com/rustwasm/wasm-pack/releases/download/v0.9.1/wasm-pack-v0.9.1-x86_64-unknown-linux-musl.tar.gz - | tar --strip-components=1 --wildcards -xzf - "*/wasm-pack" - && chmod +x wasm-pack - && mv wasm-pack $CARGO_HOME/bin/ + command: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh # Install mdbook - run: diff --git a/examples/drag-drop-browser/build-dev.sh b/examples/drag-drop-browser/build-dev.sh index 703369f..9328593 100755 --- a/examples/drag-drop-browser/build-dev.sh +++ b/examples/drag-drop-browser/build-dev.sh @@ -7,5 +7,5 @@ cd "$(dirname "$0")" mkdir -p public CSS_FILE="$(pwd)/public/app.css" -OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --dev --target web --out-dir ./public +OUTPUT_CSS=$CSS_FILE wasm-pack build --no-typescript --dev --target web --out-dir ./public --no-opt cp index.html public/