singularity-forge/rust-engine/crates/engine/src/image.rs

154 lines
5.1 KiB
Rust

//! Image decode, encode, and resize via N-API.
//!
//! Provides:
//! - Load image from bytes (PNG, JPEG, WebP, GIF)
//! - Get dimensions
//! - Resize with configurable sampling filter
//! - Export as PNG, JPEG, WebP, or GIF
use std::{io::Cursor, sync::Arc};
use image::{
codecs::{jpeg::JpegEncoder, webp::WebPEncoder},
imageops::FilterType,
DynamicImage, ImageFormat, ImageReader,
};
use napi::bindgen_prelude::*;
use napi_derive::napi;
use crate::task;
/// Sampling filter for resize operations.
#[napi]
pub enum SamplingFilter {
/// Nearest-neighbor sampling (fast, low quality).
Nearest = 1,
/// Triangle filter (linear interpolation).
Triangle = 2,
/// Catmull-Rom filter with sharper edges.
CatmullRom = 3,
/// Gaussian filter for smoother results.
Gaussian = 4,
/// Lanczos3 filter for high-quality downscaling.
Lanczos3 = 5,
}
impl From<SamplingFilter> for FilterType {
fn from(filter: SamplingFilter) -> Self {
match filter {
SamplingFilter::Nearest => Self::Nearest,
SamplingFilter::Triangle => Self::Triangle,
SamplingFilter::CatmullRom => Self::CatmullRom,
SamplingFilter::Gaussian => Self::Gaussian,
SamplingFilter::Lanczos3 => Self::Lanczos3,
}
}
}
/// Image container for native interop.
#[napi]
pub struct NativeImage {
img: Arc<DynamicImage>,
}
type ImageTask = task::Async<NativeImage>;
#[napi]
impl NativeImage {
/// Decode encoded image bytes (PNG, JPEG, WebP, GIF) into a NativeImage.
#[napi(js_name = "parse")]
pub fn parse(bytes: Uint8Array) -> ImageTask {
let bytes = bytes.as_ref().to_vec();
task::blocking("image.decode", (), move |_| -> Result<Self> {
let img = decode_image_from_bytes(&bytes)?;
Ok(Self { img: Arc::new(img) })
})
}
/// Image width in pixels.
#[napi(getter, js_name = "width")]
pub fn get_width(&self) -> u32 {
self.img.width()
}
/// Image height in pixels.
#[napi(getter, js_name = "height")]
pub fn get_height(&self) -> u32 {
self.img.height()
}
/// Encode to bytes. Format: 0=PNG, 1=JPEG, 2=WebP, 3=GIF.
#[napi(js_name = "encode")]
pub fn encode(&self, format: u8, quality: u8) -> task::Async<Vec<u8>> {
let img = Arc::clone(&self.img);
task::blocking("image.encode", (), move |_| {
encode_image(&img, format, quality)
})
}
/// Resize to exact dimensions. Returns a new NativeImage.
#[napi(js_name = "resize")]
pub fn resize(&self, width: u32, height: u32, filter: SamplingFilter) -> ImageTask {
let img = Arc::clone(&self.img);
task::blocking("image.resize", (), move |_| {
Ok(Self {
img: Arc::new(img.resize_exact(width, height, filter.into())),
})
})
}
}
fn decode_image_from_bytes(bytes: &[u8]) -> Result<DynamicImage> {
let reader = ImageReader::new(Cursor::new(bytes))
.with_guessed_format()
.map_err(|e| Error::from_reason(format!("Failed to detect image format: {e}")))?;
reader
.decode()
.map_err(|e| Error::from_reason(format!("Failed to decode image: {e}")))
}
/// Compute a capacity hint for the encode buffer using checked arithmetic.
///
/// Returns an error instead of panicking when `w * h * bytes_per_pixel`
/// overflows `usize`.
fn encode_capacity(w: u32, h: u32, bytes_per_pixel: usize) -> Result<usize> {
(w as usize)
.checked_mul(h as usize)
.and_then(|wh| wh.checked_mul(bytes_per_pixel))
.ok_or_else(|| Error::from_reason("Image dimensions too large for encode buffer"))
}
fn encode_image(img: &DynamicImage, format: u8, quality: u8) -> Result<Vec<u8>> {
let (w, h) = (img.width(), img.height());
match format {
0 => {
let mut buffer = Vec::with_capacity(encode_capacity(w, h, 4)?);
img.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Png)
.map_err(|e| Error::from_reason(format!("Failed to encode PNG: {e}")))?;
Ok(buffer)
}
1 => {
let mut buffer = Vec::with_capacity(encode_capacity(w, h, 3)?);
let encoder = JpegEncoder::new_with_quality(&mut buffer, quality);
img.write_with_encoder(encoder)
.map_err(|e| Error::from_reason(format!("Failed to encode JPEG: {e}")))?;
Ok(buffer)
}
2 => {
let mut buffer = Vec::with_capacity(encode_capacity(w, h, 4)?);
let encoder = WebPEncoder::new_lossless(&mut buffer);
img.write_with_encoder(encoder)
.map_err(|e| Error::from_reason(format!("Failed to encode WebP: {e}")))?;
Ok(buffer)
}
3 => {
let mut buffer = Vec::with_capacity(encode_capacity(w, h, 1)?);
img.write_to(&mut Cursor::new(&mut buffer), ImageFormat::Gif)
.map_err(|e| Error::from_reason(format!("Failed to encode GIF: {e}")))?;
Ok(buffer)
}
_ => Err(Error::from_reason(format!(
"Invalid image format: {format}"
))),
}
}