use std::io::BufWriter;
use std::path::Path;
use crate::{CaptureError, Image, unpremultiply};
const QUALITY_RANGE: std::ops::RangeInclusive<u8> = 1..=100;
pub fn write_jpg(image: &Image, path: impl AsRef<Path>, quality: u8) -> Result<(), CaptureError> {
if image.is_empty() {
return Err(CaptureError::EmptyImage);
}
let mut rgb = Vec::with_capacity(image.pixels().len() * 3);
for pixel in image.pixels() {
let [r, g, b, _] = unpremultiply(*pixel);
rgb.extend_from_slice(&[r, g, b]);
}
let file = std::fs::File::create(path).map_err(CaptureError::Io)?;
let quality = quality.clamp(*QUALITY_RANGE.start(), *QUALITY_RANGE.end());
let encoder = jpeg_encoder::Encoder::new(BufWriter::new(file), quality);
encoder
.encode(
&rgb,
u16::try_from(image.width()).unwrap_or(u16::MAX),
u16::try_from(image.height()).unwrap_or(u16::MAX),
jpeg_encoder::ColorType::Rgb,
)
.map_err(|error| CaptureError::Io(std::io::Error::other(error)))
}
#[derive(Debug, Clone, Copy)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers, with no default to fill a new field from"
)]
pub struct Nv12Frame<'a> {
pub luma: &'a [u8],
pub luma_stride: usize,
pub chroma: &'a [u8],
pub chroma_stride: usize,
pub width: u32,
pub height: u32,
}
pub fn write_jpg_nv12(
frame: &Nv12Frame<'_>,
path: impl AsRef<Path>,
quality: u8,
) -> Result<(), CaptureError> {
let Nv12Frame {
luma,
luma_stride,
chroma,
chroma_stride,
width,
height,
} = *frame;
if width == 0 || height == 0 {
return Err(CaptureError::EmptyImage);
}
let (w, h) = (width as usize, height as usize);
let chroma_rows = h.div_ceil(2);
if luma_stride < w
|| chroma_stride < w.div_ceil(2) * 2
|| luma.len() < luma_stride * h
|| chroma.len() < chroma_stride * chroma_rows
{
return Err(CaptureError::EmptyImage);
}
let mut packed_luma = Vec::with_capacity(w * h);
for row in 0..h {
let start = row * luma_stride;
packed_luma.extend_from_slice(&luma[start..start + w]);
}
let chroma_cols = w.div_ceil(2) * 2;
let mut packed_chroma = Vec::with_capacity(chroma_cols * chroma_rows);
for row in 0..chroma_rows {
let start = row * chroma_stride;
packed_chroma.extend_from_slice(&chroma[start..start + chroma_cols]);
}
let file = std::fs::File::create(path).map_err(CaptureError::Io)?;
let quality = quality.clamp(*QUALITY_RANGE.start(), *QUALITY_RANGE.end());
let encoder = jpeg_encoder::Encoder::new(BufWriter::new(file), quality);
encoder
.encode(
&interleave_nv12(&packed_luma, &packed_chroma, w, h),
u16::try_from(width).unwrap_or(u16::MAX),
u16::try_from(height).unwrap_or(u16::MAX),
jpeg_encoder::ColorType::Ycbcr,
)
.map_err(|error| CaptureError::Io(std::io::Error::other(error)))
}
fn interleave_nv12(luma: &[u8], chroma: &[u8], width: usize, height: usize) -> Vec<u8> {
let chroma_cols = width.div_ceil(2) * 2;
let mut out = Vec::with_capacity(width * height * 3);
for y in 0..height {
for x in 0..width {
let cb_index = (y / 2) * chroma_cols + (x / 2) * 2;
out.push(luma.get(y * width + x).copied().unwrap_or(0));
out.push(chroma.get(cb_index).copied().unwrap_or(128));
out.push(chroma.get(cb_index + 1).copied().unwrap_or(128));
}
}
out
}