Skip to content

Commit

Permalink
First attempt at generating animated thumbnails
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
turt2live committed Jan 12, 2018
1 parent 9bb3aa3 commit c0cdb77
Show file tree
Hide file tree
Showing 4 changed files with 95 additions and 31 deletions.
32 changes: 18 additions & 14 deletions config.sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import (
"bytes"
"context"
"errors"
"fmt"
"image"
"image/draw"
"image/gif"
"os"

"github.com/disintegration/imaging"
"github.com/sirupsen/logrus"
Expand Down Expand Up @@ -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
Expand All @@ -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 comments on commit c0cdb77

Please sign in to comment.