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

Handle WEBP animations. #116

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ again when you get more games or want to update the category overlays.
* *(optional)* Append `-skip<preference>` to skip searching and downloading parts from certain artwork elements. Available choices : `-skipbanner`,`-skipcover`,`-skiphero`,`-skiplogo`. For example: Appending `-skiplogo -skipbanner` will prevent steamgrid to search and download logo and banners for any games.
* *(optional)* Append `-skipsteam` to not download the default artworks from Steam.
* *(optional)* Append `-skipgoogle` to skip search and downloads from Google.
* *(optional)* Append `--webpasapng` to convert all WEBP animations to APNG - they are displayed faster but take more time and memory to convert
* *(optional)* Append `--coverwebpasapng` to convert covers and banners' WEBP animations to APNG - skip hero and logo as they are larger
* *(optional)* Append `--convertmaxmem <GB>` to limit memory usage for conversion from WEBP to APNG, if it would go over the limit, the conversion will be skipped.
* *(tip)* Run with `--help` to see all available options again.
6. Read the report and open Steam in grid view to check the results.

Expand Down
24 changes: 22 additions & 2 deletions download.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"strconv"
"strings"

"github.com/kmicki/webpanimation"
"go.deanishe.net/fuzzy"
)

Expand Down Expand Up @@ -445,11 +446,30 @@ func DownloadImage(gridDir string, game *Game, artStyle string, artStyleExtensio
response.Body.Close()

// catch false aspect ratios
image, _, err := image.Decode(bytes.NewBuffer(imageBytes))
var webpImage *webpanimation.WebpAnimationDecoded
defer func() {
if webpImage != nil {
webpanimation.ReleaseDecoder(webpImage)
}
}()
var dlImage image.Image
if strings.Contains(contentType, "webp") {
webpImage, err = webpanimation.GetInfo(bytes.NewBuffer(imageBytes))
if err == nil {
webpFrame, ok := webpanimation.GetNextFrame(webpImage)
if ok {
dlImage = webpFrame.Image
} else {
err = errors.New("can't read the first frame of WEBP animation")
}
}
} else {
dlImage, _, err = image.Decode(bytes.NewBuffer(imageBytes))
}
if err != nil {
return "", err
}
imageSize := image.Bounds().Max
imageSize := dlImage.Bounds().Max
if artStyle == "Banner" && imageSize.X < imageSize.Y {
return "", nil
} else if artStyle == "Cover" && imageSize.X > imageSize.Y {
Expand Down
258 changes: 232 additions & 26 deletions overlays.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package main

import (
"bytes"
"errors"
"fmt"
"image"
"runtime/debug"

// "image/draw"
"image/jpeg"
Expand All @@ -12,7 +15,8 @@ import (
"path/filepath"
"strings"

"github.com/kettek/apng"
"github.com/kmicki/apng"
"github.com/kmicki/webpanimation"
"golang.org/x/image/draw"
)

Expand Down Expand Up @@ -69,28 +73,86 @@ func LoadOverlays(dir string, artStyles map[string][]string) (overlays map[strin

// ApplyOverlay to the game image, depending on the category. The
// resulting image is saved over the original.
func ApplyOverlay(game *Game, overlays map[string]image.Image, artStyleExtensions []string) error {
func ApplyOverlay(game *Game, overlays map[string]image.Image, artStyleExtensions []string, convertWebpToApng bool, convertWebpToApngCoversBanners bool, maxMem uint64) error {
if game.CleanImageBytes == nil || len(game.Tags) == 0 {
return nil
}

buf := new(bytes.Buffer)
bufReady := false
var errBuff error
errBuff = nil

convertWebpToApng = convertWebpToApng || (convertWebpToApngCoversBanners &&
(strings.Contains(artStyleExtensions[1], "cover")) || (strings.Contains(artStyleExtensions[1], "banner")))

isApng := false
isWebp := false
formatFound := false

var err error
var webpImage *webpanimation.WebpAnimationDecoded
defer func() {
if webpImage != nil {
webpanimation.ReleaseDecoder(webpImage)
}
}()

// Try WEBP
var gameImage image.Image
apngImage, err := apng.DecodeAll(bytes.NewBuffer(game.CleanImageBytes))
webpImage, err = webpanimation.GetInfo(bytes.NewBuffer(game.CleanImageBytes))
if err == nil {
if len(apngImage.Frames) > 1 {
isApng = true
formatFound = true
if err != nil {
formatFound = false
} else if webpImage.FrameCnt <= 1 {
webpFrame, ok := webpanimation.GetNextFrame(webpImage)
if ok {
gameImage = webpFrame.Image
} else {
err = errors.New("can't get the first frame of single-frame WEBP image")
}
} else {
gameImage = apngImage.Frames[0].Image
isWebp = true
memNeeded := uint64(webpImage.Width) * uint64(webpImage.Height) * 4 * uint64(webpImage.FrameCnt)
if convertWebpToApng && maxMem > 0 {
if memNeeded > maxMem {
fmt.Println("WEBP animation too big to convert to APNG. Leaving WEBP.")
convertWebpToApng = false
} else if memNeeded > maxMem/2 {
// free up memory for big conversion
debug.FreeOSMemory()
}
}
}
} else {
gameImage, _, err = image.Decode(bytes.NewBuffer(game.CleanImageBytes))
if err != nil {
return err
}

// Try APNG
var apngImage apng.APNG
if !formatFound {
apngImage, err = apng.DecodeAll(bytes.NewBuffer(game.CleanImageBytes))
if err == nil {
if len(apngImage.Frames) > 1 {
isApng = true
} else {
gameImage = apngImage.Frames[0].Image
}
} else {
gameImage, _, err = image.Decode(bytes.NewBuffer(game.CleanImageBytes))
if err != nil {
return err
}
}
}

applied := false
var webpanim *webpanimation.WebpAnimation
defer func() {
if webpanim != nil {
webpanim.ReleaseMemory()
//fmt.Println("WEBPAnim Memory Released")
}
}()
for _, tag := range game.Tags {
// Normalize tag name by lower-casing it and remove trailing "s" from
// plurals. Also, <, > and / are replaced with - because you can't have
Expand All @@ -108,28 +170,111 @@ func ApplyOverlay(game *Game, overlays map[string]image.Image, artStyleExtension
overlaySize := overlayImage.Bounds().Max

if isApng {
fmt.Printf("Apply Overlay to APNG.")
originalSize := apngImage.Frames[0].Image.Bounds().Max

// Scale overlay to imageSize so the images won't get that huge…
overlayScaled := image.NewRGBA(image.Rect(0, 0, originalSize.X, originalSize.Y))
if originalSize.X != overlaySize.X && originalSize.Y != overlaySize.Y {
// https://godoc.org/golang.org/x/image/draw#Kernel.Scale
draw.ApproxBiLinear.Scale(overlayScaled, overlayScaled.Bounds(), overlayImage, overlayImage.Bounds(), draw.Over, nil)
} else {
draw.Draw(overlayScaled, overlayScaled.Bounds(), overlayImage, image.Point{}, draw.Src)
}

for i, frame := range apngImage.Frames {
// Scale overlay to imageSize so the images won't get that huge…
overlayScaled := image.NewRGBA(image.Rect(0, 0, originalSize.X, originalSize.Y))
result := image.NewRGBA(image.Rect(0, 0, originalSize.X, originalSize.Y))
if originalSize.X != overlaySize.X && originalSize.Y != overlaySize.Y {
// https://godoc.org/golang.org/x/image/draw#Kernel.Scale
draw.ApproxBiLinear.Scale(overlayScaled, overlayScaled.Bounds(), overlayImage, overlayImage.Bounds(), draw.Over, nil)
} else {
draw.Draw(overlayScaled, overlayScaled.Bounds(), overlayImage, image.ZP, draw.Src)
}
// No idea why these offsets are negative:
draw.Draw(result, result.Bounds(), frame.Image, image.Point{0 - frame.XOffset, 0 - frame.YOffset}, draw.Over)
draw.Draw(result, result.Bounds(), overlayScaled, image.Point{0, 0}, draw.Over)
apngImage.Frames[i].Image = result
apngImage.Frames[i].XOffset = 0
apngImage.Frames[i].YOffset = 0
apngImage.Frames[i].BlendOp = apng.BLEND_OP_OVER
fmt.Printf("\rApply Overlay to APNG. Overlayed frame %8d/%d", i, len(apngImage.Frames))
}
applied = true
fmt.Printf("\rOverlay applied to %v frames of APNG \n", len(apngImage.Frames))
} else if isWebp {
fmt.Printf("Apply Overlay to WEBP.")
if webpImage == nil {
fmt.Printf("\rWebPImage not initialized.\n")
continue
}
originalSize := image.Point{webpImage.Width, webpImage.Height}
var webpConfig webpanimation.WebPConfig
var encoder *apng.FrameByFrameEncoder
if convertWebpToApng {
bufReady = true
encoder = apng.InitializeEncoding(buf, uint32(webpImage.FrameCnt), uint(webpImage.LoopCount))
} else {
webpanim = webpanimation.NewWebpAnimation(webpImage.Width, webpImage.Height, webpImage.LoopCount)
webpanim.WebPAnimEncoderOptions.SetKmin(9)
webpanim.WebPAnimEncoderOptions.SetKmax(17)
webpConfig = webpanimation.NewWebpConfig()
webpConfig.SetLossless(1)
}

// Scale overlay to imageSize so the images won't get that huge…
overlayScaled := image.NewRGBA(image.Rect(0, 0, originalSize.X, originalSize.Y))
var result *image.RGBA
if originalSize.X != overlaySize.X && originalSize.Y != overlaySize.Y {
// https://godoc.org/golang.org/x/image/draw#Kernel.Scale
draw.ApproxBiLinear.Scale(overlayScaled, overlayScaled.Bounds(), overlayImage, overlayImage.Bounds(), draw.Over, nil)
} else {
draw.Draw(overlayScaled, overlayScaled.Bounds(), overlayImage, image.Point{}, draw.Src)
}

i := 0
var lastTimestamp int
frame, ok := webpanimation.GetNextFrame(webpImage)
for ok {
if v, o := frame.Image.(*image.RGBA); o {
result = v
} else {
result = image.NewRGBA(image.Rect(0, 0, originalSize.X, originalSize.Y))
draw.Draw(result, result.Bounds(), frame.Image, image.Point{0, 0}, draw.Over)
}
draw.Draw(result, result.Bounds(), overlayScaled, image.Point{0, 0}, draw.Over)

var delay uint16
if i == 0 {
delay = 0
} else {
delay = uint16(frame.Timestamp - lastTimestamp)
}
lastTimestamp = frame.Timestamp

if convertWebpToApng {
apngFrame := apng.Frame{
Image: result,
IsDefault: false,
XOffset: 0,
YOffset: 0,
DisposeOp: apng.DISPOSE_OP_NONE,
BlendOp: apng.BLEND_OP_OVER,
DelayNumerator: delay,
DelayDenominator: 1000,
}
encoder.EncodeFrame(apngFrame)

fmt.Printf("\rApply Overlay to WEBP as APNG. Overlayed frame %8d/%d", i, webpImage.FrameCnt)
} else {
err = webpanim.AddFrame(result, frame.Timestamp, webpConfig)
fmt.Printf("\rApply Overlay to WEBP. Overlayed frame %8d/%d", i, webpImage.FrameCnt)
}
i++
frame, ok = webpanimation.GetNextFrame(webpImage)
}
applied = true
if convertWebpToApng {
errBuff = encoder.Finish()
fmt.Printf("\rOverlay applied to %v frames of WEBP as APNG \n", webpImage.FrameCnt)
} else {
fmt.Printf("\rOverlay applied to %v frames of WEBP \n", webpImage.FrameCnt)
}
} else {
fmt.Printf("Apply Overlay to Single Image.")
originalSize := gameImage.Bounds().Max

// We expect overlays in the correct format so we have to scale the image if it doesn't fit
Expand All @@ -144,21 +289,82 @@ func ApplyOverlay(game *Game, overlays map[string]image.Image, artStyleExtension
draw.Draw(result, result.Bounds(), overlayImage, image.Point{0, 0}, draw.Over)
gameImage = result
applied = true
fmt.Printf("\rApplied Overlay to Single Image.\n")
}
}

if !applied {
return nil
if isWebp && convertWebpToApng {
bufReady = true

// Convert to APNG without overlay
fmt.Printf("Convert WEBP to APNG.")
if webpImage == nil {
fmt.Printf("\rWebPImage not initialized.\n")
return nil
}
originalSize := image.Point{webpImage.Width, webpImage.Height}
encoder := apng.InitializeEncoding(buf, uint32(webpImage.FrameCnt), uint(webpImage.LoopCount))

i := 0
var lastTimestamp int
frame, ok := webpanimation.GetNextFrame(webpImage)
var result *image.RGBA
for ok {
if v, o := frame.Image.(*image.RGBA); o {
result = v
} else {
result = image.NewRGBA(image.Rect(0, 0, originalSize.X, originalSize.Y))
draw.Draw(result, result.Bounds(), frame.Image, image.Point{0, 0}, draw.Over)
}

var delay uint16
if i == 0 {
delay = 0
} else {
delay = uint16(frame.Timestamp - lastTimestamp)
}
lastTimestamp = frame.Timestamp

apngFrame := apng.Frame{
Image: result,
IsDefault: false,
XOffset: 0,
YOffset: 0,
DisposeOp: apng.DISPOSE_OP_NONE,
BlendOp: apng.BLEND_OP_OVER,
DelayNumerator: delay,
DelayDenominator: 1000,
}
encoder.EncodeFrame(apngFrame)

fmt.Printf("\rConvert to WEBP as APNG. Frame %8d/%d", i, webpImage.FrameCnt)
i++
frame, ok = webpanimation.GetNextFrame(webpImage)
}

errBuff = encoder.Finish()
applied = true
fmt.Printf("\rConverted %v frames from WEBP to APNG \n", webpImage.FrameCnt)
} else {
return nil
}
}

buf := new(bytes.Buffer)
if game.ImageExt == ".jpg" || game.ImageExt == ".jpeg" {
err = jpeg.Encode(buf, gameImage, &jpeg.Options{95})
} else if game.ImageExt == ".png" && isApng {
err = apng.Encode(buf, apngImage)
} else if game.ImageExt == ".png" {
err = png.Encode(buf, gameImage)
if bufReady {
err = errBuff
} else {
if game.ImageExt == ".jpg" || game.ImageExt == ".jpeg" {
err = jpeg.Encode(buf, gameImage, &jpeg.Options{Quality: 95})
} else if (game.ImageExt == ".png" && isApng) || (isWebp && convertWebpToApng) {
err = apng.Encode(buf, apngImage)
} else if (game.ImageExt == ".png" && !isWebp) || (formatFound && !isWebp) {
err = png.Encode(buf, gameImage)
} else if isWebp {
err = webpanim.Encode(buf)
}
}

if err != nil {
return err
}
Expand Down
Loading