Skip to content

Commit 5b9aa9b

Browse files
committed
encoder: add stride & various input formats support
Signed-off-by: Marc-André Lureau <[email protected]>
1 parent d1c57d2 commit 5b9aa9b

File tree

5 files changed

+376
-60
lines changed

5 files changed

+376
-60
lines changed

src/encode.rs

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

122211
impl<'a> Encoder<'a> {
123212
/// Creates a new encoder from a given array of pixel data and image dimensions.
213+
/// The data must be in RGB(A) order, without fill borders (no stride).
124214
///
125215
/// The number of channels will be inferred automatically (the valid values
126216
/// are 3 or 4). The color space will be set to sRGB by default.
127217
#[inline]
128218
#[allow(clippy::cast_possible_truncation)]
129219
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 })
220+
EncoderBuilder::new(data, width, height).build()
140221
}
141222

142223
/// Returns a new encoder with modified color space.
@@ -181,7 +262,7 @@ impl<'a> Encoder<'a> {
181262
}
182263
let (head, tail) = buf.split_at_mut(QOI_HEADER_SIZE); // can't panic
183264
head.copy_from_slice(&self.header.encode());
184-
let n_written = encode_impl_all(BytesMut::new(tail), self.data, self.header.channels)?;
265+
let n_written = self.encode_impl_all(BytesMut::new(tail))?;
185266
Ok(QOI_HEADER_SIZE + n_written)
186267
}
187268

@@ -203,8 +284,62 @@ impl<'a> Encoder<'a> {
203284
#[inline]
204285
pub fn encode_to_stream<W: Write>(&self, writer: &mut W) -> Result<usize> {
205286
writer.write_all(&self.header.encode())?;
206-
let n_written =
207-
encode_impl_all(GenericWriter::new(writer), self.data, self.header.channels)?;
287+
let n_written = self.encode_impl_all(GenericWriter::new(writer))?;
208288
Ok(n_written + QOI_HEADER_SIZE)
209289
}
290+
291+
#[inline]
292+
fn encode_impl_all<W: Writer>(&self, out: W) -> Result<usize> {
293+
let width = self.header.width as usize;
294+
let height = self.header.height as usize;
295+
let stride = self.stride;
296+
match self.source_channels {
297+
SourceChannels::Rgb => {
298+
encode_impl::<_, 3, 3>(out, self.data, width, height, stride, Pixel::read)
299+
}
300+
SourceChannels::Bgr => {
301+
encode_impl::<_, 3, 3>(out, self.data, width, height, stride, |px, c| {
302+
px.update_rgb(c[2], c[1], c[0]);
303+
})
304+
}
305+
SourceChannels::Rgba => {
306+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, Pixel::read)
307+
}
308+
SourceChannels::Argb => {
309+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
310+
px.update_rgba(c[1], c[2], c[3], c[0]);
311+
})
312+
}
313+
SourceChannels::Rgbx => {
314+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
315+
px.read(&c[..3]);
316+
})
317+
}
318+
SourceChannels::Xrgb => {
319+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
320+
px.update_rgb(c[1], c[2], c[3]);
321+
})
322+
}
323+
SourceChannels::Bgra => {
324+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
325+
px.update_rgba(c[2], c[1], c[0], c[3]);
326+
})
327+
}
328+
SourceChannels::Abgr => {
329+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
330+
px.update_rgba(c[3], c[2], c[1], c[0]);
331+
})
332+
}
333+
SourceChannels::Bgrx => {
334+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
335+
px.update_rgb(c[2], c[1], c[0]);
336+
})
337+
}
338+
SourceChannels::Xbgr => {
339+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
340+
px.update_rgb(c[3], c[2], c[1]);
341+
})
342+
}
343+
}
344+
}
210345
}

src/error.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ pub enum Error {
1616
InvalidImageDimensions { width: u32, height: u32 },
1717
/// Image dimensions are inconsistent with image buffer length
1818
InvalidImageLength { size: usize, width: u32, height: u32 },
19+
/// Image stride is inconsistent with image dimension and buffer length
20+
InvalidImageStride { size: usize, width: u32, height: u32, stride: usize },
1921
/// Output buffer is too small to fit encoded/decoded image
2022
OutputBufferTooSmall { size: usize, required: usize },
2123
/// Input buffer ended unexpectedly before decoding was finished
@@ -48,6 +50,9 @@ impl Display for Error {
4850
Self::InvalidImageLength { size, width, height } => {
4951
write!(f, "invalid image length: {size} bytes for {width}x{height}")
5052
}
53+
Self::InvalidImageStride { size, width, height, stride } => {
54+
write!(f, "invalid image stride: {stride} for {size} bytes of {width}x{height}")
55+
}
5156
Self::OutputBufferTooSmall { size, required } => {
5257
write!(f, "output buffer size too small: {size} (required: {required})")
5358
}

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, SourceChannels};

0 commit comments

Comments
 (0)