set custom output/aspect/scale formats, add package-level error vars
This commit is contained in:
parent
95da07dbb9
commit
67c50a0539
@ -1,7 +1,7 @@
|
|||||||
package thumbnail
|
package thumbnail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"errors"
|
||||||
"image/gif"
|
"image/gif"
|
||||||
"image/jpeg"
|
"image/jpeg"
|
||||||
"image/png"
|
"image/png"
|
||||||
@ -12,22 +12,55 @@ import (
|
|||||||
lru "github.com/hashicorp/golang-lru"
|
lru "github.com/hashicorp/golang-lru"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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_PADDED AspectFormat = 80
|
||||||
|
ASPECT_SVELTE AspectFormat = 81
|
||||||
|
ASPECT__DEFAULT = ASPECT_PADDED
|
||||||
|
|
||||||
|
SCALEFMT_NN ScaleFormat = 120
|
||||||
|
SCALEFMT_BILINEAR ScaleFormat = 121
|
||||||
|
SCALEFMT__DEFAULT = SCALEFMT_BILINEAR
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidOption error = errors.New("Invalid format parameter")
|
||||||
|
ErrUnsupportedFiletype error = errors.New("Unsupported filetype")
|
||||||
|
)
|
||||||
|
|
||||||
type Thumbnailer struct {
|
type Thumbnailer struct {
|
||||||
Width int
|
width int
|
||||||
Height int
|
height int
|
||||||
|
ofmt OutputFormat
|
||||||
|
afmt AspectFormat
|
||||||
|
sfmt ScaleFormat
|
||||||
|
|
||||||
thumbCache *lru.Cache // threadsafe
|
thumbCache *lru.Cache // threadsafe
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewThumbnailer(Width, Height, MaxCacheSize int) *Thumbnailer {
|
func NewThumbnailer(Width, Height, MaxCacheSize int) *Thumbnailer {
|
||||||
|
return NewThumbnailerEx(Width, Height, MaxCacheSize, OUTPUT__DEFAULT, ASPECT__DEFAULT, SCALEFMT__DEFAULT)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewThumbnailerEx(Width, Height, MaxCacheSize int, of OutputFormat, af AspectFormat, sf ScaleFormat) *Thumbnailer {
|
||||||
thumbCache, err := lru.New(MaxCacheSize)
|
thumbCache, err := lru.New(MaxCacheSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Thumbnailer{
|
return &Thumbnailer{
|
||||||
Width: Width,
|
width: Width,
|
||||||
Height: Height,
|
height: Height,
|
||||||
|
ofmt: of,
|
||||||
|
afmt: af,
|
||||||
|
sfmt: sf,
|
||||||
thumbCache: thumbCache,
|
thumbCache: thumbCache,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -98,7 +131,7 @@ func (this *Thumbnailer) RenderFile_NoCache(absPath string) ([]byte, error) {
|
|||||||
return this.RenderScaledFfmpeg(absPath)
|
return this.RenderScaledFfmpeg(absPath)
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("No thumbnailer for file type '%s'", extension)
|
return nil, ErrUnsupportedFiletype
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
60
image.go
60
image.go
@ -1,7 +1,9 @@
|
|||||||
package thumbnail
|
package thumbnail
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"image"
|
"image"
|
||||||
|
"image/jpeg"
|
||||||
|
|
||||||
"code.ivysaur.me/imagequant"
|
"code.ivysaur.me/imagequant"
|
||||||
)
|
)
|
||||||
@ -13,32 +15,27 @@ func (this *Thumbnailer) RenderScaledImage(src image.Image) ([]byte, error) {
|
|||||||
destH := 0
|
destH := 0
|
||||||
|
|
||||||
if srcW > srcH {
|
if srcW > srcH {
|
||||||
destW = this.Width
|
destW = this.width
|
||||||
destH = this.Height * srcH / srcW
|
destH = this.height * srcH / srcW
|
||||||
} else {
|
} else {
|
||||||
destW = this.Width * srcW / srcH
|
destW = this.width * srcW / srcH
|
||||||
destH = this.Height
|
destH = this.height
|
||||||
}
|
}
|
||||||
|
|
||||||
offsetX := (this.Width - destW) / 2
|
offsetX := (this.width - destW) / 2
|
||||||
offsetY := (this.Height - destH) / 2
|
offsetY := (this.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.width, Y: this.height}})
|
||||||
|
|
||||||
|
switch this.sfmt {
|
||||||
|
|
||||||
|
case SCALEFMT_BILINEAR:
|
||||||
|
|
||||||
for y := 0; y < destH; y += 1 {
|
for y := 0; y < destH; y += 1 {
|
||||||
for x := 0; x < destW; x += 1 {
|
for x := 0; x < destW; x += 1 {
|
||||||
|
|
||||||
/*
|
|
||||||
// NN
|
|
||||||
mapx := int(float64(x) * scaleW)
|
|
||||||
mapy := int(float64(y) * scaleH)
|
|
||||||
dest.Set(x+offsetX, y+offsetY, src.At(mapx, mapy))
|
|
||||||
*/
|
|
||||||
|
|
||||||
// Bilinear
|
|
||||||
c00 := src.At(int(float64(x)*scaleW), int(float64(y)*scaleH))
|
c00 := src.At(int(float64(x)*scaleW), int(float64(y)*scaleH))
|
||||||
c01 := src.At(int((float64(x)+0.5)*scaleW), int(float64(y)*scaleH))
|
c01 := src.At(int((float64(x)+0.5)*scaleW), int(float64(y)*scaleH))
|
||||||
c10 := src.At(int(float64(x)*scaleW), int((float64(y)+0.5)*scaleH))
|
c10 := src.At(int(float64(x)*scaleW), int((float64(y)+0.5)*scaleH))
|
||||||
@ -49,5 +46,34 @@ func (this *Thumbnailer) RenderScaledImage(src image.Image) ([]byte, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return crush(dest, imagequant.SPEED_FASTEST)
|
case SCALEFMT_NN:
|
||||||
|
|
||||||
|
for y := 0; y < destH; y += 1 {
|
||||||
|
for x := 0; x < destW; x += 1 {
|
||||||
|
mapx := int(float64(x) * scaleW)
|
||||||
|
mapy := int(float64(y) * scaleH)
|
||||||
|
dest.Set(x+offsetX, y+offsetY, src.At(mapx, mapy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
switch this.ofmt {
|
||||||
|
case OUTPUT_PNG_CRUSH:
|
||||||
|
return crush(dest, imagequant.SPEED_FASTEST)
|
||||||
|
|
||||||
|
case OUTPUT_JPG:
|
||||||
|
buff := bytes.Buffer{}
|
||||||
|
err := jpeg.Encode(&buff, dest, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buff.Bytes(), nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidOption
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
18
video.go
18
video.go
@ -11,10 +11,22 @@ func (this *Thumbnailer) 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.height, this.width, this.height,
|
||||||
this.Height, this.Width, this.Width,
|
this.height, this.width, this.width,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BUG(): always produces ASPECT_SVELTE shape - need to re-pad for ASPECT_PADDED
|
||||||
|
|
||||||
|
var vcodec string
|
||||||
|
switch this.ofmt {
|
||||||
|
case OUTPUT_JPG:
|
||||||
|
vcodec = "jpg"
|
||||||
|
case OUTPUT_PNG_CRUSH:
|
||||||
|
vcodec = "png"
|
||||||
|
default:
|
||||||
|
return nil, ErrInvalidOption
|
||||||
|
}
|
||||||
|
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-loglevel", "0",
|
"-loglevel", "0",
|
||||||
@ -23,7 +35,7 @@ func (this *Thumbnailer) RenderScaledFfmpeg(absPath string) ([]byte, error) {
|
|||||||
"-vf", scaleCmd,
|
"-vf", scaleCmd,
|
||||||
"-frames:v", "1",
|
"-frames:v", "1",
|
||||||
"-f", "image2pipe",
|
"-f", "image2pipe",
|
||||||
"-c:v", "png",
|
"-c:v", vcodec,
|
||||||
"-",
|
"-",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user