package imgedit import ( "context" "encoding/json" "fmt" "image" "image/color" "log" "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} } log.Printf("result of processed image: %T", img) 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) //case "shadow": // shadow := makeBoxShadow(srcImg, color.Black, 4, 10, 0) // composit := imaging.OverlayCenter(shadow, srcImg, 1.0) // return composit, nil } 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 }