diff --git a/src/decoder/stream.rs b/src/decoder/stream.rs index 17b49d60..9f4b4dd7 100644 --- a/src/decoder/stream.rs +++ b/src/decoder/stream.rs @@ -1662,6 +1662,16 @@ mod tests { write_chunk(w, b"fcTL", &data); } + /// Writes an fdAT chunk. + /// See https://wiki.mozilla.org/APNG_Specification#.60fdAT.60:_The_Frame_Data_Chunk + fn write_fdat(w: &mut impl Write, sequence_number: u32, image_data: &[u8]) { + let mut data = Vec::new(); + data.write_u32::(sequence_number) + .unwrap(); + data.write_all(&image_data).unwrap(); + write_chunk(w, b"fdAT", &data); + } + /// Writes PNG signature and chunks that can precede an fdAT chunk that is expected /// to have /// - `sequence_number` set to 0 @@ -1724,4 +1734,75 @@ mod tests { let msg = format!("{err}"); assert_eq!("fdAT chunk shorter than 4 bytes", msg); } + + #[test] + fn test_frame_split_across_two_fdat_chunks() { + // Generate test data where the 2nd animation frame is split across 2 fdAT chunks. + // + // This is similar to the example given in + // https://wiki.mozilla.org/APNG_Specification#Chunk_Sequence_Numbers: + // + // ``` + // Sequence number Chunk + // (none) `acTL` + // 0 `fcTL` first frame + // (none) `IDAT` first frame / default image + // 1 `fcTL` second frame + // 2 first `fdAT` for second frame + // 3 second `fdAT` for second frame + // ``` + let png = { + let mut png = Vec::new(); + write_fdat_prefix(&mut png, 2, 8); + let image_data = generate_rgba8_with_width(8); + write_fdat(&mut png, 2, &image_data[..30]); + write_fdat(&mut png, 3, &image_data[30..]); + write_iend(&mut png); + png + }; + + // Start decoding. + let decoder = Decoder::new(png.as_slice()); + let mut reader = decoder.read_info().unwrap(); + let mut buf = vec![0; reader.output_buffer_size()]; + let Some(animation_control) = reader.info().animation_control else { + panic!("No acTL"); + }; + assert_eq!(animation_control.num_frames, 2); + + // Process the 1st animation frame. + let first_frame: Vec; + { + reader.next_frame(&mut buf).unwrap(); + first_frame = buf.clone(); + + // Note that the doc comment of `Reader::next_frame` says that "[...] + // can be checked afterwards by calling `info` **after** a successful call and + // inspecting the `frame_control` data.". (Note the **emphasis** on "after".) + let Some(frame_control) = reader.info().frame_control else { + panic!("No fcTL (1st frame)"); + }; + // The sequence number is taken from the `fcTL` chunk that comes before the `IDAT` + // chunk. + assert_eq!(frame_control.sequence_number, 0); + } + + // Process the 2nd animation frame. + let second_frame: Vec; + { + reader.next_frame(&mut buf).unwrap(); + second_frame = buf.clone(); + + // Same as above - updated `frame_control` is available *after* the `next_frame` call. + let Some(frame_control) = reader.info().frame_control else { + panic!("No fcTL (2nd frame)"); + }; + // The sequence number is taken from the `fcTL` chunk that comes before the two `fdAT` + // chunks. Note that sequence numbers inside `fdAT` chunks are not publically exposed + // (but they are still checked when decoding to verify that they are sequential). + assert_eq!(frame_control.sequence_number, 1); + } + + assert_eq!(first_frame, second_frame); + } } diff --git a/src/test_utils.rs b/src/test_utils.rs index 89a2b7df..56f668ed 100644 --- a/src/test_utils.rs +++ b/src/test_utils.rs @@ -73,8 +73,8 @@ pub fn write_rgba8_ihdr_with_width(w: &mut impl Write, width: u32) { write_chunk(w, b"IHDR", &data); } -/// Writes an IDAT chunk. -pub fn write_rgba8_idat_with_width(w: &mut impl Write, width: u32) { +/// Generates RGBA8 `width` x `width` image and wraps it in a store-only zlib container. +pub fn generate_rgba8_with_width(width: u32) -> Vec { // Generate arbitrary test pixels. let image_pixels = { let mut row = Vec::new(); @@ -101,9 +101,16 @@ pub fn write_rgba8_idat_with_width(w: &mut impl Write, width: u32) { store_only_compressor.write_data(&image_pixels).unwrap(); store_only_compressor.finish().unwrap(); - write_chunk(w, b"IDAT", &zlib_data); + zlib_data +} + +/// Writes an IDAT chunk. +pub fn write_rgba8_idat_with_width(w: &mut impl Write, width: u32) { + write_chunk(w, b"IDAT", &generate_rgba8_with_width(width)); } -fn write_iend(w: &mut impl Write) { +/// Writes an IEND chunk. +/// See http://www.libpng.org/pub/png/spec/1.2/PNG-Chunks.html#C.IEND +pub fn write_iend(w: &mut impl Write) { write_chunk(w, b"IEND", &[]); }