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

GIF output palette size doubles (from 8 to 16) #3401

Closed
3 tasks done
thadclay opened this issue Oct 6, 2022 · 9 comments
Closed
3 tasks done

GIF output palette size doubles (from 8 to 16) #3401

thadclay opened this issue Oct 6, 2022 · 9 comments

Comments

@thadclay
Copy link

thadclay commented Oct 6, 2022

Possible bug

Is this a possible bug in a feature of sharp, unrelated to installation?

  • Running npm install sharp completes without error.
  • Running node -e "require('sharp')" completes without error.

If you cannot confirm both of these, please open an installation issue instead.

Are you using the latest version of sharp?

  • I am using the latest version of sharp as reported by npm view sharp dist-tags.latest.

If you cannot confirm this, please upgrade to the latest version and try again before opening an issue.

If you are using another package which depends on a version of sharp that is not the latest, please open an issue against that package instead.

What is the output of running npx envinfo --binaries --system --npmPackages=sharp --npmGlobalPackages=sharp?

System:
OS: macOS 12.6
CPU: (8) x64 Apple M1 Pro
Memory: 28.89 MB / 16.00 GB
Shell: 3.2.57 - /bin/bash
Binaries:
Node: 14.17.2 - ~/.nvm/versions/node/v14.17.2/bin/node
npm: 6.14.13 - ~/.nvm/versions/node/v14.17.2/bin/npm
npmPackages:
sharp: ^0.31.1 => 0.31.1

What are the steps to reproduce?

  • Take any input animated gif
  • Call
    await sharp('input_filename.gif', { animated: true }).toFile('output_filename.gif');
  • File size is more than double the original.

What is the expected behaviour?

Output file size should be the same or nearly the same as the input file size.

Please provide a minimal, standalone code sample, without other dependencies, that demonstrates this problem

await sharp('input_filename.gif', { animated: true }).toFile('output_filename.gif');

Please provide sample image(s) that help explain this problem

https://media.giphy.com/media/jUwpNzg9IcyrK/giphy.gif

@thadclay thadclay added the triage label Oct 6, 2022
@lovell
Copy link
Owner

lovell commented Oct 8, 2022

This looks like a quirk of the way that the palette-reuse logic in libvips currently behaves with respect to transparency.

The original image has 8 colours:

  Colors: 8
    0: (191,189,182,  0)	  #BFBDB600
    1: (173,138, 95,  0)	  #AD8A5F00
    2: ( 58,140, 33,  0)	  #3A8C2100
    3: ( 71, 75,206,  0)	  #474BCE00
    4: (123, 74, 41,  0)	  #7B4A2900
    5: ( 33, 62, 16,  0)	  #213E1000
    6: (234,189, 44,255)	  #EABD2CFF
    7: (230,222,239,  0)	  #E6DEEF00

The output image has 16 colours, but is only using 9 of them:

  Colors: 16
    0: ( 76,105,113,255)	  #4C6971FF
    1: (191,189,182,  0)	  #BFBDB600
    2: (230,222,239,  0)	  #E6DEEF00
    3: ( 71, 75,206,  0)	  #474BCE00
    4: (173,138, 95,  0)	  #AD8A5F00
    5: (234,189, 44,  0)	  #EABD2C00
    6: ( 58,140, 33,  0)	  #3A8C2100
    7: ( 33, 62, 16,  0)	  #213E1000
    8: (123, 74, 41,  0)	  #7B4A2900
    9: (  0,  0,  0,  0)	  black
    10: (  0,  0,  0,  0)	  black
    11: (  0,  0,  0,  0)	  black
    12: (  0,  0,  0,  0)	  black
    13: (  0,  0,  0,  0)	  black
    14: (  0,  0,  0,  0)	  black
    15: (  0,  0,  0,  0)	  black

It looks like the transparency value from entry (234, 189, 44, 255) is lost and becomes (234, 189, 44, 0) in the output, and there's an extra transparency entry ( 76, 105, 113, 255) added.

@lovell lovell added question and removed triage labels Nov 3, 2022
@glomotion
Copy link

glomotion commented Nov 7, 2022

I think we're also seeing this happening in production. Src gif image is 800x800 and 6.2mb - but a 450x450 resized version ends up being 18mb 😅

@lovell
Copy link
Owner

lovell commented Nov 8, 2022

@glomotion Please can you provide a sample image to help confirm this is the same issue.

@lovell
Copy link
Owner

lovell commented Nov 9, 2022

@glomotion Thanks, this example is not quite the same issue, as the colour count remains the same for output as input.

It looks like the original image was created via gifsicle, which is slow to encode and spends that CPU time doing extra things like optimising LZW clear codes. See dloebl/cgif#51 for a possible future upstream improvement relating to this.

This is probably then a performance vs file size trade-off. If you care about file size, animated GIF is usually not the right answer. Converting this image to a video file should result in a file that is at least 5x smaller.

@lovell lovell changed the title Saving GIF Doubles File Size GIF output palette size doubles (from 8 to 16) Nov 9, 2022
@dloebl
Copy link

dloebl commented Nov 12, 2022

Hi @glomotion.

I had a look:
The GIF you provided is very noisy. When resized the transparency information (interframe file size optimization) is almost lost - which is bad for the compression rate. With gifsicle, which as @lovell already mentioned is a good benchmark for GIF file size optimization, the file size is as follows (330x330):

$ gifsicle -O3  --resize-fit 330x_ test.gif -o gifsicle.gif
$ vipsthumbnail --size 330x test.gif[n=-1] -o vips.gif
$ du -h --si gifsicle.gif vips.gif
8.3M    gifsicle.gif
11M     vips.gif

However, libvips v8.13 added a new option to gifsave which supports a lossy interframe optimization, which can decrease the file size further:

$ vipsthumbnail --size 330x test.gif[n=-1] -o vips_lossy.gif[interframe-maxerror=8.0]
$ du -h --si vips_lossy.gif
1.8M    vips_lossy.gif
gifsicle.gif vips.gif vips_lossy.gif
gifsicle vips vips_lossy
8.3 MB 11 MB 1.8 MB

I'm not sure if this option is exposed in sharp (I don't have too much context on sharp). Please note that the parameter interframe-maxerror is lossy, so you would want to be careful with it. If it's too high you are going to see ghosting artifacts.

There are still some possible improvements for cgif on my ToDo:

  1. Reduce file size: clear code optimization dloebl/cgif#51
  2. In this case, it would help to denoise the transparency information.

All things that could be improved on cgif.

Note: I had to resize to 330x330, because the maximum allowed file size on GitHub is 10 MB.

@lovell
Copy link
Owner

lovell commented Nov 14, 2022

Thanks @dloebl, the two new-ish *-maxerror parameters are not yet exposed in sharp, but we should do so as there's a lot of value for reducing massive animated GIF images (that could have been a video 😄 ).

@lovell
Copy link
Owner

lovell commented Nov 14, 2022

The new interFrameMaxError and interPaletteMaxError GIF options are exposed in commit 5740f45 and will be part of v0.31.3.

Example usage:

// interFrameMaxError API available from v0.31.3
await sharp('in.gif', { animated: true })
  .gif({ interFrameMaxError: 8 })
  .toFile('optim.gif');

@lovell
Copy link
Owner

lovell commented Dec 21, 2022

v0.31.3 now available with support for interFrameMaxError and interPaletteMaxError.

@lovell lovell closed this as completed Dec 21, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants