- текстура, чёткость, дымка, виньетка и зерно в неразрушающем редакторе - живой WebGL2-предпросмотр эффектов и CPU-фолбэк - секции редактора свёрнуты по умолчанию, состояние в localStorage - вкладка «Редактирование» доступна только из просмотрщика
143 lines
4.3 KiB
Go
143 lines
4.3 KiB
Go
package e2e
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/gen2brain/webp"
|
|
|
|
"galo/internal/albums"
|
|
"galo/internal/photos"
|
|
)
|
|
|
|
// TestEffectsEdit covers the effects round trip: the parameters are stored in
|
|
// the photo metadata, the background worker renders the edited version, the
|
|
// saved parameters carry the normalized effects, a reset removes everything,
|
|
// and re-rendering the same edit (including grain) is deterministic.
|
|
func TestEffectsEdit(t *testing.T) {
|
|
ctx := context.Background()
|
|
st, closeDB := openStack(t, filepath.Join(t.TempDir(), "galo.db"), t.TempDir())
|
|
defer closeDB()
|
|
|
|
user, _, err := st.auth.Register(ctx, "effects@test.local", "", "password123", "E")
|
|
if err != nil {
|
|
t.Fatalf("register: %v", err)
|
|
}
|
|
album, err := st.albums.Create(ctx, user.ID, albums.CreateInput{Title: "Effects"})
|
|
if err != nil {
|
|
t.Fatalf("create album: %v", err)
|
|
}
|
|
p, err := st.photos.Upload(ctx, photos.UploadInput{
|
|
AlbumID: album.ID, UserID: user.ID, Filename: "e.png", Data: makePNG(t),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("upload: %v", err)
|
|
}
|
|
|
|
w := photos.NewWorker(st.photos, time.Second, 10, time.Minute, 2)
|
|
w.ProcessOnce(ctx)
|
|
|
|
params := photos.EditParams{Effects: photos.EffectsParams{
|
|
Texture: 25,
|
|
Clarity: 20,
|
|
Dehaze: 15,
|
|
Vignette: photos.VignetteParams{
|
|
Mode: photos.VignetteColor, Amount: 40, Midpoint: 60, Feather: 40, Color: "#ff8800",
|
|
},
|
|
Grain: photos.GrainParams{Amount: 35, Size: 30, Roughness: 60},
|
|
}}
|
|
edited, err := st.photos.EditPhoto(ctx, p.ID, user.ID, params)
|
|
if err != nil {
|
|
t.Fatalf("edit: %v", err)
|
|
}
|
|
if edited.EditParams == nil {
|
|
t.Fatal("edit returned no parameters")
|
|
}
|
|
if _, ok := edited.EditParams["effects"]; !ok {
|
|
t.Fatalf("effects params not returned: %+v", edited.EditParams)
|
|
}
|
|
|
|
w.ProcessOnce(ctx)
|
|
got, err := st.photos.Get(ctx, p.ID, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if got.EditPreviewURL == "" {
|
|
t.Fatalf("no edited preview url: %+v", got)
|
|
}
|
|
if got.EditPreviewKey == "" {
|
|
t.Fatalf("no edited preview key: %+v", got)
|
|
}
|
|
effects, ok := got.EditParams["effects"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("effects params not stored: %+v", got.EditParams)
|
|
}
|
|
vignette, ok := effects["vignette"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("vignette params not stored: %+v", effects)
|
|
}
|
|
if vignette["mode"] != photos.VignetteColor {
|
|
t.Fatalf("vignette mode = %v, want %q", vignette["mode"], photos.VignetteColor)
|
|
}
|
|
if vignette["color"] != "#ff8800" {
|
|
t.Fatalf("vignette color = %v, want #ff8800", vignette["color"])
|
|
}
|
|
if vignette["midpoint"] != float64(60) {
|
|
t.Fatalf("vignette midpoint = %v, want 60", vignette["midpoint"])
|
|
}
|
|
grain, ok := effects["grain"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("grain params not stored: %+v", effects)
|
|
}
|
|
if grain["size"] != float64(30) || grain["roughness"] != float64(60) {
|
|
t.Fatalf("grain params = %+v, want size 30 roughness 60", grain)
|
|
}
|
|
first := previewPixels(t, st, got.EditPreviewKey)
|
|
|
|
reset, err := st.photos.ResetEdit(ctx, p.ID, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("reset: %v", err)
|
|
}
|
|
if len(reset.EditParams) != 0 {
|
|
t.Fatalf("reset kept params: %+v", reset.EditParams)
|
|
}
|
|
if reset.EditURL != "" || reset.EditPreviewURL != "" || reset.EditPreviewKey != "" {
|
|
t.Fatalf("reset kept the edited version: %+v", reset)
|
|
}
|
|
|
|
// The same parameters and the same photo id must render the same grain.
|
|
if _, err := st.photos.EditPhoto(ctx, p.ID, user.ID, params); err != nil {
|
|
t.Fatalf("edit again: %v", err)
|
|
}
|
|
w.ProcessOnce(ctx)
|
|
again, err := st.photos.Get(ctx, p.ID, user.ID)
|
|
if err != nil {
|
|
t.Fatalf("get after second edit: %v", err)
|
|
}
|
|
if again.EditPreviewURL == "" || again.EditPreviewKey == "" {
|
|
t.Fatalf("no edited preview after second edit: %+v", again)
|
|
}
|
|
second := previewPixels(t, st, again.EditPreviewKey)
|
|
if !bytes.Equal(first, second) {
|
|
t.Fatal("repeated render with the same grain parameters is not deterministic")
|
|
}
|
|
}
|
|
|
|
// previewPixels decodes an edited preview object into its pixel bytes.
|
|
func previewPixels(t *testing.T, st *stack, key string) []uint8 {
|
|
t.Helper()
|
|
data, err := st.storage.Get(context.Background(), key)
|
|
if err != nil {
|
|
t.Fatalf("read preview object: %v", err)
|
|
}
|
|
img, err := webp.Decode(bytes.NewReader(data))
|
|
if err != nil {
|
|
t.Fatalf("decode preview: %v", err)
|
|
}
|
|
return imaging.Clone(img).Pix
|
|
}
|