Files
image2api/backend/internal/service/thumbnail.go
T
2026-07-03 00:15:41 +08:00

72 lines
2.1 KiB
Go

package service
import (
"bytes"
"image"
"image/jpeg"
"strings"
"golang.org/x/image/draw"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
_ "golang.org/x/image/webp"
)
// thumbSuffix is appended to an image's object key to form its thumbnail key
// ("u/x.png" → "u/x.png.thumb.jpg"). List views load the thumbnail; preview and
// download always use the original.
const thumbSuffix = ".thumb.jpg"
// thumbMaxDim bounds the thumbnail's longest side. 512px is crisp for grid
// cards / table rows while staying ~20-50 KB as JPEG.
const thumbMaxDim = 512
// ThumbKey returns the thumbnail object key for an image key.
func ThumbKey(rel string) string { return rel + thumbSuffix }
// IsThumbKey reports whether name refers to a thumbnail object, and OrigKey
// maps a thumbnail key back to its original image key.
func IsThumbKey(name string) bool { return strings.HasSuffix(name, thumbSuffix) }
func OrigKey(name string) string { return strings.TrimSuffix(name, thumbSuffix) }
// makeThumbnail downscales an image to thumbMaxDim (longest side) and encodes
// it as JPEG. Images already small enough are re-encoded as-is (so the thumb
// object always exists once generated). Returns an error for undecodable input
// (e.g. video bytes) — callers treat thumbnailing as best-effort.
func makeThumbnail(b []byte) ([]byte, error) {
src, _, err := image.Decode(bytes.NewReader(b))
if err != nil {
return nil, err
}
bounds := src.Bounds()
w, h := bounds.Dx(), bounds.Dy()
tw, th := w, h
if w > thumbMaxDim || h > thumbMaxDim {
if w >= h {
tw = thumbMaxDim
th = h * thumbMaxDim / w
} else {
th = thumbMaxDim
tw = w * thumbMaxDim / h
}
if tw < 1 {
tw = 1
}
if th < 1 {
th = 1
}
}
// JPEG has no alpha — composite onto white so transparent PNGs don't go black.
dst := image.NewRGBA(image.Rect(0, 0, tw, th))
draw.Draw(dst, dst.Bounds(), image.White, image.Point{}, draw.Src)
draw.CatmullRom.Scale(dst, dst.Bounds(), src, bounds, draw.Over, nil)
var out bytes.Buffer
if err := jpeg.Encode(&out, dst, &jpeg.Options{Quality: 78}); err != nil {
return nil, err
}
return out.Bytes(), nil
}