mirror of
https://github.com/Dangoware/sqp.git
synced 2025-07-03 20:12:55 -05:00
Compare commits
No commits in common. "main" and "v0.1.0" have entirely different histories.
13 changed files with 70 additions and 385 deletions
30
Cargo.toml
30
Cargo.toml
|
@ -1,20 +1,24 @@
|
||||||
[workspace]
|
[package]
|
||||||
resolver = "2"
|
name = "sqp"
|
||||||
members = [
|
description = """
|
||||||
"sqp",
|
The squishiest image format!
|
||||||
"sqp_tools",
|
"""
|
||||||
]
|
repository = "https://github.com/Dangoware/sqp"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
authors = ["G2 <ke0bhogsg@gmail.com>"]
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
categories = ["encoding", "compression", "graphics", "multimedia::images", "multimedia::encoding"]
|
||||||
|
|
||||||
[workspace.package]
|
[dependencies]
|
||||||
authors = ["G2-Games <ke0bhogsg@gmail.com>"]
|
byteorder = "1.5"
|
||||||
|
integer-encoding = "4.0"
|
||||||
[workspace.lints.rust]
|
rayon = "1.10"
|
||||||
unsafe_code = "forbid"
|
thiserror = "1.0"
|
||||||
|
|
||||||
[profile.production]
|
[profile.production]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
strip = true
|
|
||||||
lto = true
|
lto = true
|
||||||
opt-level = "z"
|
strip = true
|
||||||
codegen-units = 1
|
codegen-units = 1
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
|
|
18
README.md
18
README.md
|
@ -1,10 +1,8 @@
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img title="SQP" alt="SQP Logo" width="500px" src="https://github.com/user-attachments/assets/cf2fd7f4-f825-4bb4-9427-1b7181be4639">
|
<img width="400px" src="https://github.com/user-attachments/assets/98f94c1c-ed6f-49a3-b906-c328035d981e">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
[](https://lib.rs/crates/sqp)
|
# SQP
|
||||||
[](https://docs.rs/sqp/)
|
|
||||||
|
|
||||||
**SQP** (**SQ**uishy **P**icture Format) is an image format designed
|
**SQP** (**SQ**uishy **P**icture Format) is an image format designed
|
||||||
for ease of implementation and learning about compression and image formats
|
for ease of implementation and learning about compression and image formats
|
||||||
while attaining a relatively good compression ratio. The general idea is to
|
while attaining a relatively good compression ratio. The general idea is to
|
||||||
|
@ -21,7 +19,6 @@ speeds.
|
||||||
- Support for various color formats (RGBA, Grayscale, etc.)
|
- Support for various color formats (RGBA, Grayscale, etc.)
|
||||||
- Decent compression ratios, the lossless compression can often beat PNG
|
- Decent compression ratios, the lossless compression can often beat PNG
|
||||||
especially on images with transparency
|
especially on images with transparency
|
||||||
- Lossy alpha compression!
|
|
||||||
- Relatively simple
|
- Relatively simple
|
||||||
- Squishy! 🍡
|
- Squishy! 🍡
|
||||||
|
|
||||||
|
@ -33,14 +30,3 @@ speeds.
|
||||||
- Decoder-based frame interpolation
|
- Decoder-based frame interpolation
|
||||||
- Floating point color
|
- Floating point color
|
||||||
- Metadata?
|
- 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/)
|
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "sqp"
|
|
||||||
description = """
|
|
||||||
The squishiest image format!
|
|
||||||
"""
|
|
||||||
repository = "https://github.com/Dangoware/sqp"
|
|
||||||
license = "MIT OR Apache-2.0"
|
|
||||||
authors.workspace = true
|
|
||||||
version = "0.1.1"
|
|
||||||
edition = "2021"
|
|
||||||
categories = ["encoding", "compression", "graphics", "multimedia::images", "multimedia::encoding"]
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
byteorder = "1.5"
|
|
||||||
integer-encoding = "4.0"
|
|
||||||
rayon = "1.10"
|
|
||||||
thiserror = "2.0"
|
|
|
@ -1,15 +0,0 @@
|
||||||
[package]
|
|
||||||
name = "sqp_tools"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
authors.workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0"
|
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
|
||||||
image = { version = "0.25", default-features = false, features = ["bmp", "gif", "ico", "jpeg", "png", "pnm", "qoi", "rayon", "tga", "tiff", "webp"] }
|
|
||||||
sqp = { path = "../sqp" }
|
|
||||||
text_io = "0.1"
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
|
@ -1,168 +0,0 @@
|
||||||
mod utils;
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use clap::{Args, Parser, Subcommand};
|
|
||||||
use image::ImageReader;
|
|
||||||
use anyhow::{bail, Result};
|
|
||||||
use sqp::{ColorFormat, CompressionType};
|
|
||||||
use utils::{color_format, color_format_to_type, color_type_to_format, exists_decision, Assume};
|
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
|
||||||
#[command(version, about, long_about = None)]
|
|
||||||
struct Cli {
|
|
||||||
#[command(subcommand)]
|
|
||||||
command: Subcommands,
|
|
||||||
|
|
||||||
/// Overwrite output files
|
|
||||||
#[arg(short = 'y', long = "overwrite", conflicts_with = "assumeno")]
|
|
||||||
assumeyes: bool,
|
|
||||||
|
|
||||||
/// Do not overwrite output files
|
|
||||||
#[arg(short = 'n', long = "preserve", conflicts_with = "assumeyes")]
|
|
||||||
assumeno: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
enum Subcommands {
|
|
||||||
/// Encode an image to SQP format
|
|
||||||
Encode(EncodeArgs),
|
|
||||||
|
|
||||||
/// Decode an SQP image into another format
|
|
||||||
Decode(DecodeArgs),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
struct EncodeArgs {
|
|
||||||
/// Input image file of any type supported by `image`
|
|
||||||
input: PathBuf,
|
|
||||||
/// Output path to SQP location
|
|
||||||
output: PathBuf,
|
|
||||||
|
|
||||||
/// Quality setting, a higher value = higher quality.
|
|
||||||
#[arg(default_value_t = 100, short, long, conflicts_with = "uncompressed", value_parser = clap::value_parser!(u8).range(1..=100))]
|
|
||||||
quality: u8,
|
|
||||||
|
|
||||||
/// Create an uncompressed image.
|
|
||||||
///
|
|
||||||
/// Incompatible with quality setting.
|
|
||||||
#[arg(short, long, conflicts_with = "quality")]
|
|
||||||
uncompressed: bool,
|
|
||||||
|
|
||||||
/// The color format to use for the output image
|
|
||||||
///
|
|
||||||
/// Valid values:
|
|
||||||
/// - RGBA8
|
|
||||||
/// - RGB8
|
|
||||||
/// - GrayA8
|
|
||||||
/// - Gray8
|
|
||||||
#[arg(short, long, value_parser = color_format, verbatim_doc_comment)]
|
|
||||||
color_format: Option<ColorFormat>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Args)]
|
|
||||||
struct DecodeArgs {
|
|
||||||
/// Input SQP image file
|
|
||||||
input: PathBuf,
|
|
||||||
|
|
||||||
/// Output image file
|
|
||||||
output: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let args = Cli::parse();
|
|
||||||
|
|
||||||
let assume = if args.assumeyes {
|
|
||||||
Some(Assume::Yes)
|
|
||||||
} else if args.assumeno {
|
|
||||||
Some(Assume::No)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
match args.command {
|
|
||||||
Subcommands::Encode(a) => encode(a, assume),
|
|
||||||
Subcommands::Decode(a) => decode(a, assume),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn encode(args: EncodeArgs, assume: Option<Assume>) -> Result<()> {
|
|
||||||
if !args.input.try_exists()? {
|
|
||||||
bail!("Input file {:?} does not exist", args.input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.output.try_exists()?
|
|
||||||
&& !exists_decision("Output", "Overwrite", &args.output, assume)
|
|
||||||
{
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let image = ImageReader::open(args.input)?
|
|
||||||
.decode()?;
|
|
||||||
|
|
||||||
let width = image.width();
|
|
||||||
let height = image.height();
|
|
||||||
|
|
||||||
let color_format = args.color_format.or_else(
|
|
||||||
|| color_type_to_format(image.color())
|
|
||||||
).unwrap_or(ColorFormat::Rgba8);
|
|
||||||
|
|
||||||
let bitmap = match color_format {
|
|
||||||
ColorFormat::Rgba8 => image.into_rgba8().into_vec(),
|
|
||||||
ColorFormat::Rgb8 => image.into_rgb8().into_vec(),
|
|
||||||
ColorFormat::GrayA8 => image.into_luma_alpha8().into_vec(),
|
|
||||||
ColorFormat::Gray8 => image.into_luma8().into_vec(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let (compression_type, quality) = if args.uncompressed {
|
|
||||||
(CompressionType::None, None)
|
|
||||||
} else if args.quality == 100 {
|
|
||||||
(CompressionType::Lossless, None)
|
|
||||||
} else {
|
|
||||||
(CompressionType::LossyDct, Some(args.quality))
|
|
||||||
};
|
|
||||||
|
|
||||||
let sqp_img = sqp::SquishyPicture::from_raw(
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
color_format,
|
|
||||||
compression_type,
|
|
||||||
quality,
|
|
||||||
bitmap,
|
|
||||||
);
|
|
||||||
|
|
||||||
sqp_img.save(&args.output)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode(args: DecodeArgs, assume: Option<Assume>) -> Result<()> {
|
|
||||||
if !args.input.try_exists()? {
|
|
||||||
bail!("Input file {:?} does not exist", args.input);
|
|
||||||
}
|
|
||||||
|
|
||||||
if args.output.try_exists()?
|
|
||||||
&& !exists_decision("Output", "Overwrite", &args.output, assume)
|
|
||||||
{
|
|
||||||
return Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
let sqp_img = sqp::open(args.input)?;
|
|
||||||
|
|
||||||
let width = sqp_img.width();
|
|
||||||
let height = sqp_img.height();
|
|
||||||
let color_format = color_format_to_type(sqp_img.color_format());
|
|
||||||
|
|
||||||
let mut img = sqp_img.into_raw();
|
|
||||||
img.truncate(width as usize * height as usize * color_format.bytes_per_pixel() as usize);
|
|
||||||
|
|
||||||
image::save_buffer(
|
|
||||||
args.output,
|
|
||||||
&img,
|
|
||||||
width,
|
|
||||||
height,
|
|
||||||
color_format,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
|
@ -1,74 +0,0 @@
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use image::ColorType;
|
|
||||||
use sqp::ColorFormat;
|
|
||||||
use text_io::read;
|
|
||||||
|
|
||||||
pub enum Assume {
|
|
||||||
Yes,
|
|
||||||
No,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn color_format(s: &str) -> Result<ColorFormat, String> {
|
|
||||||
if !s.is_ascii() {
|
|
||||||
return Err(format!("Invalid color format {}", s))
|
|
||||||
}
|
|
||||||
|
|
||||||
let s_lower = s.to_lowercase();
|
|
||||||
|
|
||||||
let color_format = match s_lower.as_str() {
|
|
||||||
"rgba8" => ColorFormat::Rgba8,
|
|
||||||
"rgb8" => ColorFormat::Rgb8,
|
|
||||||
"graya8" => ColorFormat::GrayA8,
|
|
||||||
"gray8" => ColorFormat::Gray8,
|
|
||||||
_ => return Err(format!("Invalid color format {}", s)),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(color_format)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn color_type_to_format(img_color_format: ColorType) -> Option<ColorFormat> {
|
|
||||||
Some(match img_color_format {
|
|
||||||
ColorType::L8 => ColorFormat::Gray8,
|
|
||||||
ColorType::La8 => ColorFormat::GrayA8,
|
|
||||||
ColorType::Rgb8 => ColorFormat::Rgb8,
|
|
||||||
ColorType::Rgba8 => ColorFormat::Rgba8,
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn color_format_to_type(img_color_format: ColorFormat) -> ColorType {
|
|
||||||
match img_color_format {
|
|
||||||
ColorFormat::Gray8 => ColorType::L8,
|
|
||||||
ColorFormat::GrayA8 => ColorType::La8,
|
|
||||||
ColorFormat::Rgb8 => ColorType::Rgb8,
|
|
||||||
ColorFormat::Rgba8 => ColorType::Rgba8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exists_decision<P: AsRef<Path>>(place: &str, action: &str, path: &P, assume: Option<Assume>) -> bool {
|
|
||||||
let path = path.as_ref();
|
|
||||||
|
|
||||||
match assume {
|
|
||||||
Some(Assume::Yes) => return true,
|
|
||||||
Some(Assume::No) => return false,
|
|
||||||
None => (),
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
|
||||||
print!("{place} file {path:?} already exists. {action}? [y/N] ");
|
|
||||||
|
|
||||||
let opt: String = read!("{}\n");
|
|
||||||
let opt = opt.to_lowercase();
|
|
||||||
|
|
||||||
if !opt.is_empty() && opt == "y" {
|
|
||||||
return true
|
|
||||||
} else if !opt.is_empty() && opt != "y" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if opt.is_empty() {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +1,9 @@
|
||||||
use std::io::{Read, Write};
|
use std::io::{Read, Write};
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
|
|
||||||
/// A simple way to write individual bits to an input implementing [Write].
|
/// A simple way to write individual bits to an input implementing [Write].
|
||||||
pub struct BitWriter<'a, O: Write> {
|
pub struct BitWriter<'a, O: Write + WriteBytesExt> {
|
||||||
output: &'a mut O,
|
output: &'a mut O,
|
||||||
|
|
||||||
current_byte: u8,
|
current_byte: u8,
|
||||||
|
@ -12,7 +14,7 @@ pub struct BitWriter<'a, O: Write> {
|
||||||
byte_size: usize,
|
byte_size: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, O: Write> BitWriter<'a, O> {
|
impl<'a, O: Write + WriteBytesExt> BitWriter<'a, O> {
|
||||||
/// Create a new BitWriter wrapper around something which
|
/// Create a new BitWriter wrapper around something which
|
||||||
/// implements [Write].
|
/// implements [Write].
|
||||||
pub fn new(output: &'a mut O) -> Self {
|
pub fn new(output: &'a mut O) -> Self {
|
||||||
|
@ -33,12 +35,22 @@ impl<'a, O: Write> BitWriter<'a, O> {
|
||||||
self.byte_size
|
self.byte_size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the bit offset within the current byte.
|
||||||
|
pub fn bit_offset(&self) -> u8 {
|
||||||
|
self.bit_offset as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the stream is aligned to a byte.
|
||||||
|
pub fn aligned(&self) -> bool {
|
||||||
|
self.bit_offset() == 0
|
||||||
|
}
|
||||||
|
|
||||||
/// Align the writer to the nearest byte by padding with zero bits.
|
/// Align the writer to the nearest byte by padding with zero bits.
|
||||||
pub fn flush(&mut self) {
|
pub fn flush(&mut self) {
|
||||||
self.byte_offset += 1;
|
self.byte_offset += 1;
|
||||||
|
|
||||||
// Write out the current byte unfinished
|
// Write out the current byte unfinished
|
||||||
self.output.write_all(&[self.current_byte]).unwrap();
|
self.output.write_u8(self.current_byte).unwrap();
|
||||||
self.current_byte = 0;
|
self.current_byte = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,12 +79,12 @@ impl<'a, O: Write> BitWriter<'a, O> {
|
||||||
self.byte_offset += 1;
|
self.byte_offset += 1;
|
||||||
self.bit_offset = 0;
|
self.bit_offset = 0;
|
||||||
|
|
||||||
self.output.write_all(&[self.current_byte]).unwrap();
|
self.output.write_u8(self.current_byte).unwrap();
|
||||||
self.current_byte = 0;
|
self.current_byte = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.byte_size = self.byte_offset + self.bit_offset.div_ceil(8);
|
self.byte_size = self.byte_offset + (self.bit_offset + 7) / 8;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write some bytes to the output.
|
/// Write some bytes to the output.
|
||||||
|
@ -88,12 +100,12 @@ impl<'a, O: Write> BitWriter<'a, O> {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
self.byte_offset += byte_len;
|
self.byte_offset += byte_len;
|
||||||
|
|
||||||
self.byte_size = self.byte_offset + self.bit_offset.div_ceil(8);
|
self.byte_size = self.byte_offset + (self.bit_offset + 7) / 8;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A simple way to read individual bits from an input implementing [Read].
|
/// A simple way to read individual bits from an input implementing [Read].
|
||||||
pub struct BitReader<'a, I: Read> {
|
pub struct BitReader<'a, I: Read + ReadBytesExt> {
|
||||||
input: &'a mut I,
|
input: &'a mut I,
|
||||||
|
|
||||||
current_byte: Option<u8>,
|
current_byte: Option<u8>,
|
||||||
|
@ -102,16 +114,15 @@ pub struct BitReader<'a, I: Read> {
|
||||||
bit_offset: usize,
|
bit_offset: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a, I: Read> BitReader<'a, I> {
|
impl<'a, I: Read + ReadBytesExt> BitReader<'a, I> {
|
||||||
/// Create a new BitReader wrapper around something which
|
/// Create a new BitReader wrapper around something which
|
||||||
/// implements [Write].
|
/// implements [Write].
|
||||||
pub fn new(input: &'a mut I) -> Self {
|
pub fn new(input: &'a mut I) -> Self {
|
||||||
let mut buf = [0u8];
|
let first = input.read_u8().unwrap();
|
||||||
input.read_exact(&mut buf).unwrap();
|
|
||||||
Self {
|
Self {
|
||||||
input,
|
input,
|
||||||
|
|
||||||
current_byte: Some(buf[0]),
|
current_byte: Some(first),
|
||||||
|
|
||||||
byte_offset: 0,
|
byte_offset: 0,
|
||||||
bit_offset: 0,
|
bit_offset: 0,
|
||||||
|
@ -144,10 +155,7 @@ impl<'a, I: Read> BitReader<'a, I> {
|
||||||
self.byte_offset += 1;
|
self.byte_offset += 1;
|
||||||
self.bit_offset = 0;
|
self.bit_offset = 0;
|
||||||
|
|
||||||
let mut buf = [0u8];
|
self.current_byte = Some(self.input.read_u8().unwrap());
|
||||||
self.input.read_exact(&mut buf).unwrap();
|
|
||||||
|
|
||||||
self.current_byte = Some(buf[0]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
result |= bit_value << i;
|
result |= bit_value << i;
|
|
@ -100,9 +100,6 @@ pub fn idct(input: &[f32], width: usize, height: usize) -> Vec<u8> {
|
||||||
///
|
///
|
||||||
/// Instead of using this, use the [`quantization_matrix`] function to
|
/// Instead of using this, use the [`quantization_matrix`] function to
|
||||||
/// get a quantization matrix corresponding to the image quality value.
|
/// 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] = [
|
const BASE_QUANTIZATION_MATRIX: [u16; 64] = [
|
||||||
16, 11, 10, 16, 24, 40, 51, 61,
|
16, 11, 10, 16, 24, 40, 51, 61,
|
||||||
12, 12, 14, 19, 26, 58, 60, 55,
|
12, 12, 14, 19, 26, 58, 60, 55,
|
|
@ -1,13 +1,12 @@
|
||||||
//! Structs and enums which are included in the header of SQP files.
|
//! Structs and enums which are included in the header of SQP files.
|
||||||
|
|
||||||
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
|
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
|
||||||
use std::io::{self, Read, Write};
|
use std::io::{Cursor, Read, Write};
|
||||||
|
|
||||||
use crate::picture::Error;
|
use crate::picture::Error;
|
||||||
|
|
||||||
/// A DPF file header. This must be included at the beginning
|
/// A DPF file header. This must be included at the beginning
|
||||||
/// of a valid DPF file.
|
/// of a valid DPF file.
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Header {
|
pub struct Header {
|
||||||
/// Identifier. Must be set to "dangoimg".
|
/// Identifier. Must be set to "dangoimg".
|
||||||
pub magic: [u8; 8],
|
pub magic: [u8; 8],
|
||||||
|
@ -43,26 +42,21 @@ impl Default for Header {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Header {
|
impl Header {
|
||||||
/// Write the header into a byte stream implementing [`Write`].
|
pub fn to_bytes(&self) -> [u8; 19] {
|
||||||
///
|
let mut buf = Cursor::new(Vec::new());
|
||||||
/// Returns the number of bytes written.
|
|
||||||
pub fn write_into<W: Write + WriteBytesExt>(&self, output: &mut W) -> Result<usize, io::Error> {
|
buf.write_all(&self.magic).unwrap();
|
||||||
let mut count = 0;
|
buf.write_u32::<LE>(self.width).unwrap();
|
||||||
output.write_all(&self.magic)?;
|
buf.write_u32::<LE>(self.height).unwrap();
|
||||||
output.write_u32::<LE>(self.width)?;
|
|
||||||
output.write_u32::<LE>(self.height)?;
|
|
||||||
count += 16;
|
|
||||||
|
|
||||||
// Write compression info
|
// Write compression info
|
||||||
output.write_u8(self.compression_type.into())?;
|
buf.write_u8(self.compression_type.into()).unwrap();
|
||||||
output.write_u8(self.quality)?;
|
buf.write_u8(self.quality).unwrap();
|
||||||
count += 2;
|
|
||||||
|
|
||||||
// Write color format
|
// Write color format
|
||||||
output.write_u8(self.color_format as u8)?;
|
buf.write_u8(self.color_format as u8).unwrap();
|
||||||
count += 1;
|
|
||||||
|
|
||||||
Ok(count)
|
buf.into_inner().try_into().unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Length of the header in bytes.
|
/// Length of the header in bytes.
|
||||||
|
@ -71,14 +65,13 @@ impl Header {
|
||||||
19
|
19
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a header from a byte stream implementing [`Read`].
|
/// Create a header from something implementing [`Read`].
|
||||||
pub fn read_from<R: Read + ReadBytesExt>(input: &mut R) -> Result<Self, Error> {
|
pub fn read_from<T: Read + ReadBytesExt>(input: &mut T) -> Result<Self, Error> {
|
||||||
let mut magic = [0u8; 8];
|
let mut magic = [0u8; 8];
|
||||||
input.read_exact(&mut magic).unwrap();
|
input.read_exact(&mut magic).unwrap();
|
||||||
|
|
||||||
if magic != *b"dangoimg" {
|
if magic != *b"dangoimg" {
|
||||||
let bad_id = String::from_utf8_lossy(&magic).into_owned();
|
return Err(Error::InvalidIdentifier(magic));
|
||||||
return Err(Error::InvalidIdentifier(bad_id));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Header {
|
Ok(Header {
|
|
@ -18,10 +18,8 @@
|
||||||
//! let width = 2;
|
//! let width = 2;
|
||||||
//! let height = 2;
|
//! let height = 2;
|
||||||
//! let bitmap = vec![
|
//! let bitmap = vec![
|
||||||
//! 0xFF, 0xFF, 0xFF, 0xFF,
|
//! 255, 255, 255, 255, 0, 255, 0, 128,
|
||||||
//! 0x00, 0x80, 0x00, 0x80,
|
//! 255, 255, 255, 255, 0, 255, 0, 128
|
||||||
//! 0xFF, 0xFF, 0xFF, 0xFF,
|
|
||||||
//! 0x00, 0x80, 0x00, 0x80,
|
|
||||||
//! ];
|
//! ];
|
||||||
//!
|
//!
|
||||||
//! // Create a 2×2 image in memory. Nothing is compressed or encoded
|
//! // 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
|
// Interleave the offset alpha into the RGB bytes
|
||||||
data[rgb_index..rgb_index + width as usize * (color_format.pbc() - 1)]
|
data[rgb_index..rgb_index + width as usize * (color_format.pbc() - 1)]
|
||||||
.chunks(color_format.pbc() - 1)
|
.chunks(color_format.pbc() - 1)
|
||||||
.zip(data[alpha_index..alpha_index + width as usize].iter())
|
.zip(data[alpha_index..alpha_index + width as usize].into_iter())
|
||||||
.flat_map(|(a, b)| {
|
.flat_map(|(a, b)| {
|
||||||
a.iter().chain(vec![b])
|
a.into_iter().chain(vec![b])
|
||||||
})
|
})
|
||||||
.copied()
|
.copied()
|
||||||
.collect()
|
.collect()
|
|
@ -1,6 +1,8 @@
|
||||||
//! Functions and other utilities surrounding the [`SquishyPicture`] type.
|
//! Functions and other utilities surrounding the [`SquishyPicture`] type.
|
||||||
|
|
||||||
use std::{fs::File, io::{self, BufWriter, Read, Write}, path::Path};
|
use std::{fs::File, io::{self, BufWriter, Read, Write}, path::Path};
|
||||||
|
|
||||||
|
use byteorder::{ReadBytesExt, WriteBytesExt};
|
||||||
use integer_encoding::VarInt;
|
use integer_encoding::VarInt;
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
@ -11,18 +13,14 @@ use crate::{
|
||||||
operations::{add_rows, sub_rows},
|
operations::{add_rows, sub_rows},
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An error which occured while manipulating a [`SquishyPicture`].
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
/// The file signature was invalid. Must be "dangoimg".
|
#[error("incorrect identifier, got {0:02X?}")]
|
||||||
#[error("incorrect signature, expected \"dangoimg\" got {0:?}")]
|
InvalidIdentifier([u8; 8]),
|
||||||
InvalidIdentifier(String),
|
|
||||||
|
|
||||||
/// Any I/O operation failed.
|
|
||||||
#[error("io operation failed: {0}")]
|
#[error("io operation failed: {0}")]
|
||||||
IoError(#[from] io::Error),
|
IoError(#[from] io::Error),
|
||||||
|
|
||||||
/// There was an error while compressing or decompressing.
|
|
||||||
#[error("compression operation failed: {0}")]
|
#[error("compression operation failed: {0}")]
|
||||||
CompressionError(#[from] CompressionError),
|
CompressionError(#[from] CompressionError),
|
||||||
}
|
}
|
||||||
|
@ -144,11 +142,12 @@ impl SquishyPicture {
|
||||||
/// Encode the image into anything that implements [`Write`].
|
/// Encode the image into anything that implements [`Write`].
|
||||||
///
|
///
|
||||||
/// Returns the number of bytes written.
|
/// Returns the number of bytes written.
|
||||||
pub fn encode<O: Write>(&self, mut output: O) -> Result<usize, Error> {
|
pub fn encode<O: Write + WriteBytesExt>(&self, mut output: O) -> Result<usize, Error> {
|
||||||
let mut count = 0;
|
let mut count = 0;
|
||||||
|
|
||||||
// Write out the header
|
// Write out the header
|
||||||
count += self.header.write_into(&mut output)?;
|
output.write_all(&self.header.to_bytes()).unwrap();
|
||||||
|
count += self.header.len();
|
||||||
|
|
||||||
// Based on the compression type, modify the data accordingly
|
// Based on the compression type, modify the data accordingly
|
||||||
let modified_data = match self.header.compression_type {
|
let modified_data = match self.header.compression_type {
|
||||||
|
@ -203,14 +202,14 @@ impl SquishyPicture {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode the image from anything that implements [`Read`]
|
/// Decode the image from anything that implements [`Read`]
|
||||||
pub fn decode<I: Read>(mut input: I) -> Result<Self, Error> {
|
pub fn decode<I: Read + ReadBytesExt>(mut input: I) -> Result<Self, Error> {
|
||||||
let header = Header::read_from(&mut input)?;
|
let header = Header::read_from(&mut input)?;
|
||||||
|
|
||||||
let compression_info = CompressionInfo::read_from(&mut input);
|
let compression_info = CompressionInfo::read_from(&mut input);
|
||||||
|
|
||||||
let pre_bitmap = decompress(&mut input, &compression_info);
|
let pre_bitmap = decompress(&mut input, &compression_info);
|
||||||
|
|
||||||
let mut bitmap = match header.compression_type {
|
let bitmap = match header.compression_type {
|
||||||
CompressionType::None => pre_bitmap,
|
CompressionType::None => pre_bitmap,
|
||||||
CompressionType::Lossless => {
|
CompressionType::Lossless => {
|
||||||
add_rows(
|
add_rows(
|
||||||
|
@ -233,8 +232,6 @@ impl SquishyPicture {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
bitmap.truncate(header.width as usize * header.height as usize * header.color_format.pbc());
|
|
||||||
|
|
||||||
Ok(Self { header, bitmap })
|
Ok(Self { header, bitmap })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,28 +239,8 @@ impl SquishyPicture {
|
||||||
pub fn as_raw(&self) -> &Vec<u8> {
|
pub fn as_raw(&self) -> &Vec<u8> {
|
||||||
&self.bitmap
|
&self.bitmap
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the underlying raw buffer
|
|
||||||
pub fn into_raw(self) -> Vec<u8> {
|
|
||||||
self.bitmap
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The width of the image in pixels
|
|
||||||
pub fn width(&self) -> u32 {
|
|
||||||
self.header.width
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The height of the image in pixels
|
|
||||||
pub fn height(&self) -> u32 {
|
|
||||||
self.header.height
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn color_format(&self) -> ColorFormat {
|
|
||||||
self.header.color_format
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decode a stream encoded as varints.
|
|
||||||
fn decode_varint_stream(stream: &[u8]) -> Vec<i16> {
|
fn decode_varint_stream(stream: &[u8]) -> Vec<i16> {
|
||||||
let mut output = Vec::new();
|
let mut output = Vec::new();
|
||||||
let mut offset = 0;
|
let mut offset = 0;
|
||||||
|
@ -276,12 +253,8 @@ fn decode_varint_stream(stream: &[u8]) -> Vec<i16> {
|
||||||
output
|
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> {
|
pub fn open<P: AsRef<Path>>(path: P) -> Result<SquishyPicture, Error> {
|
||||||
let input = File::open(path)?;
|
let input = File::open(path)?;
|
||||||
|
|
||||||
SquishyPicture::decode(input)
|
Ok(SquishyPicture::decode(input)?)
|
||||||
}
|
}
|
Loading…
Reference in a new issue