commit 081f542e08cc9b04cf26119bd27fa37bec45216d Author: mappu Date: Thu Oct 13 22:25:11 2016 +1300 initial commit diff --git a/thumbnail.go b/thumbnail.go new file mode 100644 index 0000000..cfcd598 --- /dev/null +++ b/thumbnail.go @@ -0,0 +1,185 @@ +// Package thumbnail contains functions for taking the thumbnail of image and +// video files, and caching the result for performance. +package thumbnail + +import ( + "bytes" + "fmt" + "image" + "image/color" + "image/jpeg" + "image/png" + "os" + "sync" + //"os/exec" + "strings" +) + +type Thumbnailer struct { + Width int + Height int + MaxCacheSize int + + cacheMtx sync.RWMutex + thumbCache map[string][]byte +} + +func NewThumbnailer() *Thumbnailer { + return &Thumbnailer{ + Width: 100, + Height: 100, + MaxCacheSize: 10, + thumbCache: make(map[string][]byte, 0), + } +} + +func videoThumbnail(absPath string) { + /* + cmd := exec.Command( + "ffmpeg", + "-loglevel", "0", + "-an", + "-i", absPath, + "-vf", "thumbnail,scale=100:100", + "-frames:v", "1", + "-f", "image2pipe", + "-c:v", "png", + "-", + ) + */ +} + +func (this *Thumbnailer) RenderFile(absPath string) ([]byte, error) { + this.cacheMtx.RLock() + tcache, ok := this.thumbCache[absPath] + this.cacheMtx.RUnlock() + + if ok { + return tcache, nil + } + + // Add to cache + tcache, err := this.RenderFile_NoCache(absPath) + if err != nil { + return nil, err + } + + this.cacheMtx.Lock() + // FIXME prune old cached thumbnails + this.thumbCache[absPath] = tcache + this.cacheMtx.Unlock() + + return tcache, nil +} + +func (this *Thumbnailer) RenderFile_NoCache(absPath string) ([]byte, error) { + + fh, err := os.OpenFile(absPath, os.O_RDONLY, 0400) + if err != nil { + return nil, err + } + + defer fh.Close() + + var src image.Image + err = fmt.Errorf("No thumbnailer for file type") + + comparePath := strings.ToLower(absPath) + + if strings.HasSuffix(comparePath, "jpg") { + src, err = jpeg.Decode(fh) + + } else if strings.HasSuffix(comparePath, "png") { + src, err = png.Decode(fh) + + } + + if err != nil { + return nil, err + } + + return this.RenderScaledImage(src) +} + +func Blend(a, b color.Color) color.Color { + switch a.(type) { + case color.RGBA: + return BlendRGBA(a.(color.RGBA), b.(color.RGBA)) // FIXME there's syntax for this + + case color.YCbCr: + return BlendYCbCr(a.(color.YCbCr), b.(color.YCbCr)) + + default: + return a // ??? unknown color format + } +} + +func BlendYCbCr(a, b color.YCbCr) color.YCbCr { + return color.YCbCr{ + Y: uint8((int(a.Y) + int(b.Y)) / 2), + Cb: uint8((int(a.Cb) + int(b.Cb)) / 2), + Cr: uint8((int(a.Cr) + int(b.Cr)) / 2), + } +} + +func BlendRGBA(a, b color.RGBA) color.RGBA { + return color.RGBA{ + R: uint8((int(a.R) + int(b.R)) / 2), + G: uint8((int(a.G) + int(b.G)) / 2), + B: uint8((int(a.B) + int(b.B)) / 2), + A: uint8((int(a.A) + int(b.A)) / 2), + } +} + +func (this *Thumbnailer) 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 + } else { + destW = this.Width * srcW / srcH + destH = this.Height + } + + 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}}) + + for y := 0; y < destH; y += 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)) + 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) + + } + } + + var b bytes.Buffer + + err := png.Encode(&b, dest) + if err != nil { + return nil, err + } + + return b.Bytes(), nil +}