package image import ( "bytes" stdimage "image" "image/jpeg" "os" "sync" xdraw "golang.org/x/image/draw" ) const ( defaultCacheEntries = 500 thumbnailWidth = 160 thumbnailHeight = 90 ) type Cache struct { mu sync.Mutex max int entries map[string][]byte order []string } func NewCache(max int) *Cache { if max <= 0 { max = defaultCacheEntries } return &Cache{ max: max, entries: make(map[string][]byte), } } func (c *Cache) Thumbnail(absPath string) ([]byte, error) { if cached, ok := c.get(absPath); ok { return cached, nil } file, err := os.Open(absPath) if err != nil { return nil, err } defer file.Close() src, err := jpeg.Decode(file) if err != nil { return nil, err } dst := resizeToFit(src, thumbnailWidth, thumbnailHeight) var out bytes.Buffer if err := jpeg.Encode(&out, dst, &jpeg.Options{Quality: 75}); err != nil { return nil, err } thumb := out.Bytes() c.add(absPath, thumb) return append([]byte(nil), thumb...), nil } func (c *Cache) get(key string) ([]byte, bool) { c.mu.Lock() defer c.mu.Unlock() value, ok := c.entries[key] if !ok { return nil, false } return append([]byte(nil), value...), true } func (c *Cache) add(key string, value []byte) { c.mu.Lock() defer c.mu.Unlock() if _, ok := c.entries[key]; !ok { c.order = append(c.order, key) } c.entries[key] = append([]byte(nil), value...) for len(c.order) > c.max { oldest := c.order[0] c.order = c.order[1:] delete(c.entries, oldest) } } func resizeToFit(src stdimage.Image, maxWidth, maxHeight int) stdimage.Image { bounds := src.Bounds() width := bounds.Dx() height := bounds.Dy() if width <= 0 || height <= 0 { return stdimage.NewRGBA(stdimage.Rect(0, 0, 1, 1)) } targetWidth := maxWidth targetHeight := height * targetWidth / width if targetHeight > maxHeight { targetHeight = maxHeight targetWidth = width * targetHeight / height } if targetWidth < 1 { targetWidth = 1 } if targetHeight < 1 { targetHeight = 1 } dst := stdimage.NewRGBA(stdimage.Rect(0, 0, targetWidth, targetHeight)) xdraw.BiLinear.Scale(dst, dst.Bounds(), src, bounds, xdraw.Over, nil) return dst }