davidbyttow / govips

A lightning fast image processing and resizing library for Go
MIT License
1.25k stars 196 forks source link

Resizing animated images leads to a toilet roll in some scale values #350

Open apiks opened 1 year ago

apiks commented 1 year ago

I'm trying to resize a 500x280 GIF via the Resize function. If the scale parameter is a multiple of 5 (e.g. 0.1, 0.15 or 0.2) it works fine in unrolling the GIF.

The problem is when the scale has isn't a multiple of 5 (e.g. 0.14, 0.22 or 0.66) it can't unroll. It will always return a roll.

This behavior is only for two decimals. With more decimals it becomes even more confusing as 0.265 doesn't work either. Maybe due to the 6 in it?

After fiddling with it for a bit, rounding the scale or forcing a multiple fixes the issue, but also does not allow for the intended scale on the final image.

Additionally I've noticed that in the ResizeWithVScale function it calculates the new page height with *int(float64(pageHeight) scale)** which forces it to drop the decimal if it exists. Perhaps this is causing the issue? LibVips takes an int so I understand why it's done.

This only happens with animated images (tested only on GIF). Non-animated images work fine, likely due to their height always being an integer.

To replicate this issue, take a GIF with a width of 500 and try resizing it to the above mentioned scales.

Here's the logic I'm using to resize the GIF:

func resizeImage(img *vips.ImageRef, width, height int, upscale bool) (*vips.ImageRef, error) {
    if width == 0 && height == 0 {
        return img, nil
    }

    if width == 0 {
        scale := float64(height) / float64(img.PageHeight())
        if upscale || scale <= 1 {
            err := img.Resize(scale, vips.KernelAuto)
            if err != nil {
                return nil, err
            }
        }
        return img, nil
    }

    if height == 0 {
        scale := float64(width) / float64(img.Width())
        if upscale || scale <= 1 {
            err := img.Resize(scale, vips.KernelAuto)
            if err != nil {
                return nil, err
            }
        }
        return img, nil
    }

    hScale := float64(width) / float64(img.Width())
    vScale := float64(height) / float64(img.PageHeight())
    if upscale || (hScale <= 1 && vScale <= 1) {
        err := img.ResizeWithVScale(hScale, vScale, vips.KernelAuto)
        if err != nil {
            return nil, err
        }
    }

    return img, nil
}
TheKigen commented 1 month ago

So for anyone wondering what this issue is when resizing animated images (GIF or WebP). The issue is the total unrolled image height must be divisible by the page count to a whole number. It has nothing to do with being divisible by 5.

So if float64(pageHeight)*scale results in a float number instead of a whole number then you will get an unrolled image. I do not know why the govips library includes the + 0.5 in the image Resize function when calculating a new page height.

https://github.com/davidbyttow/govips/blob/0c98a287803d8c24539b9bfdbdf361a337343ef9/vips/image.go#L1791-L1800

I made some quick, but dirty code to do resizing of a animated image. It does just width scaling since that is why I needed for my project. It goes and tries to find the nearest width that can properly scale the height to a whole number. Its ugly code that I'm sure someone can write much better but it works for now.

    pages := img.Pages()
    origWidth := img.Width()
    scale := float64(width) / float64(origWidth)
    if pages > 1 {
        pageHeight := img.PageHeight()
        testScale := float64(pageHeight) * scale
        for testScale != float64(int(testScale)) && width != origWidth {
            if width < origWidth {
                width += 1
            } else {
                width -= 1
            }

            scale = float64(width) / float64(origWidth)
            testScale = float64(pageHeight) * scale
        }
    }

    if err = img.Resize(scale, vips.KernelAuto); err != nil {
        return err
    }