graphics/iv: add image viewer program
This commit is contained in:
Generated
+88
@@ -36,6 +36,12 @@ version = "0.1.0"
|
||||
name = "abi-serde"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "adler2"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
|
||||
|
||||
[[package]]
|
||||
name = "aead"
|
||||
version = "0.5.2"
|
||||
@@ -583,6 +589,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crc32fast"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
@@ -994,6 +1009,15 @@ version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ff"
|
||||
version = "0.13.1"
|
||||
@@ -1010,6 +1034,16 @@ version = "0.2.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30f4148e3c9b7dbe0cc7e842ad5a61b28f9025f201d78149383e778a08bc9215"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
@@ -1416,6 +1450,22 @@ version = "1.0.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
|
||||
|
||||
[[package]]
|
||||
name = "iv"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"clap",
|
||||
"cross",
|
||||
"libcolors",
|
||||
"log",
|
||||
"logsink",
|
||||
"pixie",
|
||||
"png",
|
||||
"thiserror 1.0.69",
|
||||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jiff"
|
||||
version = "0.2.15"
|
||||
@@ -1644,6 +1694,16 @@ version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
version = "0.8.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
|
||||
dependencies = [
|
||||
"adler2",
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
@@ -2207,6 +2267,15 @@ version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "pixie"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pkcs1"
|
||||
version = "0.7.5"
|
||||
@@ -2234,6 +2303,19 @@ version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.8.0"
|
||||
@@ -2852,6 +2934,12 @@ dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.1"
|
||||
|
||||
@@ -3,6 +3,7 @@ resolver = "1"
|
||||
members = [
|
||||
"dyn-loader",
|
||||
"graphics/colors",
|
||||
"graphics/iv",
|
||||
"graphics/term",
|
||||
"lib/cross",
|
||||
"lib/cryptic",
|
||||
@@ -11,6 +12,7 @@ members = [
|
||||
"lib/libpsf",
|
||||
"lib/libterm",
|
||||
"lib/logsink",
|
||||
"lib/pixie",
|
||||
"lib/runtime",
|
||||
"lib/stuff",
|
||||
"lib/uipc",
|
||||
@@ -85,6 +87,7 @@ logsink.path = "lib/logsink"
|
||||
libutil.path = "../lib/libutil"
|
||||
cryptic.path = "lib/cryptic"
|
||||
hclient.path = "lib/hclient"
|
||||
pixie.path = "lib/pixie"
|
||||
stuff.path = "lib/stuff"
|
||||
|
||||
[workspace.lints.rust]
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 208 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "iv"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
libcolors = { workspace = true, features = ["client"] }
|
||||
pixie.workspace = true
|
||||
logsink.workspace = true
|
||||
cross.workspace = true
|
||||
|
||||
bytemuck.workspace = true
|
||||
log.workspace = true
|
||||
thiserror.workspace = true
|
||||
clap.workspace = true
|
||||
toml.workspace = true
|
||||
|
||||
# TODO write own library
|
||||
png = "0.18.0"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,18 @@
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Application error: {0}")]
|
||||
Colors(#[from] libcolors::error::Error),
|
||||
#[error("Unrecognized/unsupported image format")]
|
||||
UnknownFormat,
|
||||
|
||||
#[error("PNG decode error: {0}")]
|
||||
PngDecode(#[from] png::DecodingError),
|
||||
#[error("JPEG decode error: {0}")]
|
||||
JpegDecode(#[from] pixie::jpeg::Error),
|
||||
#[error("Corrupt image")]
|
||||
CorruptImage,
|
||||
}
|
||||
@@ -0,0 +1,262 @@
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{BufReader, Read},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use libcolors::surface::{Surface, VecSurface};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
pub struct ImageData {
|
||||
pub data: Vec<u8>,
|
||||
pub pixel_format: PixelFormat,
|
||||
pub bit_depth: BitDepth,
|
||||
pub stride: usize,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum PixelFormat {
|
||||
Rgb,
|
||||
Rgba,
|
||||
#[allow(unused)]
|
||||
Bgr,
|
||||
Bgra,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum BitDepth {
|
||||
Eight,
|
||||
}
|
||||
|
||||
struct ImageLoader {
|
||||
#[allow(unused)]
|
||||
name: &'static str,
|
||||
probe: fn(&Path) -> Result<bool, Error>,
|
||||
load: fn(&Path) -> Result<ImageData, Error>,
|
||||
}
|
||||
|
||||
static IMAGE_LOADERS: &[ImageLoader] = &[
|
||||
ImageLoader {
|
||||
name: "png",
|
||||
probe: png_probe,
|
||||
load: png_load,
|
||||
},
|
||||
ImageLoader {
|
||||
name: "jpeg",
|
||||
probe: jpeg_probe,
|
||||
load: jpeg_load,
|
||||
},
|
||||
];
|
||||
|
||||
fn png_probe(path: &Path) -> Result<bool, Error> {
|
||||
let file = File::open(path).map(BufReader::new)?;
|
||||
let decoder = png::Decoder::new(file);
|
||||
Ok(decoder.read_info().is_ok())
|
||||
}
|
||||
fn jpeg_probe(path: &Path) -> Result<bool, Error> {
|
||||
let mut file = File::open(path).map(BufReader::new)?;
|
||||
let mut magic = [0; 4];
|
||||
let len = file.read(&mut magic)?;
|
||||
if len != 4 || magic != [0xFF, 0xD8, 0xFF, 0xE0] {
|
||||
Ok(false)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
fn png_load(path: &Path) -> Result<ImageData, Error> {
|
||||
let file = File::open(path).map(BufReader::new)?;
|
||||
let decoder = png::Decoder::new(file);
|
||||
let mut info = decoder.read_info()?;
|
||||
let output_buffer_size = info.output_buffer_size().ok_or(Error::CorruptImage)?;
|
||||
let mut buffer = vec![0; output_buffer_size];
|
||||
let frame = info.next_frame(&mut buffer)?;
|
||||
buffer.drain(frame.buffer_size()..);
|
||||
let pixel_format = match frame.color_type {
|
||||
png::ColorType::Rgb => PixelFormat::Rgb,
|
||||
png::ColorType::Rgba => PixelFormat::Rgba,
|
||||
t => todo!("Unsupported color type: {t:?}"),
|
||||
};
|
||||
let bit_depth = match frame.bit_depth {
|
||||
png::BitDepth::Eight => BitDepth::Eight,
|
||||
t => todo!("Unsupported bit depth: {t:?}"),
|
||||
};
|
||||
Ok(ImageData {
|
||||
data: buffer,
|
||||
pixel_format,
|
||||
bit_depth,
|
||||
stride: frame.line_size,
|
||||
width: frame.width,
|
||||
height: frame.height,
|
||||
})
|
||||
}
|
||||
fn jpeg_load(path: &Path) -> Result<ImageData, Error> {
|
||||
let file = File::open(path).map(BufReader::new)?;
|
||||
let mut decoder = Box::new(pixie::JpegDecoder::new(file));
|
||||
let rgb = decoder.decode_rgb()?;
|
||||
|
||||
Ok(ImageData {
|
||||
data: rgb.data,
|
||||
pixel_format: PixelFormat::Rgb,
|
||||
bit_depth: BitDepth::Eight,
|
||||
stride: rgb.stride,
|
||||
width: rgb.width as u32,
|
||||
height: rgb.height as u32,
|
||||
})
|
||||
}
|
||||
|
||||
impl ImageData {
|
||||
pub fn load_image<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
||||
let path = path.as_ref();
|
||||
let mut loader = None;
|
||||
for l in IMAGE_LOADERS {
|
||||
if (l.probe)(path)? {
|
||||
loader = Some(l);
|
||||
break;
|
||||
}
|
||||
}
|
||||
let loader = loader.ok_or(Error::UnknownFormat)?;
|
||||
(loader.load)(path)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn convert_pixel(
|
||||
source: &[u8],
|
||||
source_format: PixelFormat,
|
||||
source_depth: BitDepth,
|
||||
output: &mut [u8],
|
||||
output_format: PixelFormat,
|
||||
output_depth: BitDepth,
|
||||
) {
|
||||
let (r, g, b, a) = match (source_format, source_depth) {
|
||||
(PixelFormat::Rgb, BitDepth::Eight) => (source[0], source[1], source[2], 255),
|
||||
(PixelFormat::Rgba, BitDepth::Eight) => (source[0], source[1], source[2], source[3]),
|
||||
(PixelFormat::Bgr, BitDepth::Eight) => (source[2], source[1], source[0], 255),
|
||||
(PixelFormat::Bgra, BitDepth::Eight) => (source[2], source[1], source[0], source[3]),
|
||||
};
|
||||
|
||||
match (output_format, output_depth) {
|
||||
(PixelFormat::Rgb, BitDepth::Eight) => {
|
||||
output[0] = r;
|
||||
output[1] = g;
|
||||
output[2] = b;
|
||||
}
|
||||
(PixelFormat::Rgba, BitDepth::Eight) => {
|
||||
output[0] = r;
|
||||
output[1] = g;
|
||||
output[2] = b;
|
||||
output[3] = a;
|
||||
}
|
||||
(PixelFormat::Bgr, BitDepth::Eight) => {
|
||||
output[0] = b;
|
||||
output[1] = g;
|
||||
output[2] = r;
|
||||
}
|
||||
(PixelFormat::Bgra, BitDepth::Eight) => {
|
||||
output[0] = b;
|
||||
output[1] = g;
|
||||
output[2] = r;
|
||||
output[3] = a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO optimized format conversion
|
||||
pub fn convert_color(
|
||||
&self,
|
||||
target_pixel_format: PixelFormat,
|
||||
target_bit_depth: BitDepth,
|
||||
) -> ImageData {
|
||||
// TODO less than 8bpp
|
||||
let source_bytes_per_pixel = match (self.pixel_format, self.bit_depth) {
|
||||
(PixelFormat::Rgb | PixelFormat::Bgr, BitDepth::Eight) => 3,
|
||||
(PixelFormat::Rgba | PixelFormat::Bgra, BitDepth::Eight) => 4,
|
||||
};
|
||||
let output_bytes_per_pixel = match (target_pixel_format, target_bit_depth) {
|
||||
(PixelFormat::Rgb | PixelFormat::Bgr, BitDepth::Eight) => 3,
|
||||
(PixelFormat::Rgba | PixelFormat::Bgra, BitDepth::Eight) => 4,
|
||||
};
|
||||
let output_stride = (self.width as usize * output_bytes_per_pixel + 3) & !3;
|
||||
let output_buffer_size = output_stride * self.height as usize;
|
||||
let mut output_buffer = vec![0u8; output_buffer_size];
|
||||
|
||||
for y in 0..self.height {
|
||||
let source_row_i = y as usize * self.stride;
|
||||
let source_row = &self.data[source_row_i..source_row_i + self.stride];
|
||||
let output_row_i = y as usize * output_stride;
|
||||
let output_row = &mut output_buffer[output_row_i..output_row_i + output_stride];
|
||||
|
||||
// TODO SIMD impl
|
||||
for x in 0..self.width {
|
||||
let source_pixel_i = x as usize * source_bytes_per_pixel;
|
||||
let source_pixel =
|
||||
&source_row[source_pixel_i..source_pixel_i + source_bytes_per_pixel];
|
||||
let output_pixel_i = x as usize * output_bytes_per_pixel;
|
||||
let output_pixel =
|
||||
&mut output_row[output_pixel_i..output_pixel_i + output_bytes_per_pixel];
|
||||
|
||||
Self::convert_pixel(
|
||||
source_pixel,
|
||||
self.pixel_format,
|
||||
self.bit_depth,
|
||||
output_pixel,
|
||||
target_pixel_format,
|
||||
target_bit_depth,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ImageData {
|
||||
data: output_buffer,
|
||||
pixel_format: target_pixel_format,
|
||||
bit_depth: target_bit_depth,
|
||||
stride: output_stride,
|
||||
width: self.width,
|
||||
height: self.height,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_surface(&self) -> VecSurface {
|
||||
if self.pixel_format == PixelFormat::Bgra && self.bit_depth == BitDepth::Eight {
|
||||
let mut surface = VecSurface::new(self.width, self.height);
|
||||
assert_eq!(surface.stride() * 4, self.stride);
|
||||
// TODO misaligned
|
||||
surface.copy_from_slice(bytemuck::cast_slice(&self.data[..]));
|
||||
surface
|
||||
} else {
|
||||
self.convert_color(PixelFormat::Bgra, BitDepth::Eight)
|
||||
.create_surface()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO optimized scaling
|
||||
pub fn scale_surface(source: &VecSurface, destination: &mut VecSurface) {
|
||||
let scale_x = destination.width() as f32 / source.width() as f32;
|
||||
let scale_y = destination.height() as f32 / source.height() as f32;
|
||||
|
||||
let (source_scale_x, destination_scale_x, w) = if destination.width() > source.width() {
|
||||
(1.0 / scale_x, 1.0, destination.width())
|
||||
} else {
|
||||
(1.0, scale_x, source.width())
|
||||
};
|
||||
let (source_scale_y, destination_scale_y, h) = if destination.height() > source.height() {
|
||||
(1.0 / scale_y, 1.0, destination.height())
|
||||
} else {
|
||||
(1.0, scale_y, source.height())
|
||||
};
|
||||
|
||||
for y in 0..h {
|
||||
for x in 0..w {
|
||||
let sx = (source_scale_x * x as f32) as u32;
|
||||
let sy = (source_scale_y * y as f32) as u32;
|
||||
let dx = (destination_scale_x * x as f32) as u32;
|
||||
let dy = (destination_scale_y * y as f32) as u32;
|
||||
|
||||
destination.set_pixel(dx, dy, source.pixel(sx, sy));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::ExitCode,
|
||||
sync::{Arc, RwLock},
|
||||
};
|
||||
|
||||
use clap::Parser;
|
||||
use libcolors::{
|
||||
application::{
|
||||
window::{EventOutcome, Window},
|
||||
Application,
|
||||
},
|
||||
event::KeyEvent,
|
||||
geometry::{Point, Rectangle},
|
||||
input::Key,
|
||||
surface::{MappedSurface, Surface, VecSurface},
|
||||
};
|
||||
|
||||
use crate::{error::Error, format::ImageData};
|
||||
|
||||
mod error;
|
||||
mod format;
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct Args {
|
||||
filename: PathBuf,
|
||||
}
|
||||
|
||||
struct State {
|
||||
image_surface: VecSurface,
|
||||
target_surface: VecSurface,
|
||||
|
||||
scale: ScaleMethod,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum ScaleMethod {
|
||||
Fit,
|
||||
Identity,
|
||||
Maximize(u32), // x scaling
|
||||
Minimize(u32), // 1/x scaling
|
||||
}
|
||||
|
||||
impl ScaleMethod {
|
||||
pub fn bigger(self) -> ScaleMethod {
|
||||
match self {
|
||||
Self::Fit => ScaleMethod::Maximize(1),
|
||||
Self::Identity => ScaleMethod::Maximize(2),
|
||||
Self::Maximize(n) if n < 8 => Self::Maximize(n * 2),
|
||||
Self::Minimize(2) => Self::Identity,
|
||||
Self::Minimize(n) => Self::Minimize(n / 2),
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn smaller(self) -> ScaleMethod {
|
||||
match self {
|
||||
Self::Fit => ScaleMethod::Identity,
|
||||
Self::Identity => Self::Minimize(2),
|
||||
Self::Maximize(2) => Self::Identity,
|
||||
Self::Maximize(n) => Self::Maximize(n / 2),
|
||||
Self::Minimize(n) if n < 8 => Self::Minimize(n * 2),
|
||||
_ => self,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn load_image<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
|
||||
let image = ImageData::load_image(path)?;
|
||||
let image_surface = image.create_surface();
|
||||
let target_surface = image_surface.clone();
|
||||
Ok(Self {
|
||||
image_surface,
|
||||
target_surface,
|
||||
scale: ScaleMethod::Fit,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_key(&mut self, key: KeyEvent) -> EventOutcome {
|
||||
match key.key {
|
||||
Key::Up => {
|
||||
self.scale = self.scale.bigger();
|
||||
EventOutcome::Redraw
|
||||
}
|
||||
Key::Down => {
|
||||
self.scale = self.scale.smaller();
|
||||
EventOutcome::Redraw
|
||||
}
|
||||
_ => EventOutcome::None,
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_to_aspect(&mut self, target_width: u32, target_height: u32) {
|
||||
let target_aspect = self.image_surface.width() as f32 / self.image_surface.height() as f32;
|
||||
|
||||
let w;
|
||||
let h;
|
||||
if (target_height as f32 * target_aspect) as u32 > target_width {
|
||||
w = target_width;
|
||||
h = (target_width as f32 / target_aspect) as u32;
|
||||
} else {
|
||||
w = (target_height as f32 * target_aspect) as u32;
|
||||
h = target_height;
|
||||
}
|
||||
|
||||
if self.target_surface.width() == w && self.target_surface.height() == h {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut destination = VecSurface::new(w, h);
|
||||
format::scale_surface(&self.image_surface, &mut destination);
|
||||
self.target_surface = destination;
|
||||
}
|
||||
|
||||
fn redraw(&mut self, surface: &mut MappedSurface) {
|
||||
// Scale to match aspect
|
||||
let (target_width, target_height) = match self.scale {
|
||||
ScaleMethod::Fit => (surface.width(), surface.height()),
|
||||
ScaleMethod::Identity => (self.image_surface.width(), self.image_surface.height()),
|
||||
ScaleMethod::Maximize(n) => (
|
||||
self.image_surface.width() * n,
|
||||
self.image_surface.height() * n,
|
||||
),
|
||||
ScaleMethod::Minimize(n) => (
|
||||
self.image_surface.width() / n,
|
||||
self.image_surface.height() / n,
|
||||
),
|
||||
};
|
||||
self.scale_to_aspect(target_width, target_height);
|
||||
|
||||
let iw = self.target_surface.width();
|
||||
let ih = self.target_surface.height();
|
||||
let sw = surface.width();
|
||||
let sh = surface.height();
|
||||
|
||||
let center = surface.center();
|
||||
|
||||
let (off_x, w) = if iw > sw {
|
||||
((iw - sw) / 2, sw)
|
||||
} else {
|
||||
(0, iw)
|
||||
};
|
||||
let (off_y, h) = if ih > sh {
|
||||
((ih - sh) / 2, sh)
|
||||
} else {
|
||||
(0, ih)
|
||||
};
|
||||
|
||||
let origin = center - Point::new(w / 2, h / 2);
|
||||
let rect = Rectangle {
|
||||
x: off_x,
|
||||
y: off_y,
|
||||
w,
|
||||
h,
|
||||
};
|
||||
|
||||
surface.fill(0);
|
||||
surface.copy_rect(&self.target_surface, rect, origin);
|
||||
}
|
||||
}
|
||||
|
||||
fn run(args: Args) -> Result<ExitCode, Error> {
|
||||
let state = State::load_image(args.filename)?;
|
||||
let mut application = Application::new()?;
|
||||
let mut window = Window::new(&application)?;
|
||||
let state = Arc::new(RwLock::new(state));
|
||||
|
||||
let s = state.clone();
|
||||
window.set_on_redraw_requested(move |surface| {
|
||||
s.write().unwrap().redraw(surface);
|
||||
});
|
||||
let s = state.clone();
|
||||
window.set_on_key_pressed(move |key| s.write().unwrap().handle_key(key));
|
||||
|
||||
application.add_window(window);
|
||||
|
||||
Ok(application.run())
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
logsink::setup_logging(true);
|
||||
let args = Args::parse();
|
||||
|
||||
match run(args) {
|
||||
Ok(code) => code,
|
||||
Err(error) => {
|
||||
eprintln!("{error}");
|
||||
ExitCode::FAILURE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use std::{
|
||||
use cross::signal::set_sigint_handler;
|
||||
|
||||
use crate::{
|
||||
application::window::EventOutcome,
|
||||
error::Error,
|
||||
event::{Event, EventData, WindowEvent, WindowManagementEvent},
|
||||
message::ClientMessage,
|
||||
@@ -68,6 +69,10 @@ impl Application {
|
||||
self.windows.insert(window.id(), window);
|
||||
}
|
||||
|
||||
pub fn remove_window(&mut self, window_id: u32) {
|
||||
self.windows.remove(&window_id);
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
!EXIT_SIGNAL.load(Ordering::Acquire)
|
||||
}
|
||||
@@ -88,7 +93,9 @@ impl Application {
|
||||
}
|
||||
EventData::WindowEvent(window_id, ev) => {
|
||||
if let Some(window) = self.windows.get_mut(&window_id) {
|
||||
window.handle_event(ev)?;
|
||||
if window.handle_event(ev)? == EventOutcome::Destroy {
|
||||
self.remove_window(window_id);
|
||||
}
|
||||
} else {
|
||||
log::warn!("Unknown window ID received: {window_id}");
|
||||
}
|
||||
|
||||
@@ -192,3 +192,13 @@ impl Window {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
self.connection
|
||||
.lock()
|
||||
.unwrap()
|
||||
.send(&ClientMessage::DestroyWindow(self.window_id))
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ pub trait Cast<T> {
|
||||
fn cast(self) -> T;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Rectangle<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
@@ -14,6 +15,7 @@ pub struct Rectangle<T> {
|
||||
pub h: T,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Point<T> {
|
||||
pub x: T,
|
||||
pub y: T,
|
||||
@@ -34,6 +36,17 @@ impl<U, T: Cast<U>> Cast<Point<U>> for Point<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Sub<Output = T>> Sub<Point<T>> for Point<T> {
|
||||
type Output = Point<T>;
|
||||
|
||||
fn sub(self, rhs: Point<T>) -> Self::Output {
|
||||
Point {
|
||||
x: self.x - rhs.x,
|
||||
y: self.y - rhs.y,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Rectangle<T>
|
||||
where
|
||||
T: Copy,
|
||||
@@ -141,6 +154,7 @@ impl_primitive_cast!(
|
||||
u32 => u16,
|
||||
u32 => u64,
|
||||
u32 => usize,
|
||||
u32 => i64,
|
||||
|
||||
u64 => u8,
|
||||
u64 => u16,
|
||||
|
||||
@@ -19,6 +19,19 @@ pub trait Surface: DerefMut<Target = [u32]> {
|
||||
fn height(&self) -> u32;
|
||||
fn resize(&mut self, width: u32, height: u32) -> Result<(), Error>;
|
||||
|
||||
fn rectangle(&self) -> Rectangle<u32> {
|
||||
Rectangle {
|
||||
x: 0,
|
||||
y: 0,
|
||||
w: self.width(),
|
||||
h: self.height(),
|
||||
}
|
||||
}
|
||||
|
||||
fn center(&self) -> Point<u32> {
|
||||
Point::new(self.width() / 2, self.height() / 2)
|
||||
}
|
||||
|
||||
fn clip_rectangle(&self, input: Rectangle<u32>) -> Option<Rectangle<u32>> {
|
||||
input.intersection(&Rectangle {
|
||||
x: 0,
|
||||
@@ -54,20 +67,20 @@ pub trait Surface: DerefMut<Target = [u32]> {
|
||||
source_rectangle.h,
|
||||
))?;
|
||||
let source_rectangle = foreground.clip_rectangle(source_rectangle)?;
|
||||
let clip_rectangle = source_rectangle.intersection(&destination_rectangle)?;
|
||||
let clip_w = source_rectangle.w.min(destination_rectangle.w);
|
||||
let clip_h = source_rectangle.h.min(destination_rectangle.h);
|
||||
|
||||
for y in 0..clip_rectangle.h {
|
||||
for y in 0..clip_h {
|
||||
let source_i =
|
||||
(y + source_rectangle.y) as usize * source_stride + source_rectangle.x as usize;
|
||||
let destination_i = (y + destination_rectangle.y) as usize * destination_stride
|
||||
+ destination_rectangle.y as usize;
|
||||
let source_row = &foreground[source_i..source_i + clip_rectangle.w as usize];
|
||||
let destination_row =
|
||||
&mut self[destination_i..destination_i + clip_rectangle.w as usize];
|
||||
+ destination_rectangle.x as usize;
|
||||
let source_row = &foreground[source_i..source_i + clip_w as usize];
|
||||
let destination_row = &mut self[destination_i..destination_i + clip_w as usize];
|
||||
blend_row(destination_row, source_row);
|
||||
}
|
||||
|
||||
Some(clip_rectangle)
|
||||
Some(Rectangle::from_origin_size(destination, clip_w, clip_h))
|
||||
}
|
||||
|
||||
fn fill_rect(&mut self, rectangle: Rectangle<u32>, color: u32) {
|
||||
@@ -86,6 +99,11 @@ pub trait Surface: DerefMut<Target = [u32]> {
|
||||
let i = y as usize * self.stride() + x as usize;
|
||||
self[i] = color;
|
||||
}
|
||||
|
||||
fn pixel(&self, x: u32, y: u32) -> u32 {
|
||||
let i = y as usize * self.stride() + x as usize;
|
||||
self[i]
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MappedSurface {
|
||||
@@ -96,6 +114,7 @@ pub struct MappedSurface {
|
||||
len: usize,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VecSurface {
|
||||
data: Vec<u32>,
|
||||
width: u32,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "pixie"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
bytemuck.workspace = true
|
||||
thiserror.workspace = true
|
||||
log.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,75 @@
|
||||
pub struct RgbImage {
|
||||
pub data: Vec<u8>,
|
||||
pub stride: usize,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl RgbImage {
|
||||
pub fn get(&self, x: usize, y: usize) -> Option<(u8, u8, u8)> {
|
||||
if x >= self.width || y >= self.height {
|
||||
return None;
|
||||
}
|
||||
let i = y * self.stride + x * 3;
|
||||
let rgb = &self.data[i..];
|
||||
Some((rgb[0], rgb[1], rgb[2]))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct YCbCrImage {
|
||||
pub luma: Vec<u8>,
|
||||
pub chroma_b: Vec<u8>,
|
||||
pub chroma_r: Vec<u8>,
|
||||
pub width: usize,
|
||||
pub height: usize,
|
||||
}
|
||||
|
||||
impl YCbCrImage {
|
||||
pub fn to_rgb(&self) -> RgbImage {
|
||||
ycbcr_to_rgb(self)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ycbcr_to_rgb(ycbcr: &YCbCrImage) -> RgbImage {
|
||||
let mut rgb = vec![];
|
||||
for i in 0..ycbcr.luma.len() {
|
||||
let y = ycbcr.luma[i] as f64;
|
||||
let cb = ycbcr.chroma_b[i] as f64 - 128.0;
|
||||
let cr = ycbcr.chroma_r[i] as f64 - 128.0;
|
||||
|
||||
let r = y + 1.40200 * cr;
|
||||
let g = y - 0.34414 * cb - 0.71414 * cr;
|
||||
let b = y + 1.77200 * cb;
|
||||
|
||||
rgb.push(r.clamp(0.0, 255.0) as u8);
|
||||
rgb.push(g.clamp(0.0, 255.0) as u8);
|
||||
rgb.push(b.clamp(0.0, 255.0) as u8);
|
||||
}
|
||||
|
||||
RgbImage {
|
||||
data: rgb,
|
||||
stride: 3 * ycbcr.width,
|
||||
width: ycbcr.width,
|
||||
height: ycbcr.height,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{rgb_to_ycbcr, RgbImageRef};
|
||||
|
||||
#[test]
|
||||
fn test_rgb_to_ycbcr() {
|
||||
let rgb = [86, 86, 0];
|
||||
let rgb = RgbImageRef {
|
||||
data: &rgb,
|
||||
stride: 3,
|
||||
width: 1,
|
||||
height: 1,
|
||||
};
|
||||
let ycbcr = rgb_to_ycbcr(&rgb);
|
||||
assert_eq!(&ycbcr.luma, &[76]);
|
||||
assert_eq!(&ycbcr.chroma_b, &[85]);
|
||||
assert_eq!(&ycbcr.chroma_r, &[134]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::jpeg::data::dct;
|
||||
|
||||
#[repr(align(16))]
|
||||
#[derive(Clone, Copy)]
|
||||
struct BlockAlign([u8; 0]);
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct Block8x8<T> {
|
||||
pub data: [T; 64],
|
||||
_align: BlockAlign,
|
||||
}
|
||||
|
||||
impl<T> Block8x8<T> {
|
||||
pub const fn new(data: [T; 64]) -> Self {
|
||||
Self {
|
||||
data,
|
||||
_align: BlockAlign([]),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map<U, F: Fn(T) -> U>(self, f: F) -> Block8x8<U> {
|
||||
Block8x8::new(self.data.map(f))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Copy> Block8x8<T> {
|
||||
pub fn splat(v: T) -> Self {
|
||||
Self {
|
||||
data: [v; 64],
|
||||
_align: BlockAlign([]),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get(&self, x: usize, y: usize) -> T {
|
||||
self.data[y * 8 + x]
|
||||
}
|
||||
|
||||
pub fn unzigzag(from: &[T; 64]) -> Self {
|
||||
let mut output = [from[0]; 64];
|
||||
super::unzigzag64(from, &mut output);
|
||||
Self::new(output)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> From<Block8x8<T>> for Vec<T> {
|
||||
fn from(value: Block8x8<T>) -> Self {
|
||||
value.data.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Block8x8<i16> {
|
||||
#[inline]
|
||||
pub fn idct(&self) -> Block8x8<f64> {
|
||||
dct::idct_8x8(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug> fmt::Debug for Block8x8<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let mut outer = f.debug_list();
|
||||
for y in 0..8 {
|
||||
outer.entry(&&self.data[y * 8..y * 8 + 8]);
|
||||
}
|
||||
outer.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Display> fmt::Display for Block8x8<T> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
for y in 0..8 {
|
||||
for x in 0..8 {
|
||||
if x != 0 {
|
||||
write!(f, " ")?;
|
||||
}
|
||||
fmt::Display::fmt(&self.data[y * 8 + x], f)?;
|
||||
}
|
||||
writeln!(f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::jpeg::data::Block8x8;
|
||||
|
||||
pub fn write_block_1x1<T: Copy>(
|
||||
output: &mut [T],
|
||||
output_stride: usize,
|
||||
dx: usize,
|
||||
dy: usize,
|
||||
block: &Block8x8<T>,
|
||||
) {
|
||||
for by in 0..8 {
|
||||
let block_row = &block.data[by * 8..by * 8 + 8];
|
||||
let output_offset = (dy + by) * output_stride + dx;
|
||||
if output_offset + 8 > output.len() {
|
||||
break;
|
||||
}
|
||||
output[output_offset..output_offset + 8].copy_from_slice(block_row);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_block_1x2<T: Copy>(
|
||||
output: &mut [T],
|
||||
output_stride: usize,
|
||||
dx: usize,
|
||||
dy: usize,
|
||||
block: &Block8x8<T>,
|
||||
) {
|
||||
for by in 0..16 {
|
||||
let block_row = &block.data[by / 2 * 8..by / 2 * 8 + 8];
|
||||
let output_offset = (dy + by) * output_stride + dx;
|
||||
if output_offset + 8 > output.len() {
|
||||
break;
|
||||
}
|
||||
output[output_offset..output_offset + 8].copy_from_slice(block_row);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_block_2x1<T: Copy>(
|
||||
output: &mut [T],
|
||||
output_stride: usize,
|
||||
dx: usize,
|
||||
dy: usize,
|
||||
block: &Block8x8<T>,
|
||||
) {
|
||||
for by in 0..8 {
|
||||
let block_row = &block.data[by * 8..by * 8 + 8];
|
||||
for bx in 0..16 {
|
||||
let output_offset = (dy + by) * output_stride + (dx + bx);
|
||||
if output_offset >= output.len() {
|
||||
break;
|
||||
}
|
||||
output[output_offset] = block_row[bx / 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_block_2x2<T: Copy>(
|
||||
output: &mut [T],
|
||||
output_stride: usize,
|
||||
dx: usize,
|
||||
dy: usize,
|
||||
block: &Block8x8<T>,
|
||||
) {
|
||||
for by in 0..16 {
|
||||
let block_row = &block.data[by / 2 * 8..by / 2 * 8 + 8];
|
||||
for bx in 0..16 {
|
||||
let output_offset = (dy + by) * output_stride + (dx + bx);
|
||||
if output_offset >= output.len() {
|
||||
break;
|
||||
}
|
||||
output[output_offset] = block_row[bx / 2];
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
use crate::jpeg::data::Block8x8;
|
||||
|
||||
// Table of precomputed cosine coefficients for DCT/IDCT
|
||||
#[rustfmt::skip]
|
||||
#[allow(clippy::excessive_precision, clippy::approx_constant)]
|
||||
const COSINE_TABLE: [f64; 8 * 8] = [
|
||||
1.0000000000000000, 0.9807852804032304, 0.9238795325112867, 0.8314696123025452, 0.7071067811865476, 0.5555702330196023, 0.3826834323650898, 0.1950903220161283,
|
||||
1.0000000000000000, 0.8314696123025452, 0.3826834323650898, -0.1950903220161282, -0.7071067811865475, -0.9807852804032304, -0.9238795325112868, -0.5555702330196022,
|
||||
1.0000000000000000, 0.5555702330196023, -0.3826834323650897, -0.9807852804032304, -0.7071067811865477, 0.1950903220161283, 0.9238795325112865, 0.8314696123025455,
|
||||
1.0000000000000000, 0.1950903220161283, -0.9238795325112867, -0.5555702330196022, 0.7071067811865474, 0.8314696123025455, -0.3826834323650899, -0.9807852804032307,
|
||||
1.0000000000000000, -0.1950903220161282, -0.9238795325112868, 0.5555702330196018, 0.7071067811865477, -0.8314696123025451, -0.3826834323650906, 0.9807852804032304,
|
||||
1.0000000000000000, -0.5555702330196020, -0.3826834323650903, 0.9807852804032304, -0.7071067811865467, -0.1950903220161280, 0.9238795325112867, -0.8314696123025450,
|
||||
1.0000000000000000, -0.8314696123025453, 0.3826834323650900, 0.1950903220161288, -0.7071067811865471, 0.9807852804032307, -0.9238795325112864, 0.5555702330196015,
|
||||
1.0000000000000000, -0.9807852804032304, 0.9238795325112865, -0.8314696123025451, 0.7071067811865466, -0.5555702330196015, 0.3826834323650896, -0.1950903220161286,
|
||||
];
|
||||
|
||||
#[inline]
|
||||
fn c_uv(u: usize, v: usize) -> f64 {
|
||||
if u + v == 0 {
|
||||
0.5
|
||||
} else {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
// TODO optimize IDCT
|
||||
fn partial_idct_8x8(block: &Block8x8<i16>, x: usize, y: usize) -> f64 {
|
||||
let mut sum = 0.0;
|
||||
for v in 0..8 {
|
||||
for u in 0..8 {
|
||||
let c_uv = c_uv(u, v);
|
||||
let f_uv = block.get(u, v) as f64;
|
||||
sum += c_uv * f_uv * COSINE_TABLE[x * 8 + u] * COSINE_TABLE[y * 8 + v];
|
||||
}
|
||||
}
|
||||
sum * 0.25
|
||||
}
|
||||
|
||||
pub fn idct_8x8(input: &Block8x8<i16>) -> Block8x8<f64> {
|
||||
let mut block = Block8x8::splat(0.0);
|
||||
for y in 0..8 {
|
||||
for x in 0..8 {
|
||||
let f = partial_idct_8x8(input, x, y);
|
||||
block.data[y * 8 + x] = f + 128.0;
|
||||
}
|
||||
}
|
||||
block
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
mod block;
|
||||
mod copy;
|
||||
mod dct;
|
||||
mod zigzag;
|
||||
|
||||
pub use block::Block8x8;
|
||||
pub use copy::{write_block_1x1, write_block_1x2, write_block_2x1, write_block_2x2};
|
||||
pub use zigzag::unzigzag64;
|
||||
@@ -0,0 +1,41 @@
|
||||
#[rustfmt::skip]
|
||||
const UNZIGZAG_TABLE: [usize; 64] = [
|
||||
0, 1, 5, 6, 14, 15, 27, 28,
|
||||
2, 4, 7, 13, 16, 26, 29, 42,
|
||||
3, 8, 12, 17, 25, 30, 41, 43,
|
||||
9, 11, 18, 24, 31, 40, 44, 53,
|
||||
10, 19, 23, 32, 39, 45, 52, 54,
|
||||
20, 22, 33, 38, 46, 51, 55, 60,
|
||||
21, 34, 37, 47, 50, 56, 59, 61,
|
||||
35, 36, 48, 49, 57, 58, 62, 63,
|
||||
];
|
||||
|
||||
// TODO optimize
|
||||
pub fn unzigzag64<T: Copy>(input: &[T; 64], output: &mut [T; 64]) {
|
||||
for i in 0..64 {
|
||||
output[i] = input[UNZIGZAG_TABLE[i]];
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::data::unzigzag64;
|
||||
|
||||
#[test]
|
||||
fn test_unzigzag64() {
|
||||
let mut input = [0; 64];
|
||||
let mut output = [0; 64];
|
||||
for (i, input) in input.iter_mut().enumerate() {
|
||||
*input = i + 1;
|
||||
}
|
||||
unzigzag64(&input, &mut output);
|
||||
assert_eq!(
|
||||
&output[..],
|
||||
&[
|
||||
1, 2, 6, 7, 15, 16, 28, 29, 3, 5, 8, 14, 17, 27, 30, 43, 4, 9, 13, 18, 26, 31, 42,
|
||||
44, 10, 12, 19, 25, 32, 41, 45, 54, 11, 20, 24, 33, 40, 46, 53, 55, 21, 23, 34, 39,
|
||||
47, 52, 56, 61, 22, 35, 38, 48, 51, 57, 60, 62, 36, 37, 49, 50, 58, 59, 63, 64
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,576 @@
|
||||
use std::{collections::HashMap, io::Read};
|
||||
|
||||
use bytemuck::Pod;
|
||||
|
||||
use crate::jpeg::{
|
||||
data::{self, Block8x8},
|
||||
error::Error,
|
||||
header::App0Header,
|
||||
huffman::{HuffmanDecoder, HuffmanTable},
|
||||
};
|
||||
use crate::{RgbImage, YCbCrImage};
|
||||
|
||||
pub const MAX_COMPONENTS: usize = 4;
|
||||
pub const COMPONENT_NAMES: &[&str] = &["luma", "chroma_b", "chroma_r", "<unimp>"];
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct JpegState {
|
||||
frame_info: Option<FrameInfo>,
|
||||
quantization_tables: [Option<Block8x8<u16>>; MAX_COMPONENTS],
|
||||
scan_info: Option<ScanInfo>,
|
||||
dc_huffman_tables: HashMap<u8, HuffmanTable>,
|
||||
ac_huffman_tables: HashMap<u8, HuffmanTable>,
|
||||
headers_parsed: bool,
|
||||
end_of_image: bool,
|
||||
is_progressive: bool,
|
||||
}
|
||||
|
||||
pub struct JpegDecoder<R: Read> {
|
||||
reader: R,
|
||||
state: JpegState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SamplingMode {
|
||||
Sample1x1,
|
||||
Sample1x2,
|
||||
Sample2x1,
|
||||
Sample2x2,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct FrameComponent {
|
||||
pub sampling: SamplingMode,
|
||||
pub qt_index: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FrameInfo {
|
||||
pub precision: u8,
|
||||
pub components: [Option<FrameComponent>; MAX_COMPONENTS],
|
||||
pub lines: usize,
|
||||
pub samples_per_line: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ScanInfo {
|
||||
pub dc_entropy_selectors: [u8; MAX_COMPONENTS],
|
||||
pub ac_entropy_selectors: [u8; MAX_COMPONENTS],
|
||||
pub spectral_predict_start: u8,
|
||||
pub spectral_predict_end: u8,
|
||||
pub successive_approximation: u8,
|
||||
pub component_mask: u8,
|
||||
}
|
||||
|
||||
impl SamplingMode {
|
||||
pub fn vertical(&self) -> usize {
|
||||
match self {
|
||||
Self::Sample1x1 | Self::Sample2x1 => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn horizontal(&self) -> usize {
|
||||
match self {
|
||||
Self::Sample1x1 | Self::Sample1x2 => 1,
|
||||
_ => 2,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn sample_count(&self) -> usize {
|
||||
match self {
|
||||
Self::Sample1x1 => 1,
|
||||
Self::Sample1x2 | Self::Sample2x1 => 2,
|
||||
Self::Sample2x2 => 4,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> JpegDecoder<R> {
|
||||
pub fn new(reader: R) -> Self {
|
||||
Self {
|
||||
reader,
|
||||
state: JpegState::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decode_headers(&mut self) -> Result<(), Error> {
|
||||
self.state.decode_headers(&mut self.reader)
|
||||
}
|
||||
|
||||
pub fn decode_ycbcr(&mut self) -> Result<YCbCrImage, Error> {
|
||||
self.state.decode_frame(&mut self.reader)
|
||||
}
|
||||
|
||||
pub fn decode_rgb(&mut self) -> Result<RgbImage, Error> {
|
||||
let ycbcr = self.decode_ycbcr()?;
|
||||
Ok(ycbcr.to_rgb())
|
||||
}
|
||||
}
|
||||
|
||||
impl JpegState {
|
||||
fn do_decode_headers<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
// SOI marker
|
||||
let soi_marker = read_u16be(reader)?;
|
||||
if soi_marker != 0xFFD8 {
|
||||
return Err(Error::MalformedHeader);
|
||||
}
|
||||
|
||||
let mut last_byte = 0;
|
||||
|
||||
loop {
|
||||
let mut m = read_u8(reader)?;
|
||||
if last_byte == 0xFF && (m == 0xFF || m == 0x00) {
|
||||
while m == 0xFF || m == 0x00 {
|
||||
last_byte = m;
|
||||
m = read_u8(reader)?;
|
||||
}
|
||||
}
|
||||
if last_byte == 0xFF {
|
||||
self.consume_marker(reader, m)?;
|
||||
|
||||
if self.scan_info.is_some() || self.end_of_image {
|
||||
break;
|
||||
}
|
||||
}
|
||||
last_byte = m;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn decode_headers<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
if self.headers_parsed {
|
||||
return Ok(());
|
||||
}
|
||||
self.do_decode_headers(reader)
|
||||
}
|
||||
|
||||
fn decode_frame<R: Read>(&mut self, reader: &mut R) -> Result<YCbCrImage, Error> {
|
||||
self.decode_headers(reader)?;
|
||||
if self.is_progressive {
|
||||
Err(Error::UnimplementedFeature("Progressive image decoding"))
|
||||
} else {
|
||||
decode_ycbcr_baseline(reader, self)
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_marker<R: Read>(&mut self, reader: &mut R, m: u8) -> Result<(), Error> {
|
||||
match m {
|
||||
0xC0..=0xC3 => self.consume_sof(reader, m - 0xC0),
|
||||
0xC4 => self.consume_dht(reader),
|
||||
0xE0 => self.consume_app0(reader),
|
||||
0xD9 => {
|
||||
self.end_of_image = true;
|
||||
Ok(())
|
||||
}
|
||||
0xDA => self.consume_sos(reader),
|
||||
0xDB => self.consume_dqt(reader),
|
||||
_ => Err(Error::InvalidMarker(m)),
|
||||
}
|
||||
}
|
||||
|
||||
fn consume_sof<R: Read>(&mut self, reader: &mut R, i: u8) -> Result<(), Error> {
|
||||
self.is_progressive = i >= 2;
|
||||
if self.frame_info.is_some() {
|
||||
return Err(Error::MultipleSofMarkers);
|
||||
}
|
||||
let length = read_u16be(reader)?;
|
||||
let sample_precision = read_u8(reader)?;
|
||||
let line_count = read_u16be(reader)?;
|
||||
let samples_per_line = read_u16be(reader)?;
|
||||
let component_count = read_u8(reader)?;
|
||||
// TODO parse non-8bpp images
|
||||
if sample_precision != 8 {
|
||||
return Err(Error::UnimplementedSamplePrecision(sample_precision));
|
||||
}
|
||||
if length as usize != 8 + 3 * component_count as usize {
|
||||
return Err(Error::MalformedHeader);
|
||||
}
|
||||
if component_count != 3 {
|
||||
return Err(Error::UnimplementedComponentCount(component_count));
|
||||
}
|
||||
log::debug!("SOF {sample_precision}bpp {samples_per_line}x{line_count}");
|
||||
let mut frame_info = FrameInfo {
|
||||
precision: sample_precision,
|
||||
samples_per_line: samples_per_line as usize,
|
||||
lines: line_count as usize,
|
||||
components: [const { None }; MAX_COMPONENTS],
|
||||
};
|
||||
for _ in 0..component_count {
|
||||
let component_id = read_u8(reader)? as usize;
|
||||
if component_id > MAX_COMPONENTS || component_id == 0 {
|
||||
return Err(Error::InvalidComponentIndex(component_id));
|
||||
}
|
||||
let component_id = component_id - 1;
|
||||
if frame_info.components[component_id].is_some() {
|
||||
return Err(Error::DuplicateComponent(component_id));
|
||||
}
|
||||
let sampling_factor = read_u8(reader)?;
|
||||
let sampling = match sampling_factor {
|
||||
0x11 => SamplingMode::Sample1x1,
|
||||
0x21 => SamplingMode::Sample2x1,
|
||||
0x12 => SamplingMode::Sample1x2,
|
||||
0x22 => SamplingMode::Sample2x2,
|
||||
_ => {
|
||||
let hsampling = sampling_factor >> 4;
|
||||
let vsampling = sampling_factor & 0xF;
|
||||
return Err(Error::UnimplementedSamplingFactor(
|
||||
COMPONENT_NAMES[component_id],
|
||||
hsampling,
|
||||
vsampling,
|
||||
));
|
||||
}
|
||||
};
|
||||
let qt_index = read_u8(reader)? as usize;
|
||||
frame_info.components[component_id] = Some(FrameComponent { sampling, qt_index });
|
||||
log::debug!(" [{component_id}] {sampling:?}, qt {qt_index}");
|
||||
}
|
||||
self.frame_info = Some(frame_info);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consume_dht<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
let mut length = read_u16be(reader)?.saturating_sub(2) as usize;
|
||||
let mut huffman_code_counts = [0; 16];
|
||||
let mut huffman_code_values = [0; 256];
|
||||
|
||||
while length > 16 {
|
||||
let ht_info = read_u8(reader)?;
|
||||
let dc_or_ac = ht_info >> 4;
|
||||
let index = ht_info & 0x0F;
|
||||
reader.read_exact(&mut huffman_code_counts)?;
|
||||
let total_value_count: usize =
|
||||
huffman_code_counts.into_iter().map(|c| c as usize).sum();
|
||||
length -= 17;
|
||||
if total_value_count > 256 || total_value_count > length {
|
||||
return Err(Error::InvalidHuffmanTable);
|
||||
}
|
||||
reader.read_exact(&mut huffman_code_values[..total_value_count])?;
|
||||
length -= total_value_count;
|
||||
match dc_or_ac {
|
||||
0 => {
|
||||
log::debug!(
|
||||
"DC[{index}] {huffman_code_counts:?}, {:?}",
|
||||
&huffman_code_values[..total_value_count]
|
||||
);
|
||||
let table = HuffmanTable::new(
|
||||
huffman_code_counts,
|
||||
huffman_code_values,
|
||||
true,
|
||||
self.is_progressive,
|
||||
)?;
|
||||
self.dc_huffman_tables.insert(index, table);
|
||||
}
|
||||
1 => {
|
||||
log::debug!(
|
||||
"AC[{index}] {huffman_code_counts:?}, {:?}",
|
||||
&huffman_code_values[..total_value_count]
|
||||
);
|
||||
let table = HuffmanTable::new(
|
||||
huffman_code_counts,
|
||||
huffman_code_values,
|
||||
false,
|
||||
self.is_progressive,
|
||||
)?;
|
||||
self.ac_huffman_tables.insert(index, table);
|
||||
}
|
||||
_ => {
|
||||
return Err(Error::InvalidHuffmanTable);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if length > 0 {
|
||||
return Err(Error::InvalidHuffmanTable);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consume_dqt<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
let mut length = read_u16be(reader)?.saturating_sub(2) as usize;
|
||||
while length != 0 {
|
||||
let qt_param = read_u8(reader)?;
|
||||
let qt_precision = (qt_param >> 4) as usize;
|
||||
let qt_index = (qt_param & 0x0F) as usize;
|
||||
if qt_index >= MAX_COMPONENTS {
|
||||
return Err(Error::InvalidQtIndex(qt_index));
|
||||
}
|
||||
if self.quantization_tables[qt_index].is_some() {
|
||||
return Err(Error::DuplicateQt(qt_index));
|
||||
}
|
||||
let qt_bytes_per_element = 1 << qt_precision;
|
||||
if qt_bytes_per_element * 64 + 1 > length {
|
||||
return Err(Error::MalformedHeader);
|
||||
}
|
||||
// Read 64 elements of the quantization table
|
||||
let qt_block = match qt_bytes_per_element {
|
||||
1 => {
|
||||
let mut qt_values = [0; 64];
|
||||
reader.read_exact(&mut qt_values)?;
|
||||
Block8x8::new(qt_values).map(|x| x as u16)
|
||||
}
|
||||
2 => {
|
||||
let mut qt_values = [0u16; 64];
|
||||
reader.read_exact(bytemuck::cast_slice_mut(&mut qt_values))?;
|
||||
Block8x8::new(qt_values)
|
||||
}
|
||||
_ => return Err(Error::InvalidQtPrecision(qt_bytes_per_element)),
|
||||
};
|
||||
|
||||
log::debug!("QT[{qt_index}]: {qt_bytes_per_element}Bpe\n{qt_block:3}");
|
||||
|
||||
self.quantization_tables[qt_index] = Some(qt_block);
|
||||
length -= 1 + qt_bytes_per_element * 64;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consume_app0<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
let app0: App0Header = read_struct(reader)?;
|
||||
if app0.identifier != *b"JFIF\x00" {
|
||||
return Err(Error::MalformedHeader);
|
||||
}
|
||||
assert_eq!(app0.xthumbnail, 0);
|
||||
assert_eq!(app0.ythumbnail, 0);
|
||||
assert_eq!(app0.length.read() as usize, size_of::<App0Header>());
|
||||
|
||||
// TODO skip thumbnail bytes
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn consume_sos<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
let _length = read_u16be(reader)?.saturating_sub(2) as usize;
|
||||
let mut scan = ScanInfo {
|
||||
dc_entropy_selectors: [0; MAX_COMPONENTS],
|
||||
ac_entropy_selectors: [0; MAX_COMPONENTS],
|
||||
spectral_predict_start: 0,
|
||||
spectral_predict_end: 0,
|
||||
successive_approximation: 0,
|
||||
component_mask: 0,
|
||||
};
|
||||
let component_count = read_u8(reader)? as usize;
|
||||
for _ in 0..component_count {
|
||||
let component_id = read_u8(reader)? as usize;
|
||||
let entropy_dst_selector = read_u8(reader)?;
|
||||
if component_id > MAX_COMPONENTS || component_id == 0 {
|
||||
return Err(Error::InvalidComponentIndex(component_id));
|
||||
}
|
||||
let component_id = component_id - 1;
|
||||
let dc_selector = entropy_dst_selector >> 4;
|
||||
let ac_selector = entropy_dst_selector & 0x0F;
|
||||
scan.dc_entropy_selectors[component_id] = dc_selector;
|
||||
scan.ac_entropy_selectors[component_id] = ac_selector;
|
||||
scan.component_mask |= 1 << component_id;
|
||||
}
|
||||
scan.spectral_predict_start = read_u8(reader)?;
|
||||
scan.spectral_predict_end = read_u8(reader)?;
|
||||
scan.successive_approximation = read_u8(reader)?;
|
||||
self.scan_info = Some(scan);
|
||||
self.headers_parsed = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read_u8<R: Read>(reader: &mut R) -> Result<u8, Error> {
|
||||
let mut buf = [0; 1];
|
||||
reader.read_exact(&mut buf)?;
|
||||
Ok(buf[0])
|
||||
}
|
||||
|
||||
pub fn read_u16be<R: Read>(reader: &mut R) -> Result<u16, Error> {
|
||||
let mut buf = [0; 2];
|
||||
reader.read_exact(&mut buf)?;
|
||||
Ok(u16::from_be_bytes(buf))
|
||||
}
|
||||
|
||||
pub fn read_struct<T: Pod, R: Read>(reader: &mut R) -> Result<T, Error> {
|
||||
let mut value = T::zeroed();
|
||||
reader.read_exact(bytemuck::bytes_of_mut(&mut value))?;
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn expand_number(code: u8, bits: u16) -> i16 {
|
||||
if code == 0 {
|
||||
return 0;
|
||||
}
|
||||
let l = 1 << (code - 1);
|
||||
if bits >= l {
|
||||
bits as i16
|
||||
} else {
|
||||
bits as i16 - ((1i16 << code) - 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_block<R: Read>(
|
||||
reader: &mut R,
|
||||
huffman: &mut HuffmanDecoder,
|
||||
dc_table: &HuffmanTable,
|
||||
ac_table: &HuffmanTable,
|
||||
quant_table: &Block8x8<u16>,
|
||||
dc_predictor: &mut i16,
|
||||
) -> Result<Block8x8<u8>, Error> {
|
||||
let mut block_data = [0; 64];
|
||||
|
||||
let mut l = 1;
|
||||
|
||||
// Read the DC coefficient
|
||||
let code = huffman.decode_symbol(reader, dc_table)?;
|
||||
let bits = huffman.get_bits(reader, code & 0x0F)?;
|
||||
let dccoeff = expand_number(code, bits).wrapping_add(*dc_predictor);
|
||||
*dc_predictor = dccoeff;
|
||||
block_data[0] = dccoeff.wrapping_mul(quant_table.data[0] as i16);
|
||||
|
||||
while l < 64 {
|
||||
let code = huffman.decode_symbol(reader, ac_table)?;
|
||||
if code == 0 {
|
||||
break;
|
||||
}
|
||||
|
||||
let code = if code > 15 {
|
||||
l += (code >> 4) as usize;
|
||||
code & 0x0F
|
||||
} else {
|
||||
code
|
||||
};
|
||||
|
||||
let bits = huffman.get_bits(reader, code)?;
|
||||
|
||||
if l < 64 {
|
||||
let coeff = expand_number(code, bits);
|
||||
block_data[l] = coeff.wrapping_mul(quant_table.data[l] as i16);
|
||||
l += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let block = Block8x8::unzigzag(&block_data);
|
||||
let block = block.idct();
|
||||
let block = block.map(|f| f as u8);
|
||||
Ok(block)
|
||||
}
|
||||
|
||||
fn decode_ycbcr_baseline<R: Read>(
|
||||
reader: &mut R,
|
||||
state: &mut JpegState,
|
||||
) -> Result<YCbCrImage, Error> {
|
||||
let frame_info = state.frame_info.as_ref().unwrap();
|
||||
let scan_info = state.scan_info.as_ref().unwrap();
|
||||
let component_count = scan_info.component_mask.count_ones() as usize;
|
||||
|
||||
let components = [
|
||||
frame_info.components[0].as_ref().unwrap(),
|
||||
frame_info.components[1].as_ref().unwrap(),
|
||||
frame_info.components[2].as_ref().unwrap(),
|
||||
];
|
||||
|
||||
let max_hsample = components
|
||||
.iter()
|
||||
.map(|c| c.sampling.horizontal())
|
||||
.max()
|
||||
.unwrap();
|
||||
let max_vsample = components
|
||||
.iter()
|
||||
.map(|c| c.sampling.vertical())
|
||||
.max()
|
||||
.unwrap();
|
||||
|
||||
let component_len = frame_info.samples_per_line * frame_info.lines;
|
||||
let mut component_data = [
|
||||
vec![0; component_len],
|
||||
vec![0; component_len],
|
||||
vec![0; component_len],
|
||||
];
|
||||
|
||||
let mut huffman = HuffmanDecoder::default();
|
||||
|
||||
let mut dc_predictors = [0; MAX_COMPONENTS];
|
||||
|
||||
let mcu_width_pixels = 8 * max_hsample;
|
||||
let mcu_height_pixels = 8 * max_vsample;
|
||||
|
||||
let mcu_columns = frame_info.samples_per_line.div_ceil(mcu_width_pixels);
|
||||
let mcu_rows = frame_info.lines.div_ceil(mcu_height_pixels);
|
||||
|
||||
'outer: for mcu_y in 0..mcu_rows {
|
||||
for mcu_x in 0..mcu_columns {
|
||||
for component_id in 0..component_count {
|
||||
// TODO move this to block decode
|
||||
if let Some(marker) = huffman.marker {
|
||||
match marker {
|
||||
0xD9 => {
|
||||
break 'outer;
|
||||
}
|
||||
_ => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
let hsamples = components[component_id].sampling.horizontal();
|
||||
let vsamples = components[component_id].sampling.vertical();
|
||||
let dc_index = scan_info.dc_entropy_selectors[component_id];
|
||||
let ac_index = scan_info.ac_entropy_selectors[component_id];
|
||||
let qt_index = frame_info.components[component_id]
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.qt_index;
|
||||
let dc_table = state
|
||||
.dc_huffman_tables
|
||||
.get(&dc_index)
|
||||
.ok_or(Error::MissingDCHT(dc_index))?;
|
||||
let ac_table = state
|
||||
.ac_huffman_tables
|
||||
.get(&ac_index)
|
||||
.ok_or(Error::MissingACHT(ac_index))?;
|
||||
let quant_table = state.quantization_tables[qt_index]
|
||||
.as_ref()
|
||||
.ok_or(Error::MissingQT(qt_index))?;
|
||||
|
||||
let upsample_y = max_vsample / vsamples;
|
||||
let upsample_x = max_hsample / hsamples;
|
||||
let output = &mut component_data[component_id];
|
||||
|
||||
for vsample in 0..vsamples {
|
||||
for hsample in 0..hsamples {
|
||||
let block = decode_block(
|
||||
reader,
|
||||
&mut huffman,
|
||||
dc_table,
|
||||
ac_table,
|
||||
quant_table,
|
||||
&mut dc_predictors[component_id],
|
||||
)?;
|
||||
let dst_block_x = (mcu_x * max_hsample + hsample) * 8;
|
||||
let dst_block_y = (mcu_y * max_vsample + vsample) * 8;
|
||||
|
||||
let write_fn = match (upsample_x, upsample_y) {
|
||||
(1, 1) => data::write_block_1x1,
|
||||
(1, 2) => data::write_block_1x2,
|
||||
(2, 1) => data::write_block_2x1,
|
||||
(2, 2) => data::write_block_2x2,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
write_fn(
|
||||
output,
|
||||
frame_info.samples_per_line,
|
||||
dst_block_x,
|
||||
dst_block_y,
|
||||
&block,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let [y, cb, cr] = component_data;
|
||||
|
||||
let ycbcr = YCbCrImage {
|
||||
luma: y,
|
||||
chroma_b: cb,
|
||||
chroma_r: cr,
|
||||
width: frame_info.samples_per_line,
|
||||
height: frame_info.lines,
|
||||
};
|
||||
|
||||
Ok(ycbcr)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
use std::io;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("{0}")]
|
||||
Io(#[from] io::Error),
|
||||
#[error("Unhandled marker byte: {0:#02x}")]
|
||||
InvalidMarker(u8),
|
||||
#[error("Malformed JPEG/JFIF header")]
|
||||
MalformedHeader,
|
||||
#[error("Multiple SOF markers within one image")]
|
||||
MultipleSofMarkers,
|
||||
#[error("Duplicate quantization table [{0}]")]
|
||||
DuplicateQt(usize),
|
||||
#[error("Invalid quantization table index [{0}]")]
|
||||
InvalidQtIndex(usize),
|
||||
#[error("Duplicate component [{0}]")]
|
||||
DuplicateComponent(usize),
|
||||
#[error("Invalid component index [{0}]")]
|
||||
InvalidComponentIndex(usize),
|
||||
#[error("Malformed Huffman bit sequence: {0:016b}")]
|
||||
MalformedHuffmanCode(u16),
|
||||
#[error("Invalid huffman table")]
|
||||
InvalidHuffmanTable,
|
||||
#[error("Invalid quantization table precision: {0}-byte")]
|
||||
InvalidQtPrecision(usize),
|
||||
|
||||
#[error("Missing DC Huffman table [{0}]")]
|
||||
MissingDCHT(u8),
|
||||
#[error("Missing AC Huffman table [{0}]")]
|
||||
MissingACHT(u8),
|
||||
#[error("Missing quantization table [{0}]")]
|
||||
MissingQT(usize),
|
||||
|
||||
// Unimplemented features
|
||||
#[error("Unsupported sample precision: {0} bits/sample")]
|
||||
UnimplementedSamplePrecision(u8),
|
||||
#[error("Unsupported {0} sampling factor: {1}x{2}")]
|
||||
UnimplementedSamplingFactor(&'static str, u8, u8),
|
||||
#[error("Unsupported component count: {0}")]
|
||||
UnimplementedComponentCount(u8),
|
||||
#[error("{0} is not supported")]
|
||||
UnimplementedFeature(&'static str),
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use bytemuck::{Pod, Zeroable};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
|
||||
#[repr(transparent)]
|
||||
pub struct BigEndian<T>([u8; size_of::<T>()], PhantomData<T>)
|
||||
where
|
||||
[u8; size_of::<T>()]: Sized + Zeroable + Pod;
|
||||
|
||||
#[derive(Debug, Clone, Copy, Pod, Zeroable)]
|
||||
#[repr(C)]
|
||||
pub struct App0Header {
|
||||
pub length: BigEndian<u16>,
|
||||
pub identifier: [u8; 5],
|
||||
pub version: BigEndian<u16>,
|
||||
pub units: u8,
|
||||
pub xdensity: BigEndian<u16>,
|
||||
pub ydensity: BigEndian<u16>,
|
||||
pub xthumbnail: u8,
|
||||
pub ythumbnail: u8,
|
||||
}
|
||||
|
||||
macro_rules! impl_big_endian {
|
||||
($($ty:ty),+) => {
|
||||
$(
|
||||
impl BigEndian<$ty> {
|
||||
pub fn read(&self) -> $ty {
|
||||
<$ty>::from_be_bytes(self.0)
|
||||
}
|
||||
}
|
||||
)+
|
||||
};
|
||||
}
|
||||
|
||||
impl_big_endian!(u16, u32);
|
||||
@@ -0,0 +1,253 @@
|
||||
use std::{io::Read, iter};
|
||||
|
||||
use crate::{jpeg::decoder::read_u8, jpeg::error::Error};
|
||||
|
||||
pub const LUT_BITS: u8 = 9;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct LutEntry {
|
||||
pub symbol: u8,
|
||||
pub length: u8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Lut<const BITS: usize>(Box<[LutEntry; 1 << BITS]>)
|
||||
where
|
||||
[LutEntry; 1 << BITS]: Sized;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HuffmanTable {
|
||||
pub(crate) bits: [u8; 16],
|
||||
pub(crate) huffval: [u8; 256],
|
||||
|
||||
lut: Lut<{ LUT_BITS as usize }>,
|
||||
}
|
||||
|
||||
impl<const BITS: usize> Lut<BITS>
|
||||
where
|
||||
[Option<LutEntry>; 1 << BITS]: Sized,
|
||||
{
|
||||
pub fn new() -> Self {
|
||||
Self(Box::new(
|
||||
[LutEntry {
|
||||
length: 0,
|
||||
symbol: 0,
|
||||
}; 1 << BITS],
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HuffmanDecoder {
|
||||
buffer: u64,
|
||||
bit_count: usize,
|
||||
pub marker: Option<u8>,
|
||||
}
|
||||
|
||||
impl HuffmanTable {
|
||||
pub fn new(
|
||||
counts: [u8; 16],
|
||||
values: [u8; 256],
|
||||
is_dc: bool,
|
||||
is_progressive: bool,
|
||||
) -> Result<Self, Error> {
|
||||
// TODO handle progressive decoding
|
||||
_ = is_dc;
|
||||
_ = is_progressive;
|
||||
|
||||
// Build huffman code table
|
||||
let (huffcode, huffsize) = Self::derive_huffman_codes(&counts)?;
|
||||
let mut lut = Lut::new();
|
||||
|
||||
for (i, &size) in huffsize
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(_, &size)| size <= LUT_BITS)
|
||||
{
|
||||
let extra_bits = LUT_BITS - size;
|
||||
let start = (huffcode[i] as usize) << extra_bits;
|
||||
|
||||
for j in 0..1 << extra_bits {
|
||||
lut.0[start + j] = LutEntry {
|
||||
symbol: values[i],
|
||||
length: size,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
let mut p = 0;
|
||||
let mut si = huffsize[0];
|
||||
let mut code = 0;
|
||||
let mut maxcode = [0; 18];
|
||||
while p < huffsize.len() && huffsize[p] != 0 {
|
||||
while p < huffsize.len() && huffsize[p] == si {
|
||||
code += 1;
|
||||
p += 1;
|
||||
}
|
||||
maxcode[si as usize] = code << (16 - si);
|
||||
|
||||
code <<= 1;
|
||||
si += 1;
|
||||
}
|
||||
|
||||
p = 0;
|
||||
let mut offset = [0; 18];
|
||||
for l in 0..16 {
|
||||
if counts[l] == 0 {
|
||||
maxcode[l] = -1;
|
||||
} else {
|
||||
offset[l] = (p as i32) - (huffcode[p] as i32);
|
||||
p += usize::from(counts[l]);
|
||||
}
|
||||
}
|
||||
|
||||
offset[17] = 0;
|
||||
offset[17] = 0x000F_FFFF;
|
||||
|
||||
Ok(Self {
|
||||
lut,
|
||||
bits: counts,
|
||||
huffval: values,
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn lookup(&self, index: usize) -> LutEntry {
|
||||
self.lut.0[index]
|
||||
}
|
||||
|
||||
fn derive_huffman_codes(counts: &[u8; 16]) -> Result<(Vec<u16>, Vec<u8>), Error> {
|
||||
let huffsize = counts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.fold(Vec::new(), |mut acc, (i, &value)| {
|
||||
acc.extend(iter::repeat_n((i + 1) as u8, value as usize));
|
||||
acc
|
||||
});
|
||||
let mut huffcode = vec![0u16; huffsize.len()];
|
||||
let mut code_size = huffsize[0];
|
||||
let mut code = 0;
|
||||
|
||||
for (i, &size) in huffsize.iter().enumerate() {
|
||||
while code_size < size {
|
||||
code <<= 1;
|
||||
code_size += 1;
|
||||
}
|
||||
|
||||
if code >= 1 << size {
|
||||
return Err(Error::InvalidHuffmanTable);
|
||||
}
|
||||
|
||||
huffcode[i] = code as u16;
|
||||
code += 1;
|
||||
}
|
||||
|
||||
Ok((huffcode, huffsize))
|
||||
}
|
||||
}
|
||||
|
||||
impl HuffmanDecoder {
|
||||
pub fn decode_symbol<R: Read>(
|
||||
&mut self,
|
||||
reader: &mut R,
|
||||
table: &HuffmanTable,
|
||||
) -> Result<u8, Error> {
|
||||
if self.bit_count < 16 {
|
||||
self.read_bits(reader)?;
|
||||
}
|
||||
|
||||
let bits = self.peek_bits(LUT_BITS);
|
||||
let lut_entry = table.lookup(bits as usize);
|
||||
if lut_entry.length != 0 {
|
||||
self.consume_bits(lut_entry.length);
|
||||
return Ok(lut_entry.symbol);
|
||||
}
|
||||
|
||||
let mut code = 0;
|
||||
let mut first = 0;
|
||||
let mut index = 0;
|
||||
for len in 1..=16 {
|
||||
code = (code << 1) | self.get_bits(reader, 1)?;
|
||||
let count = table.bits[len - 1] as usize;
|
||||
if code as usize >= first && code as usize - first < count {
|
||||
let symbol = table.huffval[index + (code as usize - first)];
|
||||
return Ok(symbol);
|
||||
}
|
||||
index += count;
|
||||
first = (first + count) << 1;
|
||||
}
|
||||
|
||||
Err(Error::MalformedHuffmanCode(code))
|
||||
}
|
||||
|
||||
fn consume_bits(&mut self, count: u8) {
|
||||
self.buffer <<= count as usize;
|
||||
self.bit_count -= count as usize;
|
||||
}
|
||||
|
||||
pub fn get_bits<R: Read>(&mut self, reader: &mut R, count: u8) -> Result<u16, Error> {
|
||||
if self.bit_count < 16 {
|
||||
self.read_bits(reader)?;
|
||||
}
|
||||
let bits = self.peek_bits(count);
|
||||
self.consume_bits(count);
|
||||
Ok(bits)
|
||||
}
|
||||
|
||||
fn peek_bits(&mut self, count: u8) -> u16 {
|
||||
assert!(count <= 16);
|
||||
assert!(self.bit_count >= count as usize);
|
||||
((self.buffer.wrapping_shr(64 - count as u32)) & ((1 << count) - 1)) as u16
|
||||
}
|
||||
|
||||
fn read_bits<R: Read>(&mut self, reader: &mut R) -> Result<(), Error> {
|
||||
while self.bit_count <= 56 {
|
||||
let byte = match self.marker {
|
||||
Some(_) => 0,
|
||||
None => read_u8(reader)?,
|
||||
};
|
||||
|
||||
if byte == 0xFF {
|
||||
let mut next_byte = read_u8(reader)?;
|
||||
|
||||
if next_byte != 0x00 {
|
||||
while next_byte == 0xFF {
|
||||
next_byte = read_u8(reader)?;
|
||||
}
|
||||
|
||||
match next_byte {
|
||||
0x00 => todo!(),
|
||||
_ => self.marker = Some(next_byte),
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
self.buffer |= (byte as u64) << (56 - self.bit_count);
|
||||
self.bit_count += 8;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::jpeg::huffman::HuffmanDecoder;
|
||||
|
||||
#[test]
|
||||
fn test_huffman_bit_read() {
|
||||
let mut huffman = HuffmanDecoder::default();
|
||||
let bitstring = b"\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78\x12\x34\x56\x78";
|
||||
let mut reader = &bitstring[..];
|
||||
let bits = huffman.get_bits(&mut reader, 3).unwrap();
|
||||
assert_eq!(bits, 0);
|
||||
let bits = huffman.get_bits(&mut reader, 2).unwrap();
|
||||
assert_eq!(bits, 0b10);
|
||||
let bits = huffman.get_bits(&mut reader, 2).unwrap();
|
||||
assert_eq!(bits, 0b01);
|
||||
let bits = huffman.get_bits(&mut reader, 1).unwrap();
|
||||
assert_eq!(bits, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
pub mod data;
|
||||
pub mod decoder;
|
||||
pub mod error;
|
||||
pub mod header;
|
||||
pub mod huffman;
|
||||
|
||||
pub use error::Error;
|
||||
@@ -0,0 +1,8 @@
|
||||
#![feature(generic_const_exprs)]
|
||||
#![allow(incomplete_features)]
|
||||
|
||||
pub mod image;
|
||||
pub mod jpeg;
|
||||
|
||||
pub use image::{RgbImage, YCbCrImage};
|
||||
pub use jpeg::decoder::JpegDecoder;
|
||||
@@ -76,6 +76,7 @@ const PROGRAMS: &[(&str, &str)] = &[
|
||||
("colors", "bin/colors"),
|
||||
("colors-bar", "bin/colors-bar"),
|
||||
("term", "bin/term"),
|
||||
("iv", "bin/iv"),
|
||||
// red
|
||||
("red", "bin/red"),
|
||||
// rdb
|
||||
|
||||
Reference in New Issue
Block a user