From 67c50a05396ebfa5b6e2813fa6bfe7f0bcd8d764 Mon Sep 17 00:00:00 2001 From: mappu Date: Mon, 5 Dec 2016 21:48:48 +1300 Subject: [PATCH] set custom output/aspect/scale formats, add package-level error vars --- Thumbnailer.go | 45 +++++++++++++++++++++++++++----- image.go | 70 ++++++++++++++++++++++++++++++++++---------------- video.go | 18 ++++++++++--- 3 files changed, 102 insertions(+), 31 deletions(-) diff --git a/Thumbnailer.go b/Thumbnailer.go index 9e62fc7..94d6625 100644 --- a/Thumbnailer.go +++ b/Thumbnailer.go @@ -1,7 +1,7 @@ package thumbnail import ( - "fmt" + "errors" "image/gif" "image/jpeg" "image/png" @@ -12,22 +12,55 @@ import ( 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 { - Width int - Height int + width int + height int + ofmt OutputFormat + afmt AspectFormat + sfmt ScaleFormat thumbCache *lru.Cache // threadsafe } 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) if err != nil { panic(err) } return &Thumbnailer{ - Width: Width, - Height: Height, + width: Width, + height: Height, + ofmt: of, + afmt: af, + sfmt: sf, thumbCache: thumbCache, } } @@ -98,7 +131,7 @@ func (this *Thumbnailer) RenderFile_NoCache(absPath string) ([]byte, error) { return this.RenderScaledFfmpeg(absPath) default: - return nil, fmt.Errorf("No thumbnailer for file type '%s'", extension) + return nil, ErrUnsupportedFiletype } diff --git a/image.go b/image.go index a8a5c8b..a1648ba 100644 --- a/image.go +++ b/image.go @@ -1,7 +1,9 @@ package thumbnail import ( + "bytes" "image" + "image/jpeg" "code.ivysaur.me/imagequant" ) @@ -13,41 +15,65 @@ func (this *Thumbnailer) RenderScaledImage(src image.Image) ([]byte, error) { destH := 0 if srcW > srcH { - destW = this.Width - destH = this.Height * srcH / srcW + destW = this.width + destH = this.height * srcH / srcW } else { - destW = this.Width * srcW / srcH - destH = this.Height + destW = this.width * srcW / srcH + destH = this.height } - offsetX := (this.Width - destW) / 2 - offsetY := (this.Height - destH) / 2 + offsetX := (this.width - destW) / 2 + offsetY := (this.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.width, Y: this.height}}) - for y := 0; y < destH; y += 1 { - for x := 0; x < destW; x += 1 { + switch this.sfmt { - /* - // NN + case SCALEFMT_BILINEAR: + + for y := 0; y < destH; y += 1 { + for x := 0; x < destW; x += 1 { + c00 := src.At(int(float64(x)*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)) + c11 := src.At(int((float64(x)+0.5)*scaleW), int((float64(y)+0.5)*scaleH)) + cBlend := Blend(Blend(c00, c01), Blend(c10, c11)) + dest.Set(x+offsetX, y+offsetY, cBlend) + + } + } + + 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)) - */ - - // Bilinear - c00 := src.At(int(float64(x)*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)) - c11 := src.At(int((float64(x)+0.5)*scaleW), int((float64(y)+0.5)*scaleH)) - cBlend := Blend(Blend(c00, c01), Blend(c10, c11)) - dest.Set(x+offsetX, y+offsetY, cBlend) - + } } + + } + + 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 + } - return crush(dest, imagequant.SPEED_FASTEST) } diff --git a/video.go b/video.go index d479208..aeaa242 100644 --- a/video.go +++ b/video.go @@ -11,10 +11,22 @@ func (this *Thumbnailer) 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.height, this.width, this.height, + 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( "ffmpeg", "-loglevel", "0", @@ -23,7 +35,7 @@ func (this *Thumbnailer) RenderScaledFfmpeg(absPath string) ([]byte, error) { "-vf", scaleCmd, "-frames:v", "1", "-f", "image2pipe", - "-c:v", "png", + "-c:v", vcodec, "-", )