182 lines
5.8 KiB
Go
182 lines
5.8 KiB
Go
// Package layercompositions owns durable Seedream layer-editor documents.
|
|
package layercompositions
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.nianxx.cn/wangxuming/NianAIGC/backend/internal/assets"
|
|
)
|
|
|
|
var (
|
|
ErrConflict = errors.New("layer composition version conflict")
|
|
ErrInvalid = errors.New("invalid layer composition")
|
|
ErrAssetNotFound = errors.New("layer composition asset not found")
|
|
)
|
|
|
|
type Box [4]float64
|
|
|
|
type Layer struct {
|
|
ID string `json:"id"`
|
|
AssetID string `json:"assetId"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Visible bool `json:"visible"`
|
|
Box Box `json:"box"`
|
|
Rotation float64 `json:"rotation"`
|
|
Source string `json:"source"`
|
|
EditJobID string `json:"editJobId,omitempty"`
|
|
OriginalAssetID string `json:"originalAssetId,omitempty"`
|
|
}
|
|
|
|
type Composition struct {
|
|
JobID string `json:"jobId"`
|
|
OwnerID string `json:"-"`
|
|
BaseAssetID string `json:"baseAssetId"`
|
|
BaseVisible bool `json:"baseVisible"`
|
|
Layers []Layer `json:"layers"`
|
|
Version int64 `json:"version"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type Repository interface {
|
|
FindLayerComposition(context.Context, string, string) (Composition, bool, error)
|
|
SaveLayerComposition(context.Context, Composition, int64) (Composition, error)
|
|
}
|
|
|
|
type AssetCatalog interface {
|
|
GetOwner(context.Context, string, string) (assets.Asset, bool, error)
|
|
}
|
|
|
|
type Service struct {
|
|
repository Repository
|
|
assets AssetCatalog
|
|
}
|
|
|
|
func NewService(repository Repository, catalog AssetCatalog) *Service {
|
|
return &Service{repository: repository, assets: catalog}
|
|
}
|
|
|
|
func (service *Service) Get(ctx context.Context, ownerID, jobID string) (Composition, bool, error) {
|
|
if strings.TrimSpace(ownerID) == "" || strings.TrimSpace(jobID) == "" {
|
|
return Composition{}, false, fmt.Errorf("%w: ownerId and jobId are required", ErrInvalid)
|
|
}
|
|
return service.repository.FindLayerComposition(ctx, ownerID, jobID)
|
|
}
|
|
|
|
func (service *Service) Save(ctx context.Context, ownerID, jobID string, expectedVersion int64, document Composition) (Composition, error) {
|
|
document.OwnerID = strings.TrimSpace(ownerID)
|
|
document.JobID = strings.TrimSpace(jobID)
|
|
document.Version = 0
|
|
if expectedVersion < 0 {
|
|
return Composition{}, fmt.Errorf("%w: version must not be negative", ErrInvalid)
|
|
}
|
|
if err := normalizeAndValidate(&document); err != nil {
|
|
return Composition{}, err
|
|
}
|
|
assetIDs := []string{document.BaseAssetID}
|
|
for _, layer := range document.Layers {
|
|
assetIDs = append(assetIDs, layer.AssetID)
|
|
if layer.OriginalAssetID != "" {
|
|
assetIDs = append(assetIDs, layer.OriginalAssetID)
|
|
}
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, assetID := range assetIDs {
|
|
if seen[assetID] {
|
|
continue
|
|
}
|
|
seen[assetID] = true
|
|
asset, found, err := service.assets.GetOwner(ctx, document.OwnerID, assetID)
|
|
if err != nil {
|
|
return Composition{}, fmt.Errorf("load layer asset: %w", err)
|
|
}
|
|
if !found || (asset.Kind != assets.KindImage && asset.Kind != assets.KindReference && asset.Kind != assets.KindMask) {
|
|
return Composition{}, fmt.Errorf("%w: %s", ErrAssetNotFound, assetID)
|
|
}
|
|
}
|
|
return service.repository.SaveLayerComposition(ctx, document, expectedVersion)
|
|
}
|
|
|
|
func normalizeAndValidate(document *Composition) error {
|
|
if document.OwnerID == "" || document.JobID == "" || strings.TrimSpace(document.BaseAssetID) == "" {
|
|
return fmt.Errorf("%w: ownerId, jobId, and baseAssetId are required", ErrInvalid)
|
|
}
|
|
document.BaseAssetID = strings.TrimSpace(document.BaseAssetID)
|
|
if len(document.Layers) > 100 {
|
|
return fmt.Errorf("%w: at most 100 layers are supported", ErrInvalid)
|
|
}
|
|
if document.Layers == nil {
|
|
document.Layers = []Layer{}
|
|
}
|
|
ids := map[string]bool{}
|
|
for index := range document.Layers {
|
|
layer := &document.Layers[index]
|
|
layer.ID = strings.TrimSpace(layer.ID)
|
|
layer.AssetID = strings.TrimSpace(layer.AssetID)
|
|
layer.Name = strings.TrimSpace(layer.Name)
|
|
layer.Description = strings.TrimSpace(layer.Description)
|
|
layer.Source = strings.TrimSpace(layer.Source)
|
|
layer.EditJobID = strings.TrimSpace(layer.EditJobID)
|
|
layer.OriginalAssetID = strings.TrimSpace(layer.OriginalAssetID)
|
|
if layer.ID == "" || layer.AssetID == "" || layer.Name == "" {
|
|
return fmt.Errorf("%w: every layer requires id, assetId, and name", ErrInvalid)
|
|
}
|
|
if ids[layer.ID] {
|
|
return fmt.Errorf("%w: duplicate layer id %s", ErrInvalid, layer.ID)
|
|
}
|
|
ids[layer.ID] = true
|
|
if len([]rune(layer.Name)) > 120 || len([]rune(layer.Description)) > 500 {
|
|
return fmt.Errorf("%w: layer text is too long", ErrInvalid)
|
|
}
|
|
if !validLayerSource(layer.Source) {
|
|
return fmt.Errorf("%w: unsupported layer source %s", ErrInvalid, layer.Source)
|
|
}
|
|
if err := validateBox(layer.Box); err != nil {
|
|
return fmt.Errorf("%w: layer %s: %v", ErrInvalid, layer.ID, err)
|
|
}
|
|
if math.IsNaN(layer.Rotation) || math.IsInf(layer.Rotation, 0) {
|
|
return fmt.Errorf("%w: layer rotation must be finite", ErrInvalid)
|
|
}
|
|
layer.Rotation = normalizeRotation(layer.Rotation)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validLayerSource(value string) bool {
|
|
switch value {
|
|
case "provider", "upload", "library", "seedream-edit":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func validateBox(box Box) error {
|
|
for _, value := range box {
|
|
if math.IsNaN(value) || math.IsInf(value, 0) || value < 0 || value > 1000 {
|
|
return errors.New("box coordinates must be finite values between 0 and 1000")
|
|
}
|
|
}
|
|
if box[2]-box[0] < 1 || box[3]-box[1] < 1 {
|
|
return errors.New("box must have a positive width and height")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func normalizeRotation(value float64) float64 {
|
|
value = math.Mod(value, 360)
|
|
if value > 180 {
|
|
value -= 360
|
|
}
|
|
if value <= -180 {
|
|
value += 360
|
|
}
|
|
return math.Round(value*1000) / 1000
|
|
}
|