From c0cdb77dd644df95910cc6ee3342079580ede1cc Mon Sep 17 00:00:00 2001 From: Travis Ralston Date: Thu, 11 Jan 2018 23:23:36 -0700 Subject: [PATCH] First attempt at generating animated thumbnails This doesn't work on two grounds: * There is significant pixelation * The resulting image size is wrong (but the frames are correct) This is part of #28 and needs some cleaning up before it's an active code path. Pushing now because this code path won't be activated under normal circumstances. --- config.sample.yaml | 32 ++++---- .../matrix-media-repo/config/config.go | 7 +- .../thumbnail_service/thumbnail_service.go | 5 ++ .../services/thumbnail_service/thumbnailer.go | 82 +++++++++++++++---- 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/config.sample.yaml b/config.sample.yaml index de30b473..d51132e0 100644 --- a/config.sample.yaml +++ b/config.sample.yaml @@ -82,25 +82,29 @@ thumbnails: # the default for when no width or height is requested. The media repository will return # either an exact match or the next largest size of thumbnail. sizes: - - width: 32 - height: 32 - - width: 96 - height: 96 - - width: 320 - height: 240 - - width: 640 - height: 480 - - width: 800 - height: 600 + - width: 32 + height: 32 + - width: 96 + height: 96 + - width: 320 + height: 240 + - width: 640 + height: 480 + - width: 800 + height: 600 # The content types to thumbnail when requested. Types that are not supported by the media repo # will not be thumbnailed (adding application/json here won't work). Clients may still not request # thumbnails for these types - this won't make clients automatically thumbnail these file types. types: - - "image/jpeg" - - "image/jpg" - - "image/png" - - "image/gif" + - "image/jpeg" + - "image/jpg" + - "image/png" + - "image/gif" + + # The maximum file size to thumbnail when a capable animated thumbnail is requested. If the image + # is larger than this, the thumbnail will be generated as a static image. + maxAnimateSizeBytes: 10485760 # 10MB default, 0 to disable # Controls for the rate limit functionality rateLimit: diff --git a/src/github.com/turt2live/matrix-media-repo/config/config.go b/src/github.com/turt2live/matrix-media-repo/config/config.go index e9a4aabf..d39a0aec 100644 --- a/src/github.com/turt2live/matrix-media-repo/config/config.go +++ b/src/github.com/turt2live/matrix-media-repo/config/config.go @@ -38,9 +38,10 @@ type MediaRepoConfig struct { } `yaml:"downloads"` Thumbnails struct { - MaxSourceBytes int64 `yaml:"maxSourceBytes"` - NumWorkers int `yaml:"numWorkers"` - Types []string `yaml:"types,flow"` + MaxSourceBytes int64 `yaml:"maxSourceBytes"` + NumWorkers int `yaml:"numWorkers"` + Types []string `yaml:"types,flow"` + MaxAnimateSizeBytes int64 `yaml:"maxAnimateSizeBytes"` Sizes []struct { Width int `yaml:"width"` Height int `yaml:"height"` diff --git a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go index 7e6ce84b..3ce7e840 100644 --- a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go +++ b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnail_service.go @@ -101,6 +101,11 @@ func (s *thumbnailService) GetThumbnail(media *types.Media, width int, height in return nil, errors.New("cannot generate thumbnail for this media's content type") } + if animated && config.Get().Thumbnails.MaxAnimateSizeBytes > 0 && config.Get().Thumbnails.MaxAnimateSizeBytes < media.SizeBytes { + s.log.Warn("Attempted to animate a media record that is too large. Assuming animated=false") + animated = false + } + forceThumbnail := false if animated && !util.ArrayContains(animatedTypes, media.ContentType) { s.log.Warn("Cannot animate a non-animated file. Assuming animated=false") diff --git a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go index 94c008b0..2f149c01 100644 --- a/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go +++ b/src/github.com/turt2live/matrix-media-repo/services/thumbnail_service/thumbnailer.go @@ -4,6 +4,11 @@ import ( "bytes" "context" "errors" + "fmt" + "image" + "image/draw" + "image/gif" + "os" "github.com/disintegration/imaging" "github.com/sirupsen/logrus" @@ -69,21 +74,58 @@ func (t *thumbnailer) GenerateThumbnail(media *types.Media, width int, height in } } - if method == "scale" { - src = imaging.Fit(src, width, height, imaging.Lanczos) - } else if method == "crop" { - src = imaging.Fill(src, width, height, imaging.Center, imaging.Lanczos) + contentType := "image/png" + imgData := &bytes.Buffer{} + if animated && util.ArrayContains(animatedTypes, media.ContentType) { + t.log.Info("Generating animated thumbnail") + contentType = "image/gif" + + // Animated GIFs are a bit more special because we need to do it frame by frame. + // This is fairly resource intensive. The calling code is responsible for limiting this case. + + inputFile, err := os.Open(media.Location) + if err != nil { + t.log.Error("Error generating animated thumbnail: " + err.Error()) + return nil, err + } + defer inputFile.Close() + + g, err := gif.DecodeAll(inputFile) + if err != nil { + t.log.Error("Error generating animated thumbnail: " + err.Error()) + return nil, err + } + + for i := range g.Image { + frameThumb, err := thumbnailFrame(g.Image[i], method, width, height, imaging.Lanczos) + if err != nil { + t.log.Error("Error generating animated thumbnail frame: " + err.Error()) + return nil, err + } + + t.log.Info(fmt.Sprintf("Width = %d Height = %d FW=%d FH=%d", width, height, frameThumb.Bounds().Max.X, frameThumb.Bounds().Max.Y)) + g.Image[i] = image.NewPaletted(frameThumb.Bounds(), g.Image[i].Palette) + draw.Draw(g.Image[i], frameThumb.Bounds(), frameThumb, image.Pt(0, 0), draw.Over) + } + + err = gif.EncodeAll(imgData, g) + if err != nil { + t.log.Error("Error generating animated thumbnail: " + err.Error()) + return nil, err + } } else { - t.log.Error("Unrecognized thumbnail method: " + method) - return nil, errors.New("unrecognized method: " + method) - } + src, err = thumbnailFrame(src, method, width, height, imaging.Lanczos) + if err != nil { + t.log.Error("Error generating thumbnail: " + err.Error()) + return nil, err + } - // Put the image bytes into a memory buffer - imgData := &bytes.Buffer{} - err = imaging.Encode(imgData, src, imaging.PNG) - if err != nil { - t.log.Error("Unexpected error encoding thumbnail: " + err.Error()) - return nil, err + // Put the image bytes into a memory buffer + err = imaging.Encode(imgData, src, imaging.PNG) + if err != nil { + t.log.Error("Unexpected error encoding thumbnail: " + err.Error()) + return nil, err + } } // Reset the buffer pointer and store the file @@ -100,8 +142,20 @@ func (t *thumbnailer) GenerateThumbnail(media *types.Media, width int, height in } thumb.DiskLocation = location - thumb.ContentType = "image/png" + thumb.ContentType = contentType thumb.SizeBytes = fileSize return thumb, nil } + +func thumbnailFrame(src image.Image, method string, width int, height int, filter imaging.ResampleFilter) (image.Image, error) { + if method == "scale" { + src = imaging.Fit(src, width, height, filter) + } else if method == "crop" { + src = imaging.Fill(src, width, height, imaging.Center, filter) + } else { + return nil, errors.New("unrecognized method: " + method) + } + + return src, nil +}