Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Resizing Animated GIFs Without CGO or Third-Par...

logica
November 13, 2024

Resizing Animated GIFs Without CGO or Third-Party Libraries

11/13/2024 Slides for the presentation at GoLab 2024 in Florence.

If you want to see the GIF images in the slide animated, please check out the Google Slides version.
https://docs.google.com/presentation/d/1sRzow8ytY6AP6HfmXMFBuRODgwg9_Tlhdjw03zWcPGc/edit?usp=sharing

logica

November 13, 2024
Tweet

More Decks by logica

Other Decks in Programming

Transcript

  1. Animated icons & stamps (in traQ) • Icons: enable users

    to express themselves better • Stamps: more fun & live communication
  2. Let’s resize animated GIFs only with Pure Go! Ideally, only

    with standard libraries & sub repositories
  3. • Image (not video or multimedia) • Able to contain

    animation GIF ≓ Animated image https://illust55.com/1155/
  4. What’s contained in the GIF image? • In image ◦

    Image size (width, height) ◦ Global color palette • In frame ◦ Image data ◦ Delay ◦ Color palette ◦ Disposal
  5. image/gif • Standard library ◦ https://pkg.go.dev/image/gif • gif.DecodeAll() -> *gif.GIF

    struct type GIF struct { Image []*image.Paletted ← Each frame Delay []int ← For each frame LoopCount int Disposal []byte ← Painpoint Config image.Config ← Image size & color palette BackgroundIndex byte }
  6. golang.org/x/image/draw • One of sub repositories • draw.Draw() function ◦

    Draw src image on dst image ◦ op - draw.Src / draw.Over func Draw(dst Image, r image.Rectangle, src image.Image, sp image.Point, op Op)
  7. golang.org/x/image/draw • draw.Scaler interface ◦ Able to resize a single

    image type Scaler interface { Scale(dst Image, dr image.Rectangle, src image.Image, sr image.Rectangle, op Op, opts *Options) }
  8. Prepare the destination GIF image dst := &gif.GIF{ Image: make([]*image.Paletted,

    0, len(src.Image)), Delay: src.Delay, LoopCount: src.LoopCount, Disposal: src.Disposal, Config: image.Config{ ColorModel: src.Config.ColorModel, Width: width, Height: height, }, BackgroundIndex: src.BackgroundIndex, }
  9. Resize each frames for _, srcFrame := range src.Image {

    srcFrameBounds := srcFrame.Bounds() dstFrameBounds := image.Rect(0, 0, width, height) dstFrame := image.NewPaletted(dstFrameBounds, srcFrame.Palette) draw.CatmullRom.Scale(dstFrame, dstFrameBounds, srcFrame, srcFrameBounds, draw.Src, nil) dst.Image = append(dst.Image, dstFrame) }
  10. Delta frame optimization • If all frames are resized to

    dist size ◦ frame 1: ◦ frame 2~:  
  11. Again: resize each frames for _, srcFrame := range src.Image

    { srcFrameBounds := srcFrame.Bounds() dstFrameBounds := image.Rect(0, 0, width, height) dstFrame := image.NewPaletted(dstFrameBounds, srcFrame.Palette) draw.CatmullRom.Scale(dstFrame, dstFrameBounds, srcFrame, srcFrameBounds, draw.Src, nil) dst.Image = append(dst.Image, dstFrame) }
  12. Calculating each dst frame size widthRatio := float64(width) / float64(srcWidth)

    heightRatio := float64(height) / float64(srcHeight) for _, srcFrame := range src.Image { dstFrameBounds := image.Rect( int(float64(srcFrameBounds.Min.X)*widthRatio), int(float64(srcFrameBounds.Min.Y)*heightRatio), int(float64(srcFrameBounds.Max.X)*widthRatio), int(float64(srcFrameBounds.Max.Y)*heightRatio), ) dstFrame := image.NewPaletted(dstFrameBounds, srcFrame.Palette) (Resize Image & append to dst) }
  13. Abstract concept of interpolation • Interpolation = mixing pixel colors

    • Kernel = func to define how pixels mixed ◦ Bicubic ◦ lanczos2 / lanczos3 • Mixing between transparent & non-transparent pixels seems to create noize
  14. srcBounds := image.Rect(0, 0, srcWidth, srcHeight) dstBounds := image.Rect(0, 0,

    width, height) tempCanvas := image.NewNRGBA(srcBounds) Prepare canvas for rendering
  15. Pile up current frame on canvas for _, srcFrame :=

    range src.Image { (Calculate the dst frame size) draw.Draw(tempCanvas, srcFrameBounds, srcFrame, srcFrameBounds.Min, draw.Over)
  16. Trim (for delta frame optimization) dstFrame := image.NewPaletted(dstFrameBounds, srcFrame.Palette) draw.Draw(dstFrame,

    dstFrameBounds, fittedFrame, dstFrameBounds.Min, draw.Src) (Append resized frame to dst) }
  17. Disposal • Also called Frame Disposal Methods ◦ How frames

    should be treated after rendering • 3 types ◦ 0x01: None ◦ 0x02: Background ◦ 0x03: Previous
  18. 0x01: Disposal None • Do nothing, just go to next

    frame rendering https://legacy.imagemagick.org/Usage/anim_basics/
  19. 0x02: Disposal Background • Overwrite area of frame with background

    color https://legacy.imagemagick.org/Usage/anim_basics/
  20. • Delete current frame before next frame rendering ◦ Rolling

    back to “previous” 0x03: Disposal Previous https://legacy.imagemagick.org/Usage/anim_basics/
  21. bgColor := image.NewUniform( src.Config.ColorModel.(color.Palette)[src.BackgroundIndex]) for i, srcFrame := range src.Image

    { previous = deepCopyImage(tempCanvas) (Resizing process) switch src.Disposal[i] { case gif.DisposalNone: case gif.DisposalBackground: draw.Draw(tempCanvas, srcFrameBounds, bgColor, srcFrameBounds.Min, draw.Src) case gif.DisposalPrevious: draw.Draw(tempCanvas, srcBounds, previous, srcBounds.Min, draw.Src) } }
  22. A few more things to work… • Retaining the aspect

    ratio • Making resizing kernel selectable • Concurrent processing
  23. Performance assessment • Experiment: 512px x 320px -> 256px x

    160px ◦ ImageMagick CLI ▪ magick -filter Lanczos -scale 50% ◦ CLI using resigif (single thread) ◦ CLI using resigif (concurrent) ▪ Using lanczos3 resizing kernel (same as ImageMagick)
  24. Performance assessment • Metrics ◦ CPU: max utilization (top) ◦

    Memory: max utilization (top) ◦ Time: using time command ▪ Average of 5 trials
  25. Performance assessment Max CPU Max Mem Time ImageMagick 100% 1.9%

    4.64s resigif Single 120% 1.9% 4.79s resigif Concurrent 800% 1.9% 1.09s
  26. • Just a reference: Two measures conbined ◦ Deletion of

    ImageMagick ◦ Migration to distroless base image • 1/3 of the original Docker image size