Skip to main content

zvidlib_container/
cover.rs

1//! Cover art generated from a recorded video frame (issue #510).
2//!
3//! [`CoverArt`] from issue #502 is a picture the caller encodes. Most callers
4//! only want a file-browser thumbnail of the recording, and zvidlib already
5//! sees every frame, so `zvidlib::MediaOutput` captures one by
6//! default and writes it as a PNG when the output finishes.
7//!
8//! The capture happens as frames are written rather than by decoding at
9//! `finish`: the writer has the raw pixels in hand, and decoding would need a
10//! decoder for every codec an encoder can emit. Frames up to and including the
11//! chosen index are shrunk to a thumbnail as they arrive, each replacing the
12//! last, so a stream shorter than the index still ends up with its last frame.
13//! Only one thumbnail is held at a time: at most
14//! [`COVER_THUMBNAIL_MAX_EDGE`]² x 3 bytes (768 KiB), and 480x270 x 3 bytes
15//! (380 KiB) for 1080p. Frames after the chosen index cost nothing.
16
17use crate::media::{ColorRange, PixelFormat, Plane, VideoFrame};
18use crate::mp4::{CoverArt, CoverArtFormat};
19use crate::transfer::{FrameSource, Orientation};
20use crate::{Error, ErrorKind, Result};
21
22/// The zero-based presentation index [`CoverSource::default`] picks. Frame 0
23/// is often black or mid fade-in, so the default skips a few frames.
24pub const DEFAULT_COVER_FRAME: u64 = 4;
25
26/// The longest edge, in pixels, of a cover generated from a video frame.
27/// Larger frames are shrunk by a whole-number factor until they fit.
28pub const COVER_THUMBNAIL_MAX_EDGE: u32 = 512;
29
30/// Where the cover art of a `zvidlib::MediaOutput` comes from when the caller
31/// has not set one with `MediaOutput::set_cover_art`, which always takes
32/// precedence.
33#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum CoverSource {
35    /// Write no generated cover.
36    None,
37    /// Use the video frame with this zero-based presentation index, shrunk to
38    /// a thumbnail and encoded as PNG. A stream with fewer frames uses its
39    /// last frame, and a stream with no video frames gets no cover. A frame
40    /// supplied as a GPU resource cannot be read back, so it gives no cover.
41    Frame(u64),
42}
43
44impl Default for CoverSource {
45    fn default() -> Self {
46        Self::Frame(DEFAULT_COVER_FRAME)
47    }
48}
49
50impl CoverArt {
51    /// Encodes `frame` as a PNG thumbnail no larger than
52    /// [`COVER_THUMBNAIL_MAX_EDGE`] on either edge. Limited-range frames are
53    /// expanded to full range, `Yuv420p8` is converted with the BT.601 matrix
54    /// zvidlib's decoders use, and alpha is dropped.
55    pub fn from_video_frame(frame: &VideoFrame) -> Result<Self> {
56        Ok(Thumbnail::new(frame, Orientation::TopLeft)?.to_cover_art())
57    }
58}
59
60/// Tracks the frame a [`CoverSource`] selects while a stream is written.
61#[derive(Debug)]
62#[doc(hidden)]
63pub struct CoverCapture {
64    source: CoverSource,
65    thumbnail: Option<Thumbnail>,
66}
67
68impl CoverCapture {
69    #[doc(hidden)]
70    pub fn new(source: CoverSource) -> Self {
71        Self {
72            source,
73            thumbnail: None,
74        }
75    }
76
77    /// Offers the frame at presentation `index`. A frame at or before the
78    /// chosen index replaces the held thumbnail; later frames are ignored.
79    #[doc(hidden)]
80    pub fn offer(&mut self, index: u64, frame: FrameSource<'_>) {
81        let CoverSource::Frame(target) = self.source else {
82            return;
83        };
84        if index > target {
85            return;
86        }
87        // A thumbnail is never worth failing a recording over, and one that
88        // cannot be made must not leave an earlier frame standing in for it.
89        self.thumbnail = match frame {
90            FrameSource::Cpu(source) => Thumbnail::new(source.frame, source.orientation).ok(),
91            FrameSource::Graphics(_) => None,
92        };
93    }
94
95    /// Encodes the captured frame, if there is one.
96    #[doc(hidden)]
97    pub fn cover_art(&self) -> Option<CoverArt> {
98        self.thumbnail.as_ref().map(Thumbnail::to_cover_art)
99    }
100}
101
102/// A shrunk, full-range, top-down RGB copy of one frame.
103#[derive(Debug)]
104struct Thumbnail {
105    width: usize,
106    height: usize,
107    rgb: Vec<u8>,
108}
109
110impl Thumbnail {
111    fn new(frame: &VideoFrame, orientation: Orientation) -> Result<Self> {
112        let source_width = frame.dimensions.width as usize;
113        let source_height = frame.dimensions.height as usize;
114        let longest = frame.dimensions.width.max(frame.dimensions.height);
115        let scale = longest.div_ceil(COVER_THUMBNAIL_MAX_EDGE).max(1) as usize;
116        let width = source_width.div_ceil(scale);
117        let height = source_height.div_ceil(scale);
118        let reader = PixelReader::new(frame)?;
119        let mut rgb = vec![0_u8; width * height * 3];
120        for y in 0..height {
121            for x in 0..width {
122                let mut totals = [0_u32; 3];
123                let mut count = 0_u32;
124                for source_y in y * scale..((y + 1) * scale).min(source_height) {
125                    let row = match orientation {
126                        Orientation::TopLeft => source_y,
127                        Orientation::BottomLeft => source_height - 1 - source_y,
128                    };
129                    for source_x in x * scale..((x + 1) * scale).min(source_width) {
130                        let pixel = reader.rgb(source_x, row);
131                        for (total, channel) in totals.iter_mut().zip(pixel) {
132                            *total += u32::from(channel);
133                        }
134                        count += 1;
135                    }
136                }
137                let offset = (y * width + x) * 3;
138                for (channel, total) in rgb[offset..offset + 3].iter_mut().zip(totals) {
139                    *channel = ((total + count / 2) / count) as u8;
140                }
141            }
142        }
143        Ok(Self { width, height, rgb })
144    }
145
146    fn to_cover_art(&self) -> CoverArt {
147        CoverArt {
148            format: CoverArtFormat::Png,
149            data: encode_png_rgb(self.width, self.height, &self.rgb),
150        }
151    }
152}
153
154/// Reads one full-range RGB pixel at a time from a validated frame.
155struct PixelReader<'a> {
156    format: PixelFormat,
157    range: ColorRange,
158    planes: &'a [Plane],
159}
160
161impl<'a> PixelReader<'a> {
162    fn new(frame: &'a VideoFrame) -> Result<Self> {
163        let planes_needed = match frame.pixel_format {
164            PixelFormat::Yuv420p8 => 3,
165            PixelFormat::Rgba8 | PixelFormat::Bgra8 | PixelFormat::Rgb8 | PixelFormat::Gray8 => 1,
166            _ => {
167                return Err(Error::new(
168                    ErrorKind::Unsupported,
169                    "cover art cannot be made from this pixel format",
170                ));
171            }
172        };
173        if frame.planes.len() < planes_needed {
174            return Err(Error::new(
175                ErrorKind::InvalidInput,
176                "video frame is missing a plane",
177            ));
178        }
179        Ok(Self {
180            format: frame.pixel_format,
181            range: frame.color_range,
182            planes: &frame.planes,
183        })
184    }
185
186    fn rgb(&self, x: usize, y: usize) -> [u8; 3] {
187        let plane = &self.planes[0];
188        let row = &plane.data[y * plane.stride..];
189        let rgb = match self.format {
190            PixelFormat::Rgba8 => [row[x * 4], row[x * 4 + 1], row[x * 4 + 2]],
191            PixelFormat::Bgra8 => [row[x * 4 + 2], row[x * 4 + 1], row[x * 4]],
192            PixelFormat::Rgb8 => [row[x * 3], row[x * 3 + 1], row[x * 3 + 2]],
193            PixelFormat::Gray8 => [row[x]; 3],
194            PixelFormat::Yuv420p8 => {
195                let chroma = |plane: &Plane| plane.data[(y / 2) * plane.stride + x / 2];
196                return yuv_to_rgb(
197                    row[x],
198                    chroma(&self.planes[1]),
199                    chroma(&self.planes[2]),
200                    self.range,
201                );
202            }
203            _ => unreachable!("PixelReader::new rejects every other pixel format"),
204        };
205        match self.range {
206            ColorRange::Limited => rgb.map(|component| {
207                ((u32::from(component.saturating_sub(16)) * 255 + 109) / 219).min(255) as u8
208            }),
209            _ => rgb,
210        }
211    }
212}
213
214/// BT.601 in Q16 fixed point: the limited-range matrix zvidlib's decoders
215/// convert with, or its JPEG full-range form.
216fn yuv_to_rgb(y: u8, u: u8, v: u8, range: ColorRange) -> [u8; 3] {
217    let (y, u, v) = (i32::from(y), i32::from(u) - 128, i32::from(v) - 128);
218    let (luma, r, g_u, g_v, b) = match range {
219        ColorRange::Limited => ((y - 16) * 76_309, 104_597, -25_675, -53_279, 132_201),
220        _ => (y << 16, 91_881, -22_554, -46_802, 116_130),
221    };
222    let clip = |value: i32| ((value + (1 << 15)) >> 16).clamp(0, 255) as u8;
223    [
224        clip(luma + r * v),
225        clip(luma + g_u * u + g_v * v),
226        clip(luma + b * u),
227    ]
228}
229
230/// Encodes 8-bit RGB as a PNG. Each row takes whichever filter minimizes its
231/// sum of absolute differences, and the zlib stream is fixed-Huffman deflate
232/// with greedy LZ77 matching, or stored blocks if that would be smaller.
233pub(crate) fn encode_png_rgb(width: usize, height: usize, rgb: &[u8]) -> Vec<u8> {
234    let row_bytes = width * 3;
235    let mut filtered = Vec::with_capacity((row_bytes + 1) * height);
236    let mut candidate = vec![0_u8; row_bytes];
237    let mut best = vec![0_u8; row_bytes];
238    for y in 0..height {
239        let row = &rgb[y * row_bytes..(y + 1) * row_bytes];
240        let previous = (y > 0).then(|| &rgb[(y - 1) * row_bytes..y * row_bytes]);
241        let mut best_filter = 0_u8;
242        let mut best_cost = u64::MAX;
243        for filter in 0..5_u8 {
244            for x in 0..row_bytes {
245                let left = if x >= 3 { row[x - 3] } else { 0 };
246                let up = previous.map_or(0, |previous| previous[x]);
247                let up_left = if x >= 3 {
248                    previous.map_or(0, |previous| previous[x - 3])
249                } else {
250                    0
251                };
252                let prediction = match filter {
253                    0 => 0,
254                    1 => left,
255                    2 => up,
256                    3 => ((u16::from(left) + u16::from(up)) / 2) as u8,
257                    _ => paeth(left, up, up_left),
258                };
259                candidate[x] = row[x].wrapping_sub(prediction);
260            }
261            let cost = candidate
262                .iter()
263                .map(|&value| u64::from((value as i8).unsigned_abs()))
264                .sum();
265            if cost < best_cost {
266                best_cost = cost;
267                best_filter = filter;
268                best.copy_from_slice(&candidate);
269            }
270        }
271        filtered.push(best_filter);
272        filtered.extend_from_slice(&best);
273    }
274
275    let mut ihdr = Vec::with_capacity(13);
276    ihdr.extend_from_slice(&(width as u32).to_be_bytes());
277    ihdr.extend_from_slice(&(height as u32).to_be_bytes());
278    // Bit depth 8, color type 2 (RGB), deflate, adaptive filtering, no interlace.
279    ihdr.extend_from_slice(&[8, 2, 0, 0, 0]);
280
281    let mut png = b"\x89PNG\r\n\x1a\n".to_vec();
282    png_chunk(&mut png, b"IHDR", &ihdr);
283    png_chunk(&mut png, b"IDAT", &zlib(&filtered));
284    png_chunk(&mut png, b"IEND", &[]);
285    png
286}
287
288fn paeth(left: u8, up: u8, up_left: u8) -> u8 {
289    let estimate = i16::from(left) + i16::from(up) - i16::from(up_left);
290    let distance_left = (estimate - i16::from(left)).abs();
291    let distance_up = (estimate - i16::from(up)).abs();
292    let distance_up_left = (estimate - i16::from(up_left)).abs();
293    if distance_left <= distance_up && distance_left <= distance_up_left {
294        left
295    } else if distance_up <= distance_up_left {
296        up
297    } else {
298        up_left
299    }
300}
301
302fn png_chunk(png: &mut Vec<u8>, kind: &[u8; 4], data: &[u8]) {
303    png.extend_from_slice(&(data.len() as u32).to_be_bytes());
304    let start = png.len();
305    png.extend_from_slice(kind);
306    png.extend_from_slice(data);
307    let crc = crc32(&png[start..]);
308    png.extend_from_slice(&crc.to_be_bytes());
309}
310
311fn crc32(bytes: &[u8]) -> u32 {
312    let mut crc = !0_u32;
313    for &byte in bytes {
314        crc ^= u32::from(byte);
315        for _ in 0..8 {
316            crc = (crc >> 1) ^ (0xedb8_8320 & (crc & 1).wrapping_neg());
317        }
318    }
319    !crc
320}
321
322fn adler32(bytes: &[u8]) -> u32 {
323    let (mut a, mut b) = (1_u32, 0_u32);
324    for chunk in bytes.chunks(5_552) {
325        for &byte in chunk {
326            a += u32::from(byte);
327            b += a;
328        }
329        a %= 65_521;
330        b %= 65_521;
331    }
332    (b << 16) | a
333}
334
335fn zlib(data: &[u8]) -> Vec<u8> {
336    // 32 KiB window, no dictionary; 0x7801 is a multiple of 31 as FCHECK needs.
337    let mut output = vec![0x78, 0x01];
338    let compressed = deflate_fixed(data);
339    if compressed.len() < stored_len(data.len()) {
340        output.extend_from_slice(&compressed);
341    } else {
342        deflate_stored(data, &mut output);
343    }
344    output.extend_from_slice(&adler32(data).to_be_bytes());
345    output
346}
347
348fn stored_len(length: usize) -> usize {
349    length + 5 * length.div_ceil(65_535).max(1)
350}
351
352fn deflate_stored(data: &[u8], output: &mut Vec<u8>) {
353    let mut blocks = data.chunks(65_535).peekable();
354    if blocks.peek().is_none() {
355        output.extend_from_slice(&[1, 0, 0, 0xff, 0xff]);
356    }
357    while let Some(block) = blocks.next() {
358        output.push(u8::from(blocks.peek().is_none()));
359        let length = block.len() as u16;
360        output.extend_from_slice(&length.to_le_bytes());
361        output.extend_from_slice(&(!length).to_le_bytes());
362        output.extend_from_slice(block);
363    }
364}
365
366const LENGTH_BASE: [u16; 29] = [
367    3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31, 35, 43, 51, 59, 67, 83, 99, 115, 131,
368    163, 195, 227, 258,
369];
370const LENGTH_EXTRA: [u8; 29] = [
371    0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5, 0,
372];
373const DISTANCE_BASE: [u16; 30] = [
374    1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193, 257, 385, 513, 769, 1025, 1537,
375    2049, 3073, 4097, 6145, 8193, 12289, 16385, 24577,
376];
377const DISTANCE_EXTRA: [u8; 30] = [
378    0, 0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 10, 10, 11, 11, 12, 12, 13,
379    13,
380];
381const WINDOW: usize = 32_768;
382const MIN_MATCH: usize = 3;
383const MAX_MATCH: usize = 258;
384const HASH_BITS: u32 = 15;
385const MAX_CHAIN: usize = 64;
386
387/// LSB-first bit packer for a deflate stream.
388struct BitWriter {
389    bytes: Vec<u8>,
390    buffer: u64,
391    bits: u32,
392}
393
394impl BitWriter {
395    fn bits(&mut self, value: u32, count: u32) {
396        self.buffer |= u64::from(value) << self.bits;
397        self.bits += count;
398        while self.bits >= 8 {
399            self.bytes.push(self.buffer as u8);
400            self.buffer >>= 8;
401            self.bits -= 8;
402        }
403    }
404
405    /// Writes a Huffman code, which deflate packs most significant bit first.
406    fn code(&mut self, code: u32, length: u32) {
407        self.bits(code.reverse_bits() >> (32 - length), length);
408    }
409
410    fn literal_length(&mut self, symbol: u16) {
411        let symbol = u32::from(symbol);
412        match symbol {
413            0..=143 => self.code(0x30 + symbol, 8),
414            144..=255 => self.code(0x190 + symbol - 144, 9),
415            256..=279 => self.code(symbol - 256, 7),
416            _ => self.code(0xc0 + symbol - 280, 8),
417        }
418    }
419
420    fn finish(mut self) -> Vec<u8> {
421        if self.bits > 0 {
422            self.bytes.push(self.buffer as u8);
423        }
424        self.bytes
425    }
426}
427
428fn deflate_fixed(data: &[u8]) -> Vec<u8> {
429    let mut writer = BitWriter {
430        bytes: Vec::with_capacity(data.len() / 2),
431        buffer: 0,
432        bits: 0,
433    };
434    // BFINAL = 1, BTYPE = 01 (fixed Huffman codes).
435    writer.bits(1, 1);
436    writer.bits(1, 2);
437
438    let hash = |position: usize| {
439        let value = u32::from(data[position])
440            | u32::from(data[position + 1]) << 8
441            | u32::from(data[position + 2]) << 16;
442        (value.wrapping_mul(0x9e37_79b1) >> (32 - HASH_BITS)) as usize
443    };
444    let mut head = vec![usize::MAX; 1 << HASH_BITS];
445    let mut previous = vec![usize::MAX; data.len()];
446    let insert = |position: usize, head: &mut [usize], previous: &mut [usize]| {
447        if position + MIN_MATCH <= data.len() {
448            let key = hash(position);
449            previous[position] = head[key];
450            head[key] = position;
451        }
452    };
453
454    let mut position = 0;
455    while position < data.len() {
456        let mut best_length = 0;
457        let mut best_distance = 0;
458        if position + MIN_MATCH <= data.len() {
459            let limit = (data.len() - position).min(MAX_MATCH);
460            let mut candidate = head[hash(position)];
461            let mut chain = 0;
462            while candidate != usize::MAX && position - candidate <= WINDOW && chain < MAX_CHAIN {
463                let length = data[candidate..]
464                    .iter()
465                    .zip(&data[position..position + limit])
466                    .take_while(|(a, b)| a == b)
467                    .count();
468                if length > best_length {
469                    best_length = length;
470                    best_distance = position - candidate;
471                    if length == limit {
472                        break;
473                    }
474                }
475                candidate = previous[candidate];
476                chain += 1;
477            }
478        }
479        if best_length >= MIN_MATCH {
480            let length_code = LENGTH_BASE
481                .iter()
482                .rposition(|&base| usize::from(base) <= best_length)
483                .unwrap_or(0);
484            writer.literal_length(257 + length_code as u16);
485            writer.bits(
486                (best_length - usize::from(LENGTH_BASE[length_code])) as u32,
487                u32::from(LENGTH_EXTRA[length_code]),
488            );
489            let distance_code = DISTANCE_BASE
490                .iter()
491                .rposition(|&base| usize::from(base) <= best_distance)
492                .unwrap_or(0);
493            writer.code(distance_code as u32, 5);
494            writer.bits(
495                (best_distance - usize::from(DISTANCE_BASE[distance_code])) as u32,
496                u32::from(DISTANCE_EXTRA[distance_code]),
497            );
498            for offset in 0..best_length {
499                insert(position + offset, &mut head, &mut previous);
500            }
501            position += best_length;
502        } else {
503            writer.literal_length(u16::from(data[position]));
504            insert(position, &mut head, &mut previous);
505            position += 1;
506        }
507    }
508    writer.literal_length(256);
509    writer.finish()
510}
511
512#[cfg(test)]
513mod tests {
514    use super::*;
515    use crate::api::Limits;
516    use crate::media::VideoDimensions;
517    use crate::transfer::CpuFrameSource;
518
519    /// A minimal inflater for the two block types [`zlib`] emits, so the
520    /// encoder is checked against a decoder rather than against itself.
521    fn inflate(zlib: &[u8]) -> Vec<u8> {
522        assert_eq!(&zlib[..2], &[0x78, 0x01]);
523        let stream = &zlib[2..zlib.len() - 4];
524        let mut output = Vec::new();
525        let mut bit = 0_usize;
526        let read = |bit: &mut usize, count: usize| {
527            let mut value = 0_u32;
528            for index in 0..count {
529                let byte = stream[(*bit + index) / 8];
530                value |= u32::from((byte >> ((*bit + index) % 8)) & 1) << index;
531            }
532            *bit += count;
533            value
534        };
535        let read_code = |bit: &mut usize, count: usize| {
536            let mut value = 0_u32;
537            for _ in 0..count {
538                value = (value << 1) | read(bit, 1);
539            }
540            value
541        };
542        loop {
543            let last = read(&mut bit, 1);
544            match read(&mut bit, 2) {
545                0 => {
546                    bit = bit.div_ceil(8) * 8;
547                    let at = bit / 8;
548                    let length = usize::from(u16::from_le_bytes([stream[at], stream[at + 1]]));
549                    output.extend_from_slice(&stream[at + 4..at + 4 + length]);
550                    bit = (at + 4 + length) * 8;
551                }
552                1 => loop {
553                    let mut code = read_code(&mut bit, 7);
554                    let symbol = if code <= 0x17 {
555                        code + 256
556                    } else {
557                        code = (code << 1) | read(&mut bit, 1);
558                        if (0x30..=0xbf).contains(&code) {
559                            code - 0x30
560                        } else if (0xc0..=0xc7).contains(&code) {
561                            code - 0xc0 + 280
562                        } else {
563                            code = (code << 1) | read(&mut bit, 1);
564                            code - 0x190 + 144
565                        }
566                    } as usize;
567                    if symbol < 256 {
568                        output.push(symbol as u8);
569                        continue;
570                    }
571                    if symbol == 256 {
572                        break;
573                    }
574                    let index = symbol - 257;
575                    let length = usize::from(LENGTH_BASE[index])
576                        + read(&mut bit, usize::from(LENGTH_EXTRA[index])) as usize;
577                    let distance_code = read_code(&mut bit, 5) as usize;
578                    let distance = usize::from(DISTANCE_BASE[distance_code])
579                        + read(&mut bit, usize::from(DISTANCE_EXTRA[distance_code])) as usize;
580                    for _ in 0..length {
581                        output.push(output[output.len() - distance]);
582                    }
583                },
584                other => panic!("unexpected block type {other}"),
585            }
586            if last == 1 {
587                break;
588            }
589        }
590        assert_eq!(
591            u32::from_be_bytes(zlib[zlib.len() - 4..].try_into().unwrap()),
592            adler32(&output)
593        );
594        output
595    }
596
597    /// Decodes a PNG [`encode_png_rgb`] wrote into its dimensions and RGB rows.
598    fn decode_png(png: &[u8]) -> (usize, usize, Vec<u8>) {
599        assert_eq!(&png[..8], b"\x89PNG\r\n\x1a\n");
600        let mut at = 8;
601        let (mut width, mut height, mut idat) = (0, 0, Vec::new());
602        while at < png.len() {
603            let length = u32::from_be_bytes(png[at..at + 4].try_into().unwrap()) as usize;
604            let body = &png[at + 4..at + 8 + length];
605            let crc =
606                u32::from_be_bytes(png[at + 8 + length..at + 12 + length].try_into().unwrap());
607            assert_eq!(crc, crc32(body));
608            let data = &body[4..];
609            match &body[..4] {
610                b"IHDR" => {
611                    width = u32::from_be_bytes(data[..4].try_into().unwrap()) as usize;
612                    height = u32::from_be_bytes(data[4..8].try_into().unwrap()) as usize;
613                    assert_eq!(&data[8..], &[8, 2, 0, 0, 0]);
614                }
615                b"IDAT" => idat.extend_from_slice(data),
616                b"IEND" => break,
617                _ => {}
618            }
619            at += 12 + length;
620        }
621        let filtered = inflate(&idat);
622        let row_bytes = width * 3;
623        let mut rgb = vec![0_u8; row_bytes * height];
624        for y in 0..height {
625            let line = &filtered[y * (row_bytes + 1)..(y + 1) * (row_bytes + 1)];
626            for x in 0..row_bytes {
627                let left = if x >= 3 {
628                    rgb[y * row_bytes + x - 3]
629                } else {
630                    0
631                };
632                let up = if y > 0 {
633                    rgb[(y - 1) * row_bytes + x]
634                } else {
635                    0
636                };
637                let up_left = if x >= 3 && y > 0 {
638                    rgb[(y - 1) * row_bytes + x - 3]
639                } else {
640                    0
641                };
642                let prediction = match line[0] {
643                    0 => 0,
644                    1 => left,
645                    2 => up,
646                    3 => ((u16::from(left) + u16::from(up)) / 2) as u8,
647                    4 => paeth(left, up, up_left),
648                    other => panic!("unexpected filter {other}"),
649                };
650                rgb[y * row_bytes + x] = line[x + 1].wrapping_add(prediction);
651            }
652        }
653        (width, height, rgb)
654    }
655
656    fn frame(width: u32, height: u32, format: PixelFormat, planes: Vec<Plane>) -> VideoFrame {
657        let limits = Limits::default();
658        VideoFrame::new(
659            VideoDimensions::new(width, height, &limits).unwrap(),
660            format,
661            ColorRange::Full,
662            planes,
663            &limits,
664        )
665        .unwrap()
666    }
667
668    fn pattern(width: usize, height: usize, seed: u8) -> Vec<u8> {
669        (0..width * height * 3)
670            .map(|index| {
671                let pixel = index / 3;
672                let (x, y) = (pixel % width, pixel / width);
673                ((x * 7 + y * 13 + (index % 3) * 50) as u8).wrapping_add(seed)
674            })
675            .collect()
676    }
677
678    #[test]
679    fn png_round_trips_patterned_flat_and_noisy_images() {
680        let mut noise = 0x1234_5678_u32;
681        let noisy: Vec<u8> = (0..97 * 31 * 3)
682            .map(|_| {
683                noise ^= noise << 13;
684                noise ^= noise >> 17;
685                noise ^= noise << 5;
686                noise as u8
687            })
688            .collect();
689        for (width, height, rgb) in [
690            (1, 1, vec![1, 2, 3]),
691            (64, 48, pattern(64, 48, 9)),
692            (300, 200, vec![77; 300 * 200 * 3]),
693            (97, 31, noisy),
694        ] {
695            let png = encode_png_rgb(width, height, &rgb);
696            assert_eq!(decode_png(&png), (width, height, rgb));
697        }
698    }
699
700    #[test]
701    fn stored_blocks_are_used_when_huffman_coding_would_grow_the_data() {
702        let mut noise = 0x9e37_79b9_u32;
703        let data: Vec<u8> = (0..70_000)
704            .map(|_| {
705                noise ^= noise << 13;
706                noise ^= noise >> 17;
707                noise ^= noise << 5;
708                noise as u8
709            })
710            .collect();
711        let stream = zlib(&data);
712        assert_eq!(stream.len(), 2 + stored_len(data.len()) + 4);
713        assert_eq!(inflate(&stream), data);
714        assert_eq!(inflate(&zlib(&[])), Vec::<u8>::new());
715    }
716
717    #[test]
718    fn a_flat_frame_compresses_far_below_its_raw_size() {
719        let png = encode_png_rgb(512, 288, &vec![40; 512 * 288 * 3]);
720        assert!(png.len() < 4_096, "{} bytes", png.len());
721    }
722
723    #[test]
724    fn large_frames_shrink_to_the_thumbnail_edge_by_averaging() {
725        // 1030 wide needs a factor of 3 to fit 512: 344x2 from 1030x6.
726        let (width, height) = (1030_usize, 6_usize);
727        let mut rgba = vec![0_u8; width * height * 4];
728        for (pixel, value) in rgba.chunks_mut(4).enumerate() {
729            let (x, y) = (pixel % width, pixel / width);
730            // Each 3x3 block holds 0 and 30 in its first column, 60 elsewhere.
731            let level = if x % 3 == 0 { (y % 2 * 30) as u8 } else { 60 };
732            value.copy_from_slice(&[level, level / 2, 255 - level, 0]);
733        }
734        let source = frame(
735            width as u32,
736            height as u32,
737            PixelFormat::Rgba8,
738            vec![Plane {
739                data: rgba,
740                stride: width * 4,
741            }],
742        );
743        let thumbnail = Thumbnail::new(&source, Orientation::TopLeft).unwrap();
744        assert_eq!((thumbnail.width, thumbnail.height), (344, 2));
745        // Rows 0..3 average (0 + 30 + 0 + 6 * 60) / 9 = 43.3; rows 3..6
746        // average (30 + 0 + 30 + 360) / 9 = 46.7.
747        assert_eq!(&thumbnail.rgb[..3], &[43, 22, 212]);
748        assert_eq!(&thumbnail.rgb[344 * 3..344 * 3 + 3], &[47, 23, 208]);
749        // The last column covers only source column 1029, a block-first column.
750        assert_eq!(&thumbnail.rgb[343 * 3..344 * 3], &[10, 5, 245]);
751    }
752
753    #[test]
754    fn every_pixel_format_and_orientation_reads_as_top_down_rgb() {
755        let rgb = [[200_u8, 100, 50], [10, 20, 30]];
756        let packed = |bytes: &dyn Fn([u8; 3]) -> Vec<u8>| -> Vec<u8> {
757            rgb.iter().flat_map(|&pixel| bytes(pixel)).collect()
758        };
759        let one_by_two = |format, data: Vec<u8>| {
760            let stride = data.len() / 2;
761            frame(1, 2, format, vec![Plane { data, stride }])
762        };
763        for source in [
764            one_by_two(PixelFormat::Rgba8, packed(&|[r, g, b]| vec![r, g, b, 255])),
765            one_by_two(PixelFormat::Bgra8, packed(&|[r, g, b]| vec![b, g, r, 255])),
766            one_by_two(PixelFormat::Rgb8, packed(&|pixel| pixel.to_vec())),
767        ] {
768            let thumbnail = Thumbnail::new(&source, Orientation::TopLeft).unwrap();
769            assert_eq!(thumbnail.rgb, rgb.concat());
770            let flipped = Thumbnail::new(&source, Orientation::BottomLeft).unwrap();
771            assert_eq!(flipped.rgb, [rgb[1], rgb[0]].concat());
772        }
773
774        let gray = one_by_two(PixelFormat::Gray8, vec![9, 250]);
775        let thumbnail = Thumbnail::new(&gray, Orientation::TopLeft).unwrap();
776        assert_eq!(thumbnail.rgb, [9, 9, 9, 250, 250, 250]);
777
778        let mut limited = one_by_two(PixelFormat::Gray8, vec![16, 235]);
779        limited.color_range = ColorRange::Limited;
780        let thumbnail = Thumbnail::new(&limited, Orientation::TopLeft).unwrap();
781        assert_eq!(thumbnail.rgb, [0, 0, 0, 255, 255, 255]);
782    }
783
784    #[test]
785    fn yuv420_converts_with_bt601_in_either_range() {
786        let yuv = |y: u8, u: u8, v: u8, range| {
787            let mut source = frame(
788                2,
789                2,
790                PixelFormat::Yuv420p8,
791                vec![
792                    Plane {
793                        data: vec![y; 4],
794                        stride: 2,
795                    },
796                    Plane {
797                        data: vec![u],
798                        stride: 1,
799                    },
800                    Plane {
801                        data: vec![v],
802                        stride: 1,
803                    },
804                ],
805            );
806            source.color_range = range;
807            Thumbnail::new(&source, Orientation::TopLeft).unwrap().rgb[..3].to_vec()
808        };
809        assert_eq!(yuv(16, 128, 128, ColorRange::Limited), [0, 0, 0]);
810        assert_eq!(yuv(235, 128, 128, ColorRange::Limited), [255, 255, 255]);
811        // BT.601 limited-range red, green and blue, as rounded 8-bit YCbCr.
812        assert_eq!(yuv(81, 90, 240, ColorRange::Limited), [254, 0, 0]);
813        assert_eq!(yuv(145, 54, 34, ColorRange::Limited), [0, 255, 1]);
814        assert_eq!(yuv(41, 240, 110, ColorRange::Limited), [0, 0, 255]);
815        assert_eq!(yuv(128, 128, 128, ColorRange::Full), [128, 128, 128]);
816        assert_eq!(yuv(76, 85, 255, ColorRange::Full), [254, 0, 0]);
817    }
818
819    #[test]
820    fn capture_keeps_the_chosen_frame_or_the_last_one_before_it() {
821        let solid = |value: u8| {
822            frame(
823                2,
824                2,
825                PixelFormat::Gray8,
826                vec![Plane {
827                    data: vec![value; 4],
828                    stride: 2,
829                }],
830            )
831        };
832        let frames: Vec<VideoFrame> = (0..8).map(|index| solid(index * 10)).collect();
833        let offer = |capture: &mut CoverCapture, count: usize| {
834            for (index, frame) in frames.iter().take(count).enumerate() {
835                capture.offer(
836                    index as u64,
837                    FrameSource::Cpu(CpuFrameSource {
838                        frame,
839                        orientation: Orientation::TopLeft,
840                    }),
841                );
842            }
843        };
844        let first_pixel = |capture: &CoverCapture| {
845            capture
846                .cover_art()
847                .map(|cover| decode_png(&cover.data).2[0])
848        };
849
850        let mut default = CoverCapture::new(CoverSource::default());
851        offer(&mut default, 8);
852        assert_eq!(first_pixel(&default), Some(40));
853
854        let mut short = CoverCapture::new(CoverSource::Frame(6));
855        offer(&mut short, 3);
856        assert_eq!(first_pixel(&short), Some(20));
857
858        let mut none = CoverCapture::new(CoverSource::None);
859        offer(&mut none, 8);
860        assert_eq!(first_pixel(&none), None);
861
862        let empty = CoverCapture::new(CoverSource::default());
863        assert_eq!(first_pixel(&empty), None);
864    }
865}