From 3b912e557767457e2b4715aa15997a9d25d18a09 Mon Sep 17 00:00:00 2001 From: Hunter Kvalevog Date: Sat, 3 Oct 2026 21:38:12 -0500 Subject: qoidec: call rustc directly --- qoidec/Cargo.lock | 7 -- qoidec/Cargo.toml | 6 -- qoidec/build.sh | 2 + qoidec/qoidec.rs | 215 +++++++++++++++++++++++++++++++++++++++++++++++++++++ qoidec/src/main.rs | 215 ----------------------------------------------------- 5 files changed, 217 insertions(+), 228 deletions(-) delete mode 100644 qoidec/Cargo.lock delete mode 100644 qoidec/Cargo.toml create mode 100755 qoidec/build.sh create mode 100644 qoidec/qoidec.rs delete mode 100644 qoidec/src/main.rs diff --git a/qoidec/Cargo.lock b/qoidec/Cargo.lock deleted file mode 100644 index dc29f81..0000000 --- a/qoidec/Cargo.lock +++ /dev/null @@ -1,7 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "qoidec" -version = "0.1.0" diff --git a/qoidec/Cargo.toml b/qoidec/Cargo.toml deleted file mode 100644 index 22b0731..0000000 --- a/qoidec/Cargo.toml +++ /dev/null @@ -1,6 +0,0 @@ -[package] -name = "qoidec" -version = "0.1.0" -edition = "2024" - -[dependencies] diff --git a/qoidec/build.sh b/qoidec/build.sh new file mode 100755 index 0000000..d18d731 --- /dev/null +++ b/qoidec/build.sh @@ -0,0 +1,2 @@ +#!/bin/sh +rustc qoidec.rs --out-dir bin diff --git a/qoidec/qoidec.rs b/qoidec/qoidec.rs new file mode 100644 index 0000000..db533fa --- /dev/null +++ b/qoidec/qoidec.rs @@ -0,0 +1,215 @@ +// ================================================================================================ +// Basic Quite Ok Image (QOI) to PPM converter written to learn Rust +// +// ref: https://qoiformat.org/qoi-specification.pdf +// ref: https://github.com/phoboslab/qoi/blob/master/qoi.h +// ref: https://en.wikipedia.org/wiki/Netpbm#32-bit +// +// Changelog: +// 9/29/2026: Initial release +// +// License: +// Copyright (c) 2026 Hunter Kvalevog +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE. +// ================================================================================================ + +use std::env; +use std::fs; +use std::io::{self, Read, Seek, Write}; +use std::process::ExitCode; + +// Always RGBA32 +type Pixel = (u8, u8, u8, u8); + +// Always RGBA32 +struct Frame { + pub w: u32, + pub h: u32, + pub buf: Vec, +} + +impl Frame { + pub fn new(w: u32, h: u32) -> Self { + Self { + w: w, + h: h, + buf: Vec::with_capacity((w * h) as usize), + } + } +} + +trait ByteReader { + fn ru8(&mut self) -> io::Result; + fn ru32be(&mut self) -> io::Result; +} + +impl ByteReader for R { + fn ru8(&mut self) -> io::Result { + let mut buf = [0u8]; + self.read_exact(&mut buf)?; + Ok(buf[0]) + } + + fn ru32be(&mut self) -> io::Result { + let mut buf = [0u8; 4]; + self.read_exact(&mut buf)?; + Ok(u32::from_be_bytes(buf)) + } +} + +fn index_pos(p: Pixel) -> usize { + let r = p.0 as u32; + let g = p.1 as u32; + let b = p.2 as u32; + let a = p.3 as u32; + return ((r * 3 + g * 5 + b * 7 + a * 11) % 64) as usize; +} + +fn decode_qoi(inp: &mut (impl Read + Seek)) -> io::Result { + // magic was validated by caller + inp.seek(io::SeekFrom::Current(4))?; + + let w = inp.ru32be()?; + let h = inp.ru32be()?; + assert!(w > 0 && h > 0); + assert!((w as u64 * h as u64) < 400_000_000u64); + + inp.ru8()?; // don't care: channels + inp.ru8()?; // don't care: color space + + let mut frame = Frame::new(w, h); + let mut cur = (0u8, 0u8, 0u8, 0xFFu8); + let mut index = [(0u8, 0u8, 0u8, 0u8); 64]; + let mut run = 0u8; + + for _ in 0..((w * h) as usize) { + if run > 0 { + run -= 1; + } else { + let b0 = inp.ru8()?; + let pixel = match b0 { + // QOI_OP_RGB + 0b11111110 => (inp.ru8()?, inp.ru8()?, inp.ru8()?, cur.3), + // QOI_OP_RGBA + 0b11111111 => (inp.ru8()?, inp.ru8()?, inp.ru8()?, inp.ru8()?), + // 2-bit ops + _ => match b0 >> 6 { + // QOI_OP_INDEX + 0b00 => index[(b0 & 0b00111111) as usize], + // QOI_OP_DIFF + 0b01 => { + let dr = ((b0 >> 4) & 0b11) as i8 - 2; + let dg = ((b0 >> 2) & 0b11) as i8 - 2; + let db = ((b0 >> 0) & 0b11) as i8 - 2; + let r = cur.0.wrapping_add_signed(dr); + let g = cur.1.wrapping_add_signed(dg); + let b = cur.2.wrapping_add_signed(db); + (r, g, b, cur.3) + } + // QOI_OP_LUMA + 0b10 => { + let b1 = inp.ru8()?; + let dg = (b0 & 0b111111) as i8 - 32; + let dr = ((b1 >> 4) & 0b1111) as i8 + dg - 8; + let db = ((b1 >> 0) & 0b1111) as i8 + dg - 8; + let r = cur.0.wrapping_add_signed(dr); + let g = cur.1.wrapping_add_signed(dg); + let b = cur.2.wrapping_add_signed(db); + (r, g, b, cur.3) + } + // QOI_OP_RUN + 0b11 => { + run = b0 & 0b00111111; + cur + } + _ => unreachable!(), + } + }; + cur = pixel; + index[index_pos(cur)] = cur; + } + frame.buf.push(cur); + } + + Ok(frame) +} + +fn encode_ppm(out: &mut impl Write, frame: Frame) -> io::Result<()> { + out.write_all(b"P6\n")?; + out.write_all(format!("{} {}\n", frame.w, frame.h).as_bytes())?; + out.write_all(b"255\n")?; + + for p in frame.buf { + // just drop the alpha channel lol + /* + let val = + (p.0 as u32) | + (p.1 as u32) << 8 | + (p.2 as u32) << 16; + out.write_all(&val.to_le_bytes()[0..3])?; + */ + out.write_all(&[p.0, p.1, p.2])?; + } + + out.flush()?; + + Ok(()) +} + +fn main() -> ExitCode { + let Some(inp_path) = env::args().nth(1) else { + eprintln!("supply an input file"); + return ExitCode::FAILURE; + }; + + let Some(out_path) = env::args().nth(2) else { + eprintln!("supply an output file"); + return ExitCode::FAILURE; + }; + + let inp = match fs::File::open(inp_path) { + Ok(f) => f, + Err(e) => { + eprintln!("failed to open input: {e}"); + return ExitCode::FAILURE; + }, + }; + let mut inp = io::BufReader::new(inp); + + let out = match fs::File::create(out_path) { + Ok(f) => f, + Err(e) => { + eprintln!("failed to open output: {e}"); + return ExitCode::FAILURE; + }, + }; + let mut out = io::BufWriter::new(out); + + let mut hdr = [0u8; 4]; + if inp.read_exact(&mut hdr).is_err() || &hdr != b"qoif" { + eprintln!("invalid input file"); + return ExitCode::FAILURE; + } + inp.rewind().unwrap(); + + let frame = match decode_qoi(&mut inp) { + Ok(f) => f, + Err(e) => { + eprintln!("error while decoding: {e}"); + return ExitCode::FAILURE; + }, + }; + + if let Err(e) = encode_ppm(&mut out, frame) { + eprintln!("error while encoding: {e}"); + return ExitCode::FAILURE; + } + + return ExitCode::SUCCESS; +} + diff --git a/qoidec/src/main.rs b/qoidec/src/main.rs deleted file mode 100644 index db533fa..0000000 --- a/qoidec/src/main.rs +++ /dev/null @@ -1,215 +0,0 @@ -// ================================================================================================ -// Basic Quite Ok Image (QOI) to PPM converter written to learn Rust -// -// ref: https://qoiformat.org/qoi-specification.pdf -// ref: https://github.com/phoboslab/qoi/blob/master/qoi.h -// ref: https://en.wikipedia.org/wiki/Netpbm#32-bit -// -// Changelog: -// 9/29/2026: Initial release -// -// License: -// Copyright (c) 2026 Hunter Kvalevog -// -// Permission to use, copy, modify, and/or distribute this software for any -// purpose with or without fee is hereby granted. -// -// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES -// WITH REGARD TO THIS SOFTWARE. -// ================================================================================================ - -use std::env; -use std::fs; -use std::io::{self, Read, Seek, Write}; -use std::process::ExitCode; - -// Always RGBA32 -type Pixel = (u8, u8, u8, u8); - -// Always RGBA32 -struct Frame { - pub w: u32, - pub h: u32, - pub buf: Vec, -} - -impl Frame { - pub fn new(w: u32, h: u32) -> Self { - Self { - w: w, - h: h, - buf: Vec::with_capacity((w * h) as usize), - } - } -} - -trait ByteReader { - fn ru8(&mut self) -> io::Result; - fn ru32be(&mut self) -> io::Result; -} - -impl ByteReader for R { - fn ru8(&mut self) -> io::Result { - let mut buf = [0u8]; - self.read_exact(&mut buf)?; - Ok(buf[0]) - } - - fn ru32be(&mut self) -> io::Result { - let mut buf = [0u8; 4]; - self.read_exact(&mut buf)?; - Ok(u32::from_be_bytes(buf)) - } -} - -fn index_pos(p: Pixel) -> usize { - let r = p.0 as u32; - let g = p.1 as u32; - let b = p.2 as u32; - let a = p.3 as u32; - return ((r * 3 + g * 5 + b * 7 + a * 11) % 64) as usize; -} - -fn decode_qoi(inp: &mut (impl Read + Seek)) -> io::Result { - // magic was validated by caller - inp.seek(io::SeekFrom::Current(4))?; - - let w = inp.ru32be()?; - let h = inp.ru32be()?; - assert!(w > 0 && h > 0); - assert!((w as u64 * h as u64) < 400_000_000u64); - - inp.ru8()?; // don't care: channels - inp.ru8()?; // don't care: color space - - let mut frame = Frame::new(w, h); - let mut cur = (0u8, 0u8, 0u8, 0xFFu8); - let mut index = [(0u8, 0u8, 0u8, 0u8); 64]; - let mut run = 0u8; - - for _ in 0..((w * h) as usize) { - if run > 0 { - run -= 1; - } else { - let b0 = inp.ru8()?; - let pixel = match b0 { - // QOI_OP_RGB - 0b11111110 => (inp.ru8()?, inp.ru8()?, inp.ru8()?, cur.3), - // QOI_OP_RGBA - 0b11111111 => (inp.ru8()?, inp.ru8()?, inp.ru8()?, inp.ru8()?), - // 2-bit ops - _ => match b0 >> 6 { - // QOI_OP_INDEX - 0b00 => index[(b0 & 0b00111111) as usize], - // QOI_OP_DIFF - 0b01 => { - let dr = ((b0 >> 4) & 0b11) as i8 - 2; - let dg = ((b0 >> 2) & 0b11) as i8 - 2; - let db = ((b0 >> 0) & 0b11) as i8 - 2; - let r = cur.0.wrapping_add_signed(dr); - let g = cur.1.wrapping_add_signed(dg); - let b = cur.2.wrapping_add_signed(db); - (r, g, b, cur.3) - } - // QOI_OP_LUMA - 0b10 => { - let b1 = inp.ru8()?; - let dg = (b0 & 0b111111) as i8 - 32; - let dr = ((b1 >> 4) & 0b1111) as i8 + dg - 8; - let db = ((b1 >> 0) & 0b1111) as i8 + dg - 8; - let r = cur.0.wrapping_add_signed(dr); - let g = cur.1.wrapping_add_signed(dg); - let b = cur.2.wrapping_add_signed(db); - (r, g, b, cur.3) - } - // QOI_OP_RUN - 0b11 => { - run = b0 & 0b00111111; - cur - } - _ => unreachable!(), - } - }; - cur = pixel; - index[index_pos(cur)] = cur; - } - frame.buf.push(cur); - } - - Ok(frame) -} - -fn encode_ppm(out: &mut impl Write, frame: Frame) -> io::Result<()> { - out.write_all(b"P6\n")?; - out.write_all(format!("{} {}\n", frame.w, frame.h).as_bytes())?; - out.write_all(b"255\n")?; - - for p in frame.buf { - // just drop the alpha channel lol - /* - let val = - (p.0 as u32) | - (p.1 as u32) << 8 | - (p.2 as u32) << 16; - out.write_all(&val.to_le_bytes()[0..3])?; - */ - out.write_all(&[p.0, p.1, p.2])?; - } - - out.flush()?; - - Ok(()) -} - -fn main() -> ExitCode { - let Some(inp_path) = env::args().nth(1) else { - eprintln!("supply an input file"); - return ExitCode::FAILURE; - }; - - let Some(out_path) = env::args().nth(2) else { - eprintln!("supply an output file"); - return ExitCode::FAILURE; - }; - - let inp = match fs::File::open(inp_path) { - Ok(f) => f, - Err(e) => { - eprintln!("failed to open input: {e}"); - return ExitCode::FAILURE; - }, - }; - let mut inp = io::BufReader::new(inp); - - let out = match fs::File::create(out_path) { - Ok(f) => f, - Err(e) => { - eprintln!("failed to open output: {e}"); - return ExitCode::FAILURE; - }, - }; - let mut out = io::BufWriter::new(out); - - let mut hdr = [0u8; 4]; - if inp.read_exact(&mut hdr).is_err() || &hdr != b"qoif" { - eprintln!("invalid input file"); - return ExitCode::FAILURE; - } - inp.rewind().unwrap(); - - let frame = match decode_qoi(&mut inp) { - Ok(f) => f, - Err(e) => { - eprintln!("error while decoding: {e}"); - return ExitCode::FAILURE; - }, - }; - - if let Err(e) = encode_ppm(&mut out, frame) { - eprintln!("error while encoding: {e}"); - return ExitCode::FAILURE; - } - - return ExitCode::SUCCESS; -} - -- cgit v1.2.3