// 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 }