#[cfg(feature = "encode")]
use std::io::Cursor;
#[cfg(feature = "encode")]
use image::{ExtendedColorType as ECT, ImageEncoder};
use crate::{Error, KnownFormat, MemoryFormat, Result};
const MAX_OUTPUT_BYTES: usize = 1 << 30;
#[derive(Debug, Clone)]
pub struct EncodeFrame {
pub width: u32,
pub height: u32,
pub stride: u32,
pub format: MemoryFormat,
pub data: Vec<u8>,
}
#[derive(Debug)]
pub struct Encoder {
target: KnownFormat,
frames: Vec<EncodeFrame>,
quality: u8,
compression: u8,
icc_profile: Option<Vec<u8>>,
metadata: Vec<(String, String)>,
}
impl Encoder {
pub fn new(target: KnownFormat) -> Result<Self> {
if !is_supported(target) {
return Err(Error::UnsupportedFormat);
}
Ok(Self {
target,
frames: Vec::new(),
quality: 75,
compression: 6,
icc_profile: None,
metadata: Vec::new(),
})
}
pub fn target(&self) -> KnownFormat {
self.target
}
pub fn add_frame(&mut self, frame: EncodeFrame) -> &mut Self {
self.frames.push(frame);
self
}
pub fn set_quality(&mut self, quality: u8) -> &mut Self {
self.quality = quality;
self
}
pub fn set_compression(&mut self, compression: u8) -> &mut Self {
self.compression = compression;
self
}
pub fn set_icc_profile(&mut self, icc: Option<Vec<u8>>) -> &mut Self {
self.icc_profile = icc;
self
}
pub fn add_metadata(&mut self, key: String, value: String) -> &mut Self {
self.metadata.push((key, value));
self
}
pub fn encode(&self) -> Result<Vec<u8>> {
if self.frames.is_empty() {
return Err(Error::Internal("no frames queued for encode".into()));
}
if self.frames.len() > 1 {
return Err(Error::Internal(
"multi-frame encoding is not yet supported".into(),
));
}
let frame = &self.frames[0];
let rgba = to_rgba8(
&frame.data,
frame.width,
frame.height,
frame.stride,
frame.format,
)?;
encode_dispatch(self, &rgba, frame.width, frame.height)
}
pub fn clear_frames(&mut self) -> &mut Self {
self.frames.clear();
self
}
}
#[cfg(feature = "encode")]
fn is_supported(target: KnownFormat) -> bool {
matches!(
target,
KnownFormat::Png
| KnownFormat::Jpeg
| KnownFormat::Gif
| KnownFormat::WebP
| KnownFormat::Tiff
| KnownFormat::Bmp,
)
}
#[cfg(not(feature = "encode"))]
fn is_supported(_target: KnownFormat) -> bool {
false
}
#[cfg(feature = "encode")]
fn encode_dispatch(cfg: &Encoder, rgba: &[u8], width: u32, height: u32) -> Result<Vec<u8>> {
let mut out = Cursor::new(Vec::new());
let result = match cfg.target {
KnownFormat::Png => {
let mut enc = image::codecs::png::PngEncoder::new(&mut out);
if let Some(p) = cfg.icc_profile.as_ref() {
let _ = enc.set_icc_profile(p.clone());
}
enc.write_image(rgba, width, height, ECT::Rgba8)
}
KnownFormat::Jpeg => {
let mut enc = image::codecs::jpeg::JpegEncoder::new_with_quality(&mut out, cfg.quality);
if let Some(p) = cfg.icc_profile.as_ref() {
let _ = enc.set_icc_profile(p.clone());
}
enc.write_image(rgba, width, height, ECT::Rgba8)
}
KnownFormat::Gif => {
let mut enc = image::codecs::gif::GifEncoder::new(&mut out);
enc.encode(rgba, width, height, ECT::Rgba8)
}
KnownFormat::WebP => {
let mut enc = image::codecs::webp::WebPEncoder::new_lossless(&mut out);
if let Some(p) = cfg.icc_profile.as_ref() {
let _ = enc.set_icc_profile(p.clone());
}
enc.write_image(rgba, width, height, ECT::Rgba8)
}
KnownFormat::Tiff => {
let enc = image::codecs::tiff::TiffEncoder::new(&mut out);
enc.write_image(rgba, width, height, ECT::Rgba8)
}
KnownFormat::Bmp => {
let enc = image::codecs::bmp::BmpEncoder::new(&mut out);
enc.write_image(rgba, width, height, ECT::Rgba8)
}
_ => return Err(Error::UnsupportedFormat),
};
result.map_err(|e| Error::Encoder {
format: cfg.target.name(),
message: e.to_string(),
})?;
Ok(out.into_inner())
}
#[cfg(not(feature = "encode"))]
fn encode_dispatch(_cfg: &Encoder, _rgba: &[u8], _width: u32, _height: u32) -> Result<Vec<u8>> {
Err(Error::UnsupportedFormat)
}
fn to_rgba8(
data: &[u8],
width: u32,
height: u32,
stride: u32,
format: MemoryFormat,
) -> Result<Vec<u8>> {
let bpp = format.bytes_per_pixel() as usize;
let width = width as usize;
let height = height as usize;
let stride = stride as usize;
let row_bytes = width
.checked_mul(bpp)
.ok_or(Error::LimitExceeded("row width overflow"))?;
if row_bytes > stride {
return Err(Error::Malformed(format!(
"stride {stride} narrower than {row_bytes} bytes/row"
)));
}
let out_stride = width
.checked_mul(4)
.ok_or(Error::LimitExceeded("output stride overflow"))?;
let out_total = out_stride
.checked_mul(height)
.ok_or(Error::LimitExceeded("output size overflow"))?;
if out_total > MAX_OUTPUT_BYTES {
return Err(Error::LimitExceeded("output exceeds 1 GiB"));
}
if height > 0 {
let needed = (height - 1)
.checked_mul(stride)
.and_then(|n| n.checked_add(row_bytes))
.ok_or(Error::LimitExceeded("input size overflow"))?;
if data.len() < needed {
return Err(Error::Truncated("texture data shorter than dimensions"));
}
}
let mut out = Vec::with_capacity(out_total);
for y in 0..height {
let row_offset = y * stride;
let row = &data[row_offset..row_offset + row_bytes];
for x in 0..width {
let p = &row[x * bpp..x * bpp + bpp];
let (r, g, b, a) = sample_rgba8(format, p)
.ok_or(Error::Internal(format!("no rgba8 sampler for {format:?}")))?;
out.extend_from_slice(&[r, g, b, a]);
}
}
Ok(out)
}
fn sample_rgba8(fmt: MemoryFormat, p: &[u8]) -> Option<(u8, u8, u8, u8)> {
Some(match fmt {
MemoryFormat::G8 => (p[0], p[0], p[0], 255),
MemoryFormat::G8a8 => (p[0], p[0], p[0], p[1]),
MemoryFormat::G8a8Premultiplied => {
let (g, a) = unpremul_g8(p[0], p[1]);
(g, g, g, a)
}
MemoryFormat::R8g8b8 => (p[0], p[1], p[2], 255),
MemoryFormat::B8g8r8 => (p[2], p[1], p[0], 255),
MemoryFormat::R8g8b8a8 => (p[0], p[1], p[2], p[3]),
MemoryFormat::R8g8b8a8Premultiplied => unpremul_rgb8(p[0], p[1], p[2], p[3]),
MemoryFormat::B8g8r8a8 => (p[2], p[1], p[0], p[3]),
MemoryFormat::B8g8r8a8Premultiplied => unpremul_rgb8(p[2], p[1], p[0], p[3]),
MemoryFormat::A8r8g8b8 => (p[1], p[2], p[3], p[0]),
MemoryFormat::A8r8g8b8Premultiplied => unpremul_rgb8(p[1], p[2], p[3], p[0]),
MemoryFormat::A8b8g8r8 => (p[3], p[2], p[1], p[0]),
_ => return None,
})
}
fn unpremul_g8(g: u8, a: u8) -> (u8, u8) {
if a == 0 {
return (0, 0);
}
let g = ((g as u32 * 255) / a as u32).min(255) as u8;
(g, a)
}
fn unpremul_rgb8(r: u8, g: u8, b: u8, a: u8) -> (u8, u8, u8, u8) {
if a == 0 {
return (0, 0, 0, 0);
}
let unp = |c: u8| ((c as u32 * 255) / a as u32).min(255) as u8;
(unp(r), unp(g), unp(b), a)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rejects_format_with_no_encoder() {
assert!(matches!(
Encoder::new(KnownFormat::Qoi),
Err(Error::UnsupportedFormat)
));
assert!(matches!(
Encoder::new(KnownFormat::Svg),
Err(Error::UnsupportedFormat)
));
}
#[test]
fn encode_with_no_frames_is_internal_error() {
let enc = Encoder::new(KnownFormat::Png).unwrap();
assert!(matches!(enc.encode(), Err(Error::Internal(_))));
}
#[test]
fn encode_with_multiple_frames_is_internal_error() {
let mut enc = Encoder::new(KnownFormat::Png).unwrap();
let mk = || EncodeFrame {
width: 1,
height: 1,
stride: 4,
format: MemoryFormat::R8g8b8a8,
data: vec![0, 0, 0, 255],
};
enc.add_frame(mk());
enc.add_frame(mk());
assert!(matches!(enc.encode(), Err(Error::Internal(_))));
}
#[test]
fn to_rgba8_rejects_short_input() {
let data = vec![1, 2, 3, 4, 5, 6];
assert!(matches!(
to_rgba8(&data, 2, 2, 6, MemoryFormat::R8g8b8),
Err(Error::Truncated(_))
));
}
#[test]
fn to_rgba8_rejects_oversized_output() {
let stub = [0u8; 4];
assert!(matches!(
to_rgba8(&stub, 32768, 32768, 32768 * 4, MemoryFormat::R8g8b8a8),
Err(Error::LimitExceeded(_))
));
}
#[test]
fn to_rgba8_rejects_narrow_stride() {
let data = vec![0u8; 64];
assert!(matches!(
to_rgba8(&data, 4, 4, 8, MemoryFormat::R8g8b8),
Err(Error::Malformed(_))
));
}
#[cfg(feature = "encode")]
#[test]
fn png_round_trip_recovers_pixel() {
let mut enc = Encoder::new(KnownFormat::Png).unwrap();
enc.add_frame(EncodeFrame {
width: 2,
height: 1,
stride: 8,
format: MemoryFormat::R8g8b8a8,
data: vec![10, 20, 30, 255, 40, 50, 60, 255],
});
let bytes = enc.encode().expect("encode should succeed");
assert!(bytes.starts_with(b"\x89PNG"));
}
}