- текстура, чёткость, дымка, виньетка и зерно в неразрушающем редакторе - живой WebGL2-предпросмотр эффектов и CPU-фолбэк - секции редактора свёрнуты по умолчанию, состояние в localStorage - вкладка «Редактирование» доступна только из просмотрщика
199 lines
5.7 KiB
Go
199 lines
5.7 KiB
Go
package photos
|
|
|
|
import (
|
|
"bytes"
|
|
"image"
|
|
"runtime"
|
|
"sync"
|
|
|
|
"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"
|
|
)
|
|
|
|
// EditRoles lists the roles of the edited version. The edited version belongs
|
|
// to the photo whose edits produced it, so copies of that photo never share
|
|
// these roles.
|
|
func EditRoles() []string {
|
|
return []string{RoleEdit, RoleEditThumb, RoleEditMedium, RoleEditPreview}
|
|
}
|
|
|
|
// IsEditRole reports whether the role belongs to the edited version.
|
|
func IsEditRole(role string) bool {
|
|
switch role {
|
|
case RoleEdit, RoleEditThumb, RoleEditMedium, RoleEditPreview:
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// 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)
|
|
adj := newLightAdjuster(p)
|
|
|
|
rows := dst.Rect.Dy()
|
|
workers := runtime.GOMAXPROCS(0)
|
|
if workers > rows {
|
|
workers = rows
|
|
}
|
|
if workers <= 1 {
|
|
applyLightRows(dst, 0, rows, adj, &master, &red, &green, &blue)
|
|
return dst
|
|
}
|
|
// Рендер больших фото идёт по строкам параллельно.
|
|
chunk := (rows + workers - 1) / workers
|
|
var wg sync.WaitGroup
|
|
for from := 0; from < rows; from += chunk {
|
|
to := from + chunk
|
|
if to > rows {
|
|
to = rows
|
|
}
|
|
wg.Add(1)
|
|
go func(from, to int) {
|
|
defer wg.Done()
|
|
applyLightRows(dst, from, to, adj, &master, &red, &green, &blue)
|
|
}(from, to)
|
|
}
|
|
wg.Wait()
|
|
return dst
|
|
}
|
|
|
|
// applyLightRows applies the adjustments to the given range of rows.
|
|
func applyLightRows(dst *image.NRGBA, from, to int, adj lightAdjuster, master, red, green, blue *[256]uint8) {
|
|
width := dst.Rect.Dx()
|
|
for y := from; y < to; 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(adj.apply(float64(row[i]), master, red))
|
|
row[i+1] = uint8(adj.apply(float64(row[i+1]), master, green))
|
|
row[i+2] = uint8(adj.apply(float64(row[i+2]), master, blue))
|
|
}
|
|
}
|
|
}
|
|
|
|
// decodeAndAdjust decodes the original and applies the light, color and effect
|
|
// adjustments to it, so the variants and the full-size version share one
|
|
// decode. The passes run in a fixed order: light, color, then the effects
|
|
// (dehaze, texture, clarity, vignette, grain). The seed makes the grain render
|
|
// deterministic; it comes from the photo id.
|
|
func decodeAndAdjust(data []byte, p EditParams, seed uint64) (*image.NRGBA, error) {
|
|
img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
edited := applyLight(img, p.Light)
|
|
if !p.Color.IsEmpty() {
|
|
edited = applyColor(edited, p.Color)
|
|
}
|
|
if !p.Effects.IsEmpty() {
|
|
edited = applyEffects(edited, p.Effects, seed)
|
|
}
|
|
return edited, nil
|
|
}
|
|
|
|
// applyColor applies the color adjustments in place to an image that already
|
|
// has the light adjustments applied. The image is owned by the caller.
|
|
func applyColor(dst *image.NRGBA, p ColorParams) *image.NRGBA {
|
|
adj := newColorAdjuster(p)
|
|
|
|
rows := dst.Rect.Dy()
|
|
workers := runtime.GOMAXPROCS(0)
|
|
if workers > rows {
|
|
workers = rows
|
|
}
|
|
if workers <= 1 {
|
|
applyColorRows(dst, 0, rows, adj)
|
|
return dst
|
|
}
|
|
// Рендер больших фото идёт по строкам параллельно.
|
|
chunk := (rows + workers - 1) / workers
|
|
var wg sync.WaitGroup
|
|
for from := 0; from < rows; from += chunk {
|
|
to := from + chunk
|
|
if to > rows {
|
|
to = rows
|
|
}
|
|
wg.Add(1)
|
|
go func(from, to int) {
|
|
defer wg.Done()
|
|
applyColorRows(dst, from, to, adj)
|
|
}(from, to)
|
|
}
|
|
wg.Wait()
|
|
return dst
|
|
}
|
|
|
|
// applyColorRows applies the color adjustments to the given range of rows.
|
|
func applyColorRows(dst *image.NRGBA, from, to int, adj colorAdjuster) {
|
|
width := dst.Rect.Dx()
|
|
for y := from; y < to; 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
|
|
}
|
|
r, g, b := adj.apply(float64(row[i]), float64(row[i+1]), float64(row[i+2]))
|
|
row[i] = uint8(clampFloat(r+0.5, 0, 255))
|
|
row[i+1] = uint8(clampFloat(g+0.5, 0, 255))
|
|
row[i+2] = uint8(clampFloat(b+0.5, 0, 255))
|
|
}
|
|
}
|
|
}
|
|
|
|
// encodeEditedVariants renders the grid variants of an edited photo. They
|
|
// mirror the base ones, so the grid and the viewer can switch to them
|
|
// transparently.
|
|
func encodeEditedVariants(edited *image.NRGBA) (thumb, medium, preview []byte, err error) {
|
|
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, err
|
|
}
|
|
thumb = buf.Bytes()
|
|
|
|
buf.Reset()
|
|
if err := webp.Encode(&buf, mediumImg, opts); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
medium = buf.Bytes()
|
|
|
|
buf.Reset()
|
|
if err := webp.Encode(&buf, previewImg, opts); err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
preview = buf.Bytes()
|
|
return thumb, medium, preview, nil
|
|
}
|
|
|
|
// encodeEdited renders an edited image part as WebP.
|
|
func encodeEdited(edited *image.NRGBA) ([]byte, error) {
|
|
var buf bytes.Buffer
|
|
if err := webp.Encode(&buf, edited, webp.Options{Quality: 82}); err != nil {
|
|
return nil, err
|
|
}
|
|
return buf.Bytes(), nil
|
|
}
|