feat(web): вкладка «Редактирование» с инструментом «Свет»
В правой панели фото появилась вкладка «Редактирование» с инструментом «Свет»: ползунки экспозиции, контраста, светов, теней, белых и чёрных и точечная кривая с выбором канала. Правки сохраняются автоматически, переключатель «Сброс» удаляет параметры правок и отредактированную версию фото. Редактирование неразрушающее: параметры хранятся в метаданных фото, а отредактированная версия рендерится в фоне отдельным объектом (edit, edit_thumb, edit_medium, edit_preview) рядом с исходными вариантами. Оригинал не меняется, скачивание отдаёт исходный файл. Сетка, просмотр и обложки альбомов показывают отредактированную версию, когда она отрендерена.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package photos
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/gen2brain/webp"
|
||||
)
|
||||
|
||||
// Object roles holding the non-destructive edited version of a photo. They
|
||||
// mirror the base roles and are bound in addition to them.
|
||||
const (
|
||||
RoleEdit = "edit"
|
||||
RoleEditThumb = "edit_thumb"
|
||||
RoleEditMedium = "edit_medium"
|
||||
RoleEditPreview = "edit_preview"
|
||||
)
|
||||
|
||||
// applyLight renders the light adjustments into a copy of the image. The
|
||||
// original pixels are never modified in place.
|
||||
func applyLight(src image.Image, p LightParams) *image.NRGBA {
|
||||
// Clone returns a fresh NRGBA copy, so the source stays untouched.
|
||||
dst := imaging.Clone(src)
|
||||
|
||||
master := curveLUT(p.Curve.RGB)
|
||||
red := curveLUT(p.Curve.Red)
|
||||
green := curveLUT(p.Curve.Green)
|
||||
blue := curveLUT(p.Curve.Blue)
|
||||
|
||||
width := dst.Rect.Dx()
|
||||
for y := 0; y < dst.Rect.Dy(); y++ {
|
||||
row := dst.Pix[y*dst.Stride : y*dst.Stride+width*4]
|
||||
for i := 0; i+3 < len(row); i += 4 {
|
||||
if row[i+3] == 0 {
|
||||
continue
|
||||
}
|
||||
row[i] = uint8(adjustChannel(float64(row[i]), p, &master, &red))
|
||||
row[i+1] = uint8(adjustChannel(float64(row[i+1]), p, &master, &green))
|
||||
row[i+2] = uint8(adjustChannel(float64(row[i+2]), p, &master, &blue))
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// buildEditedVariants renders the edited version of a photo: the full-size
|
||||
// image plus the thumb, medium and preview variants. The variants mirror the
|
||||
// base ones, so the grid and the viewer can switch to them transparently.
|
||||
func buildEditedVariants(data []byte, p EditParams) (thumb, medium, preview, full []byte, err error) {
|
||||
img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
|
||||
if err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
edited := applyLight(img, p.Light)
|
||||
|
||||
thumbImg := imaging.Fill(edited, 200, 200, imaging.Center, imaging.Lanczos)
|
||||
mediumImg := imaging.Fill(edited, 800, 800, imaging.Center, imaging.Lanczos)
|
||||
previewImg := imaging.Fit(edited, 1200, 1200, imaging.Lanczos)
|
||||
|
||||
opts := webp.Options{Quality: 82}
|
||||
var buf bytes.Buffer
|
||||
if err := webp.Encode(&buf, thumbImg, opts); err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
thumb = buf.Bytes()
|
||||
|
||||
buf.Reset()
|
||||
if err := webp.Encode(&buf, mediumImg, opts); err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
medium = buf.Bytes()
|
||||
|
||||
buf.Reset()
|
||||
if err := webp.Encode(&buf, previewImg, opts); err != nil {
|
||||
return nil, nil, nil, nil, err
|
||||
}
|
||||
preview = buf.Bytes()
|
||||
|
||||
buf.Reset()
|
||||
if err := webp.Encode(&buf, edited, opts); err != nil {
|
||||
// Полноразмерная версия не обязательна: превью и миниатюры уже
|
||||
// готовы, поэтому ошибку кодирования полной версии не считаем
|
||||
// фатальной.
|
||||
full = nil
|
||||
} else {
|
||||
full = buf.Bytes()
|
||||
}
|
||||
return thumb, medium, preview, full, nil
|
||||
}
|
||||
Reference in New Issue
Block a user