Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "ril"
authors = ["jay3332"]
version = "0.10.0"
version = "0.11.0"
license = "MIT"
edition = "2021"
description = "Rust Imaging Library: A performant and high-level image processing crate for Rust"
Expand All @@ -22,12 +22,14 @@ libwebp-sys2 = { version = "^0.1", features = ["1_2", "mux", "demux"], optional
fontdue = { version = "^0.7", optional = true }
color_quant = { version = "^1.1", optional = true }
colorgrad = { version = "^0.6", optional = true, default_features = false }
qoi = { version = "^0.4", optional = true }

[features]
default = ["resize", "text", "quantize", "gradient"]
all-pure = ["resize", "png", "jpeg", "gif", "text", "quantize"]
all-pure = ["resize", "png", "qoi", "jpeg", "gif", "text", "quantize"]
all = ["all-pure", "webp"]
png = ["dep:png"]
qoi = ["dep:qoi"]
jpeg = ["dep:jpeg-decoder", "dep:jpeg-encoder"]
gif = ["dep:gif"]
webp = ["dep:libwebp-sys2"]
Expand Down
2 changes: 2 additions & 0 deletions src/encodings/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub mod gif;
pub mod jpeg;
#[cfg(feature = "png")]
pub mod png;
#[cfg(feature = "qoi")]
pub mod qoi;
#[cfg(feature = "webp")]
pub mod webp;

Expand Down
93 changes: 93 additions & 0 deletions src/encodings/qoi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
use crate::encode::{FrameLike, HasEncoderMetadata};
use crate::{
pixel::Dynamic, ColorType, Decoder, Encoder, Image, Pixel, SingleFrameIterator,
};
use core::marker::PhantomData;
use qoi::{Channels, ColorSpace, Decoder as QDecoder, Encoder as QEncoder};
use std::io::{Read, Write};

impl From<Channels> for ColorType {
fn from(value: Channels) -> ColorType {
match value {
Channels::Rgb => ColorType::Rgb,
Channels::Rgba => ColorType::Rgba,
}
}
}

/// A QOI encoder interface over [`qoi::Encoder`].
pub struct QoiEncoder<P, W> {
config: ColorSpace,
writer: W,
_marker: PhantomData<P>,
}

impl<P: Pixel, W: Write> Encoder<P, W> for QoiEncoder<P, W> {
type Config = ColorSpace;

fn new(writer: W, metadata: impl HasEncoderMetadata<Self::Config, P>) -> crate::Result<Self> {
Ok(Self {
config: metadata.config(),
writer,
_marker: PhantomData,
})
}

fn add_frame(&mut self, frame: &impl FrameLike<P>) -> crate::Result<()> {
let image = frame.image();
let data = image.data.iter();
// Convert the pixels to RGB or RGBA, then to bytes
let data: Box<[u8]> = if P::COLOR_TYPE.has_alpha() {
data.map(P::as_rgba).flat_map(|p| p.as_bytes()).collect()
} else {
data.map(P::as_rgb).flat_map(|p| p.as_bytes()).collect()
};
// Write to stream
QEncoder::new(&data, image.width(), image.height())?
.with_colorspace(self.config)
.encode_to_stream(&mut self.writer)?;
Ok(())
}

fn finish(self) -> crate::Result<()> {
Ok(())
}
}

pub struct QoiDecoder<P, R> {
_marker: PhantomData<(P, R)>,
}

impl<P: Pixel, R: Read> QoiDecoder<P, R> {
/// Create a new decoder that decodes into the given pixel type.
#[must_use]
pub const fn new() -> Self {
Self {
_marker: PhantomData,
}
}
}

impl<P: Pixel, R: Read> Decoder<P, R> for QoiDecoder<P, R> {
type Sequence = SingleFrameIterator<P>;

fn decode(&mut self, stream: R) -> crate::Result<Image<P>> {
let mut decoder = QDecoder::from_stream(stream)?;
// Decode the header
let header = decoder.header().to_owned();

// Convert the pixels
let pixels = decoder
.decode_to_vec()?
// Since qoi::Channels is #[repr(u8)], this works
.chunks(header.channels as usize)
.map(|chunk| P::from_dynamic(Dynamic::from_bytes(chunk)))
.collect::<Vec<P>>();

Ok(Image::from_pixels(header.width, pixels))
}

fn decode_sequence(&mut self, stream: R) -> crate::Result<Self::Sequence> {
self.decode(stream).map(SingleFrameIterator::new)
}
}
47 changes: 47 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,50 @@ impl From<gif::DecodingError> for Error {
}
}
}

#[cfg(feature = "qoi")]
impl From<qoi::Error> for Error {
fn from(value: qoi::Error) -> Self {
use crate::Error::*;
use qoi::Error::*;
match value {
InvalidMagic { .. } => DecodingError("invalid magic number".to_string()),
InvalidChannels { channels } => EncodingError(format!(
"qoi only supports either 3 or 4 channels, got {channels}"
)),
InvalidColorSpace { .. } => {
DecodingError("colorspace of image is malformed".to_string())
}
InvalidImageDimensions { width, height } => {
if width.min(height) == 0 {
EmptyImageError
} else {
EncodingError(format!(
"image dimensions of {} by {} are not valid, must be below 400Mp",
width, height
))
}
}
InvalidImageLength {
size,
width,
height,
} => IncompatibleImageData {
width,
height,
received: size,
},
OutputBufferTooSmall { size, required } => EncodingError(format!(
"buffer of size {} is too small to hold image of size {}",
size, required
)),
UnexpectedBufferEnd => {
DecodingError("buffer reached end before decoding was finished".to_string())
}
InvalidPadding => DecodingError(
"incorrectly placed stream end marker encountered during decoding".to_string(),
),
qoi::Error::IoError(error) => Error::IoError(error),
}
}
}
63 changes: 56 additions & 7 deletions src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,17 @@ use crate::encodings::gif;
use crate::encodings::jpeg;
#[cfg(feature = "png")]
use crate::encodings::png;
#[cfg(feature = "qoi")]
use crate::encodings::qoi;
#[cfg(feature = "webp")]
use crate::encodings::webp;
#[cfg(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp"))]
#[cfg(any(
feature = "png",
feature = "gif",
feature = "jpeg",
feature = "webp",
feature = "qoi"
))]
use crate::{Decoder, Encoder};

/// Represents the underlying encoding format of an image.
Expand Down Expand Up @@ -47,6 +55,9 @@ pub enum ImageFormat {

/// The image is encoded in the WebP format.
WebP,

/// The image is encoded in the QOI format.
Qoi,
}

impl Default for ImageFormat {
Expand Down Expand Up @@ -87,6 +98,7 @@ impl ImageFormat {
"bmp" => Self::Bmp,
"tiff" => Self::Tiff,
"webp" => Self::WebP,
"qoi" => Self::Qoi,
_ => Self::Unknown,
},
)
Expand Down Expand Up @@ -119,6 +131,8 @@ impl ImageFormat {
"image/bmp" => Self::Bmp,
"image/tiff" => Self::Tiff,
"image/webp" => Self::WebP,
// Not official, but in the spec
"image/qoi" => Self::Qoi,
_ => Self::Unknown,
}
}
Expand All @@ -141,6 +155,8 @@ impl ImageFormat {
&& sample[9] != 0x52
{
Self::Tiff
} else if sample.starts_with(b"qoif") {
Self::Qoi
} else {
Self::Unknown
}
Expand All @@ -154,7 +170,13 @@ impl ImageFormat {
/// # Panics
/// * No encoder implementation is found for this image encoding.
#[cfg_attr(
not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")),
not(any(
feature = "png",
feature = "gif",
feature = "jpeg",
feature = "webp",
feature = "qoi"
)),
allow(unused_variables, unreachable_code)
)]
pub fn run_encoder<P: Pixel>(&self, image: &Image<P>, dest: impl Write) -> Result<()> {
Expand All @@ -167,6 +189,8 @@ impl ImageFormat {
Self::Gif => gif::GifEncoder::encode_static(image, dest),
#[cfg(feature = "webp")]
Self::WebP => webp::WebPStaticEncoder::encode_static(image, dest),
#[cfg(feature = "qoi")]
Self::Qoi => qoi::QoiEncoder::encode_static(image, dest),
_ => panic!(
"No encoder implementation is found for this image format. \
Did you forget to enable the feature?"
Expand All @@ -183,7 +207,13 @@ impl ImageFormat {
/// # Panics
/// * No encoder implementation is found for this image encoding.
#[cfg_attr(
not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")),
not(any(
feature = "png",
feature = "gif",
feature = "jpeg",
feature = "webp",
feature = "qoi"
)),
allow(unused_variables, unreachable_code)
)]
pub fn run_sequence_encoder<P: Pixel>(
Expand All @@ -200,6 +230,8 @@ impl ImageFormat {
Self::Gif => gif::GifEncoder::encode_sequence(seq, dest),
#[cfg(feature = "webp")]
Self::WebP => webp::WebPMuxEncoder::encode_sequence(seq, dest),
#[cfg(feature = "qoi")]
Self::Qoi => qoi::QoiEncoder::encode_sequence(seq, dest),
_ => panic!(
"No encoder implementation is found for this image format. \
Did you forget to enable the feature?"
Expand All @@ -215,7 +247,13 @@ impl ImageFormat {
/// # Panics
/// * No decoder implementation is found for this image encoding.
#[cfg_attr(
not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")),
not(any(
feature = "png",
feature = "gif",
feature = "jpeg",
feature = "webp",
feature = "qoi"
)),
allow(unused_variables, unreachable_code)
)]
#[allow(clippy::needless_pass_by_value)] // would require a major refactor
Expand All @@ -228,7 +266,9 @@ impl ImageFormat {
#[cfg(feature = "gif")]
Self::Gif => gif::GifDecoder::new().decode(stream),
#[cfg(feature = "webp")]
Self::WebP => webp::WebPDecoder::default().decode(stream),
Self::WebP => webp::WebPDecoder::new().decode(stream),
#[cfg(feature = "qoi")]
Self::Qoi => qoi::QoiDecoder::new().decode(stream),
_ => panic!(
"No encoder implementation is found for this image format. \
Did you forget to enable the feature?"
Expand All @@ -244,7 +284,13 @@ impl ImageFormat {
/// # Panics
/// * No decoder implementation is found for this image encoding.
#[cfg_attr(
not(any(feature = "png", feature = "gif", feature = "jpeg", feature = "webp")),
not(any(
feature = "png",
feature = "gif",
feature = "jpeg",
feature = "webp",
feature = "qoi"
)),
allow(unused_variables, unreachable_code)
)]
#[allow(clippy::needless_pass_by_value)] // would require a major refactor
Expand All @@ -260,7 +306,9 @@ impl ImageFormat {
#[cfg(feature = "gif")]
Self::Gif => Box::new(gif::GifDecoder::new().decode_sequence(stream)?),
#[cfg(feature = "webp")]
Self::WebP => Box::new(webp::WebPDecoder::default().decode_sequence(stream)?),
Self::WebP => Box::new(webp::WebPDecoder::new().decode_sequence(stream)?),
#[cfg(feature = "qoi")]
Self::Qoi => Box::new(qoi::QoiDecoder::new().decode_sequence(stream)?),
_ => panic!(
"No encoder implementation is found for this image format. \
Did you forget to enable the feature?"
Expand All @@ -281,6 +329,7 @@ impl Display for ImageFormat {
Self::Bmp => "bmp",
Self::Tiff => "tiff",
Self::WebP => "webp",
Self::Qoi => "qoi",
Self::Unknown => "",
}
)
Expand Down
Binary file modified tests/out/animated_webp_encode_output.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/apng_encode_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/out/gh_17.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/gif_encode_output.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/png_encode_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/png_palette_encode_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/png_palette_mutation_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/out/qoi_encode_output_rgb.qoi
Binary file not shown.
Binary file added tests/out/qoi_encode_output_rgb_conv.qoi
Binary file not shown.
Binary file added tests/out/qoi_encode_output_rgba.qoi
Binary file not shown.
Binary file added tests/out/qoi_encode_output_rgba_conv.qoi
Binary file not shown.
Binary file modified tests/out/resize_gradient_output_control.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/resize_gradient_output_resized.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/text_gradient_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified tests/out/text_render_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/sample_rgb.qoi
Binary file not shown.
Binary file added tests/sample_rgba.qoi
Binary file not shown.
33 changes: 33 additions & 0 deletions tests/test_qoi.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use ril::prelude::*;

#[test]
fn test_qoi_rgb() -> ril::Result<()> {
let image = Image::<Rgb>::open("tests/sample_rgb.qoi")?;
assert_eq!(image.dimensions(), (1024, 1024));

image.save_inferred("tests/out/qoi_encode_output_rgb.qoi")
}

#[test]
fn test_qoi_rgba() -> ril::Result<()> {
let image = Image::<Rgba>::open("tests/sample_rgba.qoi")?;
assert_eq!(image.dimensions(), (1024, 1024));

image.save_inferred("tests/out/qoi_encode_output_rgba.qoi")
}

#[test]
fn test_qoi_rgba_conv() -> ril::Result<()> {
let image = Image::<L>::open("tests/sample_rgba.qoi")?;
assert_eq!(image.dimensions(), (1024, 1024));

image.save_inferred("tests/out/qoi_encode_output_rgb_conv.qoi")
}

#[test]
fn test_qoi_rgb_conv() -> ril::Result<()> {
let image = Image::<Rgba>::open("tests/sample_rgb.qoi")?;
assert_eq!(image.dimensions(), (1024, 1024));

image.save_inferred("tests/out/qoi_encode_output_rgba_conv.qoi")
}