use std::io::BufWriter;
use std::path::Path;
use crate::{CaptureError, Image, unpremultiply};
const QUALITY_RANGE: std::ops::RangeInclusive<u8> = 1..=100;
const MAX_DIMENSION: u32 = 65_500;
fn dimensions(width: u32, height: u32) -> Result<(u16, u16), CaptureError> {
let fits = |value: u32| u16::try_from(value).ok().filter(|_| value <= MAX_DIMENSION);
fits(width).zip(fits(height)).ok_or_else(|| {
CaptureError::Io(std::io::Error::other(format!(
"{width}x{height} is past JPEG's {MAX_DIMENSION} pixel limit"
)))
})
}
fn encoder(file: std::fs::File, quality: u8) -> jpeg_encoder::Encoder<BufWriter<std::fs::File>> {
let quality = quality.clamp(*QUALITY_RANGE.start(), *QUALITY_RANGE.end());
let mut encoder = jpeg_encoder::Encoder::new(BufWriter::new(file), quality);
encoder.set_sampling_factor(jpeg_encoder::SamplingFactor::F_2_2);
encoder
}
pub fn write_jpg(image: &Image, path: impl AsRef<Path>, quality: u8) -> Result<(), CaptureError> {
if image.is_empty() {
return Err(CaptureError::EmptyImage);
}
let (width, height) = dimensions(image.width(), image.height())?;
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)?;
encoder(file, quality)
.encode(&rgb, width, height, 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 (encoded_width, encoded_height) = dimensions(width, height)?;
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)?;
encoder(file, quality)
.encode(
&interleave_nv12(&packed_luma, &packed_chroma, w, h),
encoded_width,
encoded_height,
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
}