2023-08-11 00:27:32 -04:00
|
|
|
// Package collage makes a pic collage
|
|
|
|
package collage
|
|
|
|
|
|
|
|
import (
|
2023-08-11 22:16:24 -04:00
|
|
|
"errors"
|
|
|
|
"image"
|
2023-08-27 22:22:17 -04:00
|
|
|
"image/color"
|
2023-08-11 23:20:53 -04:00
|
|
|
"image/draw"
|
|
|
|
"image/jpeg"
|
2023-08-11 00:27:32 -04:00
|
|
|
"io"
|
|
|
|
"io/fs"
|
2023-08-11 23:20:53 -04:00
|
|
|
|
2024-08-08 22:57:59 -04:00
|
|
|
"github.com/disintegration/imaging"
|
2023-08-11 23:20:53 -04:00
|
|
|
"go.oneofone.dev/resize"
|
2023-08-11 00:27:32 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
type Dimension struct {
|
|
|
|
Width uint `json:"width"`
|
|
|
|
Height uint `json:"height"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Point struct {
|
|
|
|
X uint `json:"x"`
|
|
|
|
Y uint `json:"y"`
|
|
|
|
}
|
|
|
|
|
|
|
|
type Rectangle struct {
|
|
|
|
Start Point `json:"start"`
|
|
|
|
End Point `json:"end"`
|
|
|
|
}
|
|
|
|
|
2023-08-11 22:16:24 -04:00
|
|
|
func (r Rectangle) ToImgRect() image.Rectangle {
|
|
|
|
return image.Rectangle{
|
|
|
|
Min: image.Point{int(r.Start.X), int(r.Start.Y)},
|
|
|
|
Max: image.Point{int(r.End.X), int(r.End.Y)},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-08-11 23:20:53 -04:00
|
|
|
type Photo struct {
|
|
|
|
ImageName string `json:"image"`
|
|
|
|
Crop Rectangle `json:"crop"`
|
|
|
|
Frame Rectangle `json:"frame"`
|
|
|
|
}
|
|
|
|
|
2023-08-11 00:27:32 -04:00
|
|
|
type Request struct {
|
|
|
|
BackgroundImage string `json:"background_image"`
|
|
|
|
Aspect Dimension `json:"aspect"`
|
|
|
|
Dimension Dimension `json:"dimension"`
|
2023-08-11 23:20:53 -04:00
|
|
|
Photos []Photo `json:"photos"`
|
2023-08-11 00:27:32 -04:00
|
|
|
}
|
|
|
|
|
2023-09-01 15:13:54 -04:00
|
|
|
func Make(req *Request, source fs.FS, output io.Writer) error {
|
2023-08-11 22:16:24 -04:00
|
|
|
rec := image.Rect(0, 0, int(req.Aspect.Width), int(req.Aspect.Height))
|
|
|
|
canvas := image.NewRGBA64(rec)
|
2023-08-27 22:22:17 -04:00
|
|
|
white := color.RGBA{255, 255, 255, 255}
|
|
|
|
draw.Draw(canvas, rec, &image.Uniform{white}, image.Point{}, draw.Src)
|
2023-08-11 23:20:53 -04:00
|
|
|
for _, photo := range req.Photos {
|
|
|
|
img, err := GetImage(source, photo.ImageName)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
croppedImage, err := Crop(img, photo.Crop)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
destRect := FrameTranslate(req.Aspect, req.Dimension, photo.Frame).ToImgRect()
|
|
|
|
resizedImg := resize.Resize(uint(destRect.Dx()), uint(destRect.Dy()), croppedImage, resize.Lanczos3)
|
|
|
|
draw.Draw(canvas, destRect, resizedImg, image.Point{0, 0}, draw.Src)
|
|
|
|
}
|
|
|
|
var opt jpeg.Options
|
|
|
|
opt.Quality = 100
|
|
|
|
|
|
|
|
return jpeg.Encode(output, canvas, &opt)
|
|
|
|
}
|
|
|
|
|
|
|
|
func FrameTranslate(resolution Dimension, frameSize Dimension, frame Rectangle) Rectangle {
|
|
|
|
newX := func(oldX uint) uint {
|
|
|
|
return oldX * resolution.Width / frameSize.Width
|
|
|
|
}
|
|
|
|
newY := func(oldY uint) uint {
|
|
|
|
return oldY * resolution.Height / frameSize.Height
|
|
|
|
}
|
|
|
|
return Rectangle{
|
|
|
|
Start: Point{newX(frame.Start.X), newY(frame.Start.Y)},
|
|
|
|
End: Point{newX(frame.End.X), newY(frame.End.Y)},
|
2023-08-11 22:16:24 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
type HasSubImage interface {
|
|
|
|
SubImage(r image.Rectangle) image.Image
|
|
|
|
}
|
|
|
|
|
|
|
|
func Crop(img image.Image, r Rectangle) (image.Image, error) {
|
|
|
|
if imgHS, ok := img.(HasSubImage); ok {
|
|
|
|
return imgHS.SubImage(r.ToImgRect()), nil
|
|
|
|
}
|
|
|
|
return nil, errors.New("image does not support cropping")
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetImage(source fs.FS, imageName string) (image.Image, error) {
|
|
|
|
imgF, err := source.Open(imageName)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2024-08-08 22:57:59 -04:00
|
|
|
img, err := imaging.Decode(imgF, imaging.AutoOrientation(true))
|
2023-08-11 22:16:24 -04:00
|
|
|
return img, err
|
2023-08-11 00:27:32 -04:00
|
|
|
}
|