Skip to content
Snippets Groups Projects
Commit c0cdb77d authored by Travis Ralston's avatar Travis Ralston
Browse files

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.
parent 9bb3aa3f
No related branches found
No related tags found
No related merge requests found
......@@ -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:
......
......@@ -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"`
......
......@@ -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")
......
......@@ -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
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment