Skip to content

Commit a24a542

Browse files
authored
Merge pull request #6 from elmarco/raw
RFE: encoder: add stride & various raw input formats support aldanor#11
2 parents 09dadb0 + 9b0e41c commit a24a542

File tree

6 files changed

+360
-61
lines changed

6 files changed

+360
-61
lines changed

src/encode.rs

Lines changed: 183 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
#[cfg(any(feature = "std", feature = "alloc"))]
22
use alloc::{vec, vec::Vec};
3-
use core::convert::TryFrom;
43
#[cfg(feature = "std")]
54
use std::io::Write;
65

@@ -10,13 +9,16 @@ use crate::consts::{QOI_HEADER_SIZE, QOI_OP_INDEX, QOI_OP_RUN, QOI_PADDING, QOI_
109
use crate::error::{Error, Result};
1110
use crate::header::Header;
1211
use crate::pixel::{Pixel, SupportedChannels};
13-
use crate::types::{Channels, ColorSpace};
12+
use crate::types::{Channels, ColorSpace, RawChannels};
1413
#[cfg(feature = "std")]
1514
use crate::utils::GenericWriter;
1615
use crate::utils::{unlikely, BytesMut, Writer};
1716

1817
#[allow(clippy::cast_possible_truncation, unused_assignments, unused_variables)]
19-
fn encode_impl<W: Writer, const N: usize>(mut buf: W, data: &[u8]) -> Result<usize>
18+
fn encode_impl<W: Writer, const N: usize, const R: usize>(
19+
mut buf: W, data: &[u8], width: usize, height: usize, stride: usize,
20+
read_px: impl Fn(&mut Pixel<N>, &[u8]),
21+
) -> Result<usize>
2022
where
2123
Pixel<N>: SupportedChannels,
2224
[u8; N]: Pod,
@@ -30,59 +32,57 @@ where
3032
let mut px = Pixel::<N>::new().with_a(0xff);
3133
let mut index_allowed = false;
3234

33-
let n_pixels = data.len() / N;
35+
let n_pixels = width * height;
3436

35-
for (i, chunk) in data.chunks_exact(N).enumerate() {
36-
px.read(chunk);
37-
if px == px_prev {
38-
run += 1;
39-
if run == 62 || unlikely(i == n_pixels - 1) {
40-
buf = buf.write_one(QOI_OP_RUN | (run - 1))?;
41-
run = 0;
42-
}
43-
} else {
44-
if run != 0 {
45-
#[cfg(not(feature = "reference"))]
46-
{
47-
// credits for the original idea: @zakarumych (had to be fixed though)
48-
buf = buf.write_one(if run == 1 && index_allowed {
49-
QOI_OP_INDEX | hash_prev
50-
} else {
51-
QOI_OP_RUN | (run - 1)
52-
})?;
53-
}
54-
#[cfg(feature = "reference")]
55-
{
37+
let mut i = 0;
38+
for row in data.chunks(stride).take(height) {
39+
let pixel_row = &row[..width * R];
40+
for chunk in pixel_row.chunks_exact(R) {
41+
read_px(&mut px, chunk);
42+
if px == px_prev {
43+
run += 1;
44+
if run == 62 || unlikely(i == n_pixels - 1) {
5645
buf = buf.write_one(QOI_OP_RUN | (run - 1))?;
46+
run = 0;
5747
}
58-
run = 0;
59-
}
60-
index_allowed = true;
61-
let px_rgba = px.as_rgba(0xff);
62-
hash_prev = px_rgba.hash_index();
63-
let index_px = &mut index[hash_prev as usize];
64-
if *index_px == px_rgba {
65-
buf = buf.write_one(QOI_OP_INDEX | hash_prev)?;
6648
} else {
67-
*index_px = px_rgba;
68-
buf = px.encode_into(px_prev, buf)?;
49+
if run != 0 {
50+
#[cfg(not(feature = "reference"))]
51+
{
52+
// credits for the original idea: @zakarumych (had to be fixed though)
53+
buf = buf.write_one(if run == 1 && index_allowed {
54+
QOI_OP_INDEX | hash_prev
55+
} else {
56+
QOI_OP_RUN | (run - 1)
57+
})?;
58+
}
59+
#[cfg(feature = "reference")]
60+
{
61+
buf = buf.write_one(QOI_OP_RUN | (run - 1))?;
62+
}
63+
run = 0;
64+
}
65+
index_allowed = true;
66+
let px_rgba = px.as_rgba(0xff);
67+
hash_prev = px_rgba.hash_index();
68+
let index_px = &mut index[hash_prev as usize];
69+
if *index_px == px_rgba {
70+
buf = buf.write_one(QOI_OP_INDEX | hash_prev)?;
71+
} else {
72+
*index_px = px_rgba;
73+
buf = px.encode_into(px_prev, buf)?;
74+
}
75+
px_prev = px;
6976
}
70-
px_prev = px;
77+
i += 1;
7178
}
7279
}
7380

81+
assert_eq!(i, n_pixels);
7482
buf = buf.write_many(&QOI_PADDING)?;
7583
Ok(cap.saturating_sub(buf.capacity()))
7684
}
7785

78-
#[inline]
79-
fn encode_impl_all<W: Writer>(out: W, data: &[u8], channels: Channels) -> Result<usize> {
80-
match channels {
81-
Channels::Rgb => encode_impl::<_, 3>(out, data),
82-
Channels::Rgba => encode_impl::<_, 4>(out, data),
83-
}
84-
}
85-
8686
/// The maximum number of bytes the encoded image will take.
8787
///
8888
/// Can be used to pre-allocate the buffer to encode the image into.
@@ -113,30 +113,102 @@ pub fn encode_to_vec(data: impl AsRef<[u8]>, width: u32, height: u32) -> Result<
113113
Encoder::new(&data, width, height)?.encode_to_vec()
114114
}
115115

116+
pub struct EncoderBuilder<'a> {
117+
data: &'a [u8],
118+
width: u32,
119+
height: u32,
120+
stride: Option<usize>,
121+
raw_channels: Option<RawChannels>,
122+
colorspace: Option<ColorSpace>,
123+
}
124+
125+
impl<'a> EncoderBuilder<'a> {
126+
/// Creates a new encoder builder from a given array of pixel data and image dimensions.
127+
pub fn new(data: &'a (impl AsRef<[u8]> + ?Sized), width: u32, height: u32) -> Self {
128+
Self {
129+
data: data.as_ref(),
130+
width,
131+
height,
132+
stride: None,
133+
raw_channels: None,
134+
colorspace: None,
135+
}
136+
}
137+
138+
/// Set the stride of the pixel data.
139+
pub const fn stride(mut self, stride: usize) -> Self {
140+
self.stride = Some(stride);
141+
self
142+
}
143+
144+
/// Set the input format of the pixel data.
145+
pub const fn raw_channels(mut self, raw_channels: RawChannels) -> Self {
146+
self.raw_channels = Some(raw_channels);
147+
self
148+
}
149+
150+
/// Set the colorspace.
151+
pub const fn colorspace(mut self, colorspace: ColorSpace) -> Self {
152+
self.colorspace = Some(colorspace);
153+
self
154+
}
155+
156+
/// Build the encoder.
157+
pub fn build(self) -> Result<Encoder<'a>> {
158+
let EncoderBuilder { data, width, height, stride, raw_channels, colorspace } = self;
159+
160+
let size = data.len();
161+
let no_stride = stride.is_none();
162+
let stride = stride.unwrap_or(
163+
size.checked_div(height as usize)
164+
.ok_or(Error::InvalidImageDimensions { width, height })?,
165+
);
166+
let raw_channels = raw_channels.unwrap_or(if stride == width as usize * 3 {
167+
RawChannels::Rgb
168+
} else {
169+
RawChannels::Rgba
170+
});
171+
172+
if stride < width as usize * raw_channels.bytes_per_pixel() {
173+
return Err(Error::InvalidImageLength { size, width, height });
174+
}
175+
if stride * (height - 1) as usize + width as usize * raw_channels.bytes_per_pixel() < size {
176+
return Err(Error::InvalidImageLength { size, width, height });
177+
}
178+
if no_stride && size != width as usize * height as usize * raw_channels.bytes_per_pixel() {
179+
return Err(Error::InvalidImageLength { size, width, height });
180+
}
181+
182+
let channels = raw_channels.into();
183+
let colorspace = colorspace.unwrap_or_default();
184+
185+
Ok(Encoder {
186+
data,
187+
stride,
188+
raw_channels,
189+
header: Header::try_new(self.width, self.height, channels, colorspace)?,
190+
})
191+
}
192+
}
193+
116194
/// Encode QOI images into buffers or into streams.
117195
pub struct Encoder<'a> {
118196
data: &'a [u8],
197+
stride: usize,
198+
raw_channels: RawChannels,
119199
header: Header,
120200
}
121201

122202
impl<'a> Encoder<'a> {
123203
/// Creates a new encoder from a given array of pixel data and image dimensions.
204+
/// The data must be in RGB(A) order, without fill borders (extra stride).
124205
///
125206
/// The number of channels will be inferred automatically (the valid values
126207
/// are 3 or 4). The color space will be set to sRGB by default.
127208
#[inline]
128209
#[allow(clippy::cast_possible_truncation)]
129210
pub fn new(data: &'a (impl AsRef<[u8]> + ?Sized), width: u32, height: u32) -> Result<Self> {
130-
let data = data.as_ref();
131-
let mut header =
132-
Header::try_new(width, height, Channels::default(), ColorSpace::default())?;
133-
let size = data.len();
134-
let n_channels = size / header.n_pixels();
135-
if header.n_pixels() * n_channels != size {
136-
return Err(Error::InvalidImageLength { size, width, height });
137-
}
138-
header.channels = Channels::try_from(n_channels.min(0xff) as u8)?;
139-
Ok(Self { data, header })
211+
EncoderBuilder::new(data, width, height).build()
140212
}
141213

142214
/// Returns a new encoder with modified color space.
@@ -181,7 +253,7 @@ impl<'a> Encoder<'a> {
181253
}
182254
let (head, tail) = buf.split_at_mut(QOI_HEADER_SIZE); // can't panic
183255
head.copy_from_slice(&self.header.encode());
184-
let n_written = encode_impl_all(BytesMut::new(tail), self.data, self.header.channels)?;
256+
let n_written = self.encode_impl_all(BytesMut::new(tail))?;
185257
Ok(QOI_HEADER_SIZE + n_written)
186258
}
187259

@@ -203,8 +275,62 @@ impl<'a> Encoder<'a> {
203275
#[inline]
204276
pub fn encode_to_stream<W: Write>(&self, writer: &mut W) -> Result<usize> {
205277
writer.write_all(&self.header.encode())?;
206-
let n_written =
207-
encode_impl_all(GenericWriter::new(writer), self.data, self.header.channels)?;
278+
let n_written = self.encode_impl_all(GenericWriter::new(writer))?;
208279
Ok(n_written + QOI_HEADER_SIZE)
209280
}
281+
282+
#[inline]
283+
fn encode_impl_all<W: Writer>(&self, out: W) -> Result<usize> {
284+
let width = self.header.width as usize;
285+
let height = self.header.height as usize;
286+
let stride = self.stride;
287+
match self.raw_channels {
288+
RawChannels::Rgb => {
289+
encode_impl::<_, 3, 3>(out, self.data, width, height, stride, Pixel::read)
290+
}
291+
RawChannels::Bgr => {
292+
encode_impl::<_, 3, 3>(out, self.data, width, height, stride, |px, c| {
293+
px.update_rgb(c[2], c[1], c[0]);
294+
})
295+
}
296+
RawChannels::Rgba => {
297+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, Pixel::read)
298+
}
299+
RawChannels::Argb => {
300+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
301+
px.update_rgba(c[1], c[2], c[3], c[0]);
302+
})
303+
}
304+
RawChannels::Rgbx => {
305+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
306+
px.read(&c[..3]);
307+
})
308+
}
309+
RawChannels::Xrgb => {
310+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
311+
px.update_rgb(c[1], c[2], c[3]);
312+
})
313+
}
314+
RawChannels::Bgra => {
315+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
316+
px.update_rgba(c[2], c[1], c[0], c[3]);
317+
})
318+
}
319+
RawChannels::Abgr => {
320+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
321+
px.update_rgba(c[3], c[2], c[1], c[0]);
322+
})
323+
}
324+
RawChannels::Bgrx => {
325+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
326+
px.update_rgb(c[2], c[1], c[0]);
327+
})
328+
}
329+
RawChannels::Xbgr => {
330+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
331+
px.update_rgb(c[3], c[2], c[1]);
332+
})
333+
}
334+
}
335+
}
210336
}

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ pub enum Error {
2222
UnexpectedBufferEnd,
2323
/// Invalid stream end marker encountered when decoding
2424
InvalidPadding,
25+
/// Invalid stride
26+
InvalidStride { stride: usize },
2527
#[cfg(feature = "std")]
2628
/// Generic I/O error from the wrapped reader/writer
2729
IoError(std::io::Error),
@@ -57,6 +59,9 @@ impl Display for Error {
5759
Self::InvalidPadding => {
5860
write!(f, "invalid padding (stream end marker mismatch)")
5961
}
62+
Self::InvalidStride { stride } => {
63+
write!(f, "invalid stride: {stride}")
64+
}
6065
#[cfg(feature = "std")]
6166
Self::IoError(ref err) => {
6267
write!(f, "i/o error: {err}")

src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,8 +88,8 @@ pub use crate::decode::{decode_header, decode_to_buf, Decoder};
8888

8989
#[cfg(any(feature = "alloc", feature = "std"))]
9090
pub use crate::encode::encode_to_vec;
91-
pub use crate::encode::{encode_max_len, encode_to_buf, Encoder};
91+
pub use crate::encode::{encode_max_len, encode_to_buf, Encoder, EncoderBuilder};
9292

9393
pub use crate::error::{Error, Result};
9494
pub use crate::header::Header;
95-
pub use crate::types::{Channels, ColorSpace};
95+
pub use crate::types::{Channels, ColorSpace, RawChannels};

src/pixel.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use bytemuck::{cast, Pod};
55

66
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
77
#[repr(transparent)]
8+
// in RGBA order
89
pub struct Pixel<const N: usize>([u8; N]);
910

1011
impl<const N: usize> Pixel<N> {

0 commit comments

Comments
 (0)