use alloc::borrow::Cow;
use alloc::vec;
use alloc::vec::Vec;
use crate::convert::ConvertPlan;
use crate::converter::RowConverter;
use crate::negotiate::{ConvertIntent, best_match};
use crate::policy::{AlphaPolicy, ConvertOptions};
use crate::{ConvertError, PixelDescriptor};
use whereat::{At, ResultAtExt};
#[derive(Clone, Debug)]
pub struct Adapted<'a> {
pub data: Cow<'a, [u8]>,
pub descriptor: PixelDescriptor,
pub width: u32,
pub rows: u32,
}
#[track_caller]
pub fn adapt_for_encode<'a>(
data: &'a [u8],
descriptor: PixelDescriptor,
width: u32,
rows: u32,
stride: usize,
supported: &[PixelDescriptor],
) -> Result<Adapted<'a>, At<ConvertError>> {
adapt_for_encode_with_intent(
data,
descriptor,
width,
rows,
stride,
supported,
ConvertIntent::Fastest,
)
}
#[track_caller]
pub fn adapt_for_encode_with_intent<'a>(
data: &'a [u8],
descriptor: PixelDescriptor,
width: u32,
rows: u32,
stride: usize,
supported: &[PixelDescriptor],
intent: ConvertIntent,
) -> Result<Adapted<'a>, At<ConvertError>> {
if supported.is_empty() {
return Err(whereat::at!(ConvertError::EmptyFormatList));
}
if supported.contains(&descriptor) {
return Ok(Adapted {
data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
descriptor,
width,
rows,
});
}
for &target in supported {
if descriptor.channel_type() == target.channel_type()
&& descriptor.layout() == target.layout()
&& descriptor.alpha() == target.alpha()
&& descriptor.primaries == target.primaries
&& descriptor.signal_range == target.signal_range
{
return Ok(Adapted {
data: contiguous_from_strided(
data,
width,
rows,
stride,
descriptor.bytes_per_pixel(),
),
descriptor: target,
width,
rows,
});
}
}
let target = best_match(descriptor, supported, intent)
.ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
let mut converter = RowConverter::new(descriptor, target).at()?;
let src_bpp = descriptor.bytes_per_pixel();
let dst_bpp = target.bytes_per_pixel();
let dst_stride = (width as usize) * dst_bpp;
let mut output = vec![0u8; dst_stride * rows as usize];
for y in 0..rows {
let src_start = y as usize * stride;
let src_end = src_start + (width as usize * src_bpp);
let dst_start = y as usize * dst_stride;
let dst_end = dst_start + dst_stride;
converter.convert_row(
&data[src_start..src_end],
&mut output[dst_start..dst_end],
width,
);
}
Ok(Adapted {
data: Cow::Owned(output),
descriptor: target,
width,
rows,
})
}
#[track_caller]
pub fn convert_buffer(
src: &[u8],
width: u32,
rows: u32,
from: PixelDescriptor,
to: PixelDescriptor,
) -> Result<Vec<u8>, At<ConvertError>> {
if from == to {
return Ok(src.to_vec());
}
let mut converter = RowConverter::new(from, to).at()?;
let src_bpp = from.bytes_per_pixel();
let dst_bpp = to.bytes_per_pixel();
let src_stride = (width as usize) * src_bpp;
let dst_stride = (width as usize) * dst_bpp;
let mut output = vec![0u8; dst_stride * rows as usize];
for y in 0..rows {
let src_start = y as usize * src_stride;
let src_end = src_start + src_stride;
let dst_start = y as usize * dst_stride;
let dst_end = dst_start + dst_stride;
converter.convert_row(
&src[src_start..src_end],
&mut output[dst_start..dst_end],
width,
);
}
Ok(output)
}
#[track_caller]
pub fn adapt_for_encode_explicit<'a>(
data: &'a [u8],
descriptor: PixelDescriptor,
width: u32,
rows: u32,
stride: usize,
supported: &[PixelDescriptor],
options: &ConvertOptions,
) -> Result<Adapted<'a>, At<ConvertError>> {
if supported.is_empty() {
return Err(whereat::at!(ConvertError::EmptyFormatList));
}
if supported.contains(&descriptor) {
return Ok(Adapted {
data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
descriptor,
width,
rows,
});
}
for &target in supported {
if descriptor.channel_type() == target.channel_type()
&& descriptor.layout() == target.layout()
&& descriptor.alpha() == target.alpha()
&& descriptor.primaries == target.primaries
&& descriptor.signal_range == target.signal_range
{
return Ok(Adapted {
data: contiguous_from_strided(
data,
width,
rows,
stride,
descriptor.bytes_per_pixel(),
),
descriptor: target,
width,
rows,
});
}
}
let target = best_match(descriptor, supported, ConvertIntent::Fastest)
.ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
let plan = ConvertPlan::new_explicit(descriptor, target, options).at()?;
let drops_alpha = descriptor.alpha().is_some() && target.alpha().is_none();
if drops_alpha && options.alpha_policy == AlphaPolicy::DiscardIfOpaque {
let src_bpp = descriptor.bytes_per_pixel();
if !is_fully_opaque(data, width, rows, stride, src_bpp, &descriptor) {
return Err(whereat::at!(ConvertError::AlphaNotOpaque));
}
}
let mut converter = RowConverter::from_plan(plan);
let src_bpp = descriptor.bytes_per_pixel();
let dst_bpp = target.bytes_per_pixel();
let dst_stride = (width as usize) * dst_bpp;
let mut output = vec![0u8; dst_stride * rows as usize];
for y in 0..rows {
let src_start = y as usize * stride;
let src_end = src_start + (width as usize * src_bpp);
let dst_start = y as usize * dst_stride;
let dst_end = dst_start + dst_stride;
converter.convert_row(
&data[src_start..src_end],
&mut output[dst_start..dst_end],
width,
);
}
Ok(Adapted {
data: Cow::Owned(output),
descriptor: target,
width,
rows,
})
}
fn is_fully_opaque(
data: &[u8],
width: u32,
rows: u32,
stride: usize,
bpp: usize,
desc: &PixelDescriptor,
) -> bool {
if desc.alpha().is_none() {
return true;
}
let cs = desc.channel_type().byte_size();
let alpha_offset = (desc.layout().channels() - 1) * cs;
for y in 0..rows {
let row_start = y as usize * stride;
for x in 0..width as usize {
let off = row_start + x * bpp + alpha_offset;
match desc.channel_type() {
crate::ChannelType::U8 => {
if data[off] != 255 {
return false;
}
}
crate::ChannelType::U16 => {
let v = u16::from_ne_bytes([data[off], data[off + 1]]);
if v != 65535 {
return false;
}
}
crate::ChannelType::F32 => {
let v = f32::from_ne_bytes([
data[off],
data[off + 1],
data[off + 2],
data[off + 3],
]);
if v < 1.0 {
return false;
}
}
_ => return false,
}
}
}
true
}
fn contiguous_from_strided<'a>(
data: &'a [u8],
width: u32,
rows: u32,
stride: usize,
bpp: usize,
) -> Cow<'a, [u8]> {
let row_bytes = width as usize * bpp;
if stride == row_bytes {
let total = row_bytes * rows as usize;
Cow::Borrowed(&data[..total])
} else {
let mut packed = Vec::with_capacity(row_bytes * rows as usize);
for y in 0..rows as usize {
let start = y * stride;
packed.extend_from_slice(&data[start..start + row_bytes]);
}
Cow::Owned(packed)
}
}
#[cfg(test)]
mod tests {
use super::*;
use zenpixels::descriptor::{ColorPrimaries, SignalRange};
use zenpixels::policy::{AlphaPolicy, DepthPolicy, GrayExpand};
fn test_rgb8_data() -> Vec<u8> {
vec![255, 0, 0, 0, 255, 0]
}
#[test]
fn transfer_agnostic_match_requires_same_primaries() {
let data = test_rgb8_data();
let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
let target = PixelDescriptor::RGB8_SRGB;
let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
assert!(
matches!(result.data, Cow::Owned(_)),
"different primaries must trigger conversion, not zero-copy relabel"
);
}
#[test]
fn transfer_agnostic_match_requires_same_signal_range() {
let data = test_rgb8_data();
let source = PixelDescriptor::RGB8.with_signal_range(SignalRange::Narrow);
let target = PixelDescriptor::RGB8_SRGB;
let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
assert!(
matches!(result.data, Cow::Owned(_)),
"different signal range must trigger conversion, not zero-copy relabel"
);
}
#[test]
fn transfer_agnostic_match_allows_zero_copy_when_all_match() {
let data = test_rgb8_data();
let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt709);
let target = PixelDescriptor::RGB8_SRGB;
let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
assert!(
matches!(result.data, Cow::Borrowed(_)),
"should be zero-copy when only transfer differs"
);
assert_eq!(result.descriptor, target);
}
#[test]
fn exact_match_is_zero_copy() {
let data = test_rgb8_data();
let desc = PixelDescriptor::RGB8_SRGB;
let result = adapt_for_encode(&data, desc, 2, 1, 6, &[desc]).unwrap();
assert!(matches!(result.data, Cow::Borrowed(_)));
assert_eq!(result.descriptor, desc);
}
#[test]
fn explicit_variant_also_checks_primaries() {
let data = test_rgb8_data();
let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
let target = PixelDescriptor::RGB8_SRGB;
let options = ConvertOptions {
gray_expand: GrayExpand::Broadcast,
alpha_policy: AlphaPolicy::DiscardUnchecked,
depth_policy: DepthPolicy::Round,
luma: None,
};
let result =
adapt_for_encode_explicit(&data, source, 2, 1, 6, &[target], &options).unwrap();
assert!(
matches!(result.data, Cow::Owned(_)),
"explicit variant: different primaries must trigger conversion"
);
}
}