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 }