Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for Animated GIF #18

Closed
mmd-osm opened this issue Oct 6, 2018 · 17 comments
Closed

Support for Animated GIF #18

mmd-osm opened this issue Oct 6, 2018 · 17 comments

Comments

@mmd-osm
Copy link
Contributor

mmd-osm commented Oct 6, 2018

In the context of openstreetmap/openstreetmap-website#281 we're currently evaluating if a gd2 ruby library could be used to generate some rather simplistic animated gif, like the following example:

this one

libGD exposes a few relevant functions (https://libgd.github.io/manuals/2.2.5/files/gd_gif_out-c.html). which unfortunately don't seem to be included in gd2-ffij yet. From an FFI point of view, we're probably looking at adding a few entries. However, it's not quite clear, which approach you would prefer to model the respective ruby classes.

diff --git a/lib/gd2-ffij.rb b/lib/gd2-ffij.rb
index aed5114..1c2ff5b 100644
--- a/lib/gd2-ffij.rb
+++ b/lib/gd2-ffij.rb
@@ -122,6 +122,12 @@ module GD2
       :gdImageStringUp                    => [ :void,     :pointer, :pointer, :int, :int, :pointer, :int ],
       :gdImageStringFTEx                  => [ :pointer,  :pointer, :pointer, :int, :pointer, :double, :double, :int, :int, :pointer, :pointer ],
       :gdImageStringFTCircle              => [ :pointer,  :pointer, :int, :int, :double, :double, :double, :pointer, :double, :pointer, :pointer, :int ],
+      :gdImageGifAnimBeginPtr             => [ :pointer,  :pointer, :pointer, :int, :int ],
+      :gdImageGifAnimBegin                => [ :void,     :pointer, :pointer, :int, :int ],
+      :gdImageGifAnimAddPtr               => [ :pointer,  :pointer, :pointer, :int, :int, :int, :int, :int, :pointer ],
+      :gdImageGifAnimAdd                  => [ :void,     :pointer, :pointer, :int, :int, :int, :int, :int, :pointer ],
+      :gdImageGifAnimEnd                  => [ :void,     :pointer ],
+      :gdImageGifAnimEndPtr               => [ :pointer,  :pointer ],
       :gdFontGetSmall                     => [ :pointer ],
@dark-panda
Copy link
Owner

dark-panda commented Oct 6, 2018 via email

@mmd-osm
Copy link
Contributor Author

mmd-osm commented Oct 13, 2018

@dark-panda
Copy link
Owner

dark-panda commented Oct 16, 2018 via email

@dark-panda
Copy link
Owner

I've started work on this and have begun the implementation and have everything partially working, but I need to

a) get the actual image to render correctly, as at the moment I can see that the data is there but for some reason I get only a black image, i.e. the file itself contains all of the frames it should but for whatever reason won't actually produce a correct file; and

b) prepare a sane API. Right now it's fairly unintuitive -- it would be nicer to have a simple wrapper similar to how the Perl library does things.

In any event, it's "working", and hopefully will be ready to use Real Soon Now.

@gravitystorm
Copy link

Did you manage to solve the problem with rendering these animated gifs? And is there anything that we can help with or try out?

@dark-panda
Copy link
Owner

dark-panda commented Jan 28, 2019 via email

@gravitystorm
Copy link

@dark-panda Yes, we're very much still interested! Again, if you want help testing or anything, just let me know.

@mmd-osm
Copy link
Contributor Author

mmd-osm commented Mar 29, 2019

Looking at the Perl way of doing things, they seem to be calling a gifanimbegin, followed by multiple gifanimadds, and eventually a gifanimend:

A typical sequence will look like this:

  my $gifdata = $image->gifanimbegin;
  $gifdata   .= $image->gifanimadd;    # first frame
  for (1..100) {
     # make a frame of right size
     my $frame  = GD::Image->new($image->getBounds);
     add_frame_data($frame);              # add the data for this frame
     $gifdata   .= $frame->gifanimadd;     # add frame
  }
  $gifdata   .= $image->gifanimend;   # finish the animated GIF

I wonder if we could do something similar and collect the result like done in $gifdata above:

ptr below would refer to an Image object: (totally untested)

  class AnimatedGif

     def gif_anim_begin(ptr)
       size = FFI::MemoryPointer.new(:int)
       ptr = ::GD2::GD2FFI.send(:gdImageGifAnimBeginPtr, ptr.image_ptr, size, -1, 0)
       raise LibraryError if ptr.null?
       ptr.get_bytes(0, size.get_int(0))
     ensure
       ::GD2::GD2FFI.send(:gdFree, ptr)
     end

# use FFI::Pointer::NULL as default for prevptr for very first image

     def gif_anim_add(ptr, prevptr)
       size = FFI::MemoryPointer.new(:int)
       ptr = ::GD2::GD2FFI.send(:gdImageGifAnimAddPtr, ptr.image_ptr, size, 0, 0, 0, 250, 1, (prevptr == FFI::Pointer::NULL ? FFI::Pointer::NULL : prev_ptr.image_ptr)
       raise LibraryError if ptr.null?
       ptr.get_bytes(0, size.get_int(0))
     ensure
       ::GD2::GD2FFI.send(:gdFree, ptr)
     end

     def gif_anim_end()
       size = FFI::MemoryPointer.new(:int)
       ptr = ::GD2::GD2FFI.send(:gdImageGifAnimEndPtr, size)
       raise LibraryError if ptr.null?
       ptr.get_bytes(0, size.get_int(0))
     ensure
       ::GD2::GD2FFI.send(:gdFree, ptr)
     end

  end

@dark-panda
Copy link
Owner

dark-panda commented Mar 29, 2019 via email

@mmd-osm
Copy link
Contributor Author

mmd-osm commented Mar 30, 2019

I did some experiments with the following patch:

diff --git a/lib/gd2-ffij.rb b/lib/gd2-ffij.rb
index aed5114..c097096 100644
--- a/lib/gd2-ffij.rb
+++ b/lib/gd2-ffij.rb
@@ -59,6 +59,7 @@ module GD2
       :gdImageCreateFromGd2Part           => [ :pointer,  :pointer, :int, :int, :int, :int ],
       :gdImageCreateFromXbm               => [ :pointer,  :pointer ],
       :gdImageCreateFromXpm               => [ :pointer,  :pointer ],
+      :gdImagePaletteCopy                 => [ :void,     :pointer, :pointer ],
       :gdImageCompare                     => [ :int,      :pointer, :pointer ],
       :gdImageJpeg                        => [ :void,     :pointer, :pointer, :int ],
       :gdImageJpegPtr                     => [ :pointer,  :pointer, :pointer, :int ],
@@ -105,6 +106,7 @@ module GD2
       :gdImageColorExactAlpha             => [ :int,      :pointer, :int, :int, :int, :int ],
       :gdImageColorClosestAlpha           => [ :int,      :pointer, :int, :int, :int, :int ],
       :gdImageColorClosestHWB             => [ :int,      :pointer, :int, :int, :int ],
+      :gdImageColorAllocate               => [ :int,      :pointer, :int, :int, :int ],
       :gdImageColorAllocateAlpha          => [ :int,      :pointer, :int, :int, :int, :int ],
       :gdImageColorDeallocate             => [ :void,     :pointer, :int ],
       :gdAlphaBlend                       => [ :int,      :int, :int ],
@@ -122,6 +124,12 @@ module GD2
       :gdImageStringUp                    => [ :void,     :pointer, :pointer, :int, :int, :pointer, :int ],
       :gdImageStringFTEx                  => [ :pointer,  :pointer, :pointer, :int, :pointer, :double, :double, :int, :int, :pointer, :pointer ],
       :gdImageStringFTCircle              => [ :pointer,  :pointer, :int, :int, :double, :double, :double, :pointer, :double, :pointer, :pointer, :int ],
+      :gdImageGifAnimBeginPtr             => [ :pointer,  :pointer, :pointer, :int, :int ],
+      :gdImageGifAnimBegin                => [ :void,     :pointer, :pointer, :int, :int ],
+      :gdImageGifAnimAddPtr               => [ :pointer,  :pointer, :pointer, :int, :int, :int, :int, :int, :pointer ],
+      :gdImageGifAnimAdd                  => [ :void,     :pointer, :pointer, :int, :int, :int, :int, :int, :pointer ],
+      :gdImageGifAnimEnd                  => [ :void,     :pointer ],
+      :gdImageGifAnimEndPtr               => [ :pointer,  :pointer ],
       :gdFontGetSmall                     => [ :pointer ],
       :gdFontGetLarge                     => [ :pointer ],
       :gdFontGetMediumBold                => [ :pointer ],
diff --git a/lib/gd2/image.rb b/lib/gd2/image.rb
index 520c226..3a3dc3b 100644
--- a/lib/gd2/image.rb
+++ b/lib/gd2/image.rb
@@ -780,4 +780,46 @@ module GD2
       self
     end
   end
+
+  #
+  # = Description
+  #
+  # AnimtedGif images represents an animated Gif image
+  #
+
+  class AnimatedGif
+
+ #    def self.pal_copy(to, from)
+ #       ::GD2::GD2FFI.send(:gdImagePaletteCopy, to.image_ptr, from.image_ptr)
+ #    end
+
+     def self.gif_anim_begin(ptr)
+       size = FFI::MemoryPointer.new(:int)
+       ptr = ::GD2::GD2FFI.send(:gdImageGifAnimBeginPtr, ptr.image_ptr, size, -1, 0)
+       raise LibraryError if ptr.null?
+       ptr.get_bytes(0, size.get_int(0))
+     ensure
+       ::GD2::GD2FFI.send(:gdFree, ptr)
+     end
+
+     def self.gif_anim_add(ptr, prevptr, delay)
+       size = FFI::MemoryPointer.new(:int)
+       ptr = ::GD2::GD2FFI.send(:gdImageGifAnimAddPtr, ptr.image_ptr, size, 0, 0, 0, delay, 1, (prevptr.nil? ? FFI::Pointer::NULL : prevptr.image_ptr))
+       raise LibraryError if ptr.null?
+       ptr.get_bytes(0, size.get_int(0))
+     ensure
+       ::GD2::GD2FFI.send(:gdFree, ptr)
+     end
+
+     def self.gif_anim_end()
+       size = FFI::MemoryPointer.new(:int)
+       ptr = ::GD2::GD2FFI.send(:gdImageGifAnimEndPtr, size)
+       raise LibraryError if ptr.null?
+       ptr.get_bytes(0, size.get_int(0))
+     ensure
+       ::GD2::GD2FFI.send(:gdFree, ptr)
+     end
+
+  end
+
 end

Test code:

require 'gd2-ffij'

image1 = GD2::Image::IndexedColor.new(256, 256)

black = image1.palette.allocate(GD2::Color[0, 0, 0])
white = image1.palette.allocate(GD2::Color[255, 255, 255])
lime = image1.palette.allocate(GD2::Color[0, 255, 0])

image1.draw do |pen|
  pen.color = white
  pen.line(64, 64, 192, 192)
end

image2 = GD2::Image::IndexedColor.new(256, 256)
black = image2.palette.allocate(GD2::Color[0, 0, 0])
white = image2.palette.allocate(GD2::Color[255, 255, 255])
lime = image2.palette.allocate(GD2::Color[0, 255, 0])

image2.draw do |pen|
  pen.color = lime
  pen.rectangle(32, 64, 192, 192)
end

#GD2::AnimatedGif::pal_copy(image2, image1)

outfile = File.new('new_foo.gif', 'wb')
outfile.write(GD2::AnimatedGif::gif_anim_begin(image1))
outfile.write(GD2::AnimatedGif::gif_anim_add(image1, nil, 50))
outfile.write(GD2::AnimatedGif::gif_anim_add(image2, image1, 50))
outfile.write(GD2::AnimatedGif::gif_anim_end())
outfile.close

produces:

new_foo

@dark-panda
Copy link
Owner

Looking at this now. Looks pretty good. I'm going to wrap this into a class and make a few adjustments, but this is looking pretty good!

@dark-panda
Copy link
Owner

Alright folks, what do you think of https://github.com/dark-panda/gd2-ffij/tree/animated-gif ? I took the code from the pull request and restructured it into a class rather than using singleton methods. The API looks something like this:

anim = GD2::AnimatedGif.new
anim.add(image1)
anim.add(image2, delay: 50)
anim.add(image1, delay: 50)
anim.end
anim.export('filename.gif')

You can use any IO-style object in #export, similar to Image#export, so you can do...

output = StringIO.new

anim.export(output)

output.read

That way you can avoid writing to disk as necessary, and, say, stream the output to a HTTP response or whatever.

I've included a quick test, but if this API looks good, then I'll improve upon it and flesh it out a bit more.

@dark-panda
Copy link
Owner

Released in 0.4.0.

@mmd-osm
Copy link
Contributor Author

mmd-osm commented Jun 24, 2019

Thanks! By the way, we’ve noticed a small issue with the sequence of the frames, which we solved by adding the first frame twice - just to make your code behave exactly the same way as my initial example. Works fine in production since a few weeks!

See openstreetmap/openstreetmap-website#2204 (comment) for more details.

@dark-panda
Copy link
Owner

Cool, is there anything that should be done on my end, or is this just the nature of GD2? Also, are these traces available for viewing on the site, or is this an admin-only or otherwise non-user facing feature? I just want to see these things in action to see what they look like.

@mmd-osm
Copy link
Contributor Author

mmd-osm commented Jun 24, 2019

I think at one point the docs could include a hint that the very first “add” is just setting some animated gif marker and you would still need to add all frames afterwards. Libgd2 has a bit of an interesting API here.

All animations are public, you can see them by navigating to each one of the gpx traces: https://www.openstreetmap.org/traces

It may not look super fancy, but having animated gif available in Rails helped us getting rid of an external legacy import tool, which lacked essential bits like i18n and was difficult to manage from an operations POV.

@gravitystorm
Copy link

Released in 0.4.0.

I just wanted to add my thanks to @mmd-osm and @dark-panda for your work in adding animated gif support. We're using it to generate thousands of images per week. As @mmd-osm describes, it was a key part of a larger project to help openstreetmap. Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants