use std::io::Cursor;
use image::{ImageFormat, RgbaImage};
use crate::error::BrowserError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScreenshotFormat {
Png,
Jpeg,
WebP,
}
pub fn encode_image(image: &RgbaImage, format: ScreenshotFormat) -> Result<Vec<u8>, BrowserError> {
encode_image_with_quality(image, format, DEFAULT_JPEG_QUALITY)
}
const DEFAULT_JPEG_QUALITY: u8 = 80;
pub fn encode_image_with_quality(
image: &RgbaImage,
format: ScreenshotFormat,
jpeg_quality: u8,
) -> Result<Vec<u8>, BrowserError> {
let mut buf = Cursor::new(Vec::with_capacity(
image.width() as usize * image.height() as usize,
));
match format {
ScreenshotFormat::Png => image
.write_to(&mut buf, ImageFormat::Png)
.map_err(|e| BrowserError::Rendering(format!("PNG encode failed: {e}")))?,
ScreenshotFormat::Jpeg => {
let rgb = image::DynamicImage::ImageRgba8(image.clone()).to_rgb8();
let encoder =
image::codecs::jpeg::JpegEncoder::new_with_quality(&mut buf, jpeg_quality);
rgb.write_with_encoder(encoder)
.map_err(|e| BrowserError::Rendering(format!("JPEG encode failed: {e}")))?;
}
ScreenshotFormat::WebP => image
.write_to(&mut buf, ImageFormat::WebP)
.map_err(|e| BrowserError::Rendering(format!("WebP encode failed: {e}")))?,
}
Ok(buf.into_inner())
}
#[cfg(test)]
mod tests {
use super::*;
use image::Rgba;
fn red_image(w: u32, h: u32) -> RgbaImage {
RgbaImage::from_pixel(w, h, Rgba([255, 0, 0, 255]))
}
fn assert_encode_magic(w: u32, h: u32, fmt: ScreenshotFormat, magic: &[u8]) {
let img = red_image(w, h);
let out = encode_image(&img, fmt).unwrap();
assert!(!out.is_empty());
assert_eq!(&out[0..magic.len()], magic);
}
#[test]
fn encode_1x1_png_returns_nonempty_with_magic() {
assert_encode_magic(1, 1, ScreenshotFormat::Png, &[0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn encode_1x1_jpeg_returns_nonempty_with_magic() {
assert_encode_magic(1, 1, ScreenshotFormat::Jpeg, &[0xFF, 0xD8]);
}
#[test]
fn encode_100x100_png_produces_valid_data() {
assert_encode_magic(100, 100, ScreenshotFormat::Png, &[0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn encode_100x100_jpeg_produces_valid_data() {
assert_encode_magic(100, 100, ScreenshotFormat::Jpeg, &[0xFF, 0xD8]);
}
#[test]
fn encode_1x1_webp_returns_nonempty_with_riff_magic() {
assert_encode_magic(1, 1, ScreenshotFormat::WebP, b"RIFF");
}
#[test]
fn encode_100x100_webp_produces_valid_data() {
assert_encode_magic(100, 100, ScreenshotFormat::WebP, b"RIFF");
}
#[test]
fn encode_empty_image_no_panic() {
let img = red_image(0, 0);
let _ = encode_image(&img, ScreenshotFormat::Png);
let _ = encode_image(&img, ScreenshotFormat::Jpeg);
let _ = encode_image(&img, ScreenshotFormat::WebP);
}
#[test]
fn encode_with_quality_both_formats_valid_magic() {
let img = red_image(64, 64);
let jpeg = encode_image_with_quality(&img, ScreenshotFormat::Jpeg, 60).unwrap();
assert_eq!(&jpeg[0..2], &[0xFF, 0xD8]);
let png = encode_image_with_quality(&img, ScreenshotFormat::Png, 60).unwrap();
assert_eq!(&png[0..4], &[0x89, 0x50, 0x4E, 0x47]);
}
#[test]
fn encode_jpeg_lower_quality_is_not_larger() {
let img = red_image(200, 200);
let hi = encode_image_with_quality(&img, ScreenshotFormat::Jpeg, 95)
.unwrap()
.len();
let lo = encode_image_with_quality(&img, ScreenshotFormat::Jpeg, 20)
.unwrap()
.len();
assert!(
lo <= hi,
"quality 20 ({lo}B) must not exceed quality 95 ({hi}B)"
);
}
#[test]
fn all_formats_produce_nontrivial_output() {
let img = red_image(200, 200);
let png = encode_image(&img, ScreenshotFormat::Png).unwrap();
let jpeg = encode_image(&img, ScreenshotFormat::Jpeg).unwrap();
let webp = encode_image(&img, ScreenshotFormat::WebP).unwrap();
assert!(
png.len() > 100,
"PNG should be nontrivial: {} bytes",
png.len()
);
assert!(
jpeg.len() > 100,
"JPEG should be nontrivial: {} bytes",
jpeg.len()
);
assert!(
webp.len() > 100,
"WebP should be nontrivial: {} bytes",
webp.len()
);
}
}