Skip to content

Commit c53fa13

Browse files
committed
encoder: add stride & various raw input formats support
Signed-off-by: Marc-André Lureau <marcandre.lureau@redhat.com>
1 parent 6dd5ac2 commit c53fa13

File tree

4 files changed

+192
-48
lines changed

4 files changed

+192
-48
lines changed

src/encode.rs

Lines changed: 129 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@ use crate::consts::{QOI_HEADER_SIZE, QOI_OP_INDEX, QOI_OP_RUN, QOI_PADDING, QOI_
1010
use crate::error::{Error, Result};
1111
use crate::header::Header;
1212
use crate::pixel::{Pixel, SupportedChannels};
13-
use crate::types::{Channels, ColorSpace};
13+
use crate::types::{Channels, ColorSpace, RawChannels};
1414
#[cfg(feature = "std")]
1515
use crate::utils::GenericWriter;
1616
use crate::utils::{unlikely, BytesMut, Writer};
1717

1818
#[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>
19+
fn encode_impl<W: Writer, const N: usize, const R: usize>(
20+
mut buf: W, data: &[u8], width: usize, height: usize, stride: usize,
21+
read_px: impl Fn(&mut Pixel<N>, &[u8]),
22+
) -> Result<usize>
2023
where
2124
Pixel<N>: SupportedChannels,
2225
[u8; N]: Pod,
@@ -30,59 +33,56 @@ where
3033
let mut px = Pixel::<N>::new().with_a(0xff);
3134
let mut index_allowed = false;
3235

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

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-
{
38+
let mut i = 0;
39+
for row in data.chunks_exact(stride).take(height) {
40+
let pixel_row = &row[..width * R];
41+
for chunk in pixel_row.chunks_exact(R) {
42+
read_px(&mut px, chunk);
43+
if px == px_prev {
44+
run += 1;
45+
if run == 62 || unlikely(i == n_pixels - 1) {
5646
buf = buf.write_one(QOI_OP_RUN | (run - 1))?;
47+
run = 0;
5748
}
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)?;
6649
} else {
67-
*index_px = px_rgba;
68-
buf = px.encode_into(px_prev, buf)?;
50+
if run != 0 {
51+
#[cfg(not(feature = "reference"))]
52+
{
53+
// credits for the original idea: @zakarumych (had to be fixed though)
54+
buf = buf.write_one(if run == 1 && index_allowed {
55+
QOI_OP_INDEX | hash_prev
56+
} else {
57+
QOI_OP_RUN | (run - 1)
58+
})?;
59+
}
60+
#[cfg(feature = "reference")]
61+
{
62+
buf = buf.write_one(QOI_OP_RUN | (run - 1))?;
63+
}
64+
run = 0;
65+
}
66+
index_allowed = true;
67+
let px_rgba = px.as_rgba(0xff);
68+
hash_prev = px_rgba.hash_index();
69+
let index_px = &mut index[hash_prev as usize];
70+
if *index_px == px_rgba {
71+
buf = buf.write_one(QOI_OP_INDEX | hash_prev)?;
72+
} else {
73+
*index_px = px_rgba;
74+
buf = px.encode_into(px_prev, buf)?;
75+
}
76+
px_prev = px;
6977
}
70-
px_prev = px;
78+
i += 1;
7179
}
7280
}
7381

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.
@@ -116,11 +116,14 @@ pub fn encode_to_vec(data: impl AsRef<[u8]>, width: u32, height: u32) -> Result<
116116
/// Encode QOI images into buffers or into streams.
117117
pub struct Encoder<'a> {
118118
data: &'a [u8],
119+
stride: usize,
120+
raw_channels: RawChannels,
119121
header: Header,
120122
}
121123

122124
impl<'a> Encoder<'a> {
123125
/// Creates a new encoder from a given array of pixel data and image dimensions.
126+
/// The data must be in RGB(A) order, without fill borders (extra stride).
124127
///
125128
/// The number of channels will be inferred automatically (the valid values
126129
/// are 3 or 4). The color space will be set to sRGB by default.
@@ -136,7 +139,32 @@ impl<'a> Encoder<'a> {
136139
return Err(Error::InvalidImageLength { size, width, height });
137140
}
138141
header.channels = Channels::try_from(n_channels.min(0xff) as u8)?;
139-
Ok(Self { data, header })
142+
let raw_channels = RawChannels::from(header.channels);
143+
let stride = width as usize * raw_channels.bytes_per_pixel();
144+
Ok(Self { data, stride, raw_channels, header })
145+
}
146+
147+
/// Creates a new encoder from a given array of pixel data, image
148+
/// dimensions, stride, and raw format.
149+
#[inline]
150+
#[allow(clippy::cast_possible_truncation)]
151+
pub fn new_raw(
152+
data: &'a (impl AsRef<[u8]> + ?Sized), width: u32, height: u32, stride: usize,
153+
raw_channels: RawChannels,
154+
) -> Result<Self> {
155+
let data = data.as_ref();
156+
let channels = raw_channels.into();
157+
let header = Header::try_new(width, height, channels, ColorSpace::default())?;
158+
159+
if stride < width as usize * raw_channels.bytes_per_pixel() {
160+
return Err(Error::InvalidStride { stride });
161+
}
162+
let size = data.len();
163+
if stride * (height - 1) as usize + width as usize * raw_channels.bytes_per_pixel() < size {
164+
return Err(Error::InvalidImageLength { size, width, height });
165+
}
166+
167+
Ok(Self { data, stride, raw_channels, header })
140168
}
141169

142170
/// Returns a new encoder with modified color space.
@@ -181,7 +209,7 @@ impl<'a> Encoder<'a> {
181209
}
182210
let (head, tail) = buf.split_at_mut(QOI_HEADER_SIZE); // can't panic
183211
head.copy_from_slice(&self.header.encode());
184-
let n_written = encode_impl_all(BytesMut::new(tail), self.data, self.header.channels)?;
212+
let n_written = self.encode_impl_all(BytesMut::new(tail))?;
185213
Ok(QOI_HEADER_SIZE + n_written)
186214
}
187215

@@ -203,8 +231,62 @@ impl<'a> Encoder<'a> {
203231
#[inline]
204232
pub fn encode_to_stream<W: Write>(&self, writer: &mut W) -> Result<usize> {
205233
writer.write_all(&self.header.encode())?;
206-
let n_written =
207-
encode_impl_all(GenericWriter::new(writer), self.data, self.header.channels)?;
234+
let n_written = self.encode_impl_all(GenericWriter::new(writer))?;
208235
Ok(n_written + QOI_HEADER_SIZE)
209236
}
237+
238+
#[inline]
239+
fn encode_impl_all<W: Writer>(&self, out: W) -> Result<usize> {
240+
let width = self.header.width as usize;
241+
let height = self.header.height as usize;
242+
let stride = self.stride;
243+
match self.raw_channels {
244+
RawChannels::Rgb => {
245+
encode_impl::<_, 3, 3>(out, self.data, width, height, stride, |px, c| px.read(c))
246+
}
247+
RawChannels::Bgr => {
248+
encode_impl::<_, 3, 3>(out, self.data, width, height, stride, |px, c| {
249+
px.update_rgb(c[2], c[1], c[0]);
250+
})
251+
}
252+
RawChannels::Rgba => {
253+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| px.read(c))
254+
}
255+
RawChannels::Argb => {
256+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
257+
px.update_rgba(c[1], c[2], c[3], c[0])
258+
})
259+
}
260+
RawChannels::Rgbx => {
261+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
262+
px.read(&c[..3])
263+
})
264+
}
265+
RawChannels::Xrgb => {
266+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
267+
px.update_rgb(c[1], c[2], c[3])
268+
})
269+
}
270+
RawChannels::Bgra => {
271+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
272+
px.update_rgba(c[2], c[1], c[0], c[3])
273+
})
274+
}
275+
RawChannels::Abgr => {
276+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
277+
px.update_rgba(c[3], c[2], c[1], c[0])
278+
})
279+
}
280+
RawChannels::Bgrx => {
281+
encode_impl::<_, 3, 4>(out, self.data, width, height, stride, |px, c| {
282+
px.update_rgb(c[2], c[1], c[0])
283+
})
284+
}
285+
RawChannels::Xbgr => {
286+
encode_impl::<_, 4, 4>(out, self.data, width, height, stride, |px, c| {
287+
px.update_rgb(c[3], c[2], c[1])
288+
})
289+
}
290+
}
291+
}
210292
}

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,4 +92,4 @@ pub use crate::encode::{encode_max_len, encode_to_buf, Encoder};
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/types.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,60 @@ impl TryFrom<u8> for Channels {
111111
}
112112
}
113113
}
114+
115+
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
116+
pub enum RawChannels {
117+
Rgb,
118+
Bgr,
119+
Rgba,
120+
Argb,
121+
Rgbx,
122+
Xrgb,
123+
Bgra,
124+
Abgr,
125+
Bgrx,
126+
Xbgr,
127+
}
128+
129+
impl From<Channels> for RawChannels {
130+
fn from(value: Channels) -> Self {
131+
match value {
132+
Channels::Rgb => RawChannels::Rgb,
133+
Channels::Rgba => RawChannels::Rgba,
134+
}
135+
}
136+
}
137+
138+
impl From<RawChannels> for Channels {
139+
fn from(value: RawChannels) -> Self {
140+
match value {
141+
RawChannels::Rgb => Channels::Rgb,
142+
RawChannels::Bgr => Channels::Rgb,
143+
RawChannels::Rgba => Channels::Rgba,
144+
RawChannels::Argb => Channels::Rgba,
145+
RawChannels::Rgbx => Channels::Rgb,
146+
RawChannels::Xrgb => Channels::Rgb,
147+
RawChannels::Bgra => Channels::Rgba,
148+
RawChannels::Abgr => Channels::Rgba,
149+
RawChannels::Bgrx => Channels::Rgb,
150+
RawChannels::Xbgr => Channels::Rgb,
151+
}
152+
}
153+
}
154+
155+
impl RawChannels {
156+
pub(crate) fn bytes_per_pixel(&self) -> usize {
157+
match self {
158+
RawChannels::Rgb => 3,
159+
RawChannels::Bgr => 3,
160+
RawChannels::Rgba => 4,
161+
RawChannels::Argb => 4,
162+
RawChannels::Rgbx => 4,
163+
RawChannels::Xrgb => 4,
164+
RawChannels::Bgra => 4,
165+
RawChannels::Abgr => 4,
166+
RawChannels::Bgrx => 4,
167+
RawChannels::Xbgr => 4,
168+
}
169+
}
170+
}

0 commit comments

Comments
 (0)