1use std::io::Cursor;
16use std::path::{Path, PathBuf};
17use std::sync::Arc;
18
19use crate::error::{EditError, EditResult};
20
21#[derive(Debug, Clone)]
25pub struct DecodedImageData {
26 pub pixels: Vec<u8>,
28 pub width: u32,
30 pub height: u32,
32}
33
34impl DecodedImageData {
35 pub fn new(pixels: Vec<u8>, width: u32, height: u32) -> EditResult<Self> {
37 let expected = (width as usize) * (height as usize) * 4;
38 if pixels.len() != expected {
39 return Err(EditError::InvalidOperation(format!(
40 "DecodedImageData: expected {expected} bytes for {width}x{height} RGBA8, got {}",
41 pixels.len()
42 )));
43 }
44 Ok(Self {
45 pixels,
46 width,
47 height,
48 })
49 }
50}
51
52#[derive(Debug, Clone)]
56pub struct WavData {
57 pub samples: Vec<f32>,
59 pub sample_rate: u32,
61 pub channels: u16,
63}
64
65#[derive(Debug)]
71pub enum RenderSource {
72 Image(DecodedImageData),
74 Wav(WavData),
76 TestPattern,
78 Unsupported {
80 path: PathBuf,
82 },
83}
84
85impl RenderSource {
86 pub fn from_path(path: &Path) -> EditResult<Arc<Self>> {
93 let ext = path
94 .extension()
95 .and_then(|e| e.to_str())
96 .map(|e| e.to_ascii_lowercase());
97
98 match ext.as_deref() {
99 Some("png") => {
100 let data = std::fs::read(path).map_err(|e| {
101 EditError::InvalidOperation(format!("RenderSource: cannot read {path:?}: {e}"))
102 })?;
103 let source = decode_png(&data)?;
104 Ok(Arc::new(RenderSource::Image(source)))
105 }
106 Some("jpg") | Some("jpeg") => {
107 let data = std::fs::read(path).map_err(|e| {
108 EditError::InvalidOperation(format!("RenderSource: cannot read {path:?}: {e}"))
109 })?;
110 let source = decode_jpeg(&data)?;
111 Ok(Arc::new(RenderSource::Image(source)))
112 }
113 Some("wav") => {
114 let data = std::fs::read(path).map_err(|e| {
115 EditError::InvalidOperation(format!("RenderSource: cannot read {path:?}: {e}"))
116 })?;
117 let source = decode_wav(&data)?;
118 Ok(Arc::new(RenderSource::Wav(source)))
119 }
120 _ => Ok(Arc::new(RenderSource::Unsupported {
121 path: path.to_path_buf(),
122 })),
123 }
124 }
125
126 #[must_use]
136 pub fn sample_video(&self, _source_pts: i64, width: u32, height: u32) -> Vec<u8> {
137 match self {
138 RenderSource::Image(img) => scale_nearest_rgba8(img, width, height),
139 RenderSource::TestPattern => generate_smpte_bars(width, height),
140 _ => vec![0u8; (width as usize) * (height as usize) * 4],
141 }
142 }
143
144 #[must_use]
156 pub fn sample_audio(
157 &self,
158 source_pts: i64,
159 num_samples: usize,
160 channels: u16,
161 sample_rate: u32,
162 ) -> Vec<f32> {
163 match self {
164 RenderSource::Wav(wav) => {
165 slice_wav_samples(wav, source_pts, num_samples, channels, sample_rate)
166 }
167 RenderSource::TestPattern => {
168 generate_sine(source_pts, num_samples, channels, sample_rate)
169 }
170 _ => vec![0.0_f32; num_samples * channels as usize],
171 }
172 }
173}
174
175fn decode_png(data: &[u8]) -> EditResult<DecodedImageData> {
178 use oximedia_image::png::PngDecoder;
179
180 let decoder = PngDecoder::new();
181 let img = decoder
182 .decode(data)
183 .map_err(|e| EditError::InvalidOperation(format!("PNG decode error: {e:?}")))?;
184
185 let w = img.width;
186 let h = img.height;
187 let pixels_rgba8 = convert_png_to_rgba8(&img.pixels, img.color_type, w, h)?;
188 DecodedImageData::new(pixels_rgba8, w, h)
189}
190
191fn convert_png_to_rgba8(
193 raw: &[u8],
194 color_type: oximedia_image::png::PngColorType,
195 width: u32,
196 height: u32,
197) -> EditResult<Vec<u8>> {
198 use oximedia_image::png::PngColorType;
199
200 let pixel_count = (width as usize) * (height as usize);
201 let mut out = Vec::with_capacity(pixel_count * 4);
202
203 match color_type {
204 PngColorType::Rgba => {
205 if raw.len() >= pixel_count * 4 {
207 out.extend_from_slice(&raw[..pixel_count * 4]);
208 } else {
209 out.extend_from_slice(raw);
210 out.resize(pixel_count * 4, 0);
212 }
213 }
214 PngColorType::Rgb => {
215 let src_len = raw.len().min(pixel_count * 3);
217 let src_pixels = src_len / 3;
218 for i in 0..src_pixels {
219 out.push(raw[i * 3]);
220 out.push(raw[i * 3 + 1]);
221 out.push(raw[i * 3 + 2]);
222 out.push(255);
223 }
224 for _ in src_pixels..pixel_count {
226 out.extend_from_slice(&[0, 0, 0, 255]);
227 }
228 }
229 PngColorType::Grayscale => {
230 let src_len = raw.len().min(pixel_count);
232 for i in 0..src_len {
233 let v = raw[i];
234 out.extend_from_slice(&[v, v, v, 255]);
235 }
236 for _ in src_len..pixel_count {
237 out.extend_from_slice(&[0, 0, 0, 255]);
238 }
239 }
240 PngColorType::GrayscaleAlpha => {
241 let src_len = raw.len().min(pixel_count * 2);
242 let src_pixels = src_len / 2;
243 for i in 0..src_pixels {
244 let v = raw[i * 2];
245 let a = raw[i * 2 + 1];
246 out.extend_from_slice(&[v, v, v, a]);
247 }
248 for _ in src_pixels..pixel_count {
249 out.extend_from_slice(&[0, 0, 0, 255]);
250 }
251 }
252 PngColorType::Indexed => {
253 let src_len = raw.len().min(pixel_count);
255 for i in 0..src_len {
256 let v = raw[i];
257 out.extend_from_slice(&[v, v, v, 255]);
258 }
259 for _ in src_len..pixel_count {
260 out.extend_from_slice(&[0, 0, 0, 255]);
261 }
262 }
263 }
264
265 Ok(out)
266}
267
268fn decode_jpeg(data: &[u8]) -> EditResult<DecodedImageData> {
271 use oximedia_image::jpeg::JpegDecoder;
272
273 let decoder = JpegDecoder::new();
274 let frame = decoder
275 .decode(data)
276 .map_err(|e| EditError::InvalidOperation(format!("JPEG decode error: {e:?}")))?;
277
278 let w = frame.width;
279 let h = frame.height;
280 let pixel_count = (w as usize) * (h as usize);
281 let mut out = Vec::with_capacity(pixel_count * 4);
282
283 match frame.components {
284 3 => {
285 let src_len = frame.pixels.len().min(pixel_count * 3);
287 let src_pixels = src_len / 3;
288 for i in 0..src_pixels {
289 out.push(frame.pixels[i * 3]);
290 out.push(frame.pixels[i * 3 + 1]);
291 out.push(frame.pixels[i * 3 + 2]);
292 out.push(255);
293 }
294 for _ in src_pixels..pixel_count {
295 out.extend_from_slice(&[0, 0, 0, 255]);
296 }
297 }
298 1 => {
299 let src_len = frame.pixels.len().min(pixel_count);
301 for i in 0..src_len {
302 let v = frame.pixels[i];
303 out.extend_from_slice(&[v, v, v, 255]);
304 }
305 for _ in src_len..pixel_count {
306 out.extend_from_slice(&[0, 0, 0, 255]);
307 }
308 }
309 4 => {
310 let src_len = frame.pixels.len().min(pixel_count * 4);
312 let src_pixels = src_len / 4;
313 for i in 0..src_pixels {
314 let c = frame.pixels[i * 4] as f32 / 255.0;
315 let m = frame.pixels[i * 4 + 1] as f32 / 255.0;
316 let y = frame.pixels[i * 4 + 2] as f32 / 255.0;
317 let k = frame.pixels[i * 4 + 3] as f32 / 255.0;
318 #[allow(clippy::cast_possible_truncation)]
319 #[allow(clippy::cast_sign_loss)]
320 let r = ((1.0 - c) * (1.0 - k) * 255.0).round().clamp(0.0, 255.0) as u8;
321 #[allow(clippy::cast_possible_truncation)]
322 #[allow(clippy::cast_sign_loss)]
323 let g = ((1.0 - m) * (1.0 - k) * 255.0).round().clamp(0.0, 255.0) as u8;
324 #[allow(clippy::cast_possible_truncation)]
325 #[allow(clippy::cast_sign_loss)]
326 let b = ((1.0 - y) * (1.0 - k) * 255.0).round().clamp(0.0, 255.0) as u8;
327 out.extend_from_slice(&[r, g, b, 255]);
328 }
329 for _ in src_pixels..pixel_count {
330 out.extend_from_slice(&[0, 0, 0, 255]);
331 }
332 }
333 _ => {
334 out.extend(std::iter::repeat(0u8).take(pixel_count * 4));
336 }
337 }
338
339 DecodedImageData::new(out, w, h)
340}
341
342fn decode_wav(data: &[u8]) -> EditResult<WavData> {
345 use oximedia_audio::wav::WavReader;
346
347 let cursor = Cursor::new(data);
348 let mut reader = WavReader::new(cursor)
349 .map_err(|e| EditError::InvalidOperation(format!("WAV read error: {e:?}")))?;
350
351 let spec = reader.spec();
352 let samples = reader
353 .read_samples_f32()
354 .map_err(|e| EditError::InvalidOperation(format!("WAV sample read error: {e:?}")))?;
355
356 Ok(WavData {
357 samples,
358 sample_rate: spec.sample_rate,
359 channels: spec.channels,
360 })
361}
362
363fn scale_nearest_rgba8(img: &DecodedImageData, target_w: u32, target_h: u32) -> Vec<u8> {
367 if img.width == 0 || img.height == 0 {
368 return vec![0u8; (target_w as usize) * (target_h as usize) * 4];
369 }
370
371 let src_w = img.width as usize;
372 let src_h = img.height as usize;
373 let dst_w = target_w as usize;
374 let dst_h = target_h as usize;
375 let mut out = vec![0u8; dst_w * dst_h * 4];
376
377 for dy in 0..dst_h {
378 let sy = (dy * src_h / dst_h).min(src_h - 1);
379 for dx in 0..dst_w {
380 let sx = (dx * src_w / dst_w).min(src_w - 1);
381 let src_idx = (sy * src_w + sx) * 4;
382 let dst_idx = (dy * dst_w + dx) * 4;
383 out[dst_idx..dst_idx + 4].copy_from_slice(&img.pixels[src_idx..src_idx + 4]);
384 }
385 }
386
387 out
388}
389
390#[must_use]
396pub fn generate_smpte_bars(width: u32, height: u32) -> Vec<u8> {
397 const BARS: [[u8; 4]; 8] = [
400 [192, 192, 192, 255], [192, 192, 0, 255], [0, 192, 192, 255], [0, 192, 0, 255], [192, 0, 192, 255], [192, 0, 0, 255], [0, 0, 192, 255], [0, 0, 0, 255], ];
409
410 let w = width as usize;
411 let h = height as usize;
412 let mut out = vec![0u8; w * h * 4];
413
414 let top_rows = (h * 7) / 8;
416 for y in 0..top_rows {
417 for x in 0..w {
418 let bar = (x * 7 / w).min(6);
419 let idx = (y * w + x) * 4;
420 out[idx..idx + 4].copy_from_slice(&BARS[bar]);
421 }
422 }
423 let bottom_colours: [[u8; 4]; 4] = [
425 [0, 0, 128, 255], [255, 255, 255, 255], [19, 0, 77, 255], [0, 0, 0, 255], ];
430 for y in top_rows..h {
431 for x in 0..w {
432 let seg = (x * 4 / w).min(3);
433 let idx = (y * w + x) * 4;
434 out[idx..idx + 4].copy_from_slice(&bottom_colours[seg]);
435 }
436 }
437
438 out
439}
440
441#[must_use]
450pub fn generate_sine(
451 source_pts: i64,
452 num_samples: usize,
453 channels: u16,
454 sample_rate: u32,
455) -> Vec<f32> {
456 use std::f64::consts::TAU;
457
458 let sr = sample_rate as f64;
459 let freq = 1000.0_f64;
460 let amplitude = 0.25_f64; let ch = channels as usize;
462 let mut out = Vec::with_capacity(num_samples * ch);
463 let start_sample = source_pts.max(0) as u64;
464
465 for i in 0..num_samples {
466 let t = (start_sample + i as u64) as f64 / sr;
467 #[allow(clippy::cast_possible_truncation)]
468 let sample = (TAU * freq * t).sin() * amplitude;
469 #[allow(clippy::cast_possible_truncation)]
470 let sample_f32 = sample as f32;
471 for _ in 0..ch {
472 out.push(sample_f32);
473 }
474 }
475
476 out
477}
478
479fn slice_wav_samples(
486 wav: &WavData,
487 source_pts: i64,
488 num_samples: usize,
489 target_channels: u16,
490 _target_sample_rate: u32,
491) -> Vec<f32> {
492 let src_ch = wav.channels as usize;
493 let tgt_ch = target_channels as usize;
494 let total_frames = wav.samples.len() / src_ch.max(1);
495 let start_frame = source_pts.max(0) as usize;
496 let out_len = num_samples * tgt_ch;
497
498 if start_frame >= total_frames || src_ch == 0 {
499 return vec![0.0_f32; out_len];
500 }
501
502 let available = (total_frames - start_frame).min(num_samples);
503 let mut out = vec![0.0_f32; out_len];
504
505 for i in 0..available {
506 let src_frame = start_frame + i;
507 for c in 0..tgt_ch {
508 let src_c = if src_ch == 1 {
509 0
511 } else {
512 c.min(src_ch - 1)
513 };
514 let src_idx = src_frame * src_ch + src_c;
515 let dst_idx = i * tgt_ch + c;
516 if src_idx < wav.samples.len() {
517 out[dst_idx] = wav.samples[src_idx];
518 }
519 }
520 }
521
522 out
523}
524
525#[cfg(test)]
528mod tests {
529 use super::*;
530
531 #[test]
532 fn test_smpte_bars_dimensions() {
533 let bars = generate_smpte_bars(16, 8);
534 assert_eq!(bars.len(), 16 * 8 * 4);
535 }
536
537 #[test]
538 fn test_smpte_bars_not_all_black() {
539 let bars = generate_smpte_bars(8, 4);
540 let all_zero = bars.iter().all(|&b| b == 0);
541 assert!(!all_zero, "SMPTE bars should not be all black");
542 }
543
544 #[test]
545 fn test_generate_sine_length() {
546 let samples = generate_sine(0, 480, 2, 48_000);
547 assert_eq!(samples.len(), 480 * 2);
548 }
549
550 #[test]
551 fn test_generate_sine_deterministic() {
552 let a = generate_sine(0, 48, 1, 48_000);
553 let b = generate_sine(0, 48, 1, 48_000);
554 assert_eq!(a, b);
555 }
556
557 #[test]
558 fn test_generate_sine_amplitude_bounded() {
559 let samples = generate_sine(0, 4800, 1, 48_000);
560 for s in &samples {
561 assert!(s.abs() <= 0.26, "sample {s} exceeds expected amplitude");
562 }
563 }
564
565 #[test]
566 fn test_render_source_test_pattern_video() {
567 let src = RenderSource::TestPattern;
568 let frame = src.sample_video(0, 8, 4);
569 assert_eq!(frame.len(), 8 * 4 * 4);
570 }
571
572 #[test]
573 fn test_render_source_test_pattern_audio() {
574 let src = RenderSource::TestPattern;
575 let audio = src.sample_audio(0, 48, 2, 48_000);
576 assert_eq!(audio.len(), 48 * 2);
577 }
578
579 #[test]
580 fn test_render_source_unsupported_video_is_black() {
581 let src = RenderSource::Unsupported {
582 path: PathBuf::from("foo.xyz"),
583 };
584 let frame = src.sample_video(0, 4, 2);
585 assert!(
586 frame.iter().all(|&b| b == 0),
587 "unsupported source should produce black frame"
588 );
589 }
590
591 #[test]
592 fn test_render_source_unsupported_audio_is_silence() {
593 let src = RenderSource::Unsupported {
594 path: PathBuf::from("foo.xyz"),
595 };
596 let audio = src.sample_audio(0, 48, 2, 48_000);
597 assert!(
598 audio.iter().all(|&s| s == 0.0),
599 "unsupported source should produce silence"
600 );
601 }
602
603 #[test]
604 fn test_scale_nearest_identity() {
605 let img = DecodedImageData {
606 pixels: vec![255, 0, 0, 255, 0, 255, 0, 255],
607 width: 2,
608 height: 1,
609 };
610 let out = scale_nearest_rgba8(&img, 2, 1);
611 assert_eq!(out, img.pixels);
612 }
613
614 #[test]
615 fn test_scale_nearest_upscale() {
616 let img = DecodedImageData {
617 pixels: vec![255, 0, 0, 255],
618 width: 1,
619 height: 1,
620 };
621 let out = scale_nearest_rgba8(&img, 2, 2);
622 assert_eq!(out.len(), 2 * 2 * 4);
623 for chunk in out.chunks_exact(4) {
625 assert_eq!(chunk, &[255, 0, 0, 255]);
626 }
627 }
628
629 #[test]
630 fn test_slice_wav_silence_when_offset_past_end() {
631 let wav = WavData {
632 samples: vec![0.5; 16],
633 sample_rate: 48_000,
634 channels: 1,
635 };
636 let out = slice_wav_samples(&wav, 9999, 48, 2, 48_000);
637 assert!(out.iter().all(|&s| s == 0.0));
638 }
639
640 #[test]
641 fn test_slice_wav_mono_to_stereo_upmix() {
642 let wav = WavData {
643 samples: vec![1.0; 8],
644 sample_rate: 48_000,
645 channels: 1,
646 };
647 let out = slice_wav_samples(&wav, 0, 4, 2, 48_000);
648 assert_eq!(out.len(), 8);
650 for &s in &out {
651 assert!((s - 1.0).abs() < 1e-6);
652 }
653 }
654}