major refactor - separate Direct/Caching thumbnailers
This commit is contained in:
parent
ad30f36365
commit
ebb5f3cc7a
47
CachingThumbnailer.go
Normal file
47
CachingThumbnailer.go
Normal 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
42
DirectThumbnailer.go
Normal 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
|
||||
|
||||
}
|
||||
}
|
136
Thumbnailer.go
136
Thumbnailer.go
@ -2,38 +2,7 @@ package thumbnail
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/gif"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"io"
|
||||
"mime"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"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 (
|
||||
@ -41,70 +10,8 @@ var (
|
||||
ErrUnsupportedFiletype error = errors.New("Unsupported filetype")
|
||||
)
|
||||
|
||||
type Thumbnailer struct {
|
||||
width int
|
||||
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
|
||||
type Thumbnailer interface {
|
||||
RenderFile(absPath string) ([]byte, error)
|
||||
}
|
||||
|
||||
func FiletypeSupported(ext string) bool {
|
||||
@ -118,42 +25,3 @@ func FiletypeSupported(ext string) bool {
|
||||
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
37
config.go
Normal 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
3
doc.go
@ -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
|
58
image.go
58
image.go
@ -3,32 +3,70 @@ package thumbnail
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/gif"
|
||||
"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
|
||||
srcH := src.Bounds().Max.Y
|
||||
destW := 0
|
||||
destH := 0
|
||||
|
||||
if srcW > srcH {
|
||||
destW = this.width
|
||||
destH = this.height * srcH / srcW
|
||||
destW = this.cfg.Width
|
||||
destH = this.cfg.Height * srcH / srcW
|
||||
} else {
|
||||
destW = this.width * srcW / srcH
|
||||
destH = this.height
|
||||
destW = this.cfg.Width * srcW / srcH
|
||||
destH = this.cfg.Height
|
||||
}
|
||||
|
||||
offsetX := (this.width - destW) / 2
|
||||
offsetY := (this.height - destH) / 2
|
||||
offsetX := (this.cfg.Width - destW) / 2
|
||||
offsetY := (this.cfg.Height - destH) / 2
|
||||
|
||||
scaleW := float64(srcW) / float64(destW)
|
||||
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:
|
||||
|
||||
@ -56,7 +94,7 @@ func (this *Thumbnailer) RenderScaledImage(src image.Image) ([]byte, error) {
|
||||
|
||||
}
|
||||
|
||||
switch this.ofmt {
|
||||
switch this.cfg.Output {
|
||||
case OUTPUT_PNG_CRUSH:
|
||||
return crushFast(dest)
|
||||
|
||||
|
8
video.go
8
video.go
@ -7,18 +7,18 @@ import (
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func (this *Thumbnailer) RenderScaledFfmpeg(absPath string) ([]byte, error) {
|
||||
func (this *DirectThumbnailer) renderScaledFfmpeg(absPath string) ([]byte, error) {
|
||||
|
||||
scaleCmd := fmt.Sprintf(
|
||||
`thumbnail,scale='if(gt(a,%d/%d),%d,-1)':'if(gt(a,%d/%d),-1,%d)'`,
|
||||
this.height, this.width, this.height,
|
||||
this.height, this.width, this.width,
|
||||
this.cfg.Height, this.cfg.Width, this.cfg.Height,
|
||||
this.cfg.Height, this.cfg.Width, this.cfg.Width,
|
||||
)
|
||||
|
||||
// BUG(): always produces ASPECT_SVELTE shape - need to re-pad for ASPECT_PADDED
|
||||
|
||||
var vcodec string
|
||||
switch this.ofmt {
|
||||
switch this.cfg.Output {
|
||||
case OUTPUT_JPG:
|
||||
vcodec = "mjpeg" // yes really
|
||||
case OUTPUT_PNG_CRUSH:
|
||||
|
Loading…
Reference in New Issue
Block a user