Compare commits
11 commits
Author | SHA1 | Date | |
---|---|---|---|
62ffdeba3c | |||
e0d118dbcd | |||
b1c2b0881c | |||
df249d02dc | |||
96fcc9c16a | |||
1b4933a45d | |||
3ce09575dc | |||
ea893e64d4 | |||
0d7e82e771 | |||
d720fa60b0 | |||
0299dbeee3 |
|
@ -6,7 +6,7 @@ The squishiest image format!
|
|||
repository = "https://github.com/Dangoware/sqp"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["G2 <ke0bhogsg@gmail.com>"]
|
||||
version = "0.1.0"
|
||||
version = "0.1.1"
|
||||
edition = "2021"
|
||||
categories = ["encoding", "compression", "graphics", "multimedia::images", "multimedia::encoding"]
|
||||
|
||||
|
|
18
README.md
|
@ -1,8 +1,10 @@
|
|||
<p align="center">
|
||||
<img width="400px" src="https://github.com/user-attachments/assets/98f94c1c-ed6f-49a3-b906-c328035d981e">
|
||||
<img title="SQP" alt="SQP Logo" width="500px" src="https://github.com/user-attachments/assets/cf2fd7f4-f825-4bb4-9427-1b7181be4639">
|
||||
</p>
|
||||
|
||||
# SQP
|
||||
[](https://lib.rs/crates/sqp)
|
||||
[](https://docs.rs/sqp/)
|
||||
|
||||
**SQP** (**SQ**uishy **P**icture Format) is an image format designed
|
||||
for ease of implementation and learning about compression and image formats
|
||||
while attaining a relatively good compression ratio. The general idea is to
|
||||
|
@ -19,6 +21,7 @@ speeds.
|
|||
- Support for various color formats (RGBA, Grayscale, etc.)
|
||||
- Decent compression ratios, the lossless compression can often beat PNG
|
||||
especially on images with transparency
|
||||
- Lossy alpha compression!
|
||||
- Relatively simple
|
||||
- Squishy! 🍡
|
||||
|
||||
|
@ -30,3 +33,14 @@ speeds.
|
|||
- Decoder-based frame interpolation
|
||||
- Floating point color
|
||||
- Metadata?
|
||||
|
||||
## Examples
|
||||
All examples are at 30% quality in both JPEG and SQP.
|
||||
|
||||
| Original | JPEG | SQP |
|
||||
|----------|--------------|-------------|
|
||||
| <img width="300px" src="https://github.com/user-attachments/assets/e4f7b620-4cf5-407d-851b-800c52c8a14d"> | <img width="300px" src="https://github.com/user-attachments/assets/84691e8c-2f73-4a1d-b979-0863066b159f"> | <img width="300px" src="https://github.com/user-attachments/assets/ccaa8770-b641-437f-80d1-3658f94c2e21"> |
|
||||
| <img width="300px" src="https://github.com/user-attachments/assets/f0056e3b-8988-4d0d-88bf-bc73ac5b8be0"> | <img width="300px" src="https://github.com/user-attachments/assets/400c4072-ba69-45d7-8051-46a4e2867c7f"> | <img width="300px" src="https://github.com/user-attachments/assets/c4c84f64-7564-433a-a922-17da472578d9"> |
|
||||
|
||||
Images obtained from the following source:
|
||||
[https://r0k.us/graphics/kodak/](https://r0k.us/graphics/kodak/)
|
||||
|
|
|
@ -100,6 +100,9 @@ pub fn idct(input: &[f32], width: usize, height: usize) -> Vec<u8> {
|
|||
///
|
||||
/// Instead of using this, use the [`quantization_matrix`] function to
|
||||
/// get a quantization matrix corresponding to the image quality value.
|
||||
///
|
||||
/// TODO: In the future, it would be cool to figure out how to generate a
|
||||
/// quantization matrix of any size.
|
||||
const BASE_QUANTIZATION_MATRIX: [u16; 64] = [
|
||||
16, 11, 10, 16, 24, 40, 51, 61,
|
||||
12, 12, 14, 19, 26, 58, 60, 55,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
//! Structs and enums which are included in the header of SQP files.
|
||||
|
||||
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use crate::picture::Error;
|
||||
|
||||
|
@ -42,21 +42,26 @@ impl Default for Header {
|
|||
}
|
||||
|
||||
impl Header {
|
||||
pub fn to_bytes(&self) -> [u8; 19] {
|
||||
let mut buf = Cursor::new(Vec::new());
|
||||
|
||||
buf.write_all(&self.magic).unwrap();
|
||||
buf.write_u32::<LE>(self.width).unwrap();
|
||||
buf.write_u32::<LE>(self.height).unwrap();
|
||||
/// Write the header into a byte stream implementing [`Write`].
|
||||
///
|
||||
/// Returns the number of bytes written.
|
||||
pub fn write_into<W: Write + WriteBytesExt>(&self, output: &mut W) -> Result<usize, io::Error> {
|
||||
let mut count = 0;
|
||||
output.write_all(&self.magic)?;
|
||||
output.write_u32::<LE>(self.width)?;
|
||||
output.write_u32::<LE>(self.height)?;
|
||||
count += 16;
|
||||
|
||||
// Write compression info
|
||||
buf.write_u8(self.compression_type.into()).unwrap();
|
||||
buf.write_u8(self.quality).unwrap();
|
||||
output.write_u8(self.compression_type.into())?;
|
||||
output.write_u8(self.quality)?;
|
||||
count += 2;
|
||||
|
||||
// Write color format
|
||||
buf.write_u8(self.color_format as u8).unwrap();
|
||||
output.write_u8(self.color_format as u8)?;
|
||||
count += 1;
|
||||
|
||||
buf.into_inner().try_into().unwrap()
|
||||
Ok(count)
|
||||
}
|
||||
|
||||
/// Length of the header in bytes.
|
||||
|
@ -65,13 +70,14 @@ impl Header {
|
|||
19
|
||||
}
|
||||
|
||||
/// Create a header from something implementing [`Read`].
|
||||
pub fn read_from<T: Read + ReadBytesExt>(input: &mut T) -> Result<Self, Error> {
|
||||
/// Create a header from a byte stream implementing [`Read`].
|
||||
pub fn read_from<R: Read + ReadBytesExt>(input: &mut R) -> Result<Self, Error> {
|
||||
let mut magic = [0u8; 8];
|
||||
input.read_exact(&mut magic).unwrap();
|
||||
|
||||
if magic != *b"dangoimg" {
|
||||
return Err(Error::InvalidIdentifier(magic));
|
||||
let bad_id = String::from_utf8_lossy(&magic).into_owned();
|
||||
return Err(Error::InvalidIdentifier(bad_id));
|
||||
}
|
||||
|
||||
Ok(Header {
|
||||
|
|
|
@ -18,8 +18,10 @@
|
|||
//! let width = 2;
|
||||
//! let height = 2;
|
||||
//! let bitmap = vec![
|
||||
//! 255, 255, 255, 255, 0, 255, 0, 128,
|
||||
//! 255, 255, 255, 255, 0, 255, 0, 128
|
||||
//! 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
//! 0x00, 0x80, 0x00, 0x80,
|
||||
//! 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
//! 0x00, 0x80, 0x00, 0x80,
|
||||
//! ];
|
||||
//!
|
||||
//! // Create a 2×2 image in memory. Nothing is compressed or encoded
|
||||
|
|
|
@ -58,9 +58,9 @@ pub fn add_rows(width: u32, height: u32, color_format: ColorFormat, data: &[u8])
|
|||
// Interleave the offset alpha into the RGB bytes
|
||||
data[rgb_index..rgb_index + width as usize * (color_format.pbc() - 1)]
|
||||
.chunks(color_format.pbc() - 1)
|
||||
.zip(data[alpha_index..alpha_index + width as usize].into_iter())
|
||||
.zip(data[alpha_index..alpha_index + width as usize].iter())
|
||||
.flat_map(|(a, b)| {
|
||||
a.into_iter().chain(vec![b])
|
||||
a.iter().chain(vec![b])
|
||||
})
|
||||
.copied()
|
||||
.collect()
|
||||
|
|
|
@ -13,14 +13,18 @@ use crate::{
|
|||
operations::{add_rows, sub_rows},
|
||||
};
|
||||
|
||||
/// An error which occured while manipulating a [`SquishyPicture`].
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
#[error("incorrect identifier, got {0:02X?}")]
|
||||
InvalidIdentifier([u8; 8]),
|
||||
/// The file signature was invalid. Must be "dangoimg".
|
||||
#[error("incorrect signature, expected \"dangoimg\" got {0:?}")]
|
||||
InvalidIdentifier(String),
|
||||
|
||||
/// Any I/O operation failed.
|
||||
#[error("io operation failed: {0}")]
|
||||
IoError(#[from] io::Error),
|
||||
|
||||
/// There was an error while compressing or decompressing.
|
||||
#[error("compression operation failed: {0}")]
|
||||
CompressionError(#[from] CompressionError),
|
||||
}
|
||||
|
@ -146,8 +150,7 @@ impl SquishyPicture {
|
|||
let mut count = 0;
|
||||
|
||||
// Write out the header
|
||||
output.write_all(&self.header.to_bytes()).unwrap();
|
||||
count += self.header.len();
|
||||
count += self.header.write_into(&mut output)?;
|
||||
|
||||
// Based on the compression type, modify the data accordingly
|
||||
let modified_data = match self.header.compression_type {
|
||||
|
@ -241,6 +244,7 @@ impl SquishyPicture {
|
|||
}
|
||||
}
|
||||
|
||||
/// Decode a stream encoded as varints.
|
||||
fn decode_varint_stream(stream: &[u8]) -> Vec<i16> {
|
||||
let mut output = Vec::new();
|
||||
let mut offset = 0;
|
||||
|
@ -253,8 +257,12 @@ fn decode_varint_stream(stream: &[u8]) -> Vec<i16> {
|
|||
output
|
||||
}
|
||||
|
||||
/// Open an SQP from a given path. Convenience method around
|
||||
/// [`SquishyPicture::decode`]. Returns a [`Result<SquishyPicture>`].
|
||||
///
|
||||
/// If you are loading from memory, use [`SquishyPicture::decode`] instead.
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> Result<SquishyPicture, Error> {
|
||||
let input = File::open(path)?;
|
||||
|
||||
Ok(SquishyPicture::decode(input)?)
|
||||
SquishyPicture::decode(input)
|
||||
}
|
||||
|
|
BIN
src/test_images/dpf_logo.png
Normal file
After Width: | Height: | Size: 224 KiB |
BIN
src/test_images/kodim03.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
src/test_images/kodim23.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
src/test_images/sqp_text.png
Normal file
After Width: | Height: | Size: 156 KiB |
BIN
test_images/dpf_logo.png
Normal file
After Width: | Height: | Size: 224 KiB |
BIN
test_images/kodim03.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
test_images/kodim23.png
Normal file
After Width: | Height: | Size: 100 KiB |
BIN
test_images/sqp_text.png
Normal file
After Width: | Height: | Size: 156 KiB |