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 (
|
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
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 (
|
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)
|
||||||
|
|
||||||
|
8
video.go
8
video.go
@ -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:
|
||||||
|
Loading…
Reference in New Issue
Block a user