major refactor - separate Direct/Caching thumbnailers

This commit is contained in:
mappu 2018-06-09 16:25:16 +12:00
parent ad30f36365
commit ebb5f3cc7a
7 changed files with 180 additions and 151 deletions

47
CachingThumbnailer.go Normal file
View File

@ -0,0 +1,47 @@
package thumbnail
import (
lru "github.com/hashicorp/golang-lru"
)
type CachingThumbnailer struct {
t Thumbnailer
cache *lru.Cache // threadsafe, might be nil
}
func NewCachingThumbnailer(cacheSize int, opts *Config) (Thumbnailer, error) {
upstream := NewThumbnailerEx(opts)
if cacheSize == 0 {
return upstream, nil
}
thumbCache, err := lru.New(cacheSize)
if err != nil {
return nil, err
}
return &CachingThumbnailer{
t: upstream,
cache: thumbCache,
}, nil
}
func (this *CachingThumbnailer) RenderFile(absPath string) ([]byte, error) {
thumb, ok := this.cache.Get(absPath)
if ok {
return thumb.([]byte), nil
}
// Add to cache
rendered, err := this.t.RenderFile(absPath)
if err != nil {
return nil, err
}
this.cache.Add(absPath, rendered)
return rendered, nil
}

42
DirectThumbnailer.go Normal file
View File

@ -0,0 +1,42 @@
package thumbnail
import (
"mime"
"path/filepath"
"strings"
)
type DirectThumbnailer struct {
cfg Config
}
func NewThumbnailer(w, h int) Thumbnailer {
opts := DefaultConfig
opts.Width = w
opts.Height = h
return NewThumbnailerEx(&opts)
}
func NewThumbnailerEx(opts *Config) Thumbnailer {
if opts == nil {
opts = &DefaultConfig
}
return &DirectThumbnailer{cfg: *opts}
}
func (this *DirectThumbnailer) RenderFile(absPath string) ([]byte, error) {
mimeType := mime.TypeByExtension(filepath.Ext(absPath))
if strings.HasPrefix(mimeType, `image/`) {
return this.renderImageFile(absPath, mimeType)
} else if strings.HasPrefix(mimeType, `video/`) {
return this.renderScaledFfmpeg(absPath)
} else {
return nil, ErrUnsupportedFiletype
}
}

View File

@ -2,38 +2,7 @@ package thumbnail
import ( import (
"errors" "errors"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"mime"
"os"
"path/filepath"
"strings" "strings"
lru "github.com/hashicorp/golang-lru"
"golang.org/x/image/bmp"
"golang.org/x/image/webp"
)
type OutputFormat uint8
type AspectFormat uint8
type ScaleFormat uint8
const (
OUTPUT_PNG_CRUSH OutputFormat = 2
OUTPUT_JPG OutputFormat = 3
OUTPUT__DEFAULT = OUTPUT_PNG_CRUSH
ASPECT_PAD_TO_DIMENSIONS AspectFormat = 80
ASPECT_RESPECT_MAX_DIMENSION_ONLY AspectFormat = 81
ASPECT_CROP_TO_DIMENSIONS AspectFormat = 82
ASPECT__DEFAULT = ASPECT_PAD_TO_DIMENSIONS
SCALEFMT_NN ScaleFormat = 120
SCALEFMT_BILINEAR ScaleFormat = 121
SCALEFMT__DEFAULT = SCALEFMT_BILINEAR
) )
var ( var (
@ -41,70 +10,8 @@ var (
ErrUnsupportedFiletype error = errors.New("Unsupported filetype") ErrUnsupportedFiletype error = errors.New("Unsupported filetype")
) )
type Thumbnailer struct { type Thumbnailer interface {
width int RenderFile(absPath string) ([]byte, error)
height int
ofmt OutputFormat
afmt AspectFormat
sfmt ScaleFormat
thumbCache *lru.Cache // threadsafe, might be nil
}
func NewThumbnailer(Width, Height int, MaxCacheSize uint) *Thumbnailer {
return NewThumbnailerEx(Width, Height, MaxCacheSize, OUTPUT__DEFAULT, ASPECT__DEFAULT, SCALEFMT__DEFAULT)
}
func NewThumbnailerEx(Width, Height int, MaxCacheSize uint, of OutputFormat, af AspectFormat, sf ScaleFormat) *Thumbnailer {
ret := &Thumbnailer{
width: Width,
height: Height,
ofmt: of,
afmt: af,
sfmt: sf,
thumbCache: nil,
}
if MaxCacheSize > 0 {
thumbCache, err := lru.New(int(MaxCacheSize))
if err != nil {
panic(err)
}
ret.thumbCache = thumbCache
}
return ret
}
func (this *Thumbnailer) Width() int {
return this.width
}
func (this *Thumbnailer) Height() int {
return this.height
}
func (this *Thumbnailer) RenderFile(absPath string) ([]byte, error) {
if this.thumbCache != nil {
thumb, ok := this.thumbCache.Get(absPath)
if ok {
return thumb.([]byte), nil
}
}
// Add to cache
thumb, err := this.RenderFile_NoCache(absPath)
if err != nil {
return nil, err
}
if this.thumbCache != nil {
this.thumbCache.Add(absPath, thumb)
}
return thumb, nil
} }
func FiletypeSupported(ext string) bool { func FiletypeSupported(ext string) bool {
@ -118,42 +25,3 @@ func FiletypeSupported(ext string) bool {
return false return false
} }
} }
func (this *Thumbnailer) RenderFile_NoCache(absPath string) ([]byte, error) {
return this.RenderFile_NoCache_MimeType(absPath, mime.TypeByExtension(filepath.Ext(absPath)))
}
func (this *Thumbnailer) RenderFile_NoCache_MimeType(absPath, mimeType string) ([]byte, error) {
fh, err := os.OpenFile(absPath, os.O_RDONLY, 0400)
if err != nil {
return nil, err
}
defer fh.Close()
type imageDecoder func(io.Reader) (image.Image, error)
imageDecoders := map[string]imageDecoder{
`image/jpeg`: jpeg.Decode,
`image/png`: png.Decode,
`image/gif`: gif.Decode,
`image/webp`: webp.Decode,
`image/bmp`: bmp.Decode,
}
if fn, ok := imageDecoders[mimeType]; ok {
src, err := fn(fh)
if err != nil {
return nil, err
}
return this.RenderScaledImage(src)
} else if strings.HasPrefix(mimeType, `video/`) {
return this.RenderScaledFfmpeg(absPath)
} else {
return nil, ErrUnsupportedFiletype
}
}

37
config.go Normal file
View File

@ -0,0 +1,37 @@
package thumbnail
type OutputFormat uint8
type AspectFormat uint8
type ScaleFormat uint8
const (
OUTPUT_PNG_CRUSH OutputFormat = 2
OUTPUT_JPG OutputFormat = 3
OUTPUT_WEBP OutputFormat = 4
OUTPUT__DEFAULT = OUTPUT_PNG_CRUSH
ASPECT_PAD_TO_DIMENSIONS AspectFormat = 80
ASPECT_RESPECT_MAX_DIMENSION_ONLY AspectFormat = 81
ASPECT_CROP_TO_DIMENSIONS AspectFormat = 82
ASPECT__DEFAULT = ASPECT_PAD_TO_DIMENSIONS
SCALEFMT_NN ScaleFormat = 120
SCALEFMT_BILINEAR ScaleFormat = 121
SCALEFMT__DEFAULT = SCALEFMT_BILINEAR
)
type Config struct {
Width int
Height int
Output OutputFormat
Aspect AspectFormat
Scale ScaleFormat
}
var DefaultConfig = Config{
Width: 128,
Height: 128,
Output: OUTPUT__DEFAULT,
Aspect: ASPECT__DEFAULT,
Scale: SCALEFMT__DEFAULT,
}

3
doc.go
View File

@ -1,3 +0,0 @@
// Package thumbnail contains functions for taking the thumbnail of image and
// video files, and caching the result for performance.
package thumbnail

View File

@ -3,32 +3,70 @@ package thumbnail
import ( import (
"bytes" "bytes"
"image" "image"
"image/gif"
"image/jpeg" "image/jpeg"
"image/png"
"io"
"os"
"golang.org/x/image/bmp"
"golang.org/x/image/webp"
) )
func (this *Thumbnailer) RenderScaledImage(src image.Image) ([]byte, error) { type imageDecoder func(io.Reader) (image.Image, error)
var imageDecoders map[string]imageDecoder = map[string]imageDecoder{
`image/jpeg`: jpeg.Decode,
`image/png`: png.Decode,
`image/gif`: gif.Decode,
`image/webp`: webp.Decode,
`image/bmp`: bmp.Decode,
}
func (this *DirectThumbnailer) renderImageFile(absPath, mimeType string) ([]byte, error) {
fh, err := os.OpenFile(absPath, os.O_RDONLY, 0400)
if err != nil {
return nil, err
}
defer fh.Close()
fn, ok := imageDecoders[mimeType]
if !ok {
return nil, ErrUnsupportedFiletype
}
src, err := fn(fh)
if err != nil {
return nil, err
}
return this.RenderScaledImage(src)
}
func (this *DirectThumbnailer) RenderScaledImage(src image.Image) ([]byte, error) {
srcW := src.Bounds().Max.X srcW := src.Bounds().Max.X
srcH := src.Bounds().Max.Y srcH := src.Bounds().Max.Y
destW := 0 destW := 0
destH := 0 destH := 0
if srcW > srcH { if srcW > srcH {
destW = this.width destW = this.cfg.Width
destH = this.height * srcH / srcW destH = this.cfg.Height * srcH / srcW
} else { } else {
destW = this.width * srcW / srcH destW = this.cfg.Width * srcW / srcH
destH = this.height destH = this.cfg.Height
} }
offsetX := (this.width - destW) / 2 offsetX := (this.cfg.Width - destW) / 2
offsetY := (this.height - destH) / 2 offsetY := (this.cfg.Height - destH) / 2
scaleW := float64(srcW) / float64(destW) scaleW := float64(srcW) / float64(destW)
scaleH := float64(srcH) / float64(destH) scaleH := float64(srcH) / float64(destH)
dest := image.NewRGBA(image.Rectangle{Max: image.Point{X: this.width, Y: this.height}}) dest := image.NewRGBA(image.Rectangle{Max: image.Point{X: this.cfg.Width, Y: this.cfg.Height}})
switch this.sfmt { switch this.cfg.Scale {
case SCALEFMT_BILINEAR: case SCALEFMT_BILINEAR:
@ -56,7 +94,7 @@ func (this *Thumbnailer) RenderScaledImage(src image.Image) ([]byte, error) {
} }
switch this.ofmt { switch this.cfg.Output {
case OUTPUT_PNG_CRUSH: case OUTPUT_PNG_CRUSH:
return crushFast(dest) return crushFast(dest)

View File

@ -7,18 +7,18 @@ import (
"os/exec" "os/exec"
) )
func (this *Thumbnailer) RenderScaledFfmpeg(absPath string) ([]byte, error) { func (this *DirectThumbnailer) renderScaledFfmpeg(absPath string) ([]byte, error) {
scaleCmd := fmt.Sprintf( scaleCmd := fmt.Sprintf(
`thumbnail,scale='if(gt(a,%d/%d),%d,-1)':'if(gt(a,%d/%d),-1,%d)'`, `thumbnail,scale='if(gt(a,%d/%d),%d,-1)':'if(gt(a,%d/%d),-1,%d)'`,
this.height, this.width, this.height, this.cfg.Height, this.cfg.Width, this.cfg.Height,
this.height, this.width, this.width, this.cfg.Height, this.cfg.Width, this.cfg.Width,
) )
// BUG(): always produces ASPECT_SVELTE shape - need to re-pad for ASPECT_PADDED // BUG(): always produces ASPECT_SVELTE shape - need to re-pad for ASPECT_PADDED
var vcodec string var vcodec string
switch this.ofmt { switch this.cfg.Output {
case OUTPUT_JPG: case OUTPUT_JPG:
vcodec = "mjpeg" // yes really vcodec = "mjpeg" // yes really
case OUTPUT_PNG_CRUSH: case OUTPUT_PNG_CRUSH: