feat(photos): добавить эффекты и настройки панели редактора
- текстура, чёткость, дымка, виньетка и зерно в неразрушающем редакторе - живой WebGL2-предпросмотр эффектов и CPU-фолбэк - секции редактора свёрнуты по умолчанию, состояние в localStorage - вкладка «Редактирование» доступна только из просмотрщика
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user