diff --git a/.github/workflows/rust-publish.yml b/.github/workflows/rust-publish.yml
new file mode 100644
index 0000000..861f9b7
--- /dev/null
+++ b/.github/workflows/rust-publish.yml
@@ -0,0 +1,23 @@
+name: Cargo Publish
+on:
+ push:
+ tags:
+ - "rust-*"
+ workflow_dispatch:
+jobs:
+ publish:
+ name: Publish
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ - name: Change Working Directory
+ run: cd icns-rs
+ - name: Publish
+ run: cargo publish --token ${CRATES_TOKEN}
+ env:
+ CRATES_IO_TOKEN: ${{ secrets.CRATES_TOKEN }}
diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml
new file mode 100644
index 0000000..ea5fc55
--- /dev/null
+++ b/.github/workflows/rust-test.yml
@@ -0,0 +1,63 @@
+name: Rust CI
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+on:
+ pull_request:
+ paths:
+ - icns-rs/**
+ push: null
+ workflow_dispatch: null
+jobs:
+ tests:
+ name: Tests
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ - name: Change Working Directory
+ run: cd icns-rs
+ - name: Run tests
+ uses: actions-rs/cargo@v1]
+ with:
+ command: test
+ rustfmt:
+ name: Rustfmt
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ components: rustfmt
+ - name: Change Working Directory
+ run: cd icns-rs
+ - name: Run rustfmt
+ uses: actions-rs/cargo@v1
+ with:
+ command: fmt
+ args: "-- --check"
+ clippy:
+ name: Clippy
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Rust
+ uses: actions-rs/toolchain@v1
+ with:
+ toolchain: stable
+ override: true
+ components: clippy
+ - name: Change Working Directory
+ run: cd icns-rs
+ - name: Run clippy
+ uses: actions-rs/cargo@v1
+ with:
+ command: clippy
+ args: "-- -D warnings"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..144f824
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,157 @@
+# GNU LESSER GENERAL PUBLIC LICENSE
+
+Version 3, 29 June 2007
+
+Copyright (C) 2007 Free Software Foundation, Inc.
+
+
+Everyone is permitted to copy and distribute verbatim copies of this
+license document, but changing it is not allowed.
+
+This version of the GNU Lesser General Public License incorporates the
+terms and conditions of version 3 of the GNU General Public License,
+supplemented by the additional permissions listed below.
+
+## 0. Additional Definitions.
+
+As used herein, "this License" refers to version 3 of the GNU Lesser
+General Public License, and the "GNU GPL" refers to version 3 of the
+GNU General Public License.
+
+"The Library" refers to a covered work governed by this License, other
+than an Application or a Combined Work as defined below.
+
+An "Application" is any work that makes use of an interface provided
+by the Library, but which is not otherwise based on the Library.
+Defining a subclass of a class defined by the Library is deemed a mode
+of using an interface provided by the Library.
+
+A "Combined Work" is a work produced by combining or linking an
+Application with the Library. The particular version of the Library
+with which the Combined Work was made is also called the "Linked
+Version".
+
+The "Minimal Corresponding Source" for a Combined Work means the
+Corresponding Source for the Combined Work, excluding any source code
+for portions of the Combined Work that, considered in isolation, are
+based on the Application, and not on the Linked Version.
+
+The "Corresponding Application Code" for a Combined Work means the
+object code and/or source code for the Application, including any data
+and utility programs needed for reproducing the Combined Work from the
+Application, but excluding the System Libraries of the Combined Work.
+
+## 1. Exception to Section 3 of the GNU GPL.
+
+You may convey a covered work under sections 3 and 4 of this License
+without being bound by section 3 of the GNU GPL.
+
+## 2. Conveying Modified Versions.
+
+If you modify a copy of the Library, and, in your modifications, a
+facility refers to a function or data to be supplied by an Application
+that uses the facility (other than as an argument passed when the
+facility is invoked), then you may convey a copy of the modified
+version:
+
+- a) under this License, provided that you make a good faith effort
+ to ensure that, in the event an Application does not supply the
+ function or data, the facility still operates, and performs
+ whatever part of its purpose remains meaningful, or
+- b) under the GNU GPL, with none of the additional permissions of
+ this License applicable to that copy.
+
+## 3. Object Code Incorporating Material from Library Header Files.
+
+The object code form of an Application may incorporate material from a
+header file that is part of the Library. You may convey such object
+code under terms of your choice, provided that, if the incorporated
+material is not limited to numerical parameters, data structure
+layouts and accessors, or small macros, inline functions and templates
+(ten or fewer lines in length), you do both of the following:
+
+- a) Give prominent notice with each copy of the object code that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+- b) Accompany the object code with a copy of the GNU GPL and this
+ license document.
+
+## 4. Combined Works.
+
+You may convey a Combined Work under terms of your choice that, taken
+together, effectively do not restrict modification of the portions of
+the Library contained in the Combined Work and reverse engineering for
+debugging such modifications, if you also do each of the following:
+
+- a) Give prominent notice with each copy of the Combined Work that
+ the Library is used in it and that the Library and its use are
+ covered by this License.
+- b) Accompany the Combined Work with a copy of the GNU GPL and this
+ license document.
+- c) For a Combined Work that displays copyright notices during
+ execution, include the copyright notice for the Library among
+ these notices, as well as a reference directing the user to the
+ copies of the GNU GPL and this license document.
+- d) Do one of the following:
+ - 0) Convey the Minimal Corresponding Source under the terms of
+ this License, and the Corresponding Application Code in a form
+ suitable for, and under terms that permit, the user to
+ recombine or relink the Application with a modified version of
+ the Linked Version to produce a modified Combined Work, in the
+ manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.
+ - 1) Use a suitable shared library mechanism for linking with
+ the Library. A suitable mechanism is one that (a) uses at run
+ time a copy of the Library already present on the user's
+ computer system, and (b) will operate properly with a modified
+ version of the Library that is interface-compatible with the
+ Linked Version.
+- e) Provide Installation Information, but only if you would
+ otherwise be required to provide such information under section 6
+ of the GNU GPL, and only to the extent that such information is
+ necessary to install and execute a modified version of the
+ Combined Work produced by recombining or relinking the Application
+ with a modified version of the Linked Version. (If you use option
+ 4d0, the Installation Information must accompany the Minimal
+ Corresponding Source and Corresponding Application Code. If you
+ use option 4d1, you must provide the Installation Information in
+ the manner specified by section 6 of the GNU GPL for conveying
+ Corresponding Source.)
+
+## 5. Combined Libraries.
+
+You may place library facilities that are a work based on the Library
+side by side in a single library together with other library
+facilities that are not Applications and are not covered by this
+License, and convey such a combined library under terms of your
+choice, if you do both of the following:
+
+- a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities, conveyed under the terms of this License.
+- b) Give prominent notice with the combined library that part of it
+ is a work based on the Library, and explaining where to find the
+ accompanying uncombined form of the same work.
+
+## 6. Revised Versions of the GNU Lesser General Public License.
+
+The Free Software Foundation may publish revised and/or new versions
+of the GNU Lesser General Public License from time to time. Such new
+versions will be similar in spirit to the present version, but may
+differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+as you received it specifies that a certain numbered version of the
+GNU Lesser General Public License "or any later version" applies to
+it, you have the option of following the terms and conditions either
+of that published version or of any later version published by the
+Free Software Foundation. If the Library as you received it does not
+specify a version number of the GNU Lesser General Public License, you
+may choose any version of the GNU Lesser General Public License ever
+published by the Free Software Foundation.
+
+If the Library as you received it specifies that a proxy can decide
+whether future versions of the GNU Lesser General Public License shall
+apply, that proxy's public statement of acceptance of any version is
+permanent authorization for you to choose that version for the
+Library.
\ No newline at end of file
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..213606e
--- /dev/null
+++ b/README.md
@@ -0,0 +1,12 @@
+# ICNS
+
+This repository contains a multiple libraries for reading and writing ICNS files.
+ - Rust: [icns-rs](icns-rs/README.md)
+
+## Contributing
+
+Contributions are welcome! Feel free to open an issue or submit a pull request.
+
+## License
+
+This project is licensed under the LGPLv3 license. See the [LICENSE](/LICENSE) file for more details.
\ No newline at end of file
diff --git a/icns-rs/.gitignore b/icns-rs/.gitignore
new file mode 100644
index 0000000..a73dbda
--- /dev/null
+++ b/icns-rs/.gitignore
@@ -0,0 +1,4 @@
+# Cargo
+/target/
+Cargo.lock
+# ^^^^^^^^ Lock files aren't allowed in libraries
\ No newline at end of file
diff --git a/icns-rs/Cargo.toml b/icns-rs/Cargo.toml
new file mode 100644
index 0000000..073d294
--- /dev/null
+++ b/icns-rs/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "icns-rs"
+version = "0.1.0"
+edition = "2021"
+description = "A library for reading and writing Apple Icon Image (.icns) files."
+license = "LGPL-3.0-or-later"
+homepage = "https://github.com/JoshuaBrest/icns/tree/master/icns-rs"
+documentation = "https://docs.rs/icns-rs"
+repository = "https://github.com/JoshuaBrest/icns"
+catagories = ["image"]
+keywords = ["icns", "image", "apple", "icon"]
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+image = "0.24.6"
+
+[[example]]
+name = "encode"
+path = "examples/encode.rs"
diff --git a/icns-rs/README.md b/icns-rs/README.md
new file mode 100644
index 0000000..99916c5
--- /dev/null
+++ b/icns-rs/README.md
@@ -0,0 +1,74 @@
+# ICNS-RS
+
+ICNS is a file format used by Apple to store icons for macOS applications. This crate provides a simple API for reading and (and soon)writing ICNS files.
+
+## Roadmap
+
+- [x] Write ICNS files
+- [ ] Read ICNS files
+
+## Usage
+
+Here's a simple example of how to read an ICNS file:
+
+> You can find this example in `examples/encode.rs` or run it with:
+> ```sh
+> cargo run --example encode
+> ```
+
+```rust
+use std::fs::File;
+use std::io::prelude::*;
+use image::open;
+use icns_rs::{IcnsEncoder, IconFormats};
+
+fn main() -> std::io::Result<()> {
+ // Open the image
+ let image = match open("example.png") {
+ Ok(image) => image,
+ Err(e) => {
+ println!("Error opening file: {}", e);
+ return Ok(());
+ }
+ };
+
+ // Create the encoder
+ let mut encoder = IcnsEncoder::new();
+
+ encoder.data(image);
+ encoder.formats(IconFormats::recommended());
+
+ // Encode the image
+ let data = match encoder.build() {
+ Ok(data) => data,
+ Err(e) => {
+ println!("Error encoding image: {}", e);
+ return Ok(());
+ }
+ };
+
+ // Write data to file
+ let mut file = File::create("example.icns")?;
+ file.write_all(&data)?;
+
+ Ok(())
+}
+```
+
+## License
+
+This project is licensed under the GPLv3 license. See the [LICENSE](/LICENSE) file for more details.
+
+## Contributing
+
+Contributions are welcome! Feel free to open an issue or submit a pull request.
+
+## Acknowledgements
+
+This project is heavily inspired by:
+ - The Python package: [icnsutil](https://github.com/relikd/icnsutil/)
+ - The JavaScript package: [@fiahfy/icns](https://github.com/fiahfy/icns/)
+ - The JavaScript PackBits implementation: [@fiahfy/packbits](https://github.com/fiahfy/packbits/)
+ - The Wikipedia page: [Wikipedia: Apple Icon Image Format](https://en.wikipedia.org/wiki/Apple_Icon_Image_format#Icon_types)
+
+When I started building this, I didn't know there already was a ICNS lib for rust, but, after looking at it, it was not up to my standards because of the lack of ARGB, RGB, and, Mask support. I wanted to create a modern package that was easy to use and had a simple API. I also wanted to make sure that it was well documented and had a good test suite. I hope you enjoy using this package as much as I enjoyed making it!.
\ No newline at end of file
diff --git a/icns-rs/example.icns b/icns-rs/example.icns
new file mode 100644
index 0000000..232f2c6
Binary files /dev/null and b/icns-rs/example.icns differ
diff --git a/icns-rs/example.png b/icns-rs/example.png
new file mode 100644
index 0000000..205d67f
Binary files /dev/null and b/icns-rs/example.png differ
diff --git a/icns-rs/examples/encode.rs b/icns-rs/examples/encode.rs
new file mode 100644
index 0000000..c30e61b
--- /dev/null
+++ b/icns-rs/examples/encode.rs
@@ -0,0 +1,36 @@
+use icns_rs::{IcnsEncoder, IconFormats};
+use image::open;
+use std::fs::File;
+use std::io::prelude::*;
+
+fn main() -> std::io::Result<()> {
+ // Open the image
+ let image = match open("example.png") {
+ Ok(image) => image,
+ Err(e) => {
+ println!("Error opening file: {}", e);
+ return Ok(());
+ }
+ };
+
+ // Create the encoder
+ let mut encoder = IcnsEncoder::new();
+
+ encoder.data(image);
+ encoder.formats(IconFormats::recommended());
+
+ // Encode the image
+ let data = match encoder.build() {
+ Ok(data) => data,
+ Err(e) => {
+ println!("Error encoding image: {}", e);
+ return Ok(());
+ }
+ };
+
+ // Write data to file
+ let mut file = File::create("example.icns")?;
+ file.write_all(&data)?;
+
+ Ok(())
+}
diff --git a/icns-rs/rustfmt.toml b/icns-rs/rustfmt.toml
new file mode 100644
index 0000000..eaac36c
--- /dev/null
+++ b/icns-rs/rustfmt.toml
@@ -0,0 +1,3 @@
+error_on_line_overflow = true
+error_on_unformatted = true
+format_code_in_doc_comments=true
\ No newline at end of file
diff --git a/icns-rs/src/icns_format.rs b/icns-rs/src/icns_format.rs
new file mode 100644
index 0000000..7cd01ec
--- /dev/null
+++ b/icns-rs/src/icns_format.rs
@@ -0,0 +1,129 @@
+const MAGIC: [u8; 4] = [0x69, 0x63, 0x6e, 0x73]; // "icns"
+
+/// ## IcnsDataEntry
+/// This file contains both the OSType and the data.
+/// The OSType is a 4-byte identifier that tells the OS what the data is.
+/// The data is the actual image / whatever data is being stored.
+/// Data can be images, masks, metadata, etc.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct IcnsDataEntry {
+ pub os_type: [u8; 4],
+ pub data: Box<[u8]>,
+}
+
+impl IcnsDataEntry {
+ /// ## New
+ /// Creates a new IcnsDataEntry.
+ pub fn new(os_type: [u8; 4], data: Box<[u8]>) -> Self {
+ Self { os_type, data }
+ }
+
+ /// ## Length
+ /// This function gets the length of the data when compiled.
+ /// The length is 4 bytes (OSType) + 4 bytes (length) + length (data)
+ pub fn len(&self) -> u32 {
+ (8 + self.data.len()) as u32
+ }
+
+ /// ## Building the data
+ /// This function compiles the data into a single byte array.
+ /// This contains the OSType followed by the length of the data
+ /// followed by the data.
+ pub fn build(&self) -> Box<[u8]> {
+ // Total: 4 bytes (OSType) + 4 bytes (length) + length (data)
+ let mut result = Vec::with_capacity(self.len() as usize);
+
+ result.extend_from_slice(&self.os_type);
+ result.extend_from_slice(&(8 + self.data.len() as u32).to_be_bytes());
+ result.extend_from_slice(&self.data);
+
+ result.into_boxed_slice()
+ }
+}
+
+/// ## ICNSBuilder
+/// This struct holds a list of data that will be compiled into an ICNS file.
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct IconFamily {
+ pub data: Vec,
+}
+
+impl IconFamily {
+ /// ## New
+ /// Creates a new file format
+ pub fn new() -> Self {
+ Self { data: vec![] }
+ }
+
+ /// ## Adding data
+ /// This adds a new entry to the file.
+ /// Data can be images, masks, metadata, etc.
+ pub fn add_data(&mut self, data: IcnsDataEntry) -> &mut Self {
+ self.data.push(data);
+
+ self
+ }
+
+ /// ## Creating the table of contents
+ /// The table of contents is the first entry in the file.
+ /// It contains the OSType of each entry and the length of each entry.
+ pub fn create_contents_table(&self) -> IcnsDataEntry {
+ let mut buffer = Vec::with_capacity(8 * self.data.len()); // Each entry is 8 bytes
+
+ for data in &self.data {
+ buffer.extend_from_slice(&data.os_type);
+ buffer.extend_from_slice(&((&data).data.len() as u32).to_be_bytes());
+ }
+
+ IcnsDataEntry::new(
+ [0x54, 0x4F, 0x43, 0x20], // "TOC "
+ buffer.into_boxed_slice(),
+ )
+ }
+
+ /// ## Building the ICNS file
+ /// Building the file will create the table of contents
+ /// and compile all the data into a single file.
+ pub fn build(&self) -> Box<[u8]> {
+ // Calculate the total size of the file
+ let contents_table = self.create_contents_table();
+
+ // Insert the TOC first
+ let mut data = Vec::with_capacity(self.data.len() + 1);
+ data.push(contents_table);
+ for d in &self.data {
+ data.push(d.clone());
+ }
+
+ let total_size = data.iter().map(|data| data.len()).sum::();
+ let mut buffer = Vec::with_capacity(MAGIC.len() + 4 + total_size as usize);
+
+ // Add the magic bytes, the total size and the data
+ buffer.extend_from_slice(&MAGIC);
+ buffer.extend_from_slice(&(total_size as u32).to_be_bytes());
+ for data in &data {
+ buffer.extend_from_slice(&data.build());
+ }
+
+ buffer.into_boxed_slice()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn encode_icns_data_entry() {
+ let dummy_data: Vec = vec![0x00, 0x01, 0x02, 0x03];
+ let result = vec![
+ 0x00, 0x00, 0x00, 0x00, // OSType: NUL NUL NUL NUL
+ 0x00, 0x00, 0x00,
+ 0x0C, // Length: 12 (4 bytes OSType + 4 bytes length + 4 bytes data)
+ 0x00, 0x01, 0x02, 0x03, // Data: 00 01 02 03
+ ];
+
+ let entry =
+ super::IcnsDataEntry::new([0x00, 0x00, 0x00, 0x00], dummy_data.into_boxed_slice());
+
+ assert_eq!(entry.build(), result.into_boxed_slice());
+ }
+}
diff --git a/icns-rs/src/image_encoder.rs b/icns-rs/src/image_encoder.rs
new file mode 100644
index 0000000..2e78bce
--- /dev/null
+++ b/icns-rs/src/image_encoder.rs
@@ -0,0 +1,215 @@
+use std::io::Write;
+
+use crate::{icns_format::IcnsDataEntry, image_types::IconFormats, packbits};
+
+use image::{codecs::png::PngEncoder, imageops::FilterType, DynamicImage, ImageEncoder};
+
+/// The ImageBuilder struct
+/// This struct is used to build the image data, specifically,
+/// resizing the image and encoding it as a RGB, ARGB, mask,
+/// or PNG image
+pub struct ImageBuilder {
+ pub format: IconFormats,
+ pub data: DynamicImage,
+ pub filter: FilterType,
+}
+
+impl ImageBuilder {
+ pub fn new() -> Self {
+ Self {
+ format: IconFormats::IS32,
+ data: DynamicImage::new_rgb8(1, 1),
+ filter: FilterType::Nearest,
+ }
+ }
+
+ /// Sets the image format
+ /// See the `IconFormats` enum for more information
+ pub fn format(&mut self, format: IconFormats) -> &mut Self {
+ self.format = format;
+
+ self
+ }
+
+ /// Sets the image data. Encode a png and pass it as a DynamicImage.
+ pub fn data(&mut self, data: DynamicImage) -> &mut Self {
+ self.data = data;
+
+ self
+ }
+
+ /// Sets the filter type to be used when resizing the image
+ /// - `Nearest`: Nearest neighbor interpolation
+ /// - `Triangle`: Triangle interpolation
+ /// - `CatmullRom`: Catmull-Rom interpolation
+ /// - `Gaussian`: Gaussian interpolation
+ ///
+ /// The default is `Nearest` because it's the fastest
+ pub fn filter(&mut self, filter: FilterType) -> &mut Self {
+ self.filter = filter;
+
+ self
+ }
+
+ /// Encodes an image as a RGB image
+ /// You probably want to use `.build()` instead of this method
+ pub fn rgb_image(&self) -> Result, String> {
+ let size = self.format.get_size() as u32;
+ let resized = self.data.resize(size, size, self.filter);
+ let rgb8 = resized.to_rgb8();
+ let data = rgb8.pixels().collect::>();
+
+ let channels = [
+ // Offset if the type is it32
+ if self.format == IconFormats::IT32 {
+ vec![0x00, 0x00, 0x00, 0x00].into_boxed_slice()
+ } else {
+ Vec::new().into_boxed_slice()
+ },
+ // Red channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[0])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ // Green channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[1])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ // Blue channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[2])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ ];
+
+ let mut buffer = Vec::with_capacity(channels.iter().map(|c| c.len()).sum::());
+
+ for b in channels {
+ buffer.extend_from_slice(&b);
+ }
+
+ Ok(buffer.into_boxed_slice())
+ }
+
+ /// Encodes an image as a ARGB
+ /// You probably want to use `.build()` instead of this method
+ pub fn argb_image(&self) -> Result, String> {
+ let size = self.format.get_size() as u32;
+ let resized = self.data.resize(size, size, self.filter);
+ let rgba8 = resized.to_rgba8();
+ let data = rgba8.pixels().collect::>();
+
+ let channels = [
+ // File header
+ vec![0x41, 0x52, 0x47, 0x42].into_boxed_slice(), // ARGB
+ // Alpha channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[3])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ // Red channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[0])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ // Green channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[1])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ // Blue channel
+ packbits::compress(
+ data.iter()
+ .map(|pixel| pixel[2])
+ .collect::>()
+ .into_boxed_slice(),
+ ),
+ ];
+
+ let mut buffer = Vec::with_capacity(channels.iter().map(|c| c.len()).sum::());
+
+ for b in channels {
+ buffer.extend_from_slice(&b);
+ }
+
+ Ok(buffer.into_boxed_slice())
+ }
+
+ /// Encodes an image as a mask
+ /// You probably want to use `.build()` instead of this method
+ pub fn mask_image(&self) -> Result, String> {
+ let size = self.format.get_size() as u32;
+ let resized = self.data.resize(size, size, self.filter);
+ let luma = resized.to_luma_alpha8();
+ let data = luma.pixels().collect::>();
+
+ // No compression
+ let mask = data
+ .iter()
+ .map(|pixel| pixel[1])
+ .collect::>()
+ .into_boxed_slice();
+
+ Ok(mask)
+ }
+
+ /// Encodes an image as a PNG
+ pub fn png_image(&self) -> Result, String> {
+ let size = self.format.get_size() as u32;
+ let data = self.data.resize(size, size, self.filter);
+
+ let mut buffer = Vec::new();
+
+ // Required because the PngEncoder drops the writer
+ struct WriterProxy<'a> {
+ buffer: &'a mut Vec,
+ }
+
+ impl<'a> Write for WriterProxy<'a> {
+ fn write(&mut self, buf: &[u8]) -> std::io::Result {
+ self.buffer.write(buf)
+ }
+
+ fn flush(&mut self) -> std::io::Result<()> {
+ self.buffer.flush()
+ }
+ }
+
+ let encoder = PngEncoder::new(WriterProxy {
+ buffer: &mut buffer,
+ });
+
+ let color = data.color();
+
+ let result = encoder.write_image(data.into_bytes().as_slice(), size, size, color);
+
+ match result {
+ Ok(_) => Ok(buffer.into_boxed_slice()),
+ Err(e) => Err(format!("Failed to encode PNG: {}", e)),
+ }
+ }
+
+ pub fn build(&self) -> Result {
+ let data = match self.format.get_format() {
+ crate::image_types::FileFormat::RGB => self.rgb_image(),
+ crate::image_types::FileFormat::ARGB => self.argb_image(),
+ crate::image_types::FileFormat::MASK => self.mask_image(),
+ crate::image_types::FileFormat::PNG => self.png_image(),
+ }?;
+
+ Ok(IcnsDataEntry::new(self.format.get_bytes(), data))
+ }
+}
diff --git a/icns-rs/src/image_types.rs b/icns-rs/src/image_types.rs
new file mode 100644
index 0000000..966d1bb
--- /dev/null
+++ b/icns-rs/src/image_types.rs
@@ -0,0 +1,226 @@
+#[doc(hidden)]
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum FileFormat {
+ RGB,
+ ARGB,
+ MASK,
+ PNG,
+}
+
+/// # ICNS Types
+/// These are the types of icons that can be stored in an ICNS file.
+/// Not all of them are included, but the most common ones are.
+/// The full list can be found at Wikipedia
+/// https://en.wikipedia.org/wiki/Apple_Icon_Image_format#Icon_types
+#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
+pub enum IconFormats {
+ /// - OSName: is32
+ /// - Size: 16x16
+ /// - Format: 24-bit RGB icon
+ /// - OS: System 8.5+
+ IS32,
+ /// - OSName: il32
+ /// - Size: 32x32
+ /// - Format: 24-bit RGB icon
+ /// - OS: System 8.5+
+ IL32,
+ /// - OSName: ih32
+ /// - Size: 48x48
+ /// - Format: 24-bit RGB icon
+ /// - OS: System 8.5+
+ IH32,
+ /// - OSName: it32
+ /// - Size: 128x128
+ /// - Format: 24-bit RGB icon
+ /// - OS: Mac OS X 10.0+
+ IT32,
+ /// - OSName: s8mk
+ /// - Size: 16x12
+ /// - Format: 8-bit mask
+ /// - OS: System 8.5+
+ S8MK,
+ /// - OSName: l8mk
+ /// - Size: 32x32
+ /// - Format: 8-bit mask
+ /// - OS: System 8.5+
+ L8MK,
+ /// - OSName: h8mk
+ /// - Size: 48x48
+ /// - Format: 8-bit mask
+ /// - OS: System 8.5+
+ H8MK,
+ /// - OSName: t8mk
+ /// - Size: 128x128
+ /// - Format: 8-bit mask
+ /// - OS: Mac OS X 10.0+
+ T8MK,
+ /// - OSName: ic04
+ /// - Size: 16x16
+ /// - Format: ARGB
+ /// - OS: N/A
+ IC04,
+ /// - OSName: ic05
+ /// - Size: 32x32
+ /// - Format: ARGB
+ /// - OS: N/A
+ IC05,
+ /// - OSName: ic07
+ /// - Size: 128x128
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.7+
+ IC07,
+ /// - OSName: ic08
+ /// - Size: 256x256
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.5+
+ IC08,
+ /// - OSName: ic09
+ /// - Size: 512x512
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.5+
+ IC09,
+ /// - OSName: ic10
+ /// - Size: 1024x1024
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.7+
+ IC10,
+ /// - OSName: ic11
+ /// - Size: 32x32
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.8+
+ IC11,
+ /// - OSName: ic12
+ /// - Size: 64x64
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.8+
+ IC12,
+ /// - OSName: ic13
+ /// - Size: 256x256
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.8+
+ IC13,
+ /// - OSName: ic14
+ /// - Size: 512x512
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.8+
+ IC14,
+ /// - OSName: icp4
+ /// - Size: 16x16
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.7+
+ ICP4,
+ /// - OSName: icp5
+ /// - Size: 32x32
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.7+
+ ICP5,
+ /// - OSName: icp6
+ /// - Size: 64x64
+ /// - Format: PNG
+ /// - OS: Mac OS X 10.7+
+ ICP6,
+}
+
+impl IconFormats {
+ /// Get the default recommended format for the icon type.
+ pub fn recommended() -> Vec {
+ vec![
+ IconFormats::IS32,
+ IconFormats::IL32,
+ IconFormats::IH32,
+ IconFormats::IT32,
+ IconFormats::S8MK,
+ IconFormats::L8MK,
+ IconFormats::H8MK,
+ IconFormats::T8MK,
+ IconFormats::IC04,
+ IconFormats::IC05,
+ IconFormats::IC07,
+ IconFormats::IC08,
+ IconFormats::IC09,
+ IconFormats::IC10,
+ IconFormats::IC11,
+ IconFormats::IC12,
+ IconFormats::IC13,
+ IconFormats::IC14,
+ ]
+ }
+
+ pub fn get_format(&self) -> FileFormat {
+ match self {
+ IconFormats::IS32 => FileFormat::RGB,
+ IconFormats::IL32 => FileFormat::RGB,
+ IconFormats::IH32 => FileFormat::RGB,
+ IconFormats::IT32 => FileFormat::RGB,
+ IconFormats::S8MK => FileFormat::MASK,
+ IconFormats::L8MK => FileFormat::MASK,
+ IconFormats::H8MK => FileFormat::MASK,
+ IconFormats::T8MK => FileFormat::MASK,
+ IconFormats::IC04 => FileFormat::ARGB,
+ IconFormats::IC05 => FileFormat::ARGB,
+ IconFormats::IC07 => FileFormat::PNG,
+ IconFormats::IC08 => FileFormat::PNG,
+ IconFormats::IC09 => FileFormat::PNG,
+ IconFormats::IC10 => FileFormat::PNG,
+ IconFormats::IC11 => FileFormat::PNG,
+ IconFormats::IC12 => FileFormat::PNG,
+ IconFormats::IC13 => FileFormat::PNG,
+ IconFormats::IC14 => FileFormat::PNG,
+ IconFormats::ICP4 => FileFormat::PNG,
+ IconFormats::ICP5 => FileFormat::PNG,
+ IconFormats::ICP6 => FileFormat::PNG,
+ }
+ }
+
+ pub fn get_size(&self) -> usize {
+ match self {
+ IconFormats::IS32 => 16,
+ IconFormats::IL32 => 32,
+ IconFormats::IH32 => 48,
+ IconFormats::IT32 => 128,
+ IconFormats::S8MK => 16,
+ IconFormats::L8MK => 32,
+ IconFormats::H8MK => 48,
+ IconFormats::T8MK => 128,
+ IconFormats::IC04 => 16,
+ IconFormats::IC05 => 32,
+ IconFormats::IC07 => 128,
+ IconFormats::IC08 => 256,
+ IconFormats::IC09 => 512,
+ IconFormats::IC10 => 1024,
+ IconFormats::IC11 => 32,
+ IconFormats::IC12 => 64,
+ IconFormats::IC13 => 256,
+ IconFormats::IC14 => 512,
+ IconFormats::ICP4 => 16,
+ IconFormats::ICP5 => 32,
+ IconFormats::ICP6 => 64,
+ }
+ }
+
+ pub fn get_bytes(&self) -> [u8; 4] {
+ match self {
+ IconFormats::IS32 => [0x69, 0x73, 0x33, 0x32], //is32
+ IconFormats::IL32 => [0x69, 0x6c, 0x33, 0x32], //il32
+ IconFormats::IH32 => [0x69, 0x68, 0x33, 0x32], //ih32
+ IconFormats::IT32 => [0x69, 0x74, 0x33, 0x32], //it32
+ IconFormats::S8MK => [0x73, 0x38, 0x6d, 0x6b], //s8mk
+ IconFormats::L8MK => [0x6c, 0x38, 0x6d, 0x6b], //l8mk
+ IconFormats::H8MK => [0x68, 0x38, 0x6d, 0x6b], //h8mk
+ IconFormats::T8MK => [0x74, 0x38, 0x6d, 0x6b], //t8mk
+ IconFormats::IC04 => [0x69, 0x63, 0x30, 0x34], //ic04
+ IconFormats::IC05 => [0x69, 0x63, 0x30, 0x35], //ic05
+ IconFormats::IC07 => [0x69, 0x63, 0x30, 0x37], //ic07
+ IconFormats::IC08 => [0x69, 0x63, 0x30, 0x38], //ic08
+ IconFormats::IC09 => [0x69, 0x63, 0x30, 0x39], //ic09
+ IconFormats::IC10 => [0x69, 0x63, 0x31, 0x30], //ic10
+ IconFormats::IC11 => [0x69, 0x63, 0x31, 0x31], //ic11
+ IconFormats::IC12 => [0x69, 0x63, 0x31, 0x32], //ic12
+ IconFormats::IC13 => [0x69, 0x63, 0x31, 0x33], //ic13
+ IconFormats::IC14 => [0x69, 0x63, 0x31, 0x34], //ic14
+ IconFormats::ICP4 => [0x69, 0x63, 0x70, 0x34], //icp4
+ IconFormats::ICP5 => [0x69, 0x63, 0x70, 0x35], //icp5
+ IconFormats::ICP6 => [0x69, 0x63, 0x70, 0x36], //icp6
+ }
+ }
+}
diff --git a/icns-rs/src/lib.rs b/icns-rs/src/lib.rs
new file mode 100644
index 0000000..b3b55a6
--- /dev/null
+++ b/icns-rs/src/lib.rs
@@ -0,0 +1,105 @@
+pub mod icns_format;
+pub mod image_encoder;
+pub mod image_types;
+pub mod packbits;
+
+use icns_format::IconFamily;
+use image::DynamicImage;
+use image_encoder::ImageBuilder;
+pub use image_types::IconFormats;
+
+/// The main encoder struct
+/// Create a new encoder with `IcnsEncoder::new()`
+pub struct IcnsEncoder {
+ data: DynamicImage,
+ formats: Vec,
+}
+
+impl IcnsEncoder {
+ /// Creates a new IcnsEncoder
+ ///
+ /// Usage:
+ /// ```no_run
+ /// use icns_rs::{IcnsEncoder, IconFormats};
+ /// use image::open;
+ /// use std::fs::File;
+ /// use std::io::prelude::*;
+ ///
+ /// // Open the image
+ /// let image = match open("512x512@2.png") {
+ /// Ok(image) => image,
+ /// Err(e) => {
+ /// println!("Error: {}", e);
+ /// std::process::exit(1);
+ /// }
+ /// };
+ ///
+ /// // Create the encoder
+ /// let mut encoder = IcnsEncoder::new();
+ ///
+ /// encoder.data(image);
+ /// encoder.formats(IconFormats::recommended());
+ ///
+ /// // Encode the image
+ /// let data = match encoder.build() {
+ /// Ok(data) => data,
+ /// Err(e) => {
+ /// println!("Error ould not encode image");
+ /// std::process::exit(1);
+ /// }
+ /// };
+ ///
+ /// // Write data to file
+ /// let mut file = match File::create("example.icns") {
+ /// Ok(file) => file,
+ /// Err(e) => {
+ /// println!("Error: {}", e);
+ /// std::process::exit(1);
+ /// }
+ /// };
+ ///
+ /// match file.write_all(&data) {
+ /// Ok(_) => println!("Successfully wrote to file"),
+ /// Err(e) => {
+ /// println!("Error: {}", e);
+ /// std::process::exit(1);
+ /// }
+ /// };
+ /// ```
+ pub fn new() -> Self {
+ Self {
+ data: DynamicImage::new_rgb8(1, 1),
+ formats: Vec::new(),
+ }
+ }
+
+ /// Sets the image data. Encode a png and pass it as a DynamicImage.
+ pub fn data(&mut self, data: DynamicImage) -> &mut Self {
+ self.data = data;
+
+ self
+ }
+
+ /// Sets the image formats to be encoded
+ pub fn formats(&mut self, formats: Vec) -> &mut Self {
+ self.formats = formats;
+
+ self
+ }
+
+ /// Encodes the image as an ICNS file
+ pub fn build(&self) -> Result, String> {
+ let mut file = IconFamily::new();
+
+ let mut image_encoder = ImageBuilder::new();
+ image_encoder.data(self.data.clone());
+
+ for format in &self.formats {
+ let image = image_encoder.format(format.clone()).build()?;
+
+ file.add_data(image);
+ }
+
+ Ok(file.build())
+ }
+}
diff --git a/icns-rs/src/packbits.rs b/icns-rs/src/packbits.rs
new file mode 100644
index 0000000..aec65da
--- /dev/null
+++ b/icns-rs/src/packbits.rs
@@ -0,0 +1,268 @@
+/// To denote that a byte is repeated, the first byte of a sequence
+/// must be greater or equal to 128. A byte is 255 so because of this
+/// 255 - 128 = 127 is the maximum amount of bytes that can be repeated.
+/// When a byte is repeated, it is repeated at least 3 times.
+/// So add 3 to the maximum amount of bytes that can be repeated.
+const MAX_REPEAT: usize = 130;
+const ENCODE_REPEAT: u8 = 128;
+
+/// # ICNS PackBits(like) compression
+/// Apple uses a format simular to PackBits to compress the image data.
+/// PackBits is a lossless compression format that is used in TIFF files
+/// since system 6.0.5.
+/// This implementation is based on the javascript implementation by
+/// @fiahfy/packbits https://github.com/fiahfy/packbits
+///
+/// ```rust
+/// let data = vec![
+/// 0x01, 0x02, 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05
+/// ];
+///
+/// let compressed = icns_rs::packbits::compress(data.into_boxed_slice());
+///
+/// assert_eq!(
+/// compressed,
+/// vec![0x02, 0x01, 0x02, 0x02, 0x80, 0x03, 0x81, 0x04, 0x82, 0x05]
+/// .into_boxed_slice()
+/// );
+pub fn compress(raw: Box<[u8]>) -> Box<[u8]> {
+ let mut buffers: Vec> = vec![];
+
+ // I'd be happy to use a iterator here
+ // FIXME: This is a mess
+ let mut i = 0;
+ while i < raw.len() {
+ let byte = &raw[i];
+ // Check if last 1 or 2 bytes
+ if i + 2 >= raw.len() {
+ let length = raw.len() - i;
+ let mut buffer = Vec::with_capacity(1);
+ buffer.push(length as u8 - 1);
+ buffers.push(buffer.into_boxed_slice());
+ buffers.push(raw[i..].to_vec().into_boxed_slice());
+ break;
+ }
+
+ // Should be repeated if the next 2 bytes are the same
+ let should_repeat = byte == &raw[i + 1] && byte == &raw[i + 2];
+
+ if should_repeat {
+ let mut repeat_to = i + 2;
+
+ while repeat_to + 1 < raw.len()
+ && byte == &raw[repeat_to + 1]
+ && repeat_to - i + 1 < MAX_REPEAT
+ {
+ repeat_to += 1;
+ }
+
+ repeat_to += 1;
+
+ let length = repeat_to - i; // + 1 because the first byte is also included
+
+ let mut buffer = Vec::with_capacity(2);
+ buffer.push(length as u8 - 3 + ENCODE_REPEAT);
+ buffer.push(byte.clone());
+
+ buffers.push(buffer.into_boxed_slice());
+
+ // Skip the repeated bytes
+ i = repeat_to;
+ } else {
+ // Should not be repeated
+ let mut buffer_to = i + 2;
+ // ^^ Minimum length is 2 (that's why we check if we're at the last 2 bytes)
+ let mut repeats = 1;
+ let mut repeat_index = buffer_to;
+
+ while buffer_to + 1 < raw.len() && buffer_to - i + 1 < ENCODE_REPEAT as usize {
+ if &raw[buffer_to] == &raw[repeat_index] {
+ repeats += 1;
+ // If we have 2 repeats, we can stop
+ // It would be better to check to compress
+ if repeats > 2 {
+ break;
+ }
+ } else {
+ repeats = 1;
+ repeat_index = buffer_to;
+ }
+
+ buffer_to += 1;
+ }
+ buffer_to += 1;
+ if repeats > 2 {
+ buffer_to -= 3;
+ }
+
+ let length = buffer_to - i;
+ let mut buffer = Vec::with_capacity(length + 1);
+ buffer.push(length as u8 - 1);
+ buffer.extend_from_slice(&raw[i..buffer_to]);
+
+ buffers.push(buffer.into_boxed_slice());
+
+ i = buffer_to;
+ }
+ }
+
+ // Compact the buffers into a single buffer
+ let mut buffer = Vec::with_capacity(buffers.iter().map(|b| b.len()).sum());
+ for b in buffers {
+ buffer.extend_from_slice(&b);
+ }
+
+ buffer.into_boxed_slice()
+}
+
+/// # ICNS PackBits(like) decompression
+/// Apple uses a format simular to PackBits to compress the image data.
+/// PackBits is a lossless compression format that is used in TIFF files
+/// since system 6.0.5.
+/// This implementation is based on the javascript implementation by
+/// @fiahfy/packbits https://github.com/fiahfy/packbits
+///
+/// The implementation was slightly modified to work because unlike the
+/// PackBits format, the image format does not have an escape byte of
+/// 255 / 0xFF. I think the author of the javascript implementation
+/// forgot to remove the escape byte in the icns version.
+///
+/// ```rust
+/// let data = vec![0x02, 0x01, 0x02, 0x02, 0x80, 0x03, 0x81, 0x04, 0x82, 0x05];
+///
+/// let decompressed = icns_rs::packbits::decompress(data.into_boxed_slice());
+///
+/// assert_eq!(
+/// decompressed,
+/// vec![
+/// 0x01, 0x02, 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05,
+/// 0x05
+/// ]
+/// .into_boxed_slice()
+/// );
+/// ```
+pub fn decompress(data: Box<[u8]>) -> Box<[u8]> {
+ let mut buffers: Vec> = vec![];
+
+ // FIXME: Don't use a loop
+ let mut i = 0;
+ while i < data.len() {
+ // We know it's compressed if the first byte is greater or equal to 128
+ if data[i] >= ENCODE_REPEAT {
+ // How many times the byte is repeated
+ let repeats = data[i] - ENCODE_REPEAT + 3;
+ // ^^ + 3 because the first byte is also included
+ let byte = data[i + 1];
+
+ let mut buffer = Vec::with_capacity(repeats as usize);
+ for _ in 0..repeats {
+ buffer.push(byte);
+ }
+
+ buffers.push(buffer.into_boxed_slice());
+
+ i += 2; // Compressed bytes are always 2 bytes long
+ } else {
+ // Not compressed
+ let length = data[i] as usize + 1;
+ let mut buffer = Vec::with_capacity(length);
+ buffer.extend_from_slice(&data[i + 1..i + length + 1]);
+
+ buffers.push(buffer.into_boxed_slice());
+
+ i += length + 1;
+ }
+ }
+
+ // Compact the buffers into a single buffer
+ let mut buffer = Vec::with_capacity(buffers.iter().map(|b| b.len()).sum());
+ for b in buffers {
+ buffer.extend_from_slice(&b);
+ }
+
+ buffer.into_boxed_slice()
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ const BASIC_RAW: [u8; 15] = [
+ 0x01, 0x02, 0x02, 0x03, 0x03, 0x03, 0x04, 0x04, 0x04, 0x04, 0x05, 0x05, 0x05, 0x05, 0x05,
+ ];
+ const BASIC_COMPRESSED: [u8; 10] = [0x02, 0x01, 0x02, 0x02, 0x80, 0x03, 0x81, 0x04, 0x82, 0x05];
+
+ const STRESS_REPEAT_RAW: [u8; 131] = [0x01; 131];
+ const STRESS_REPEAT_COMPRESSED: [u8; 4] = [0xFF, 0x01, 0x00, 0x01];
+
+ const STRESS_NO_REPEAT_RAW: [u8; 131] = [
+ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
+ 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d,
+ 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c,
+ 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b,
+ 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a,
+ 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,
+ 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,
+ 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77,
+ 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82,
+ ];
+ const STRESS_NO_REPEAT_COMPRESSED: [u8; 133] = [
+ 0x7f, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d,
+ 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c,
+ 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b,
+ 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a,
+ 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,
+ 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,
+ 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67,
+ 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76,
+ 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x02, 0x80, 0x81, 0x82,
+ ];
+
+ #[test]
+ fn compress_basic() {
+ assert_eq!(
+ compress(BASIC_RAW.to_vec().into_boxed_slice()),
+ BASIC_COMPRESSED.to_vec().into_boxed_slice()
+ );
+ }
+
+ #[test]
+ fn compress_stress_repeat() {
+ assert_eq!(
+ compress(STRESS_REPEAT_RAW.to_vec().into_boxed_slice()),
+ STRESS_REPEAT_COMPRESSED.to_vec().into_boxed_slice()
+ );
+ }
+
+ #[test]
+ fn compress_stress_no_repeat() {
+ assert_eq!(
+ compress(STRESS_NO_REPEAT_RAW.to_vec().into_boxed_slice()),
+ STRESS_NO_REPEAT_COMPRESSED.to_vec().into_boxed_slice()
+ );
+ }
+
+ #[test]
+ fn decompress_basic() {
+ assert_eq!(
+ decompress(BASIC_COMPRESSED.to_vec().into_boxed_slice()),
+ BASIC_RAW.to_vec().into_boxed_slice()
+ );
+ }
+
+ #[test]
+ fn decompress_stress_repeat() {
+ assert_eq!(
+ decompress(STRESS_REPEAT_COMPRESSED.to_vec().into_boxed_slice()),
+ STRESS_REPEAT_RAW.to_vec().into_boxed_slice()
+ );
+ }
+
+ #[test]
+ fn decompress_stress_no_repeat() {
+ assert_eq!(
+ decompress(STRESS_NO_REPEAT_COMPRESSED.to_vec().into_boxed_slice()),
+ STRESS_NO_REPEAT_RAW.to_vec().into_boxed_slice()
+ );
+ }
+}