benwiley4000 / gif-frames

🖼 Extract frames from an animated GIF with pure JS
MIT License
170 stars 26 forks source link

Broken frames for some gifs when set cumulative to true #13

Open AlexanderIstomin opened 5 years ago

AlexanderIstomin commented 5 years ago

Not actually the bug but i tried to get frames of this gif https://media3.giphy.com/media/5bvAojR9JBgCWaezCy/giphy.gif and got frames which "overlaps" each over. Any ideas how to get proper frames ?

Screenshot 2019-04-02 at 23 12 08
benwiley4000 commented 5 years ago

What happens if you set cumulative to false?

AlexanderIstomin commented 5 years ago

works good in that way but i have issues with other gifs. So can't use neither cumulative=true neither cumulative=false for all gifs.

Screenshot 2019-04-02 at 23 15 26
AlexanderIstomin commented 5 years ago

cumulative=false. So if i trying to render each frame on top of another, i can see previous frames behind current one because last one is little smaller. If i trying to clear canvas before rendering each frame - i see distortion of last frame (white flakes)

Screenshot 2019-04-02 at 23 17 01
benwiley4000 commented 5 years ago

That's right, you should only use the cumulative option for gifs that use transparency in later frames when they match the pixel in the previous frame in order to save storage space. It technically will still work if your gif has no transparency in the rendered image, but for a transparent image like the one you shared, cumulative mode gets confused and thinks those pixels are supposed to get taken from previous frames.

It seems like you need a way to programmatically determine whether a gif needs cumulative mode or not (maybe we could build this into the library). Honestly I have no clue how you do that since I'm not a gif format expert, it would require that I do some research.

benwiley4000 commented 5 years ago

Hm, with the heart gif I'm not sure what's happening just by looking. Could you share the gif file?

benwiley4000 commented 5 years ago

Btw thanks for reporting this! Definitely a problem I hadn't considered.

AlexanderIstomin commented 5 years ago

Here is original gif https://media0.giphy.com/media/3HHvQSvdCMYD2bY1X2/giphy.gif Here is rendered frames with cumulative=true

Screenshot 2019-04-02 at 23 33 22
AlexanderIstomin commented 5 years ago

Frames with cumulative=true

Screenshot 2019-04-02 at 23 37 08
AlexanderIstomin commented 5 years ago

Didn't look at gif specification yet but perhaps there is some key that indicates that frame should disappear or stay ?

benwiley4000 commented 5 years ago

Thanks for sharing this! It's the first time I've noticed a transparent GIF that also uses the layering technique. Turns out this particular GIF seems to be using a different color as transparent (red, maybe).

In the frameInfo that gets returned we can see:


data_length: 47600
data_offset: 1586
delay: 10
disposal: 2
has_local_palette: true
height: 480
interlaced: false
palette_offset: 818
transparent_index: 242
width: 480

I'd need to do some research into how the palette offset and transparent index work, but I think those can tell us which color to consider transparent. That way we could potentially even remove the cumulative option and just let everything work automatically.

You could also give it a look if you want to help me get this done a bit faster. All the low-level code is in the omggif library: https://github.com/deanm/omggif/blob/master/omggif.js

AlexanderIstomin commented 5 years ago

Sure i will look at this library. Found this: // From the spec: // 0 - No disposal specified. The decoder is // not required to take any action. // 1 - Do not dispose. The graphic is to be left // in place. // 2 - Restore to background color. The area used by the // graphic must be restored to the background color. // 3 - Restore to previous. The decoder is required to // restore the area overwritten by the graphic with // what was there prior to rendering the graphic.

benwiley4000 commented 5 years ago

Hm, that might help! Thanks! Clearly I should go read the GIF spec. 😄

benwiley4000 commented 5 years ago

As you can tell, I've only ever tested this library on GIFs which don't try crazy things like changing which color is "transparent". 😆

benwiley4000 commented 5 years ago

Hey @AlexanderIstomin, just to update you, this is on my todo list but I am giving a conference talk next week and probably won't have time to work on this until the following weekend. You can feel free to give it a shot in a pull request in the meantime, or if you're ok waiting, I can get to it in about a week.

benwiley4000 commented 5 years ago

Hey another update. I am sorry to have put this off for so long.. it's been hard to keep track of all my priorities. I am on vacation for another week or so and when I return I may have some time to tackle this problem. I'll add it to my calendar for the following weekend. :)

artboard-studio commented 4 years ago

Hey guys, fantastic library. Any update on automating this? For now, here is how I have solved the problem, but you can see the issue here:

let url = './image.gif';
gifFrames({url, frames: 0})
    .then(async fd => {
        const cumulative = fd[0].frameInfo.disposal !== 1 ? false : true;
        gifFrames({url, frames: 'all', outputType: 'canvas', cumulative})
            .then(async frameData => {
                // do stuff with frameData
            });
});

the solution was inspired by this StackOverflow: https://stackoverflow.com/a/58405203/1954936 and it works for me, for now.

benwiley4000 commented 4 years ago

Thanks for sharing that! I'm so sorry for being inattentive, I've had a lot going on recently. Do you want to make a PR? Otherwise I can try to find some time soon.

artboard-studio commented 4 years ago

@benwiley4000 No apologies needed, we all have the same tight schedule I believe. I can certainly have look at the source code, as I am currently looking to find a way to run the code in a worker or make it a bit less blocking. I will make a PR if I find a solution.

benwiley4000 commented 4 years ago

Thanks! Let me know

Le sam. 7 mars 2020 14 h 34, Hooman Askari notifications@github.com a écrit :

@benwiley4000 https://github.com/benwiley4000 No apologies needed, we all have the same tight schedule I believe. I can certainly have look at the source code, as I am currently looking to find a way to run the code in a worker or make it a bit less blocking. I will make a PR if I find a solution.

— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/benwiley4000/gif-frames/issues/13?email_source=notifications&email_token=ADHOD3NBTVH774NHWP5KONLRGKOUPA5CNFSM4HDE47UKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEOEDS4A#issuecomment-596130160, or unsubscribe https://github.com/notifications/unsubscribe-auth/ADHOD3OS5MNGBAV5PWM7ZR3RGKOUPANCNFSM4HDE47UA .

artboard-studio commented 4 years ago

Hey Ben, I ended up using SuperGif library because I needed the raw image data (for performance reasons) so it's unlikely I may be able to create a PR for this repo, but here is a hint, SuperGif solved this issue like so: https://github.com/wizpanda/super-gif/blob/master/src/super-gif.ts#L182 This, however, should be fixed in the save-pixels library.

I hope this helps, for you or anyone else who is to create a PR :)

benwiley4000 commented 4 years ago

Thank you!

TrevorSundberg commented 4 years ago

I just wanted to share this snippet I wrote that respects the disposal and does cumulative rendering when needed. It may not be 100% correct, but I haven't found a gif yet that it's failed on and I've tried hundreds of gifs on giphy. It should be used with cumulative: false and outputType: "canvas". Note that this will decode all the frames and render them:

const renderCumulativeFrames = (frameData) => {
  if (frameData.length === 0) {
    return frameData;
  }
  const previous = document.createElement("canvas");
  const previousContext = previous.getContext("2d");
  const current = document.createElement("canvas");
  const currentContext = current.getContext("2d");

  // Setting the canvas width will clear the canvas, so we only want to do it once.
  const firstFrameCanvas = frameData[0].getImage();

  // It also apperas that 'gif-frames' always returns a consistent sized canvas for all frames.
  previous.width = firstFrameCanvas.width;
  previous.height = firstFrameCanvas.height;
  current.width = firstFrameCanvas.width;
  current.height = firstFrameCanvas.height;

  for (const frame of frameData) {
    // Copy the current to the previous.
    previousContext.clearRect(0, 0, previous.width, previous.height);
    previousContext.drawImage(current, 0, 0);

    // Draw the current frame to the cumulative buffer.
    const canvas = frame.getImage();
    const context = canvas.getContext("2d");
    currentContext.drawImage(canvas, 0, 0);
    context.clearRect(0, 0, canvas.width, canvas.height);
    context.drawImage(current, 0, 0);

    const {frameInfo} = frame;
    const {disposal} = frameInfo;
    // If the disposal method is clear to the background color, then clear the canvas.
    if (disposal === 2) {
      currentContext.clearRect(frameInfo.x, frameInfo.y, frameInfo.width, frameInfo.height);
    // If the disposal method is reset to the previous, then copy the previous over the current.
    } else if (disposal === 3) {
      currentContext.clearRect(0, 0, current.width, current.height);
      currentContext.drawImage(previous, 0, 0);
    }
    frame.getImage = () => canvas;
  }
  return frameData;
};

And an example of calling it:

gifFrames({
      cumulative: false,
      frames: "all",
      outputType: "canvas",
      url
    }).then((frameData) => renderCumulativeFrames(frameData))
pnkfluffy commented 4 years ago

@TrevorSundberg Wow, that's incredible. I've been looking everywhere for a way to do this. Would be awesome to implement into the actual library.

benwiley4000 commented 4 years ago

Hey I'm sorry I haven't had time to look at this yet.. it sounds like we found something that works so if someone wanted to make a PR I would definitely review it asap. Thanks!

pnkfluffy commented 4 years ago

Everyone seems pretty busy so I decided to submit one. @benwiley4000, I just deleted your code that had to do with cumulative gif dissection, added @TrevorSundberg 's stuff in an additional promise, and boom. Tested it with a few cumulative and none cumulative gifs and it seemed to work fine. Let me know if its all good.