Вместо полноразмерной отредактированной версии просмотр запрашивает у сервера только видимую область кадра: координаты относительные, поэтому запрос не зависит от размера оригинала. Рендер куска занимает около 0.15 с против 0.8 с для всей картинки, а полноразмерная версия больше не готовится вовсе. Зум больше не сбрасывается, когда приходит отрендеренная версия правок: состояние просмотра сбрасывается только при смене самого фото.
168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
package photos
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"image/png"
|
|
"math"
|
|
"testing"
|
|
)
|
|
|
|
func TestParseEditParamsClampsValues(t *testing.T) {
|
|
raw := []byte(`{"light":{"exposure":99,"contrast":-500,"highlights":10,"curve":{"rgb":[{"x":2,"y":-1},{"x":0.5,"y":0.5}]}}}`)
|
|
params, err := ParseEditParams(raw)
|
|
if err != nil {
|
|
t.Fatalf("parse: %v", err)
|
|
}
|
|
if params.Light.Exposure != MaxExposureStops {
|
|
t.Fatalf("exposure = %v, want %v", params.Light.Exposure, MaxExposureStops)
|
|
}
|
|
if params.Light.Contrast != -MaxPercent {
|
|
t.Fatalf("contrast = %v, want %v", params.Light.Contrast, -MaxPercent)
|
|
}
|
|
if len(params.Light.Curve.RGB) != 2 {
|
|
t.Fatalf("curve points = %d, want 2", len(params.Light.Curve.RGB))
|
|
}
|
|
// Точки сортируются по X, а значения зажимаются в диапазон 0..1.
|
|
first := params.Light.Curve.RGB[0]
|
|
if first.X != 0.5 || first.Y != 0.5 {
|
|
t.Fatalf("first curve point = %+v, want {0.5 0.5}", first)
|
|
}
|
|
second := params.Light.Curve.RGB[1]
|
|
if second.X != 1 || second.Y != 0 {
|
|
t.Fatalf("second curve point = %+v, want {1 0}", second)
|
|
}
|
|
}
|
|
|
|
func TestEditParamsIsEmpty(t *testing.T) {
|
|
var zero EditParams
|
|
if !zero.IsEmpty() {
|
|
t.Fatal("zero params must be empty")
|
|
}
|
|
zero.Light.Exposure = 0.1
|
|
if zero.IsEmpty() {
|
|
t.Fatal("params with exposure must not be empty")
|
|
}
|
|
curved := EditParams{}
|
|
curved.Light.Curve.RGB = []CurvePoint{{X: 0, Y: 0}, {X: 1, Y: 0.5}}
|
|
if curved.IsEmpty() {
|
|
t.Fatal("params with a bent curve must not be empty")
|
|
}
|
|
linear := EditParams{}
|
|
linear.Light.Curve.RGB = []CurvePoint{{X: 0, Y: 0}, {X: 1, Y: 1}}
|
|
if !linear.IsEmpty() {
|
|
t.Fatal("a linear curve must count as empty")
|
|
}
|
|
}
|
|
|
|
func TestCurveLUTIdentity(t *testing.T) {
|
|
lut := curveLUT(nil)
|
|
for i := 0; i < 256; i++ {
|
|
if int(lut[i]) != i {
|
|
t.Fatalf("lut[%d] = %d, want %d", i, lut[i], i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCurveLUTSteepens(t *testing.T) {
|
|
lut := curveLUT([]CurvePoint{{X: 0, Y: 0}, {X: 0.5, Y: 0.25}, {X: 1, Y: 1}})
|
|
if lut[0] != 0 || lut[255] != 255 {
|
|
t.Fatalf("curve ends = %d..%d, want 0..255", lut[0], lut[255])
|
|
}
|
|
if lut[128] >= 128 {
|
|
t.Fatalf("lut[128] = %d, want a darker midtone", lut[128])
|
|
}
|
|
}
|
|
|
|
func TestApplyLightBrightensAndKeepsSource(t *testing.T) {
|
|
src := image.NewNRGBA(image.Rect(0, 0, 2, 2))
|
|
for i := range src.Pix {
|
|
src.Pix[i] = 100
|
|
}
|
|
src.Pix[3] = 255
|
|
before := make([]byte, len(src.Pix))
|
|
copy(before, src.Pix)
|
|
|
|
edited := applyLight(src, LightParams{Exposure: 1})
|
|
if edited.Pix[0] <= before[0] {
|
|
t.Fatalf("exposure did not brighten: %d <= %d", edited.Pix[0], before[0])
|
|
}
|
|
for i := range src.Pix {
|
|
if src.Pix[i] != before[i] {
|
|
t.Fatalf("source pixel %d changed: %d != %d", i, src.Pix[i], before[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestApplyLightContrast(t *testing.T) {
|
|
src := image.NewNRGBA(image.Rect(0, 0, 1, 1))
|
|
for i := range src.Pix {
|
|
src.Pix[i] = 200
|
|
}
|
|
src.Pix[3] = 255
|
|
edited := applyLight(src, LightParams{Contrast: 50})
|
|
if edited.Pix[0] <= 200 {
|
|
t.Fatalf("contrast did not push the bright tone up: %d", edited.Pix[0])
|
|
}
|
|
// Контраст работает вокруг средней точки: тёмные тона уходят вниз.
|
|
dark := image.NewNRGBA(image.Rect(0, 0, 1, 1))
|
|
for i := range dark.Pix {
|
|
dark.Pix[i] = 60
|
|
}
|
|
dark.Pix[3] = 255
|
|
editedDark := applyLight(dark, LightParams{Contrast: 50})
|
|
if editedDark.Pix[0] >= 60 {
|
|
t.Fatalf("contrast did not push the dark tone down: %d", editedDark.Pix[0])
|
|
}
|
|
}
|
|
|
|
func TestBuildEditedVariants(t *testing.T) {
|
|
src := image.NewNRGBA(image.Rect(0, 0, 400, 300))
|
|
for y := 0; y < 300; y++ {
|
|
for x := 0; x < 400; x++ {
|
|
i := y*src.Stride + x*4
|
|
src.Pix[i] = uint8(x % 256)
|
|
src.Pix[i+1] = uint8(y % 256)
|
|
src.Pix[i+2] = 128
|
|
src.Pix[i+3] = 255
|
|
}
|
|
}
|
|
edited, err := decodeAndAdjust(mustEncodePNG(t, src), EditParams{Light: LightParams{Exposure: 0.5}})
|
|
if err != nil {
|
|
t.Fatalf("decode and adjust: %v", err)
|
|
}
|
|
thumb, medium, preview, err := encodeEditedVariants(edited)
|
|
if err != nil {
|
|
t.Fatalf("encode variants: %v", err)
|
|
}
|
|
full, err := encodeEdited(edited)
|
|
if err != nil {
|
|
t.Fatalf("encode full: %v", err)
|
|
}
|
|
if len(thumb) == 0 || len(medium) == 0 || len(preview) == 0 || len(full) == 0 {
|
|
t.Fatalf("empty variants: %d %d %d %d", len(thumb), len(medium), len(preview), len(full))
|
|
}
|
|
}
|
|
|
|
func TestAdjustChannelClamps(t *testing.T) {
|
|
lut := curveLUT(nil)
|
|
if v := adjustChannel(255, LightParams{Exposure: 5}, &lut, &lut); v != 255 {
|
|
t.Fatalf("value = %v, want 255", v)
|
|
}
|
|
if v := adjustChannel(0, LightParams{Exposure: -5}, &lut, &lut); v != 0 {
|
|
t.Fatalf("value = %v, want 0", v)
|
|
}
|
|
if v := adjustChannel(128, LightParams{Contrast: 0}, &lut, &lut); math.Abs(v-128) > 0.001 {
|
|
t.Fatalf("value = %v, want 128", v)
|
|
}
|
|
}
|
|
|
|
func mustEncodePNG(t *testing.T, img image.Image) []byte {
|
|
t.Helper()
|
|
var buf bytes.Buffer
|
|
if err := png.Encode(&buf, img); err != nil {
|
|
t.Fatalf("encode png: %v", err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|