Files
silo-server/internal/collage/collage.go
T

112 lines
3.2 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package collage composes poster collage images from multiple source images.
package collage
import (
"bytes"
"errors"
"image"
"image/color"
"image/jpeg"
_ "image/png"
"golang.org/x/image/draw"
_ "golang.org/x/image/webp"
)
// ErrNotEnoughImages is returned when no source images are usable.
var ErrNotEnoughImages = errors.New("collage: no usable source images")
const (
posterWidth = 600
posterHeight = 900
jpegQuality = 92
)
// ComposePoster builds a poster collage from raw image bytes.
// Returns JPEG bytes suitable for feeding into processCollectionImage.
//
// Layout:
// - 1 image: scale-fill the full canvas
// - 2-3 images: 2 side-by-side columns
// - 4+ images: 2×2 grid (first 4 used)
func ComposePoster(images [][]byte) ([]byte, error) {
decoded := make([]image.Image, 0, len(images))
for _, data := range images {
img, _, err := image.Decode(bytes.NewReader(data))
if err != nil {
continue
}
decoded = append(decoded, img)
}
if len(decoded) == 0 {
return nil, ErrNotEnoughImages
}
if len(decoded) > 4 {
decoded = decoded[:4]
}
canvas := image.NewNRGBA(image.Rect(0, 0, posterWidth, posterHeight))
// Fill with dark background in case images don't cover fully.
for y := range posterHeight {
for x := range posterWidth {
canvas.SetNRGBA(x, y, color.NRGBA{R: 24, G: 24, B: 27, A: 255})
}
}
switch len(decoded) {
case 1:
scaleFill(canvas, decoded[0], image.Rect(0, 0, posterWidth, posterHeight))
case 2, 3:
halfW := posterWidth / 2
scaleFill(canvas, decoded[0], image.Rect(0, 0, halfW, posterHeight))
scaleFill(canvas, decoded[1], image.Rect(halfW, 0, posterWidth, posterHeight))
default:
halfW := posterWidth / 2
halfH := posterHeight / 2
scaleFill(canvas, decoded[0], image.Rect(0, 0, halfW, halfH))
scaleFill(canvas, decoded[1], image.Rect(halfW, 0, posterWidth, halfH))
scaleFill(canvas, decoded[2], image.Rect(0, halfH, halfW, posterHeight))
scaleFill(canvas, decoded[3], image.Rect(halfW, halfH, posterWidth, posterHeight))
}
var buf bytes.Buffer
if err := jpeg.Encode(&buf, canvas, &jpeg.Options{Quality: jpegQuality}); err != nil {
return nil, err
}
return buf.Bytes(), nil
}
// scaleFill scales src to cover dst completely (crop-to-fill), centering the
// source image so excess is cropped equally from both sides.
func scaleFill(dst *image.NRGBA, src image.Image, rect image.Rectangle) {
dstW := rect.Dx()
dstH := rect.Dy()
srcBounds := src.Bounds()
srcW := srcBounds.Dx()
srcH := srcBounds.Dy()
// Compute scale factor to cover the destination rectangle.
scaleX := float64(dstW) / float64(srcW)
scaleY := float64(dstH) / float64(srcH)
scale := scaleX
if scaleY > scaleX {
scale = scaleY
}
// Scaled source dimensions.
scaledW := int(float64(srcW) * scale)
scaledH := int(float64(srcH) * scale)
// Compute crop offset to center.
offsetX := (scaledW - dstW) / 2
offsetY := (scaledH - dstH) / 2
// Scale the full source into a temporary image, then copy the centered region.
tmp := image.NewNRGBA(image.Rect(0, 0, scaledW, scaledH))
draw.BiLinear.Scale(tmp, tmp.Bounds(), src, srcBounds, draw.Over, nil)
// Copy the centered crop into the destination rectangle.
draw.Copy(dst, rect.Min, tmp, image.Rect(offsetX, offsetY, offsetX+dstW, offsetY+dstH), draw.Over, nil)
}