1use alloc::borrow::Cow;
62use alloc::vec;
63use alloc::vec::Vec;
64
65use crate::convert::ConvertPlan;
66use crate::converter::RowConverter;
67use crate::negotiate::{ConvertIntent, best_match};
68use crate::policy::{AlphaPolicy, ConvertOptions};
69use crate::{ConvertError, PixelDescriptor};
70use whereat::{At, ResultAtExt};
71
72#[derive(Clone, Debug)]
74pub struct Adapted<'a> {
75 pub data: Cow<'a, [u8]>,
77 pub descriptor: PixelDescriptor,
79 pub width: u32,
81 pub rows: u32,
83}
84
85#[track_caller]
101pub fn adapt_for_encode<'a>(
102 data: &'a [u8],
103 descriptor: PixelDescriptor,
104 width: u32,
105 rows: u32,
106 stride: usize,
107 supported: &[PixelDescriptor],
108) -> Result<Adapted<'a>, At<ConvertError>> {
109 adapt_for_encode_with_intent(
110 data,
111 descriptor,
112 width,
113 rows,
114 stride,
115 supported,
116 ConvertIntent::Fastest,
117 )
118}
119
120#[track_caller]
124pub fn adapt_for_encode_with_intent<'a>(
125 data: &'a [u8],
126 descriptor: PixelDescriptor,
127 width: u32,
128 rows: u32,
129 stride: usize,
130 supported: &[PixelDescriptor],
131 intent: ConvertIntent,
132) -> Result<Adapted<'a>, At<ConvertError>> {
133 if supported.is_empty() {
134 return Err(whereat::at!(ConvertError::EmptyFormatList));
135 }
136
137 if supported.contains(&descriptor) {
139 return Ok(Adapted {
140 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
141 descriptor,
142 width,
143 rows,
144 });
145 }
146
147 for &target in supported {
152 if descriptor.channel_type() == target.channel_type()
153 && descriptor.layout() == target.layout()
154 && descriptor.alpha() == target.alpha()
155 && descriptor.primaries == target.primaries
156 && descriptor.signal_range == target.signal_range
157 {
158 return Ok(Adapted {
159 data: contiguous_from_strided(
160 data,
161 width,
162 rows,
163 stride,
164 descriptor.bytes_per_pixel(),
165 ),
166 descriptor: target,
167 width,
168 rows,
169 });
170 }
171 }
172
173 let target = best_match(descriptor, supported, intent)
175 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
176
177 let mut converter = RowConverter::new(descriptor, target).at()?;
178
179 let src_bpp = descriptor.bytes_per_pixel();
180 let dst_bpp = target.bytes_per_pixel();
181 let dst_stride = (width as usize) * dst_bpp;
182 let mut output = vec![0u8; dst_stride * rows as usize];
183
184 for y in 0..rows {
185 let src_start = y as usize * stride;
186 let src_end = src_start + (width as usize * src_bpp);
187 let dst_start = y as usize * dst_stride;
188 let dst_end = dst_start + dst_stride;
189 converter.convert_row(
190 &data[src_start..src_end],
191 &mut output[dst_start..dst_end],
192 width,
193 );
194 }
195
196 Ok(Adapted {
197 data: Cow::Owned(output),
198 descriptor: target,
199 width,
200 rows,
201 })
202}
203
204#[track_caller]
208pub fn convert_buffer(
209 src: &[u8],
210 width: u32,
211 rows: u32,
212 from: PixelDescriptor,
213 to: PixelDescriptor,
214) -> Result<Vec<u8>, At<ConvertError>> {
215 if from == to {
216 return Ok(src.to_vec());
217 }
218
219 let mut converter = RowConverter::new(from, to).at()?;
220 let src_bpp = from.bytes_per_pixel();
221 let dst_bpp = to.bytes_per_pixel();
222 let src_stride = (width as usize) * src_bpp;
223 let dst_stride = (width as usize) * dst_bpp;
224 let mut output = vec![0u8; dst_stride * rows as usize];
225
226 for y in 0..rows {
227 let src_start = y as usize * src_stride;
228 let src_end = src_start + src_stride;
229 let dst_start = y as usize * dst_stride;
230 let dst_end = dst_start + dst_stride;
231 converter.convert_row(
232 &src[src_start..src_end],
233 &mut output[dst_start..dst_end],
234 width,
235 );
236 }
237
238 Ok(output)
239}
240
241#[track_caller]
247pub fn adapt_for_encode_explicit<'a>(
248 data: &'a [u8],
249 descriptor: PixelDescriptor,
250 width: u32,
251 rows: u32,
252 stride: usize,
253 supported: &[PixelDescriptor],
254 options: &ConvertOptions,
255) -> Result<Adapted<'a>, At<ConvertError>> {
256 if supported.is_empty() {
257 return Err(whereat::at!(ConvertError::EmptyFormatList));
258 }
259
260 if supported.contains(&descriptor) {
262 return Ok(Adapted {
263 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
264 descriptor,
265 width,
266 rows,
267 });
268 }
269
270 for &target in supported {
272 if descriptor.channel_type() == target.channel_type()
273 && descriptor.layout() == target.layout()
274 && descriptor.alpha() == target.alpha()
275 && descriptor.primaries == target.primaries
276 && descriptor.signal_range == target.signal_range
277 {
278 return Ok(Adapted {
279 data: contiguous_from_strided(
280 data,
281 width,
282 rows,
283 stride,
284 descriptor.bytes_per_pixel(),
285 ),
286 descriptor: target,
287 width,
288 rows,
289 });
290 }
291 }
292
293 let target = best_match(descriptor, supported, ConvertIntent::Fastest)
295 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
296
297 let plan = ConvertPlan::new_explicit(descriptor, target, options).at()?;
299
300 let drops_alpha = descriptor.alpha().is_some() && target.alpha().is_none();
302 if drops_alpha && options.alpha_policy == AlphaPolicy::DiscardIfOpaque {
303 let src_bpp = descriptor.bytes_per_pixel();
304 if !is_fully_opaque(data, width, rows, stride, src_bpp, &descriptor) {
305 return Err(whereat::at!(ConvertError::AlphaNotOpaque));
306 }
307 }
308
309 let mut converter = RowConverter::from_plan(plan);
310 let src_bpp = descriptor.bytes_per_pixel();
311 let dst_bpp = target.bytes_per_pixel();
312 let dst_stride = (width as usize) * dst_bpp;
313 let mut output = vec![0u8; dst_stride * rows as usize];
314
315 for y in 0..rows {
316 let src_start = y as usize * stride;
317 let src_end = src_start + (width as usize * src_bpp);
318 let dst_start = y as usize * dst_stride;
319 let dst_end = dst_start + dst_stride;
320 converter.convert_row(
321 &data[src_start..src_end],
322 &mut output[dst_start..dst_end],
323 width,
324 );
325 }
326
327 Ok(Adapted {
328 data: Cow::Owned(output),
329 descriptor: target,
330 width,
331 rows,
332 })
333}
334
335fn is_fully_opaque(
337 data: &[u8],
338 width: u32,
339 rows: u32,
340 stride: usize,
341 bpp: usize,
342 desc: &PixelDescriptor,
343) -> bool {
344 if desc.alpha().is_none() {
345 return true;
346 }
347 let cs = desc.channel_type().byte_size();
348 let alpha_offset = (desc.layout().channels() - 1) * cs;
349 for y in 0..rows {
350 let row_start = y as usize * stride;
351 for x in 0..width as usize {
352 let off = row_start + x * bpp + alpha_offset;
353 match desc.channel_type() {
354 crate::ChannelType::U8 => {
355 if data[off] != 255 {
356 return false;
357 }
358 }
359 crate::ChannelType::U16 => {
360 let v = u16::from_ne_bytes([data[off], data[off + 1]]);
361 if v != 65535 {
362 return false;
363 }
364 }
365 crate::ChannelType::F32 => {
366 let v = f32::from_ne_bytes([
367 data[off],
368 data[off + 1],
369 data[off + 2],
370 data[off + 3],
371 ]);
372 if v < 1.0 {
373 return false;
374 }
375 }
376 _ => return false,
377 }
378 }
379 }
380 true
381}
382
383fn contiguous_from_strided<'a>(
385 data: &'a [u8],
386 width: u32,
387 rows: u32,
388 stride: usize,
389 bpp: usize,
390) -> Cow<'a, [u8]> {
391 let row_bytes = width as usize * bpp;
392 if stride == row_bytes {
393 let total = row_bytes * rows as usize;
395 Cow::Borrowed(&data[..total])
396 } else {
397 let mut packed = Vec::with_capacity(row_bytes * rows as usize);
399 for y in 0..rows as usize {
400 let start = y * stride;
401 packed.extend_from_slice(&data[start..start + row_bytes]);
402 }
403 Cow::Owned(packed)
404 }
405}
406
407#[cfg(test)]
408mod tests {
409 use super::*;
410 use zenpixels::descriptor::{ColorPrimaries, SignalRange};
411 use zenpixels::policy::{AlphaPolicy, DepthPolicy, GrayExpand};
412
413 fn test_rgb8_data() -> Vec<u8> {
415 vec![255, 0, 0, 0, 255, 0]
416 }
417
418 #[test]
419 fn transfer_agnostic_match_requires_same_primaries() {
420 let data = test_rgb8_data();
421 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
422 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
425
426 assert!(
430 matches!(result.data, Cow::Owned(_)),
431 "different primaries must trigger conversion, not zero-copy relabel"
432 );
433 }
434
435 #[test]
436 fn transfer_agnostic_match_requires_same_signal_range() {
437 let data = test_rgb8_data();
438 let source = PixelDescriptor::RGB8.with_signal_range(SignalRange::Narrow);
439 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
442
443 assert!(
445 matches!(result.data, Cow::Owned(_)),
446 "different signal range must trigger conversion, not zero-copy relabel"
447 );
448 }
449
450 #[test]
451 fn transfer_agnostic_match_allows_zero_copy_when_all_match() {
452 let data = test_rgb8_data();
453 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt709);
455 let target = PixelDescriptor::RGB8_SRGB;
457
458 let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
459
460 assert!(
462 matches!(result.data, Cow::Borrowed(_)),
463 "should be zero-copy when only transfer differs"
464 );
465 assert_eq!(result.descriptor, target);
466 }
467
468 #[test]
469 fn exact_match_is_zero_copy() {
470 let data = test_rgb8_data();
471 let desc = PixelDescriptor::RGB8_SRGB;
472
473 let result = adapt_for_encode(&data, desc, 2, 1, 6, &[desc]).unwrap();
474
475 assert!(matches!(result.data, Cow::Borrowed(_)));
476 assert_eq!(result.descriptor, desc);
477 }
478
479 #[test]
480 fn explicit_variant_also_checks_primaries() {
481 let data = test_rgb8_data();
482 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
483 let target = PixelDescriptor::RGB8_SRGB;
484 let options = ConvertOptions {
485 gray_expand: GrayExpand::Broadcast,
486 alpha_policy: AlphaPolicy::DiscardUnchecked,
487 depth_policy: DepthPolicy::Round,
488 luma: None,
489 };
490
491 let result =
492 adapt_for_encode_explicit(&data, source, 2, 1, 6, &[target], &options).unwrap();
493
494 assert!(
495 matches!(result.data, Cow::Owned(_)),
496 "explicit variant: different primaries must trigger conversion"
497 );
498 }
499}