rokka-io / imagine-vips

libvips adapter for php imagine
https://rokka.io
Other
41 stars 8 forks source link

Add animated gif support #3

Closed chregu closed 4 years ago

chregu commented 6 years ago

As libvips doesn't support animated gifs, we maybe can fallback to gifsicle for this.

Example code for creating one from an existing animated gif

    $im = new \Imagine\Vips\Imagine();
    $ori = $im->open("animated.gif");
    $i=0;
    /**
     * parse colors, loop for global options and disposal, delay from each frame with
     * gifsicle --info first and use them later instead of hardcoding
     */
    $options = "gifsicle -v --colors 256 --loop=forever -O3";
    /** @var \Imagine\Vips\Image $merged */
    foreach ($ori->layers() as $layer) {

        //merge each layer into the merged image to not have ugly transparency effects when doing operations
        if ($i > 0) {
            $merged = $merged->paste($layer, new Point(0, 0));
        } else {
            $merged = $layer;
        }
        $frame = clone $merged;
        // do the actual operation
        $frame->effects()->negative();
        $frame->save("anim$i.gif");
        $options .= " --delay 4 anim$i.gif";
        $i++;
    }

    $command =  $options . " -o animated.new.gif \n";
    echo $command;
    // run the gifsicle command
    echo `$command`;

We could of course also use imagick to do something similar instead of gifsicle, eg. the identify -verbose command (also available in PHP imagick) gives you also all the "delay, loop" information one needs, see http://www.imagemagick.org/Usage/scripts/gif2anim for inspiration. And maybe better to rely on a php extension than a CLI. gifsicle of course makes the more optimized gifs, but that could be another step.

jcupitt commented 6 years ago

libvips can load all the frame in an animated GIF with n=-1, it's saving that's not supported. Looking at the output, it doesn't seem to note the frame times, that's something we should fix:

$ vipsheader -a image006.gif[n=-1]
image006.gif: 169x2750 uchar, 4 bands, srgb, gifload
width: 169
height: 2750
bands: 4
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 1
yres: 1
filename: image006.gif
vips-loader: gifload
page-height: 2750

I think the big problem with write is generating the best palette, and perhaps doing inter-frame optimisation. We should probably add a dependency to a specialist library for this.

If we just want a simple, quick hack, we could make a 3:3:2 cube palette and dither with that.

We could also record the palette used for load, and recode with that on write. This could be a bit slow though: you'd probably need to build a large table to avoid a search for each pixel.

jcupitt commented 6 years ago

Oh haha, and the page-height is wrong, oh dear. I'll fix that at least.

jcupitt commented 6 years ago

OK, I now see:

$ vipsheader -a image006.gif[n=-1]
image006.gif: 169x2750 uchar, 4 bands, srgb, gifload
width: 169
height: 2750
bands: 4
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 1
yres: 1
filename: image006.gif
vips-loader: gifload
page-height: 125
gif-delay: 12

There should be a gif-loops field too.

chregu commented 6 years ago

So, there's basic animated gif support now, but you need to fallback to the imagick library (in imagine) to use this which seems the more proper way than integrating that in the vips adapter itself.

To use it, do it like that:

(most operations on the library don't support layers yet, but that's easy to change)

$im = new \Imagine\Vips\Imagine();
$ori = $im->open("animated.gif");
$ori->layers()->coalesce();
$resized = $ori->resize(new Box(200,200));
$imagick = $resized->convertToAlternative();
$imagick->save("test.gif" ,['animated' => true, 'animated.delay' => 4]);

It's currently only in layers-support branch.

The gif won't be highly optimized, but good enough for later optimization.

chregu commented 6 years ago

gif-delay: 12 Cool, that is useful, a gif-loops would be great too

We could at least do now this in the above example

$delay = 10;

try {
    //try to get the original delay
    $delay = $ori->getVips()->get('gif-delay');
} catch (\Jcupitt\Vips\Exception $e) {}
$imagick->save("test.gif" ,['animated' => true, 'animated.delay' => $delay]);
chregu commented 6 years ago

Tested now that gif-loops value. I assume the "unit" of that is "ticks" and a second has 100 ticks in gif (so the unit is "10ms").

But with playing around with it, I encountered something strange.

I have this image (sorry for the nervous thing, should find a better one ;))

test

I set the delay to 40ms (or 4 ticks), which is what the original uses. imagemagick identify -verbose also reports that for the frames. But vipsheader tells me gif-delay: 16

Bug or is imagemagick not setting some global delay correctly?

chregu commented 6 years ago

The original btw shows the "right" gif-delay value, I uploaded it here: https://github.com/jcupitt/php-vips-ext/issues/16#issuecomment-346008940 (linking to avoid more nervousness here ;))

jcupitt commented 6 years ago

libvips is just attaching the number in the gif header, which I think is 1/100ths of a second. I agree, I see something different from identify.

What is "ticks", exactly? Could it be 1/60ths of second, the frame time for most displays?

chregu commented 6 years ago

No idea where the ticks come from, just saw it here http://php.net/manual/en/imagick.setimagedelay.php

Sets the image delay. For an animated image this is the amount of time that this frame of the image should be displayed for, before displaying the next frame.

dbu commented 6 years ago

just some random input without having followed everything: i remember from creating animations that each layer can have an individual time how long its displayed. afaik there is no regular clock interval in an animated gif. but maybe imagemagick does something extra on the way to convert to movie formats.

chregu commented 6 years ago

@dbu at least the Imagine layer only has a global delay option. But you're right that an animated gif can have different delays per frame.

jcupitt commented 6 years ago

It looks like ticks defaults to 1/100th, but you can change it.

https://www.imagemagick.org/discourse-server/viewtopic.php?t=14739

I can't get identify to report the delay consistently. For your GIF I see:

john@kiwi:~/pics$ identify nervous.gif 
nervous.gif[0] GIF 550x400 550x400+0+0 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[1] GIF 365x319 550x400+37+81 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[2] GIF 379x328 550x400+30+72 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[3] GIF 448x331 550x400+30+69 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[4] GIF 403x331 550x400+98+69 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[5] GIF 351x314 550x400+150+86 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[6] GIF 418x336 550x400+66+64 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
nervous.gif[7] GIF 459x336 550x400+25+64 8-bit sRGB 128c 58.2KB 0.000u 0:00.000
john@kiwi:~/pics$ vipsheader -a nervous.gif 
nervous.gif: 550x400 uchar, 4 bands, srgb, gifload
width: 550
height: 400
bands: 4
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 1
yres: 1
filename: nervous.gif
vips-loader: gifload
gif-delay: 4

I'm not sure what to do now. I'll have a look at looping instead.

chregu commented 6 years ago

maybe imagemagick sets some global header to some value noone else is actually using. To reproduce without php imagick, but imagemagick cli and gifsicle

$ vipsheader -f gif-delay animated.gif
4

$ convert animated.gif test.gif ; vipsheader -f gif-delay test.gif
16

$ gifsicle animated.gif -o test2.gif ; vipsheader -f gif-delay test2.gif
4

$ gifsicle test.gif -o test3.gif; vipsheader -f gif-delay test3.gif
16

$ identify -verbose test.gif  | grep Delay | head -1
  Delay: 4x100

$ identify -verbose test2.gif  | grep Delay | head -1
  Delay: 4x100

$ identify -verbose test3.gif  | grep Delay | head -1
  Delay: 4x100

vipsheader also reports 16 when I convert another image with another delay.

jcupitt commented 6 years ago

OK, we have gif-loop as well.

jcupitt commented 6 years ago

16 was the default value vips was using. I've changed it to default to 4, so it should match IM.

Thanks for the -verbose tip -- I now see a match:

john@kiwi:~/pics$ vipsheader -f gif-delay nervous.gif 
4
john@kiwi:~/pics$ vipsheader -f gif-delay 3198.gif 
3
john@kiwi:~/pics$ identify -verbose nervous.gif | grep Delay | head -1
  Delay: 4x100
john@kiwi:~/pics$ identify -verbose 3198.gif | grep Delay | head -1
  Delay: 3x100

\o/

chregu commented 6 years ago

Delay looks fine now after some quick tests. Loop although always reports '0' for me (again with convert by imagemagick)

$ convert -loop 3 animated.gif  test.gif
$ vipsheader -a  -f gif-loop test.gif
0
jcupitt commented 6 years ago

Argh I'd misunderstood how extension blocks worked. It seems to be OK now.

chregu commented 6 years ago

Looks better, but is there an off-by-one error now?

$ convert -loop 5 animated.gif test.gif; vipsheader -a  -f gif-loop test.gif
4
$ convert -loop 1 animated.gif test.gif; vipsheader -a  -f gif-loop test.gif
0
$ convert -loop 0 animated.gif test.gif; vipsheader -a  -f gif-loop test.gif
0

(the last one is correct ;))

chregu commented 6 years ago

oh, found another issue with gif-delay:

following code for http://files.chregu.tv/liip-blog-animated.gif

$image = \Jcupitt\Vips\Image::newFromFile("liip-blog-animated.gif", ['n' => -1]);
print $image->get("gif-delay") ."\n";

Result: 116 (there are 96 frames)

Without ['n' => -1]

$image = \Jcupitt\Vips\Image::newFromFile("liip-blog-animated.gif");
print $image->get("gif-delay") ."\n";

Result the to be expected 4

vipsheader also delivers the correct 4

jcupitt commented 6 years ago

gif-loop seems to be working for me. I see:

john@kiwi:~/pics$ convert -loop 5 3198.gif x.gif
john@kiwi:~/pics$ vipsheader -a x.gif
x.gif: 298x193 uchar, 3 bands, srgb, gifload
width: 298
height: 193
bands: 3
format: uchar
coding: none
interpretation: srgb
xoffset: 0
yoffset: 0
xres: 1
yres: 1
filename: x.gif
vips-loader: gifload
gif-delay: 3
gif-loop: 5

That's with IM 7.0.3, if that makes any difference.

jcupitt commented 6 years ago

gifload is reporting the last delay it saw, and the final frame of your logo animation has a time of 116.

Perhaps it should only report the first time?

jcupitt commented 6 years ago

OK, only the first delay is reported now.

chregu commented 6 years ago

Oh, didn't check that the last delay is longer than the first. Sorry about that, but certainly makes more sense to report the first one.

chregu commented 4 years ago

This is good enough for now. Closing issue finally.