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::{ColorModel, ConvertError, PixelDescriptor};
70use whereat::{At, ResultAtExt};
71
72fn assert_not_cmyk(desc: &PixelDescriptor) {
77 assert!(
78 desc.color_model() != ColorModel::Cmyk,
79 "CMYK pixel data cannot be processed by zenpixels-convert. \
80 Use a CMS (e.g., moxcms) with an ICC profile for CMYK↔RGB conversion."
81 );
82}
83
84#[derive(Clone, Debug)]
86pub struct Adapted<'a> {
87 pub data: Cow<'a, [u8]>,
89 pub descriptor: PixelDescriptor,
91 pub width: u32,
93 pub rows: u32,
95}
96
97#[track_caller]
113pub fn adapt_for_encode<'a>(
114 data: &'a [u8],
115 descriptor: PixelDescriptor,
116 width: u32,
117 rows: u32,
118 stride: usize,
119 supported: &[PixelDescriptor],
120) -> Result<Adapted<'a>, At<ConvertError>> {
121 adapt_for_encode_with_intent(
122 data,
123 descriptor,
124 width,
125 rows,
126 stride,
127 supported,
128 ConvertIntent::Fastest,
129 )
130}
131
132#[track_caller]
136pub fn adapt_for_encode_with_intent<'a>(
137 data: &'a [u8],
138 descriptor: PixelDescriptor,
139 width: u32,
140 rows: u32,
141 stride: usize,
142 supported: &[PixelDescriptor],
143 intent: ConvertIntent,
144) -> Result<Adapted<'a>, At<ConvertError>> {
145 assert_not_cmyk(&descriptor);
146 if supported.is_empty() {
147 return Err(whereat::at!(ConvertError::EmptyFormatList));
148 }
149
150 if supported.contains(&descriptor) {
152 return Ok(Adapted {
153 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
154 descriptor,
155 width,
156 rows,
157 });
158 }
159
160 for &target in supported {
165 if descriptor.channel_type() == target.channel_type()
166 && descriptor.layout() == target.layout()
167 && descriptor.alpha() == target.alpha()
168 && descriptor.primaries == target.primaries
169 && descriptor.signal_range == target.signal_range
170 {
171 return Ok(Adapted {
172 data: contiguous_from_strided(
173 data,
174 width,
175 rows,
176 stride,
177 descriptor.bytes_per_pixel(),
178 ),
179 descriptor: target,
180 width,
181 rows,
182 });
183 }
184 }
185
186 let target = best_match(descriptor, supported, intent)
188 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
189
190 let mut converter = RowConverter::new(descriptor, target).at()?;
191
192 let src_bpp = descriptor.bytes_per_pixel();
193 let dst_bpp = target.bytes_per_pixel();
194 let dst_stride = (width as usize) * dst_bpp;
195 let mut output = vec![0u8; dst_stride * rows as usize];
196
197 for y in 0..rows {
198 let src_start = y as usize * stride;
199 let src_end = src_start + (width as usize * src_bpp);
200 let dst_start = y as usize * dst_stride;
201 let dst_end = dst_start + dst_stride;
202 converter.convert_row(
203 &data[src_start..src_end],
204 &mut output[dst_start..dst_end],
205 width,
206 );
207 }
208
209 Ok(Adapted {
210 data: Cow::Owned(output),
211 descriptor: target,
212 width,
213 rows,
214 })
215}
216
217#[track_caller]
221pub fn convert_buffer(
222 src: &[u8],
223 width: u32,
224 rows: u32,
225 from: PixelDescriptor,
226 to: PixelDescriptor,
227) -> Result<Vec<u8>, At<ConvertError>> {
228 assert_not_cmyk(&from);
229 assert_not_cmyk(&to);
230 if from == to {
231 return Ok(src.to_vec());
232 }
233
234 let mut converter = RowConverter::new(from, to).at()?;
235 let src_bpp = from.bytes_per_pixel();
236 let dst_bpp = to.bytes_per_pixel();
237 let src_stride = (width as usize) * src_bpp;
238 let dst_stride = (width as usize) * dst_bpp;
239 let mut output = vec![0u8; dst_stride * rows as usize];
240
241 for y in 0..rows {
242 let src_start = y as usize * src_stride;
243 let src_end = src_start + src_stride;
244 let dst_start = y as usize * dst_stride;
245 let dst_end = dst_start + dst_stride;
246 converter.convert_row(
247 &src[src_start..src_end],
248 &mut output[dst_start..dst_end],
249 width,
250 );
251 }
252
253 Ok(output)
254}
255
256#[track_caller]
262pub fn adapt_for_encode_explicit<'a>(
263 data: &'a [u8],
264 descriptor: PixelDescriptor,
265 width: u32,
266 rows: u32,
267 stride: usize,
268 supported: &[PixelDescriptor],
269 options: &ConvertOptions,
270) -> Result<Adapted<'a>, At<ConvertError>> {
271 assert_not_cmyk(&descriptor);
272 if supported.is_empty() {
273 return Err(whereat::at!(ConvertError::EmptyFormatList));
274 }
275
276 if supported.contains(&descriptor) {
278 return Ok(Adapted {
279 data: contiguous_from_strided(data, width, rows, stride, descriptor.bytes_per_pixel()),
280 descriptor,
281 width,
282 rows,
283 });
284 }
285
286 for &target in supported {
288 if descriptor.channel_type() == target.channel_type()
289 && descriptor.layout() == target.layout()
290 && descriptor.alpha() == target.alpha()
291 && descriptor.primaries == target.primaries
292 && descriptor.signal_range == target.signal_range
293 {
294 return Ok(Adapted {
295 data: contiguous_from_strided(
296 data,
297 width,
298 rows,
299 stride,
300 descriptor.bytes_per_pixel(),
301 ),
302 descriptor: target,
303 width,
304 rows,
305 });
306 }
307 }
308
309 let target = best_match(descriptor, supported, ConvertIntent::Fastest)
311 .ok_or_else(|| whereat::at!(ConvertError::EmptyFormatList))?;
312
313 let plan = ConvertPlan::new_explicit(descriptor, target, options).at()?;
315
316 let drops_alpha = descriptor.alpha().is_some() && target.alpha().is_none();
318 if drops_alpha && options.alpha_policy == AlphaPolicy::DiscardIfOpaque {
319 let src_bpp = descriptor.bytes_per_pixel();
320 if !is_fully_opaque(data, width, rows, stride, src_bpp, &descriptor) {
321 return Err(whereat::at!(ConvertError::AlphaNotOpaque));
322 }
323 }
324
325 let mut converter = RowConverter::from_plan(plan);
326 let src_bpp = descriptor.bytes_per_pixel();
327 let dst_bpp = target.bytes_per_pixel();
328 let dst_stride = (width as usize) * dst_bpp;
329 let mut output = vec![0u8; dst_stride * rows as usize];
330
331 for y in 0..rows {
332 let src_start = y as usize * stride;
333 let src_end = src_start + (width as usize * src_bpp);
334 let dst_start = y as usize * dst_stride;
335 let dst_end = dst_start + dst_stride;
336 converter.convert_row(
337 &data[src_start..src_end],
338 &mut output[dst_start..dst_end],
339 width,
340 );
341 }
342
343 Ok(Adapted {
344 data: Cow::Owned(output),
345 descriptor: target,
346 width,
347 rows,
348 })
349}
350
351fn is_fully_opaque(
353 data: &[u8],
354 width: u32,
355 rows: u32,
356 stride: usize,
357 bpp: usize,
358 desc: &PixelDescriptor,
359) -> bool {
360 if desc.alpha().is_none() {
361 return true;
362 }
363 let cs = desc.channel_type().byte_size();
364 let alpha_offset = (desc.layout().channels() - 1) * cs;
365 for y in 0..rows {
366 let row_start = y as usize * stride;
367 for x in 0..width as usize {
368 let off = row_start + x * bpp + alpha_offset;
369 match desc.channel_type() {
370 crate::ChannelType::U8 => {
371 if data[off] != 255 {
372 return false;
373 }
374 }
375 crate::ChannelType::U16 => {
376 let v = u16::from_ne_bytes([data[off], data[off + 1]]);
377 if v != 65535 {
378 return false;
379 }
380 }
381 crate::ChannelType::F32 => {
382 let v = f32::from_ne_bytes([
383 data[off],
384 data[off + 1],
385 data[off + 2],
386 data[off + 3],
387 ]);
388 if v < 1.0 {
389 return false;
390 }
391 }
392 _ => return false,
393 }
394 }
395 }
396 true
397}
398
399fn contiguous_from_strided<'a>(
401 data: &'a [u8],
402 width: u32,
403 rows: u32,
404 stride: usize,
405 bpp: usize,
406) -> Cow<'a, [u8]> {
407 let row_bytes = width as usize * bpp;
408 if stride == row_bytes {
409 let total = row_bytes * rows as usize;
411 Cow::Borrowed(&data[..total])
412 } else {
413 let mut packed = Vec::with_capacity(row_bytes * rows as usize);
415 for y in 0..rows as usize {
416 let start = y * stride;
417 packed.extend_from_slice(&data[start..start + row_bytes]);
418 }
419 Cow::Owned(packed)
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426 use zenpixels::descriptor::{ColorPrimaries, SignalRange};
427 use zenpixels::policy::{AlphaPolicy, DepthPolicy};
428
429 fn test_rgb8_data() -> Vec<u8> {
431 vec![255, 0, 0, 0, 255, 0]
432 }
433
434 #[test]
435 fn transfer_agnostic_match_requires_same_primaries() {
436 let data = test_rgb8_data();
437 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
438 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
441
442 assert!(
446 matches!(result.data, Cow::Owned(_)),
447 "different primaries must trigger conversion, not zero-copy relabel"
448 );
449 }
450
451 #[test]
452 fn transfer_agnostic_match_requires_same_signal_range() {
453 let data = test_rgb8_data();
454 let source = PixelDescriptor::RGB8.with_signal_range(SignalRange::Narrow);
455 let target = PixelDescriptor::RGB8_SRGB; let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
458
459 assert!(
461 matches!(result.data, Cow::Owned(_)),
462 "different signal range must trigger conversion, not zero-copy relabel"
463 );
464 }
465
466 #[test]
467 fn transfer_agnostic_match_allows_zero_copy_when_all_match() {
468 let data = test_rgb8_data();
469 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt709);
471 let target = PixelDescriptor::RGB8_SRGB;
473
474 let result = adapt_for_encode(&data, source, 2, 1, 6, &[target]).unwrap();
475
476 assert!(
478 matches!(result.data, Cow::Borrowed(_)),
479 "should be zero-copy when only transfer differs"
480 );
481 assert_eq!(result.descriptor, target);
482 }
483
484 #[test]
485 fn exact_match_is_zero_copy() {
486 let data = test_rgb8_data();
487 let desc = PixelDescriptor::RGB8_SRGB;
488
489 let result = adapt_for_encode(&data, desc, 2, 1, 6, &[desc]).unwrap();
490
491 assert!(matches!(result.data, Cow::Borrowed(_)));
492 assert_eq!(result.descriptor, desc);
493 }
494
495 #[test]
496 #[should_panic(expected = "CMYK pixel data cannot be processed")]
497 fn cmyk_rejected_by_adapt_for_encode() {
498 let cmyk_data = vec![0u8; 4 * 4]; let _ = adapt_for_encode(
500 &cmyk_data,
501 PixelDescriptor::CMYK8,
502 2,
503 2,
504 8,
505 &[PixelDescriptor::RGB8_SRGB],
506 );
507 }
508
509 #[test]
510 #[should_panic(expected = "CMYK pixel data cannot be processed")]
511 fn cmyk_rejected_by_convert_buffer() {
512 let cmyk_data = vec![0u8; 4 * 4];
513 let _ = convert_buffer(
514 &cmyk_data,
515 2,
516 2,
517 PixelDescriptor::CMYK8,
518 PixelDescriptor::RGB8_SRGB,
519 );
520 }
521
522 #[test]
523 #[should_panic(expected = "CMYK pixel data cannot be processed")]
524 fn cmyk_rejected_by_convert_buffer_as_target() {
525 let rgb_data = vec![0u8; 3 * 4];
526 let _ = convert_buffer(
527 &rgb_data,
528 2,
529 2,
530 PixelDescriptor::RGB8_SRGB,
531 PixelDescriptor::CMYK8,
532 );
533 }
534
535 #[test]
536 fn explicit_variant_also_checks_primaries() {
537 let data = test_rgb8_data();
538 let source = PixelDescriptor::RGB8.with_primaries(ColorPrimaries::Bt2020);
539 let target = PixelDescriptor::RGB8_SRGB;
540 let options = ConvertOptions::forbid_lossy()
541 .with_alpha_policy(AlphaPolicy::DiscardUnchecked)
542 .with_depth_policy(DepthPolicy::Round);
543
544 let result =
545 adapt_for_encode_explicit(&data, source, 2, 1, 6, &[target], &options).unwrap();
546
547 assert!(
548 matches!(result.data, Cow::Owned(_)),
549 "explicit variant: different primaries must trigger conversion"
550 );
551 }
552}