diff --git a/README.md b/README.md index 315d006..d0a9651 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,16 @@ Fast encoder/decoder for [QOI image format](https://qoiformat.org/), implemented - `no_std` support. - Roundtrip-tested vs the reference C implementation; fuzz-tested. +### Note about this fork + +This fork implements a slight improvement to the original specs, which leaves unused the `QOI_OP_RGBA` chunk flag with RGB. + +Here, we use this flag for the new `QOI_OP_RUN2` chunk. It's like the `QOI_OP_RUN` chunk, but followed by two bytes representing `run` (BE). (only for RGB, as the flag is already used for RGBA) + +The decoder remains fully compatible with the original one (except when using `QOI_OP_RGBA` in a RGB image). The encoder is fully compatible for RGBA, not for RGB (except using the `reference` feature). + +Why this? Because it enables significant improvements for compressing images with large uniform areas (such as screen captures), or for encoding a diff-filtered video stream where successive frames often have identical regions. (see [syeve](https://framagit.org/ZettaScript/syeve) for the video encoding) + ### Examples ```rust diff --git a/src/consts.rs b/src/consts.rs index 2281c24..0e39c4b 100644 --- a/src/consts.rs +++ b/src/consts.rs @@ -4,6 +4,7 @@ pub const QOI_OP_LUMA: u8 = 0x80; // 10xxxxxx pub const QOI_OP_RUN: u8 = 0xc0; // 11xxxxxx pub const QOI_OP_RGB: u8 = 0xfe; // 11111110 pub const QOI_OP_RGBA: u8 = 0xff; // 11111111 +pub const QOI_OP_RUN2: u8 = 0xff; // 11111111 pub const QOI_MASK_2: u8 = 0xc0; // (11)000000 diff --git a/src/decode.rs b/src/decode.rs index 019dac2..a4343d0 100644 --- a/src/decode.rs +++ b/src/decode.rs @@ -49,9 +49,21 @@ where px.update_rgb(*r, *g, *b); data = dtail; } - [QOI_OP_RGBA, r, g, b, a, dtail @ ..] if RGBA => { - px.update_rgba(*r, *g, *b, *a); - data = dtail; + [QOI_OP_RGBA, dtail @ ..] => { + if RGBA { + if let [r, g, b, a, dtail @ ..] = dtail { + px.update_rgba(*r, *g, *b, *a); + data = dtail; + } + } else if let [b1, b2, dtail @ ..] = dtail { + *px_out = px.into(); + let run = (u16::from_be_bytes([*b1, *b2]) as usize).min(pixels.len()); + let (phead, ptail) = pixels.split_at_mut(run); // can't panic + phead.fill(px.into()); + pixels = ptail; + data = dtail; + continue; + } } [b1 @ QOI_OP_RUN..=QOI_OP_RUN_END, dtail @ ..] => { *px_out = px.into(); diff --git a/src/encode.rs b/src/encode.rs index 0ed8476..ce2b36f 100644 --- a/src/encode.rs +++ b/src/encode.rs @@ -6,7 +6,7 @@ use std::io::Write; use bytemuck::Pod; -use crate::consts::{QOI_HEADER_SIZE, QOI_OP_INDEX, QOI_OP_RUN, QOI_PADDING, QOI_PADDING_SIZE}; +use crate::consts::{QOI_HEADER_SIZE, QOI_OP_INDEX, QOI_OP_RUN, QOI_PADDING, QOI_PADDING_SIZE, QOI_OP_RUN2}; use crate::error::{Error, Result}; use crate::header::Header; use crate::pixel::{Pixel, SupportedChannels}; @@ -26,7 +26,7 @@ where let mut index = [Pixel::new(); 256]; let mut px_prev = Pixel::new().with_a(0xff); let mut hash_prev = px_prev.hash_index(); - let mut run = 0_u8; + let mut run = 0_u16; let mut px = Pixel::::new().with_a(0xff); let mut index_allowed = false; @@ -36,8 +36,20 @@ where px.read(chunk); if px == px_prev { run += 1; + #[cfg(not(feature = "reference"))] + if N == 3 { + if run == 65535 || unlikely(i == n_pixels - 1) { + let [b1, b2] = (run-1).to_be_bytes(); + buf = buf.write_many(&[QOI_OP_RUN2, b1, b2])?; + run = 0; + } + } else if run == 62 || unlikely(i == n_pixels - 1) { + buf = buf.write_one(QOI_OP_RUN | (run as u8 - 1))?; + run = 0; + } + #[cfg(feature = "reference")] if run == 62 || unlikely(i == n_pixels - 1) { - buf = buf.write_one(QOI_OP_RUN | (run - 1))?; + buf = buf.write_one(QOI_OP_RUN | (run as u8 - 1))?; run = 0; } } else { @@ -45,15 +57,18 @@ where #[cfg(not(feature = "reference"))] { // credits for the original idea: @zakarumych (had to be fixed though) - buf = buf.write_one(if run == 1 && index_allowed { - QOI_OP_INDEX | hash_prev + buf = if run == 1 && index_allowed { + buf.write_one(QOI_OP_INDEX | hash_prev)? + } else if N == 4 || run < 63 { + buf.write_one(QOI_OP_RUN | (run as u8 - 1))? } else { - QOI_OP_RUN | (run - 1) - })?; + let [b1, b2] = (run-1).to_be_bytes(); + buf.write_many(&[QOI_OP_RUN2, b1, b2])? + }; } #[cfg(feature = "reference")] { - buf = buf.write_one(QOI_OP_RUN | (run - 1))?; + buf = buf.write_one(QOI_OP_RUN | (run as u8 - 1))?; } run = 0; }