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" ) // 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 adjustments to // it, so the variants and the full-size version share one decode. func decodeAndAdjust(data []byte, p EditParams) (*image.NRGBA, error) { img, err := imaging.Decode(bytes.NewReader(data), imaging.AutoOrientation(true)) if err != nil { return nil, err } return applyLight(img, p.Light), nil } // 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 }