// Package photos generates thumbnails for image libraries. // // Photos reuse the generic item, cover, and file endpoints; the only // photo-specific work is producing a small thumbnail, because serving a // folder of 12-megapixel originals to a grid would be unusable on a TV box. package photos import ( "context" "database/sql" "fmt" "image" "os" "path/filepath" // Decoders registered for image.Decode. _ "image/gif" _ "image/jpeg" _ "image/png" "github.com/mithral/vellum/internal/media" "golang.org/x/image/draw" _ "golang.org/x/image/webp" ) // Kind is the media kind this module owns. const Kind = "photos" // thumbMax is the longest edge of a generated thumbnail. const thumbMax = 512 func init() { media.RegisterCoverExtractor(Kind, ExtractCover) } // ExtractCover decodes the image and writes a downscaled JPEG thumbnail. // // A format the standard library cannot decode returns ("", nil): no thumbnail // is a normal outcome, and the grid falls back to a placeholder rather than // the request failing. func ExtractCover(ctx context.Context, db *sql.DB, item media.Item, absPath, cacheDir string) (string, error) { f, err := os.Open(absPath) if err != nil { return "", nil } defer f.Close() src, _, err := image.Decode(f) if err != nil { return "", nil } dst := scaleToFit(src, thumbMax) dir, err := media.CoverCacheDir(cacheDir, Kind) if err != nil { return "", err } outPath := filepath.Join(dir, fmt.Sprintf("%d.jpg", item.ID)) tmp := outPath + ".tmp" out, err := os.Create(tmp) if err != nil { return "", fmt.Errorf("photos: create thumbnail: %w", err) } if err := encodeJPEG(out, dst); err != nil { out.Close() _ = os.Remove(tmp) return "", err } if err := out.Close(); err != nil { _ = os.Remove(tmp) return "", err } if err := os.Rename(tmp, outPath); err != nil { _ = os.Remove(tmp) return "", err } return outPath, nil } // scaleToFit scales src down so its longest edge is at most max. Images // already smaller than max are returned unchanged. func scaleToFit(src image.Image, max int) image.Image { b := src.Bounds() w, h := b.Dx(), b.Dy() if w <= max && h <= max { return src } scale := float64(max) / float64(w) if h > w { scale = float64(max) / float64(h) } nw, nh := int(float64(w)*scale), int(float64(h)*scale) if nw < 1 { nw = 1 } if nh < 1 { nh = 1 } dst := image.NewRGBA(image.Rect(0, 0, nw, nh)) // CatmullRom is a good quality/speed trade for thumbnails. draw.CatmullRom.Scale(dst, dst.Bounds(), src, b, draw.Src, nil) return dst }