Compare commits

..

No commits in common. "main" and "0.1.1" have entirely different histories.
main ... 0.1.1

8 changed files with 191 additions and 736 deletions

View File

@ -1,27 +0,0 @@
# .github/workflows/release.yml
on:
release:
types: [created]
jobs:
release:
name: release ${{ matrix.target }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-pc-windows-gnu
archive: zip
- target: x86_64-unknown-linux-musl
archive: tar.gz tar.xz tar.zst
steps:
- uses: actions/checkout@master
- name: Compile and release
uses: rust-build/rust-build.action@v1.4.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
RUSTTARGET: ${{ matrix.target }}
ARCHIVE_TYPES: ${{ matrix.archive }}

View File

@ -1,22 +0,0 @@
name: Rust
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build
run: cargo build --verbose
- name: Run tests
run: cargo test --verbose

2
.gitignore vendored
View File

@ -1,4 +1,2 @@
/target /target
/qoi_test_images /qoi_test_images
*.qoi
*.png

282
Cargo.lock generated
View File

@ -3,65 +3,16 @@
version = 3 version = 3
[[package]] [[package]]
name = "adler2" name = "adler"
version = "2.0.0" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "anstream"
version = "0.6.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
[[package]]
name = "anstyle-parse"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2109dbce0e72be3ec00bed26e6a7479ca384ad226efdd66db8fa2e3a38c83125"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]] [[package]]
name = "array-init" name = "array-init"
version = "2.1.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d62b7694a562cdf5a74227903507c56ab2cc8bdd1f781ed5cb4cf9c9f810bfc" checksum = "bfb6d71005dc22a708c7496eee5c8dc0300ee47355de6256c3b35b12b5fef596"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
@ -75,52 +26,6 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "clap"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb3b4b9e5a7c7514dfa52869339ee98b3156b0bfb4e8a77c4ff4babb64b1604f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b17a95aa67cc7b5ebd32aa5370189aa0d79069ef1c64ce893bd30fb24bff20ec"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb84c814227b90d6895e01398aee0d8033c00e7466aca416fb6a8e0eb19d8a7"
[[package]]
name = "colorchoice"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
[[package]] [[package]]
name = "colors-transform" name = "colors-transform"
version = "0.2.11" version = "0.2.11"
@ -129,206 +34,59 @@ checksum = "9226dbc05df4fb986f48d730b001532580883c4c06c5d1c213f4b34c1c157178"
[[package]] [[package]]
name = "crc32fast" name = "crc32fast"
version = "1.4.2" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]]
name = "fdeflate"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c6f4c64c1d33a3111c4466f7365ebdcc37c5bd1ea0d62aae2e3d722aacbedb"
dependencies = [
"simd-adler32",
]
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.35" version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c" checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
[[package]] [[package]]
name = "log" name = "log"
version = "0.4.22" version = "0.4.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e"
dependencies = [
"cfg-if",
]
[[package]] [[package]]
name = "miniz_oxide" name = "miniz_oxide"
version = "0.8.0" version = "0.5.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1" checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34"
dependencies = [ dependencies = [
"adler2", "adler",
"simd-adler32",
] ]
[[package]] [[package]]
name = "png" name = "png"
version = "0.17.14" version = "0.17.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52f9d46a34a05a6a57566bc2bfae066ef07585a6e3fa30fbbdff5936380623f0" checksum = "8f0e7f4c94ec26ff209cee506314212639d6c91b80afb82984819fafce9df01c"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crc32fast", "crc32fast",
"fdeflate",
"flate2", "flate2",
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "proc-macro2"
version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [
"unicode-ident",
]
[[package]] [[package]]
name = "qoi" name = "qoi"
version = "0.9.9" version = "0.1.1"
dependencies = [ dependencies = [
"array-init", "array-init",
"clap",
"colors-transform", "colors-transform",
"log", "log",
"png", "png",
] ]
[[package]]
name = "quote"
version = "1.0.37"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
dependencies = [
"proc-macro2",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "2.0.87"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "windows-sys"
version = "0.59.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"

View File

@ -1,6 +1,6 @@
[package] [package]
name = "qoi" name = "qoi"
version = "0.9.9" version = "0.1.1"
edition = "2021" edition = "2021"
authors = ["nihil carcosa <nihil@valhrafnaz.gay>"] authors = ["nihil carcosa <nihil@valhrafnaz.gay>"]
description = "CLI tool and rust library for the de- and encoding of images to the QOI format." description = "CLI tool and rust library for the de- and encoding of images to the QOI format."
@ -10,15 +10,14 @@ repository = "https://git.valhrafnaz.gay/valhrafnaz/qoi-img"
license-file = "LICENSE" license-file = "LICENSE"
keywords = ["image_compression", "encoding", "qoi", "cli", "library"] keywords = ["image_compression", "encoding", "qoi", "cli", "library"]
categories = ["command-line-utilities", "encoding", "graphics", "compression"] categories = ["command-line-utilities", "encoding", "graphics", "compression"]
exclude = ["/qoi_test_images"] exlude = ["/qoi_test_images"]
publish = ["gitea"] publish = false
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
array-init = "2.0.1" array-init = "2.0.1"
png = "0.17.14" png = "0.17.6"
log = "0.4.22" log = "0.4.17"
colors-transform = "0.2.11" colors-transform = "0.2.11"
clap = { version = "4.5.18", features = ["derive"] }

View File

@ -1,21 +1,11 @@
# QOI (Quite OK Image Format) Rust De-/Encoder # QOI (Quite OK Image Format) Rust De-/Encoder
This is a rust CLI application and library to decode and encode raw pixel data to and from the .qoi image format. This is a (currently not bug free) implementation of the QOI image compression algorithm in Rust.
See [qoiformat.org](https://qoiformat.org/) for more information on the format. Both decoder and encoder pass all the test images provided by the format's maintainers.
I have created this largely to learn rust myself and do not recommend using this crate over the [Image crate](https://crates.io/crates/image). Currently supports de- and encoding from and to PNG.
Please observe that this crate is licensed under the GPL-v3-or-later only and can thus not be for non-FOSS projects. No MIT/BSD dual-licensing will be considered.
## To install Current state is highly buggy and strangely only functions with 256 x 256 images.
Simply grab the [latest release](https://git.valhrafnaz.gay/valhrafnaz/qoi-img/releases/latest) and place the binary in a path that is searched via your `$PATH` variable. Please keep in mind that the release binary provided is for linux-x86_64 only (code uses u64 and as thus is likely not functional on IA-32). Should you wish to run the program on different operating systems, please refer to build instructions below.
## To build ## To build
Make sure rustup has cargo and your preferred toolchain installed. run `cargo build -r` to build a stable version for your rustc toolchain in `./target/release`
Clone the repository by running `git clone https://git.valhrafnaz.gay/valhrafnaz/qoi-img.git`
Move into the directory `cd qoi-img`
Run `cargo build -r` to build a stable version for your rustc toolchain in `./target/release`.

View File

@ -1,17 +1,14 @@
//! # qoi_img //! # qoi_img
//! `qoi_img` is a bad, from-scratch implementation of the decoder and encoder for the `.qoi` file format as described as on [qoiformat.org](https://qoiformat.org/qoi-specification.pdf). //! `qoi_img` is a bad, from-scratch implementation of the decoder and encoder for the `.qoi` file format as described as on [qoiformat.org](https://qoiformat.org/qoi-specification.pdf).
//! This crate should not be published as better crates are available, e.g. [rapid-qoi](https://crates.io/crates/rapid-qoi). //! This crate should not be published as better crates are available, e.g. [rapid-qoi](https://crates.io/crates/rapid-qoi).
#![allow(dead_code, unused_variables)] #![allow(dead_code, unused_variables)]
pub mod qoi_lib { pub mod qoi_lib {
use log::{debug, info, Level, LevelFilter, Record, SetLoggerError}; use log::{debug, info, Level, LevelFilter, Record, SetLoggerError};
use std::fmt; use std::fmt;
use std::fs::*; use std::fs::File;
use std::io::prelude::*; use std::io::prelude::*;
use std::io::BufWriter;
use std::path::Path;
use png;
use array_init; use array_init;
@ -43,7 +40,7 @@ pub mod qoi_lib {
} }
//boilerplate implementation of the log crate //boilerplate implementation of the log crate
pub struct SimpleLogger; struct SimpleLogger;
impl log::Log for SimpleLogger { impl log::Log for SimpleLogger {
fn enabled(&self, metadata: &log::Metadata) -> bool { fn enabled(&self, metadata: &log::Metadata) -> bool {
@ -63,11 +60,9 @@ pub mod qoi_lib {
/// ///
/// ``` /// ```
/// # use std::error::Error; /// # use std::error::Error;
/// # use crate::qoi::qoi_lib::*; /// # use crate::qoi_test::qoi_img::*;
/// # use log::*;
/// # fn main() -> Result<(), Box<ImgError>> { /// # fn main() -> Result<(), Box<ImgError>> {
/// let level = LevelFilter::Debug; /// init().expect("Failed to initialize logger");
/// init(level).expect("Failed to initialize logger");
/// # /// #
/// # Ok(()) /// # Ok(())
/// # /// #
@ -78,11 +73,9 @@ pub mod qoi_lib {
/// ///
/// ``` /// ```
/// # use std::error::Error; /// # use std::error::Error;
/// # use crate::qoi::qoi_lib::*; /// # use crate::qoi_test::qoi_img::*;
/// # use log::*;
/// # fn main() -> Result<(), ImgError> { /// # fn main() -> Result<(), ImgError> {
/// let level = LevelFilter::Debug; /// match init() {
/// match init(level) {
/// Ok(()) => (), /// Ok(()) => (),
/// Err(e) => println!("Logger failed to initialize!") /// Err(e) => println!("Logger failed to initialize!")
/// } /// }
@ -91,8 +84,8 @@ pub mod qoi_lib {
/// # /// #
/// # } /// # }
/// ``` /// ```
pub fn init(level: LevelFilter) -> Result<(), SetLoggerError> { pub fn init() -> Result<(), SetLoggerError> {
log::set_logger(&LOGGER).map(|()| log::set_max_level(level)) log::set_logger(&LOGGER).map(|()| log::set_max_level(LevelFilter::Debug))
} }
/// Custom image struct, which is used to store decoded data. Used by [encode_from_image] to encode the necessary data in bytes. Requires a Vector over [Pixel] values, `Vec<Pixel>`, /// Custom image struct, which is used to store decoded data. Used by [encode_from_image] to encode the necessary data in bytes. Requires a Vector over [Pixel] values, `Vec<Pixel>`,
@ -104,7 +97,7 @@ pub mod qoi_lib {
/// Create a new image via constructor [`Image::new()`]; /// Create a new image via constructor [`Image::new()`];
/// ```rust /// ```rust
/// # use std::error::Error; /// # use std::error::Error;
/// # use crate::qoi::qoi_lib::*; /// # use crate::qoi_test::qoi_img::*;
/// # fn main() -> Result<(), Box<ImgError>> { /// # fn main() -> Result<(), Box<ImgError>> {
/// ///
/// let pixels: Vec<u8> = vec![0;1024*1024*4]; /// let pixels: Vec<u8> = vec![0;1024*1024*4];
@ -136,13 +129,7 @@ pub mod qoi_lib {
channels: u8, channels: u8,
colorspace: u8, colorspace: u8,
) -> Result<Image, ImgError> { ) -> Result<Image, ImgError> {
let alpha: bool; let pixels: Vec<Pixel> = match Image::pixels_from_bytes(data) {
if channels == 4 {
alpha = true;
} else {
alpha = false;
}
let pixels: Vec<Pixel> = match Image::pixels_from_bytes(data, alpha) {
Ok(out) => out, Ok(out) => out,
Err(error) => return Err(error), Err(error) => return Err(error),
}; };
@ -177,82 +164,21 @@ pub mod qoi_lib {
img img
} }
//Expects pixel data in order left to right, top to bottom, with values for rgba in sequential order fn pixels_from_bytes(data: Vec<u8>) -> Result<Vec<Pixel>, ImgError> {
fn pixels_from_bytes(data: Vec<u8>, alpha: bool) -> Result<Vec<Pixel>, ImgError> {
let mut pixels: Vec<Pixel> = Vec::with_capacity(data.len() / 4); let mut pixels: Vec<Pixel> = Vec::with_capacity(data.len() / 4);
if alpha { if data.len() % 4 == 0 {
if data.len() % 4 == 0 { for i in 0..data.len() / 4 {
for i in 0..data.len() / 4 { pixels.push(Pixel {
pixels.push(Pixel { r: data[i * 4],
r: data[i * 4], g: data[i * 4 + 1],
g: data[i * 4 + 1], b: data[i * 4 + 2],
b: data[i * 4 + 2], a: data[i * 4 + 3],
a: data[i * 4 + 3], });
});
}
Ok(pixels)
} else {
Err(ImgError::DataError)
} }
Ok(pixels)
} else { } else {
if data.len() % 4 == 0 { Err(ImgError::DataError)
for i in 0..data.len() / 3 {
pixels.push(Pixel {
r: data[i * 3],
g: data[i * 3 + 1],
b: data[i * 3 + 2],
a: 255,
});
}
Ok(pixels)
} else {
Err(ImgError::DataError)
}
} }
}
pub fn to_bytes(&self) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::with_capacity(self.height as usize * self.width as usize * 4 as usize);
for pixel in &self.pixels {
buf.push(pixel.r);
buf.push(pixel.g);
buf.push(pixel.b);
buf.push(pixel.a);
}
return buf;
}
pub fn write_png(&self, path: &str) {
let mut file_path: String = String::new();
file_path.push_str(path);
if !path.contains(".png") {
file_path.push_str(".png");
}
let path = Path::new(&file_path);
let file = match File::create(path) {
Ok(f) => f,
Err(e) => panic!("ERROR during writing output file: {e:?}")
};
let buf: Vec<u8> = self.to_bytes();
let ref mut w = BufWriter::new(file);
let mut encoder = png::Encoder::new(w, self.width, self.height);
encoder.set_color(png::ColorType::Rgba);
encoder.set_depth(png::BitDepth::Eight);
encoder.set_source_gamma(png::ScaledFloat::new(1.0 / 2.2)); // 1.0 / 2.2, unscaled, but rounded
let source_chromaticities = png::SourceChromaticities::new( // Using unscaled instantiation here
(0.31270, 0.32900),
(0.64000, 0.33000),
(0.30000, 0.60000),
(0.15000, 0.06000)
);
encoder.set_source_chromaticities(source_chromaticities);
let mut writer = encoder.write_header().unwrap();
match writer.write_image_data(&buf) {
Ok(_a) => (),
Err(e) => panic!("Cannot write output file! {e:?}")
}
writer.finish().unwrap();
} }
} }
@ -473,7 +399,7 @@ pub mod qoi_lib {
} }
let mut encoded_bytes: Vec<u8> = Vec::new(); let mut encoded_bytes: Vec<u8> = Vec::new();
let mut run: u64 = 0; let mut run: u8 = 0;
let head = Header { let head = Header {
magic: ['q', 'o', 'i', 'f'], magic: ['q', 'o', 'i', 'f'],
@ -488,7 +414,7 @@ pub mod qoi_lib {
encoded_bytes.push(i); encoded_bytes.push(i);
} }
let mut counter: u64 = 0; let mut counter: u32 = 0;
for pixel in img.pixels { for pixel in img.pixels {
counter += 1; counter += 1;
@ -506,21 +432,18 @@ pub mod qoi_lib {
encoded_bytes.push(QOI_OP_RUN | (62 - RUN_BIAS)); encoded_bytes.push(QOI_OP_RUN | (62 - RUN_BIAS));
run -= 62; run -= 62;
} else if run % 62 > 0 { } else if run % 62 > 0 {
let run_remainder: u8 = run.try_into().unwrap(); encoded_bytes.push(QOI_OP_RUN | (run - RUN_BIAS));
encoded_bytes.push(QOI_OP_RUN | (run_remainder - RUN_BIAS));
run = 0; run = 0;
} else { } else {
break; break;
} }
} }
} else { } else {
let run8: u8 = run.try_into().unwrap(); encoded_bytes.push(QOI_OP_RUN | (run - RUN_BIAS));
encoded_bytes.push(QOI_OP_RUN | (run8 - RUN_BIAS));
run = 0; run = 0;
} }
} }
match chunk { match chunk {
(ChunkType::Index, Some((index, irr1, irr2))) => { (ChunkType::Index, Some((index, irr1, irr2))) => {
encoded_bytes.push(QOI_OP_INDEX | index); encoded_bytes.push(QOI_OP_INDEX | index);
@ -586,17 +509,14 @@ pub mod qoi_lib {
encoded_bytes.push(QOI_OP_RUN | (62 - RUN_BIAS)); encoded_bytes.push(QOI_OP_RUN | (62 - RUN_BIAS));
run -= 62; run -= 62;
} else if run % 62 > 0 { } else if run % 62 > 0 {
let run_remainder: u8 = run.try_into().unwrap(); encoded_bytes.push(QOI_OP_RUN | (run - RUN_BIAS));
encoded_bytes.push(QOI_OP_RUN | (run_remainder - RUN_BIAS));
run = 0; run = 0;
} else { } else {
break; break;
} }
} }
} else { } else {
let run8: u8 = run.try_into().unwrap(); encoded_bytes.push(QOI_OP_RUN | (run - RUN_BIAS));
encoded_bytes.push(QOI_OP_RUN | (run8 - RUN_BIAS));
// run = 0;
} }
} }
@ -617,23 +537,10 @@ pub mod qoi_lib {
encoded_bytes encoded_bytes
} }
/// Writes Image as byte vector to file with name given as string slice.
/// ```rust
/// # use qoi::qoi_lib::*;
/// # fn main() {
///
/// let bytes: Vec<u8> = vec![];
/// let name = "qoi-image";
/// write_to_file(bytes, name);
/// #
/// #
/// # }
/// ```
pub fn write_to_file(bytes: Vec<u8>, filename: &str) -> std::io::Result<()> { pub fn write_to_file(bytes: Vec<u8>, filename: &str) -> std::io::Result<()> {
let mut file_path: String = String::from(filename); let mut file_path: String = String::from(filename);
if !filename.contains(".qoi") { file_path.push_str(".qoi");
file_path.push_str(".qoi");
}
let mut buffer = File::create(file_path)?; let mut buffer = File::create(file_path)?;
let mut pos = 0; let mut pos = 0;
@ -853,17 +760,14 @@ pub mod qoi_lib {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use png::ColorType;
use super::*; use super::*;
use std::fs::File;
use std::io; use std::io;
use std::io::{BufReader, Read}; use std::io::{BufReader, Read};
#[test] #[test]
fn diff_test() { fn diff_test() {
let level: LevelFilter = LevelFilter::Debug; init().expect("Logger initialisation failed!");
init(level).expect("Logger initialisation failed!");
let pix1: Pixel = Pixel::new(0, 0, 0, 255); let pix1: Pixel = Pixel::new(0, 0, 0, 255);
let pix2: Pixel = Pixel::new(255, 255, 255, 255); let pix2: Pixel = Pixel::new(255, 255, 255, 255);
@ -876,135 +780,44 @@ pub mod qoi_lib {
assert_eq!(pix3.diff(&pix4), (-5, -5, -5)); assert_eq!(pix3.diff(&pix4), (-5, -5, -5));
} }
/* #[test] #[test]
fn qoi_to_qoi_test() -> io::Result<()> { fn encode_test() -> io::Result<()> {
//Open path to test images let f: File = File::open("test.qoi")?;
let path: &Path = Path::new("./qoi_test_images/"); let mut reader = BufReader::new(f);
let dir: ReadDir = match path.read_dir() { let mut bytes: Vec<u8> = Vec::new();
Ok(d) => d,
Err(e) => panic!("Error reading path {e:?}"),
};
//Loop over files in directory, attempt to decode .qoi images and reencode
for entry in dir {
let file_path = match entry { reader.read_to_end(&mut bytes)?;
Ok(d) => d.path(),
Err(e) => panic!("Non-functional dir entry! \n {e:?}")
};
let file_path_str = match file_path.to_str() {
Some(s) => s,
None => ""
};
if !(file_path_str.contains(".qoi")) {
continue;
}
let file = match File::open(&file_path) { let out_img: super::Image;
Ok(f) => f,
Err(e) => panic!("Error reading file with path {:?}", file_path_str),
};
let mut reader = BufReader::new(file);
let mut bytes: Vec<u8> = Vec::new();
reader.read_to_end(&mut bytes)?; match super::decode(bytes) {
Ok(img) => out_img = img,
let output_image: super::Image; Err(err) => panic!("wallah geht nicht :/ {:?}", err),
match super::decode(bytes) {
Ok(img) => output_image = img,
Err(err) => panic!("Image decode failed for {:?}" , file_path.to_str())
}
let mut name = match file_path.file_name() {
Some(s) => match s.to_str() {
Some(ss) => ss,
None => panic!("File Name Error!")
},
None => panic!("File Name Error!"),
};
name = match name.strip_suffix(".qoi") {
Some(n) => n,
None => name,
};
write_to_file(encode_from_image(output_image), name).expect("Writing image failed!");
} }
write_to_file(encode_from_image(out_img), "test_dec").expect("wallahi!");
Ok(()) Ok(())
} }
#[test] #[test]
fn png_to_qoi_test() -> io::Result<()> { fn decode_test() -> io::Result<()> {
//Open path to test images let f: File = File::open("testcard_rgba.qoi")?;
let path: &Path = Path::new("./qoi_test_images/"); let mut reader = BufReader::new(f);
let dir: ReadDir = match path.read_dir() { let mut bytes: Vec<u8> = Vec::new();
Ok(d) => d,
Err(e) => panic!("Error reading path {e:?}"),
};
//Loop over files in directory, attempt to decode png and encode as qoi, compare to qoi
for entry in dir {
let file_path = match entry { reader.read_to_end(&mut bytes)?;
Ok(d) => d.path(),
Err(e) => panic!("Non-functional dir entry! \n {e:?}")
};
let file_path_str = match file_path.to_str() {
Some(s) => s,
None => ""
};
if !(file_path_str.contains(".png")) {
continue;
}
debug!("{:}",file_path_str);
let file = match File::open(&file_path) {
Ok(f) => f,
Err(e) => panic!("Cannot read file! \n {e:?}")
};
let decoder = png::Decoder::new(file);
let mut reader = match decoder.read_info() {
Ok(reader) => reader,
Err(e) => panic!("ERROR: couldn't decode file: {e:}"),
};
//read image metadata
let width: u32 = reader.info().width;
let height: u32 = reader.info().height;
//for now: hardcoded to 4
let channels = match reader.info().color_type {
ColorType::Rgb => 3,
ColorType::Rgba => 4,
_ => panic!("ERROR: Incompatible png file!")
};
//create buffer matching the size of png-decoder output, writing size to output let out_img: super::Image;
let mut buf = vec![0; reader.output_buffer_size()];
let info = match reader.next_frame(&mut buf) {
Ok(i) => i,
Err(e) => panic!("ERROR: {e:?}"),
};
let bytes = &buf[..info.buffer_size()];
let byte_vec: Vec<u8> = bytes.to_vec();
//create bitmap data from raw byte vector match super::decode(bytes) {
let img: Image = match Image::new(byte_vec, height, width, channels, 0) { Ok(img) => out_img = img,
Ok(image) => image, Err(err) => panic!("Ja bruder war nicht so erfolgreich ahahahahahha {:?}", err),
Err(err) => panic!("Problem generating image: {:?}", err),
};
let encoded_buffer = super::encode_from_image(img);
let mut name = match file_path.file_name() {
None => panic!("whoops!"),
Some(n) => match n.to_str() {
None => panic!("im shiddin"),
Some(s) => s,
},
};
name = match name.strip_suffix(".png") {
Some(n) => n,
None => name,
};
write_to_file(encoded_buffer,name ).expect("Can't write resulting file!");
} }
write_to_file(encode_from_image(out_img), "testcard_new").expect("Boowomb!");
Ok(()) Ok(())
} }
*/
#[test] #[test]
fn tag_test() { fn tag_test() {
//init().expect("Logger initialisation failed!"); //init().expect("Logger initialisation failed!");

View File

@ -1,14 +1,12 @@
use std::env;
use clap::{Args,Parser, Subcommand};
use std::fs::File; use std::fs::File;
use std::io;
use std::io::{BufReader, Read}; use std::io::{BufReader, Read};
use std::process;
use std::time::SystemTime; use std::time::SystemTime;
use colors_transform::{Color, Hsl, Rgb}; use colors_transform::{Color, Hsl, Rgb};
use png; use png;
use qoi::qoi_lib::*; use qoi::qoi_lib::*;
use log::{error,info};
fn encode_checkerboard() { fn encode_checkerboard() {
let mut pixels: Vec<Pixel> = Vec::with_capacity(64 * 64); let mut pixels: Vec<Pixel> = Vec::with_capacity(64 * 64);
@ -61,7 +59,7 @@ fn encode_debug() {
1 => img_data.push(rgb.get_green() as u8), 1 => img_data.push(rgb.get_green() as u8),
2 => img_data.push(rgb.get_blue() as u8), 2 => img_data.push(rgb.get_blue() as u8),
3 => img_data.push(alpha as u8), 3 => img_data.push(alpha as u8),
_ => error!("unrecoverable for-loop failure"), _ => panic!("unrecoverable for-loop failure"),
} }
} }
} }
@ -75,21 +73,50 @@ fn encode_debug() {
let stop = match start.elapsed() { let stop = match start.elapsed() {
Ok(elapsed) => elapsed.as_millis(), Ok(elapsed) => elapsed.as_millis(),
Err(e) => { Err(e) => {
error!("Error: {e:?}"); println!("Error: {e:?}");
return (); return ();
} }
}; };
info!("Encode took: {} ms.", stop); println!("Encode took: {} ms.", stop);
write_to_file(img_bytes, "test").expect("Error writing file!"); write_to_file(img_bytes, "test").expect("Error writing file!");
} }
fn demo() { fn demo() {
let decoder = png::Decoder::new(File::open("testcard_rgba.png").unwrap());
let mut reader = match decoder.read_info() {
Ok(reader) => reader,
Err(e) => panic!("ERROR: couldn't read file: {e:}"),
};
let mut buf = vec![0; reader.output_buffer_size()];
let info = match reader.next_frame(&mut buf) {
Ok(i) => i,
Err(e) => panic!("ERROR: {e:?}"),
};
let bytes = &buf[..info.buffer_size()];
let byte_vec: Vec<u8> = bytes.to_vec();
let img: Image = match Image::new(byte_vec, 256, 256, 4, 0) {
Ok(image) => image,
Err(err) => panic!("Problem generating imag: {:?}", err),
};
let start = SystemTime::now();
write_to_file(encode_from_image(img), "img").expect("Error writing file!");
let stop = match start.elapsed() {
Ok(elapsed) => elapsed.as_millis(),
Err(e) => {
println!("Error: {e:?}");
return ();
}
};
println!("Encode took: {} ms.", stop);
let start = SystemTime::now(); let start = SystemTime::now();
encode_checkerboard(); encode_checkerboard();
let stop = match start.elapsed() { let stop = match start.elapsed() {
Ok(elapsed) => elapsed.as_millis(), Ok(elapsed) => elapsed.as_millis(),
Err(e) => { Err(e) => {
error!("Error: {e:?}"); println!("Error: {e:?}");
return (); return ();
} }
}; };
@ -98,14 +125,12 @@ fn demo() {
} }
//Attempts to encode given png image as second argument into qoi //Attempts to encode given png image as second argument into qoi
fn encode(in_path: &str, out_path: &str) { fn encode(args: &Vec<String>) {
//Path is fetched from arguments
let path = &args[2];
//Init png decoder, attempt to decode png into bitmap, throw error if unsuccessful //Init png decoder, attempt to decode png into bitmap, throw error if unsuccessful
let file:File = File::open(in_path).unwrap_or_else(|e| { let decoder = png::Decoder::new(File::open(path).unwrap());
println!("Error: {:?}", e.to_string());
process::exit(1);
});
let decoder = png::Decoder::new(file);
let mut reader = match decoder.read_info() { let mut reader = match decoder.read_info() {
Ok(reader) => reader, Ok(reader) => reader,
Err(e) => panic!("ERROR: couldn't read file: {e:}"), Err(e) => panic!("ERROR: couldn't read file: {e:}"),
@ -134,19 +159,30 @@ fn encode(in_path: &str, out_path: &str) {
Err(err) => panic!("Problem generating image: {:?}", err), Err(err) => panic!("Problem generating image: {:?}", err),
}; };
//in case out_path is erroneously passed with suffix //encode generated bitmap
let filename = match out_path.strip_suffix(".png") { if args.len() >= 4 {
Some(s) => s, let filename: &String = &args[3];
None => out_path write_to_file(encode_from_image(img), filename).expect("ERROR: Can't write file.");
}; } else {
let mut filename = path.clone();
write_to_file(encode_from_image(img), filename).expect("ERROR: Can't write file."); for _i in 0..4 {
info!("Encoding successful!"); filename.pop();
}
write_to_file(encode_from_image(img), filename.as_str()).expect("ERROR: Can't write file.");
}
println!("Encoding successful!");
} }
fn decode(args: &Vec<String>) -> io::Result<()> {
let mut path: String = String::new();
if args.len() > 2 {
path.push_str(args[2].as_str());
} else {
println!("ERROR: incorrect number of arguments! (specify file to decode!).");
()
}
fn decode(path: &str) -> Result<Image, std::io::Error> { let f: File = match File::open(path.as_str()) {
let f: File = match File::open(path) {
Ok(f) => f, Ok(f) => f,
Err(e) => panic!("ERROR: {e:?}"), Err(e) => panic!("ERROR: {e:?}"),
}; };
@ -156,158 +192,68 @@ fn decode(path: &str) -> Result<Image, std::io::Error> {
reader.read_to_end(&mut bytes)?; reader.read_to_end(&mut bytes)?;
match qoi::qoi_lib::decode(bytes) { match qoi::qoi_lib::decode(bytes) {
Ok(img) => { Ok(_img) => println!("Decoding successful!"),
info!("Decoding successful!");
return Ok(img);
},
Err(err) => panic!("ERROR: {err:?}"), Err(err) => panic!("ERROR: {err:?}"),
} }
Ok(())
} }
fn bench(input: &str, output: Option<String>) { fn bench(args: &Vec<String>) {
if args.len() < 4 {
panic!("ERROR: invalid number of arguments!");
}
let start = SystemTime::now(); let start = SystemTime::now();
let out_path = match output { encode(args);
Some(s) => s,
None => input.strip_suffix(".png").unwrap_or(input).to_owned()
};
encode(input, &out_path);
match start.elapsed() { match start.elapsed() {
Ok(elapsed) => { Ok(elapsed) => println!("Encode took {} μs", elapsed.as_micros()),
if elapsed.as_millis() == 0 {
println!("Encode took {:?} μs to complete", elapsed.as_micros());
} else if elapsed.as_millis() > 999 {
println!("Encode took {:.3} s to complete", elapsed.as_secs_f32());
} else {
println!("Encode took {:?} ms to complete", elapsed.as_millis());
}
},
Err(e) => panic!("ERROR: {e:?}"), Err(e) => panic!("ERROR: {e:?}"),
} }
let mut new_arg: Vec<String> = Vec::new();
new_arg.push(String::from(""));
new_arg.push(String::from(""));
let mut to_push: String = args[3].clone();
to_push.push_str(".qoi");
new_arg.push(to_push);
let start = SystemTime::now(); let start = SystemTime::now();
let mut out_path: String = out_path.to_owned(); decode(&new_arg).expect(
if !out_path.contains(".qoi") { "ERROR: Unspecified error during io-pipeline. Ensure file path is valid and can be read.",
out_path.push_str(".qoi"); );
}
match decode(&out_path) {
Ok(img) => {
//Never fails as long as memory does not corrupt thanks to above push_str op.
let png_path = out_path.strip_suffix(".qoi").unwrap();
img.write_png(&png_path);
},
Err(e) => panic!("Error: {e:?}")
}
match start.elapsed() { match start.elapsed() {
Ok(elapsed) => { Ok(elapsed) => println!("Decode took {} μs", elapsed.as_micros()),
if elapsed.as_millis() == 0 {
println!("Encode took {:?} μs to complete", elapsed.as_micros());
} else if elapsed.as_millis() > 999 {
println!("Encode took {:.3} s to complete", elapsed.as_secs_f32());
} else {
println!("Encode took {:?} ms to complete", elapsed.as_millis());
}
},
Err(e) => panic!("ERROR: {e:?}"), Err(e) => panic!("ERROR: {e:?}"),
} }
} }
#[derive(Parser)]
#[command(name = "QOI Image Transcoder")]
#[command(version, about, long_about = None)]
#[command(next_line_help = true)]
struct Cli {
/// Specify log verbosity, respects multiple applications.
#[arg(short,long, action = clap::ArgAction::Count)]
verbose: Option<u8>,
#[command(subcommand)]
command: Commands
}
#[derive(Subcommand)]
enum Commands {
/// Encode given [IMAGE] from { png } to qoi.
Encode(EncodeArgs),
/// Decode given qoi to specified [FORMAT].
Decode(DecodeArgs),
/// Benchmark en- and decoder by passing in [IMAGE] and optionally specifying [OUTPUT] file.
Bench(BenchArgs),
/// Demo the application.
Demo {
}
}
#[derive(Args)]
struct BenchArgs {
/// File to be encoded.
#[arg(short,long)]
input: String,
/// Optional output path.
#[arg(short,long)]
output: Option<String>
}
#[derive(Args)]
struct DecodeArgs {
/// Qoi file to be decoded
#[arg(short,long)]
input: String,
/// Format to transcode into
#[arg(short,long)]
format: String,
/// Optional file path
#[arg(short,long)]
output: Option<String>
}
#[derive(Args)]
struct EncodeArgs {
// File to be encoded
#[arg(short,long)]
input: String,
// Optional output path
#[arg(short,long)]
output: Option<String>
}
fn main() { fn main() {
let cli: Cli = Cli::parse(); //Initialize logger
init().expect("Failed to initialize logger.");
let args: Vec<String> = env::args().collect();
match &cli.command { if args.len() == 1 {
Commands::Bench(args) => { panic!("ERROR: no arguments send!");
bench(&args.input, args.output.clone()); }
},
Commands::Decode(args)=> {
if args.format != "png" {
panic!("Unsupported output format!")
} else {
let img = match decode(&args.input) {
Ok(i) => i,
Err(e) => panic!("Error: {e:?}")
};
let out_path = match &args.output {
Some(s) => s,
None => &args.input
};
img.write_png(&out_path);
}
},
Commands::Encode(args) => {
let out_path = match &args.output {
Some(s) => s,
None => args.input.strip_suffix(".png").unwrap_or_else(||{
println!("Error: Could not construct output arg from input arg. Please provide explicitly");
process::exit(1);
})
};
encode(&args.input, &out_path); match args[1].as_str() {
}, "demo" => {
Commands::Demo { } => demo() demo();
}
//can only handle pngs for now
"encode" => {
encode(&args);
}
"decode" => {
decode(&args).expect("ERROR: Unspecified error during io-pipeline. Ensure file path is valid and can be read.");
}
"bench" => {
bench(&args);
}
"help" => {
println!("qoi supports the following commands: \n encode [IMAGE] (encodes given png-encoded into .qoi) \n decode [IMAGE] decodes given .qoi to .png \n bench [INPUT] [OUTPUT] encodes input .png into .qoi with encoding speed measured in microseconds.")
}
_ => {
panic!("Invalid arguments!")
}
} }
} }