First technically working code

This commit is contained in:
Pyrex 2024-04-07 15:32:35 -07:00
commit d04ec371d9
9 changed files with 450 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
/outputs

85
Cargo.lock generated Normal file
View File

@ -0,0 +1,85 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "adler"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "crc32fast"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
dependencies = [
"cfg-if",
]
[[package]]
name = "fdeflate"
version = "0.3.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645"
dependencies = [
"simd-adler32",
]
[[package]]
name = "flate2"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "miniz_oxide"
version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
dependencies = [
"adler",
"simd-adler32",
]
[[package]]
name = "png"
version = "0.17.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06e4b0d3d1312775e782c86c91a111aa1f910cbb65e1337f9975b5f9a554b5e1"
dependencies = [
"bitflags",
"crc32fast",
"fdeflate",
"flate2",
"miniz_oxide",
]
[[package]]
name = "rximg"
version = "0.1.0"
dependencies = [
"png",
]
[[package]]
name = "simd-adler32"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"

9
Cargo.toml Normal file
View File

@ -0,0 +1,9 @@
[package]
name = "rximg"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
png = "0.17.13"

BIN
inputs/avatar2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 761 KiB

111
src/compression.rs Normal file
View File

@ -0,0 +1,111 @@
use std::io::Write;
use crate::{constants::{MAGIC, TILE_SZ}, transform};
struct PixelTile {
// i32: representation that supports Walsh-Hadamard
pixels: [i32; TILE_SZ * TILE_SZ]
}
struct CoefTile {
coefs: [i32; TILE_SZ * TILE_SZ]
}
pub fn compress(
width: u16,
height: u16,
// n_components: u16,
layers: &[&[u8]],
) -> Vec<u8> {
let mut output = vec![];
// validation
for l in 0..layers.len() {
assert!(layers[l].len() == width as usize * height as usize);
}
// write header
output.write(&MAGIC).unwrap();
output.write(&(width as u16).to_le_bytes()).unwrap();
output.write(&(height as u16).to_le_bytes()).unwrap();
// output.write(&n_components.to_le_bytes()).unwrap();
output.write(&(layers.len() as u16).to_le_bytes()).unwrap();
let mut tiles = vec![];
for layer in layers.iter() {
for x0 in (0..width).step_by(TILE_SZ) {
for y0 in (0..height).step_by(TILE_SZ) {
let pixel_tile = PixelTile::from_layer(
x0 as usize, y0 as usize, layer,
width as usize, height as usize
);
let coef_tile = CoefTile::from_pixel_tile(&pixel_tile);
tiles.push(coef_tile);
}
}
}
for t in tiles.iter() {
t.write_zero(&mut output);
}
for t in tiles.iter() {
t.write_rest(&mut output);
}
output
}
impl PixelTile {
fn from_layer(
x0: usize,
y0: usize,
layer: &[u8],
width: usize,
height: usize
) -> PixelTile {
let mut pixels = [0; TILE_SZ * TILE_SZ];
for x in 0..TILE_SZ {
for y in 0..TILE_SZ {
let src_x = x0 + x;
let src_y = y0 + y;
pixels[y * TILE_SZ + x] =
if src_x < width && src_y < height {
layer[src_y * width + src_x] as i32
} else {
0
};
}
}
return PixelTile { pixels };
}
}
impl CoefTile {
fn from_pixel_tile(pt: &PixelTile) -> CoefTile {
let mut coefs = pt.pixels.clone();
// rows
for y in 0..TILE_SZ {
transform::encode(&mut coefs, y * TILE_SZ, 1);
}
// columns
for x in 0..TILE_SZ {
transform::encode(&mut coefs, x * TILE_SZ, 1);
}
return CoefTile { coefs }
}
fn write_zero(&self, output: &mut Vec<u8>) {
output.write(&self.coefs[0].to_le_bytes()).unwrap();
}
fn write_rest(&self, output: &mut Vec<u8>) {
for i in 1..self.coefs.len() {
output.write(&self.coefs[i].to_le_bytes()).unwrap();
}
}
}

2
src/constants.rs Normal file
View File

@ -0,0 +1,2 @@
pub const MAGIC: [u8; 4] = *b"rxi\0";
pub const TILE_SZ: usize = 8;

111
src/decompression.rs Normal file
View File

@ -0,0 +1,111 @@
use std::io::{BufReader, Read};
use crate::{constants::{MAGIC, TILE_SZ}, transform};
struct PixelTile {
// i32: representation that supports Walsh-Hadamard
pixels: [i32; TILE_SZ * TILE_SZ]
}
#[derive(Clone)]
struct CoefTile {
coefs: [i32; TILE_SZ * TILE_SZ]
}
pub fn decompress(
input: &[u8]
) -> (u16, u16, Vec<Vec<u8>>) {
let mut reader = BufReader::new(input);
// read header
let mut magic_buffer = [0; MAGIC.len()];
reader.read_exact(&mut magic_buffer).unwrap();
assert_eq!(MAGIC, magic_buffer);
let mut u16_buffer = [0; 2];
reader.read_exact(&mut u16_buffer).unwrap();
let width = u16::from_le_bytes(u16_buffer);
reader.read_exact(&mut u16_buffer).unwrap();
let height = u16::from_le_bytes(u16_buffer);
reader.read_exact(&mut u16_buffer).unwrap();
let n_layers = u16::from_le_bytes(u16_buffer);
let width_in_tiles = (width as usize + TILE_SZ - 1) / TILE_SZ;
let height_in_tiles = (height as usize + TILE_SZ - 1) / TILE_SZ;
let n_tiles = width_in_tiles * height_in_tiles * (n_layers as usize);
let mut tiles = vec![CoefTile::new(); n_tiles];
for i in 0..n_tiles {
tiles[i].load_zero(&mut reader);
}
for i in 0..n_tiles {
tiles[i].load_rest(&mut reader);
}
let mut tile_i = 0;
let mut layers = vec![vec![0; width as usize * height as usize]; n_layers as usize];
for layer in 0..n_layers {
for x0 in (0..width).step_by(TILE_SZ) {
for y0 in (0..height).step_by(TILE_SZ) {
let pixel_tile = tiles[tile_i].to_pixel_tile();
tile_i += 1;
pixel_tile.to_layer(
x0 as usize, y0 as usize, &mut layers[layer as usize],
width as usize, height as usize
);
}
}
}
return (width, height, layers)
}
impl CoefTile {
fn new() -> CoefTile {
CoefTile { coefs: [0; TILE_SZ * TILE_SZ] }
}
fn load_zero(&mut self, reader: &mut BufReader<&[u8]>) {
let mut coef_zero_buffer = [0; 4];
reader.read_exact(&mut coef_zero_buffer).unwrap();
self.coefs[0] = i32::from_le_bytes(coef_zero_buffer);
}
fn load_rest(&mut self, reader: &mut BufReader<&[u8]>) {
let mut coef_buffer = [0; 4];
for i in 1..self.coefs.len() {
reader.read_exact(&mut coef_buffer).unwrap();
self.coefs[i] = i32::from_le_bytes(coef_buffer);
}
}
fn to_pixel_tile(&self) -> PixelTile {
let mut pixels = self.coefs.clone();
// columns
for x in 0..TILE_SZ {
transform::decode(&mut pixels, x * TILE_SZ, 1);
}
// rows
for y in 0..TILE_SZ {
transform::decode(&mut pixels, y * TILE_SZ, 1);
}
return PixelTile { pixels };
}
}
impl PixelTile {
fn to_layer(&self, x0: usize, y0: usize, layer: &mut [u8], width: usize, height: usize) {
for x in 0..TILE_SZ {
for y in 0..TILE_SZ {
let dst_x = x0 + x;
let dst_y = y0 + y;
if dst_x < width && dst_y < height {
layer[dst_y * width + dst_x] = self.pixels[y * TILE_SZ + x] as u8;
}
}
}
}
}

90
src/main.rs Normal file
View File

@ -0,0 +1,90 @@
use std::fs::File;
use png::{BitDepth, ColorType};
mod compression;
mod constants;
mod decompression;
mod transform;
fn main() {
for chunk in vec![
[0, 0, 0, 0, 0, 0, 0, 0],
[0, 1, 2, 3, 4, 5, 6, 7]
] {
let orig = chunk;
let mut enc = chunk.clone();
transform::encode(&mut enc, 0, 1);
let mut dec = enc.clone();
transform::decode(&mut dec, 0, 1);
dbg!(orig, enc, dec);
}
hard_main();
}
fn hard_main() {
let (width, height, r, g, b) = load_image();
let compressed =
compression::compress(width as u16, height as u16, &[&r, &g, &b]);
/*
let mut output_file = File::create("outputs/avatar2.rxi").unwrap();
output_file.write_all(&compressed).unwrap();
*/
let (width2, height2, decompressed) =
decompression::decompress(&compressed);
assert_eq!(3, decompressed.len());
save_image(width2 as usize, height2 as usize, decompressed);
}
fn load_image() -> (usize, usize, Vec<u8>, Vec<u8>, Vec<u8>) {
let decoder = png::Decoder::new(File::open("inputs/avatar2.png").unwrap());
let mut reader = decoder.read_info().unwrap();
let mut buf = vec![0; reader.output_buffer_size()];
let info = reader.next_frame(&mut buf).unwrap();
let bytes = &buf[..info.buffer_size()];
let width = reader.info().width;
let height = reader.info().height;
assert_eq!(BitDepth::Eight, reader.info().bit_depth);
assert_eq!(3, reader.info().bytes_per_pixel());
assert_eq!(ColorType::Rgb, reader.info().color_type);
let r: Vec<u8> = bytes[0..].iter().cloned().step_by(3).collect();
let g: Vec<u8> = bytes[1..].iter().cloned().step_by(3).collect();
let b: Vec<u8> = bytes[2..].iter().cloned().step_by(3).collect();
assert_eq!(r.len(), (width * height) as usize);
assert_eq!(g.len(), (width * height) as usize);
assert_eq!(b.len(), (width * height) as usize);
(width as usize, height as usize, r, g, b)
}
fn save_image(width: usize, height: usize, image: Vec<Vec<u8>>) {
assert_eq!(image.len(), 3);
let mut encoder = png::Encoder::new(
File::create("outputs/avatar2.png").unwrap(),
width as u32, height as u32,
);
encoder.set_color(ColorType::Rgb);
let mut idata: Vec<u8> = vec![0; width * height * 3];
for i in [0, 1, 2] {
for (dst, src) in
idata[i..].iter_mut().step_by(3).zip(image[i].iter())
{
*dst = *src;
}
}
let mut writer = encoder.write_header().unwrap();
writer.write_image_data(&idata).unwrap();
}

40
src/transform.rs Normal file
View File

@ -0,0 +1,40 @@
use crate::constants::TILE_SZ;
// This is the Walsh-Hadamard transform, specialized to 8px
// Ported from:
// - https://colab.research.google.com/drive/1WjtKwUcqxWafAumFO9ET74RsckZSsw6n#scrollTo=e3r9-RBpcgwH
pub fn encode(data: &mut [i32], zero: usize, stride: usize) {
transform(data, zero, stride, 1)
}
pub fn decode(data: &mut [i32], zero: usize, stride: usize) {
transform(data, zero, stride, 8)
}
fn transform(data: &mut [i32], zero: usize, stride: usize, divisor: i32) {
let ix = |t| zero + t * stride;
let mut row = [0; TILE_SZ];
for i in 0..TILE_SZ {
row[i] = data[ix(i)];
}
for bit in [4, 2, 1] {
for i in 0..TILE_SZ {
if i & bit == 0 {
let j = i | bit;
let tmp = row[i];
row[i] = row[i].wrapping_add(row[j]);
row[j] = tmp.wrapping_sub(row[j]);
}
}
}
// reorder according to gray code
// basically, this moves important stuff towards the left
const GRAY: [usize; TILE_SZ] = [0, 4, 6, 2, 3, 7, 5, 1];
for i in 0..TILE_SZ {
data[ix(GRAY[i])] = row[i] / divisor;
}
}