weiro/services/imgedit/processing.go

172 lines
3.9 KiB
Go

package imgedit
import (
"context"
"encoding/json"
"fmt"
"image"
"image/color"
"os"
"path/filepath"
"github.com/disintegration/imaging"
"lmika.dev/lmika/weiro/models"
)
type imageProcessor struct {
newParams func() any
processImage func(ctx context.Context, srcImg image.Image, params any) (image.Image, error)
}
type shadowProcessorArgs struct {
Color string `json:"color"`
OffsetY int `json:"offset_y,string"`
}
var processors = map[string]imageProcessor{
"shadow": {
newParams: func() any {
return &shadowProcessorArgs{
Color: "#000000",
OffsetY: 0,
}
},
processImage: func(ctx context.Context, srcImg image.Image, params any) (image.Image, error) {
p := params.(*shadowProcessorArgs)
shadowColor, err := parseHexColor(p.Color)
if err != nil {
return nil, fmt.Errorf("invalid shadow color: %w", err)
}
shadow := makeBoxShadow(srcImg, shadowColor, 4, 10, p.OffsetY)
composit := imaging.OverlayCenter(shadow, srcImg, 1.0)
return composit, nil
},
},
}
func (s *Service) reprocess(ctx context.Context, session *models.ImageEditSession) (imageSource, error) {
var img imageSource
for _, p := range session.Processors {
// Check if there's currently a cached image of this processor
cachedImageFile := filepath.Join(s.scratchDir, session.GUID, fmt.Sprintf("%v.%v", p.VersionID, session.ImageExt))
if s, err := os.Stat(cachedImageFile); err == nil && !s.IsDir() {
img = fileImageSource(cachedImageFile)
continue
}
// Need to process the image
var srcImg image.Image
if img != nil {
var err error
srcImg, err = img.image()
if err != nil {
return nil, err
}
}
resImg, err := s.processImage(ctx, srcImg, p)
if err != nil {
return nil, err
}
// Cache the processed image
if err := imaging.Save(resImg, cachedImageFile); err != nil {
return nil, err
}
img = imageImageSource{resImg}
}
return img, nil
}
func (s *Service) processImage(ctx context.Context, srcImg image.Image, processor models.ImageEditProcessor) (image.Image, error) {
switch processor.Type {
case "copy-upload":
var p models.CopyUploadProps
if err := json.Unmarshal(processor.Props, &p); err != nil {
return nil, err
}
_, rc, err := s.uploadService.OpenUpload(ctx, p.UploadID)
if err != nil {
return nil, err
}
f, err := rc()
if err != nil {
return nil, err
}
defer f.Close()
return imaging.Decode(f)
}
proc, ok := processors[processor.Type]
if !ok {
return nil, fmt.Errorf("unknown processor type: %v", processor.Type)
}
paramType := proc.newParams()
if err := json.Unmarshal(processor.Props, paramType); err != nil {
return nil, err
}
return proc.processImage(ctx, srcImg, paramType)
}
type imageSource interface {
image() (image.Image, error)
}
type fileImageSource string
func (f fileImageSource) image() (image.Image, error) {
return imaging.Open(string(f))
}
type imageImageSource struct {
img image.Image
}
func (i imageImageSource) image() (image.Image, error) {
return i.img, nil
}
func parseHexColor(s string) (color.Color, error) {
// Remove leading hash if present
if len(s) > 0 && s[0] == '#' {
s = s[1:]
}
// Parse based on length
var r, g, b, a uint8
switch len(s) {
case 6:
// RGB format
var rgb uint32
if _, err := fmt.Sscanf(s, "%06x", &rgb); err != nil {
return nil, fmt.Errorf("invalid hex color format: %w", err)
}
r = uint8((rgb >> 16) & 0xFF)
g = uint8((rgb >> 8) & 0xFF)
b = uint8(rgb & 0xFF)
a = 0xFF
case 8:
// RGBA format
var rgba uint32
if _, err := fmt.Sscanf(s, "%08x", &rgba); err != nil {
return nil, fmt.Errorf("invalid hex color format: %w", err)
}
r = uint8((rgba >> 24) & 0xFF)
g = uint8((rgba >> 16) & 0xFF)
b = uint8((rgba >> 8) & 0xFF)
a = uint8(rgba & 0xFF)
default:
return nil, fmt.Errorf("invalid hex color length: expected 6 or 8 characters, got %d", len(s))
}
return color.RGBA{R: r, G: g, B: b, A: a}, nil
}