image-rs / image-gif

GIF en- and decoder
Apache License 2.0
148 stars 42 forks source link

"repeat" loop is not recognized by Chrome #147

Closed happylinks closed 10 months ago

happylinks commented 10 months ago

I'm creating gifs with repeat: -1, which should loop. In finder this works fine, but in browsers (Chrome for example) it only plays once. I found this post about it: https://stackoverflow.com/questions/10867789/animated-gif-only-loops-once-in-chrome-and-firefox

Aparantly you need to add this extension command: "0x21,0xFF,0x0B,"NETSCAPE","2.0",0x03,0x01,0x00,0x00,0x00" for it to loop.

Is this something we can add to this library? I can make a PR, but any guidance would be great.

fintelia commented 10 months ago

Thanks for tracking this down! I'd be in favor of accepting a PR to fix this

okaneco commented 10 months ago

I looked into this and I'm not sure how to replicate it.

The number of repetitions is a u16, so the program won't compile without an integer cast. Converting from -1_iX will result in u16::MAX. An infinite loop corresponds to 0, according to http://www.vurdalakov.net/misc/gif/netscape-looping-application-extension. Perhaps Finder is automatically looping a one-repeat GIF?

pub enum Repeat {
    Finite(u16),
    Infinite,
}

https://docs.rs/gif/0.12.0/gif/enum.Repeat.html

The extension block is written when Encoder::set_repeat is called, which includes the NETSCAPE2.0 command. Either a 0 or the number of repetitions is written.

https://github.com/image-rs/image-gif/blob/442225a78d55c9deb900fc4805117db7b778d822/src/encoder.rs#L287-L297

If Repeat::Finite(0) is specified, then the block isn't written. https://github.com/image-rs/image-gif/blob/442225a78d55c9deb900fc4805117db7b778d822/src/encoder.rs#L272-L276

Here's the example program I used to generate a finite and infinite gif, then inspect the image binary data.
Create the file in the `gif/examples` folder. ```rust // loop.rs fn main() { use gif::{Encoder, Frame, Repeat}; const WIDTH: usize = 8; const SQUARED: usize = WIDTH * WIDTH; let black = [0; SQUARED * 3]; let white = [255; SQUARED * 3]; let (width, height) = (WIDTH as u16, WIDTH as u16); let mut image_finite = std::fs::File::create("./blinker_fin.gif").unwrap(); let mut image_infinite = std::fs::File::create("./blinker_inf.gif").unwrap(); let mut encoder_finite = Encoder::new(&mut image_finite, width, height, &[]).unwrap(); let mut encoder_infinite = Encoder::new(&mut image_infinite, width, height, &[]).unwrap(); encoder_finite .set_repeat(Repeat::Finite(-1i16 as u16)) .unwrap(); encoder_infinite.set_repeat(Repeat::Infinite).unwrap(); for buffer in [black, white] { let mut frame = Frame::from_rgb(width, height, &buffer); frame.delay = 50; encoder_finite.write_frame(&frame).unwrap(); encoder_infinite.write_frame(&frame).unwrap(); } } ``` You can diff the binary of the gifs with the following and see the only difference is whether `0u16` or `u16::MAX` is written. ```bash cargo run --example loop xxd blinker_fin.gif > fin.hex xxd blinker_inf.gif > inf.hex diff fin.hex inf.hex ``` ```bash < 00000020: 3003 01ff ff00 21f9 0404 3200 0000 2c00 0.....!...2...,. --- > 00000020: 3003 0100 0000 21f9 0404 3200 0000 2c00 0.....!...2...,. ^^^^^ ```
happylinks commented 10 months ago

Did some testing, just to be helpful.

Gif generated with this library: https://tella-static.s3.us-east-2.amazonaws.com/clfic1aqm00000gmk3leg87cr.gif

"Fixed" with ffmpeg -i <input gif> -loop 0 -c copy <output gif>: https://tella-static.s3.us-east-2.amazonaws.com/clfic1aqm00000gmk3leg87cr-fixed.gif

Hex diff:

image

I'm not seeing the NETSCAPE2.0 part at all in the original gif, I set repeat to -1.

I'll test your code as well.

happylinks commented 10 months ago

Tested your example code and it looks fine indeed, it repeats infinitely in chrome.

Maybe it has to do with the library I'm using: https://gitlab.freedesktop.org/gstreamer/gst-plugins-rs/-/blob/main/video/gif/src/gifenc/imp.rs#L120 Code looks good at first glance though. I'll dig a bit deeper.

happylinks commented 10 months ago

Ok yeah, it's not this project, it's the gstreamer plugin passing "0" for repeats. Sorry about the noise! I'll fix it in that package.