diff --git a/controllers/thumbnail_controller/thumbnail_controller.go b/controllers/thumbnail_controller/thumbnail_controller.go index 0858ad44bf9901e9d05b4ca668a2a2a51879d306..dea0ab35eee146d947e0b430da4af578972913a5 100644 --- a/controllers/thumbnail_controller/thumbnail_controller.go +++ b/controllers/thumbnail_controller/thumbnail_controller.go @@ -33,7 +33,10 @@ var supportedThumbnailTypes = []string{ } // Of the SupportedThumbnailTypes, these are the 'animated' types -var animatedTypes = []string{"image/gif"} +var animatedTypes = []string{ + "image/gif", + "image/png", +} var localCache = cache.New(30*time.Second, 60*time.Second) diff --git a/controllers/thumbnail_controller/thumbnail_resource_handler.go b/controllers/thumbnail_controller/thumbnail_resource_handler.go index 883f67251e4d2c268a5d082fd871626e370c668a..f781912390f4a10e8ef723fbf86d4d5e2bf62b2a 100644 --- a/controllers/thumbnail_controller/thumbnail_resource_handler.go +++ b/controllers/thumbnail_controller/thumbnail_resource_handler.go @@ -18,6 +18,7 @@ import ( "github.com/chai2010/webp" "github.com/disintegration/imaging" + "github.com/kettek/apng" "github.com/prometheus/client_golang/prometheus" "github.com/sirupsen/logrus" "github.com/turt2live/matrix-media-repo/common" @@ -222,8 +223,8 @@ func GenerateThumbnail(media *types.Media, width int, height int, method string, contentType := "image/png" imgData := &bytes.Buffer{} - if allowAnimated && animated { - ctx.Log.Info("Generating animated thumbnail") + if allowAnimated && animated && media.ContentType == "image/gif" { + ctx.Log.Info("Generating animated gif thumbnail") contentType = "image/gif" // Animated GIFs are a bit more special because we need to do it frame by frame. @@ -290,6 +291,69 @@ func GenerateThumbnail(media *types.Media, width int, height int, method string, ctx.Log.Error("Error generating animated thumbnail: " + err.Error()) return nil, err } + } else if allowAnimated && animated && media.ContentType == "image/png" { + ctx.Log.Info("Generating animated png thumbnail") + contentType = "image/png" + + // scale animated pngs frame by frame + + mediaStream, err := datastore.DownloadStream(ctx, media.DatastoreId, media.Location) + if err != nil { + ctx.Log.Error("Error resolving datastore path: ", err) + return nil, err + } + defer cleanup.DumpAndCloseStream(mediaStream) + + p, err := apng.DecodeAll(mediaStream) + if err != nil { + ctx.Log.Error("Error generating animated thumbnail: " + err.Error()) + return nil, err + } + + // prepare a blank frame to use as swap space + frameImg := image.NewRGBA(p.Frames[0].Image.Bounds()) + + widthRatio := float64(width) / float64(p.Frames[0].Image.Bounds().Dx()) + heightRatio := float64(width) / float64(p.Frames[0].Image.Bounds().Dy()) + + for i := range p.Frames { + frame := p.Frames[i] + img := frame.Image + + // Clear the transparency of the previous frame + if frame.DisposeOp == apng.DISPOSE_OP_NONE { + frame.DisposeOp = apng.DISPOSE_OP_BACKGROUND + } else { + draw.Draw(frameImg, frameImg.Bounds(), image.Transparent, image.ZP, draw.Src) + } + + // Copy the frame to a new image and use that + draw.Draw(frameImg, image.Rectangle{image.Point{frame.XOffset, frame.YOffset}, image.Point{img.Bounds().Dx(), img.Bounds().Dy()}}, img, image.ZP, draw.Over) + + // Do the thumbnailing on the copied frame + frameThumb, err := thumbnailFrame(frameImg, method, width, height, imaging.Linear, nil) + if err != nil { + ctx.Log.Error("Error generating animated thumbnail frame: " + err.Error()) + return nil, err + } + p.Frames[i].Image = frameThumb + newXOffset := int(math.Floor(float64(frame.XOffset) * widthRatio)) + newYOffset := int(math.Floor(float64(frame.YOffset) * heightRatio)) + // we need to make sure that these are still in the image bounds + if p.Frames[0].Image.Bounds().Dx() <= newXOffset + frameThumb.Bounds().Dx() { + newXOffset = p.Frames[0].Image.Bounds().Dx() - frameThumb.Bounds().Dx() + } + if p.Frames[0].Image.Bounds().Dy() <= newYOffset + frameThumb.Bounds().Dy() { + newYOffset = p.Frames[0].Image.Bounds().Dy() - frameThumb.Bounds().Dy() + } + p.Frames[i].XOffset = newXOffset + p.Frames[i].YOffset = newYOffset + } + err = apng.Encode(imgData, p) + if err != nil { + ctx.Log.Error("Error generating animated thumbnail: " + err.Error()) + return nil, err + } } else { src, err = thumbnailFrame(src, method, width, height, imaging.Linear, orientation) if err != nil { @@ -402,15 +466,32 @@ func pickImageFrame(media *types.Media, ctx rcontext.RequestContext) (image.Imag } defer cleanup.DumpAndCloseStream(mediaStream) - g, err := gif.DecodeAll(mediaStream) - if err != nil { - ctx.Log.Error("Error picking frame: " + err.Error()) - return nil, err + stillFrameRatio := float64(ctx.Config.Thumbnails.StillFrame) + getFrameIndex := func (numFrames int) int { + frameIndex := int(math.Floor(math.Min(1, math.Max(0, stillFrameRatio)) * float64(numFrames))) + ctx.Log.Info("Picking frame ", frameIndex, " for animated file") + return frameIndex } - stillFrameRatio := float64(ctx.Config.Thumbnails.StillFrame) - frameIndex := int(math.Floor(math.Min(1, math.Max(0, stillFrameRatio)) * float64(len(g.Image)))) - ctx.Log.Info("Picking frame ", frameIndex, " for animated file") + if media.ContentType == "image/gif" { + g, err := gif.DecodeAll(mediaStream) + if err != nil { + ctx.Log.Error("Error picking frame: " + err.Error()) + return nil, err + } - return g.Image[frameIndex], nil + frameIndex := getFrameIndex(len(g.Image)) + return g.Image[frameIndex], nil + } + if media.ContentType == "image/png" { + p, err := apng.DecodeAll(mediaStream) + if err != nil { + ctx.Log.Error("Error picking frame: " + err.Error()) + return nil, err + } + + frameIndex := getFrameIndex(len(p.Frames)) + return p.Frames[frameIndex].Image, nil + } + return nil, errors.New("Unknown animation type: " + media.ContentType) } diff --git a/go.mod b/go.mod index ba50122e115e0fc89b5dffcbae21a64b08d1ff8a..7ba66ab05a129bb7543a1f8b09a82e92db15dab8 100644 --- a/go.mod +++ b/go.mod @@ -34,6 +34,7 @@ require ( github.com/ipfs/interface-go-ipfs-core v0.2.6 github.com/jehiah/go-strftime v0.0.0-20171201141054-1d33003b3869 // indirect github.com/jonboulle/clockwork v0.1.0 // indirect + github.com/kettek/apng v0.0.0-20191108220231-414630eed80f github.com/lestrrat/go-envload v0.0.0-20180220120943-6ed08b54a570 // indirect github.com/lestrrat/go-file-rotatelogs v0.0.0-20180223000712-d3151e2a480f github.com/lestrrat/go-strftime v0.0.0-20180220042222-ba3bf9c1d042 // indirect diff --git a/go.sum b/go.sum index cfa9fededeee46e85f0584d833423457dcfbdfb1..ce5d5e0deba2c81041784c60672fc962f1dad98a 100644 --- a/go.sum +++ b/go.sum @@ -358,6 +358,8 @@ github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7 github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/kami-zh/go-capturer v0.0.0-20171211120116-e492ea43421d/go.mod h1:P2viExyCEfeWGU259JnaQ34Inuec4R38JCyBx2edgD0= +github.com/kettek/apng v0.0.0-20191108220231-414630eed80f h1:dnCYnTSltLuPMfc7dMrkz2uBUcEf/OFBR8yRh3oRT98= +github.com/kettek/apng v0.0.0-20191108220231-414630eed80f/go.mod h1:x78/VRQYKuCftMWS0uK5e+F5RJ7S4gSlESRWI0Prl6Q= github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=