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