Skip to content

Commit 018962c

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

File tree

4 files changed

+193
-48
lines changed

4 files changed

+193
-48
lines changed

src/encode.rs

Lines changed: 130 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,57 @@ 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(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

82+
assert_eq!(i, n_pixels);
7483
buf = buf.write_many(&QOI_PADDING)?;
7584
Ok(cap.saturating_sub(buf.capacity()))
7685
}
7786

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-
8687
/// The maximum number of bytes the encoded image will take.
8788
///
8889
/// Can be used to pre-allocate the buffer to encode the image into.
@@ -116,11 +117,14 @@ pub fn encode_to_vec(data: impl AsRef<[u8]>, width: u32, height: u32) -> Result<
116117
/// Encode QOI images into buffers or into streams.
117118
pub struct Encoder<'a> {
118119
data: &'a [u8],
120+
stride: usize,
121+
raw_channels: RawChannels,
119122
header: Header,
120123
}
121124

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

142171
/// Returns a new encoder with modified color space.
@@ -181,7 +210,7 @@ impl<'a> Encoder<'a> {
181210
}
182211
let (head, tail) = buf.split_at_mut(QOI_HEADER_SIZE); // can't panic
183212
head.copy_from_slice(&self.header.encode());
184-
let n_written = encode_impl_all(BytesMut::new(tail), self.data, self.header.channels)?;
213+
let n_written = self.encode_impl_all(BytesMut::new(tail))?;
185214
Ok(QOI_HEADER_SIZE + n_written)
186215
}
187216

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

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)