diff --git a/imop/blend.go b/imop/blend.go index ce5a4f6..37908ad 100644 --- a/imop/blend.go +++ b/imop/blend.go @@ -11,40 +11,174 @@ package imop import ( + "fmt" + "math" + "sort" + "github.com/esimov/caire/utils" ) const ( - Darken = "darken" - Lighten = "lighten" - Multiply = "multiply" - Screen = "screen" - Overlay = "overlay" + Normal = "normal" + Darken = "darken" + Lighten = "lighten" + Multiply = "multiply" + Screen = "screen" + Overlay = "overlay" + SoftLight = "soft_light" + HardLight = "hard_light" + ColorDodge = "color_dodge" + ColorBurn = "color_burn" + Difference = "difference" + Exclusion = "exclusion" + + // Non-separable blend modes + Hue = "hue" + Saturation = "saturation" + ColorMode = "color" + Luminosity = "luminosity" ) -// Blend holds the currently active blend mode. +// Blend struct contains the currently active blend mode and all the supported blend modes. type Blend struct { - OpType string + Current string + Modes []string +} + +// Color represents the RGB channel of a specific color. +type Color struct { + R, G, B float64 } // NewBlend initializes a new Blend. func NewBlend() *Blend { - return &Blend{} -} - -// Set activate one of the supported blend mode. -func (o *Blend) Set(opType string) { - bModes := []string{Darken, Lighten, Multiply, Screen, Overlay} - - if utils.Contains(bModes, opType) { - o.OpType = opType + return &Blend{ + Modes: []string{ + Normal, + Darken, + Lighten, + Multiply, + Screen, + Overlay, + SoftLight, + HardLight, + ColorDodge, + ColorBurn, + Difference, + Exclusion, + Hue, + Saturation, + ColorMode, + Luminosity, + }, } } -// Get returns the currently active blend mode. -func (o *Blend) Get() string { - if len(o.OpType) > 0 { - return o.OpType +// Set activate one of the supported blend modes. +func (bl *Blend) Set(blendType string) error { + if utils.Contains(bl.Modes, blendType) { + bl.Current = blendType + return nil } - return "" + return fmt.Errorf("unsupported blend mode") +} + +// Get returns the active blend mode. +func (bl *Blend) Get() string { + return bl.Current +} + +// Lum gets the luminosity of a color. +func (bl *Blend) Lum(rgb Color) float64 { + return 0.3*rgb.R + 0.59*rgb.G + 0.11*rgb.B +} + +// SetLum set the luminosity on a color. +func (bl *Blend) SetLum(rgb Color, l float64) Color { + delta := l - bl.Lum(rgb) + return bl.clip(Color{ + rgb.R + delta, + rgb.G + delta, + rgb.B + delta, + }) +} + +// clip clips the channels of a color between certain min and max values. +func (bl *Blend) clip(rgb Color) Color { + r, g, b := rgb.R, rgb.G, rgb.B + + l := bl.Lum(rgb) + min := utils.Min(r, g, b) + max := utils.Max(r, g, b) + + if min < 0 { + r = l + (((r - l) * l) / (l - min)) + g = l + (((g - l) * l) / (l - min)) + b = l + (((b - l) * l) / (l - min)) + } + if max > 1 { + r = l + (((r - l) * (1 - l)) / (max - l)) + g = l + (((g - l) * (1 - l)) / (max - l)) + b = l + (((b - l) * (1 - l)) / (max - l)) + } + + return Color{R: r, G: g, B: b} +} + +// Sat gets the saturation of a color. +func (bl *Blend) Sat(rgb Color) float64 { + return utils.Max(rgb.R, rgb.G, rgb.B) - utils.Min(rgb.R, rgb.G, rgb.B) +} + +// channel is a key/value struct pair used for sorting the color channels +// based on the color components having the minimum, middle, and maximum +// values upon entry to the function. +// The key component holds the channel name and val is the value it has. +type channel struct { + key string + val float64 +} + +func (bl *Blend) SetSat(rgb Color, s float64) Color { + color := map[string]float64{ + "R": rgb.R, + "G": rgb.G, + "B": rgb.B, + } + channels := make([]channel, 0, 3) + for k, v := range color { + channels = append(channels, channel{k, v}) + } + // Sort the color channels based on their values. + sort.Slice(channels, func(i, j int) bool { return channels[i].val < channels[j].val }) + minChan, midChan, maxChan := channels[0].key, channels[1].key, channels[2].key + + if color[maxChan] > color[minChan] { + color[midChan] = (((color[midChan] - color[minChan]) * s) / (color[maxChan] - color[minChan])) + color[maxChan] = s + } else { + color[midChan], color[maxChan] = 0, 0 + } + color[minChan] = 0 + + return Color{ + R: color["R"], + G: color["G"], + B: color["B"], + } +} + +// Applies the alpha blending formula for a blend operation. +// See: https://www.w3.org/TR/compositing-1/#blending +func (bl *Blend) AlphaCompose( + backdropAlpha, + sourceAlpha, + compositeAlpha, + backdropColor, + sourceColor, + compositeColor float64, +) float64 { + return ((1 - sourceAlpha/compositeAlpha) * backdropColor) + + (sourceAlpha / compositeAlpha * + math.Round((1-backdropAlpha)*sourceColor+backdropAlpha*compositeColor)) } diff --git a/imop/blend_test.go b/imop/blend_test.go new file mode 100644 index 0000000..7d54fbc --- /dev/null +++ b/imop/blend_test.go @@ -0,0 +1,204 @@ +package imop + +import ( + "image" + "image/color" + "image/draw" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestBlend_Basic(t *testing.T) { + assert := assert.New(t) + + op := NewBlend() + assert.Empty(op.Get()) + err := op.Set("blend_mode_not_supported") + assert.Error(err) + op.Set(Darken) + assert.Equal(Darken, op.Get()) + op.Set(Lighten) + assert.Equal(Lighten, op.Get()) + + rgb := Color{R: 0xff, G: 0xff, B: 0xff} + lum := op.Lum(rgb) + assert.Equal(255.0, lum) + + rgb = Color{R: 0, G: 0, B: 0} + lum = op.Lum(rgb) + assert.Equal(0.0, lum) + + rgb = Color{R: 127, G: 127, B: 127} + lum = op.Lum(rgb) + assert.Equal(127.0, lum) + + foreground := Color{R: 0xff, G: 0xff, B: 0xff} + background := Color{R: 0, G: 0, B: 0} + + assert.Equal(0.0, op.Sat(foreground)) + sat := op.SetSat(background, op.Sat(foreground)) + assert.Equal(Color{R: 0, G: 0, B: 0}, sat) +} + +func TestBlend_Modes(t *testing.T) { + // Note: all the expected values are taken by using as reference the results + // obtained in Photoshop by overlapping two layers and applying the blend mode. + assert := assert.New(t) + + op := InitOp() + blend := NewBlend() + + pinkFront := color.RGBA{R: 214, G: 20, B: 65, A: 255} + orangeBack := color.RGBA{R: 250, G: 121, B: 17, A: 255} + + rect := image.Rect(0, 0, 1, 1) + bmp := NewBitmap(rect) + source := image.NewNRGBA(rect) + backdrop := image.NewNRGBA(rect) + + op.Set(SrcOver) + + // Darken + blend.Set(Darken) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected := []uint8{214, 20, 17, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Multiply + blend.Set(Multiply) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{209, 9, 4, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Screen + blend.Set(Screen) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{254, 131, 77, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Overlay + blend.Set(Overlay) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{253, 18, 8, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // SoftLight + blend.Set(SoftLight) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{232, 19, 23, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // HardLight + blend.Set(HardLight) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{251, 67, 9, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // ColorDodge + blend.Set(ColorDodge) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{255, 131, 22, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // ColorBurn + blend.Set(ColorBurn) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{249, 0, 0, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Difference + blend.Set(Difference) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{35, 101, 48, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Exclusion + blend.Set(Exclusion) + draw.Draw(source, rect, &image.Uniform{pinkFront}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{orangeBack}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{44, 122, 73, 255} + assert.EqualValues(expected, bmp.Img.Pix) +} + +func TestBlend_NonSeparableModes(t *testing.T) { + assert := assert.New(t) + + op := InitOp() + blend := NewBlend() + + frontColor := color.RGBA{R: 250, G: 121, B: 17, A: 255} + backColor := color.RGBA{R: 214, G: 20, B: 65, A: 255} + + rect := image.Rect(0, 0, 1, 1) + bmp := NewBitmap(rect) + source := image.NewNRGBA(rect) + backdrop := image.NewNRGBA(rect) + + op.Set(SrcOver) + + // Hue + blend.Set(Hue) + draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected := []uint8{255, 97, 133, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Saturation + blend.Set(Saturation) + draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{233, 126, 39, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Color + blend.Set(ColorMode) + draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{255, 97, 133, 255} + assert.EqualValues(expected, bmp.Img.Pix) + + // Luminosity + blend.Set(Luminosity) + draw.Draw(source, rect, &image.Uniform{frontColor}, image.Point{}, draw.Src) + draw.Draw(backdrop, rect, &image.Uniform{backColor}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, blend) + + expected = []uint8{148, 66, 0, 255} + assert.EqualValues(expected, bmp.Img.Pix) +} diff --git a/imop/composite.go b/imop/composite.go index abc8ff2..4383164 100644 --- a/imop/composite.go +++ b/imop/composite.go @@ -3,16 +3,13 @@ // Porter and Duff presented in their paper 12 different composition operation, but the // core image/draw core package implements only the source-over-destination and source. // This package implements all of the 12 composite operation together with some blending modes. - -// This package is used mainly to debug the seam carving operation -// when the face detection option and the image mask is enabled. -// When the GUI mode and the debugging option is activated it will show -// the image mask and the detected faces in a distinct color. package imop import ( + "fmt" "image" "image/color" + "math" "github.com/esimov/caire/utils" ) @@ -20,7 +17,7 @@ import ( const ( Clear = "clear" Copy = "copy" - Dest = "dst" + Dst = "dst" SrcOver = "src_over" DstOver = "dst_over" SrcIn = "src_in" @@ -38,10 +35,10 @@ type Bitmap struct { Img *image.NRGBA } -// Composite defines a struct with the active and all the supported composition operations. +// Composite struct contains the currently active composition operation and all the supported operations. type Composite struct { - currentOp string - ops []string + CurrentOp string + Ops []string } // NewBitmap initializes a new Bitmap. @@ -54,9 +51,11 @@ func NewBitmap(rect image.Rectangle) *Bitmap { // InitOp initializes a new composition operation. func InitOp() *Composite { return &Composite{ - currentOp: Copy, - ops: []string{ + CurrentOp: SrcOver, + Ops: []string{ + Clear, Copy, + Dst, SrcOver, DstOver, SrcIn, @@ -71,191 +70,400 @@ func InitOp() *Composite { } // Set changes the current composition operation. -func (op *Composite) Set(cop string) { - op.currentOp = cop +func (op *Composite) Set(cop string) error { + if utils.Contains(op.Ops, cop) { + op.CurrentOp = cop + return nil + } + return fmt.Errorf("unsupported composition operation") +} + +// Set changes the current composition operation. +func (op *Composite) Get() string { + return op.CurrentOp } // Draw applies the currently active Ported-Duff composition operation formula, // taking as parameter the source and the destination image and draws the result into the bitmap. // If a blend mode is activated it will plug in the alpha blending formula also into the equation. -func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend *Blend) { +func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, bl *Blend) { dx, dy := src.Bounds().Dx(), src.Bounds().Dy() - if bitmap == nil { - bitmap = NewBitmap(src.Bounds()) - } var ( r, g, b, a uint32 rn, gn, bn, an float64 ) - if utils.Contains(op.ops, op.currentOp) { - for x := 0; x < dx; x++ { - for y := 0; y < dy; y++ { - r1, g1, b1, a1 := src.At(x, y).RGBA() - r2, g2, b2, a2 := dst.At(x, y).RGBA() + for x := 0; x < dx; x++ { + for y := 0; y < dy; y++ { + r1, g1, b1, a1 := src.At(x, y).RGBA() + r2, g2, b2, a2 := dst.At(x, y).RGBA() - rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8 - rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8 + rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8 + rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8 - // normalize the values. - rsn := float64(rs) / 255 - gsn := float64(gs) / 255 - bsn := float64(bs) / 255 - asn := float64(as) / 255 + // normalize the values. + rsn := float64(rs) / 255 + gsn := float64(gs) / 255 + bsn := float64(bs) / 255 + asn := float64(as) / 255 - rbn := float64(rb) / 255 - gbn := float64(gb) / 255 - bbn := float64(bb) / 255 - abn := float64(ab) / 255 + rbn := float64(rb) / 255 + gbn := float64(gb) / 255 + bbn := float64(bb) / 255 + abn := float64(ab) / 255 - // applying the alpha composition formula - switch op.currentOp { - case Clear: - rn, gn, bn, an = 0, 0, 0, 0 - case Copy: - rn = asn * rsn - gn = asn * gsn - bn = asn * bsn - an = asn * asn - case Dest: - rn = abn * rbn - gn = abn * gbn - bn = abn * bbn - an = abn * abn - case SrcOver: - rn = asn*rsn + abn*rbn*(1-asn) - gn = asn*gsn + abn*gbn*(1-asn) - bn = asn*bsn + abn*bbn*(1-asn) - an = asn + abn*(1-asn) - case DstOver: - rn = asn*rsn*(1-abn) + abn*rbn - gn = asn*gsn*(1-abn) + abn*gbn - bn = asn*bsn*(1-abn) + abn*bbn - an = asn*(1-abn) + abn - case SrcIn: - rn = asn * rsn * abn - gn = asn * gsn * abn - bn = asn * bsn * abn - an = asn * abn - case DstIn: - rn = abn * rbn * asn - gn = abn * gbn * asn - bn = abn * bbn * asn - an = abn * asn - case SrcOut: - rn = asn * rsn * (1 - abn) - gn = asn * gsn * (1 - abn) - bn = asn * bsn * (1 - abn) - an = asn * (1 - abn) - case DstOut: - rn = abn * rbn * (1 - asn) - gn = abn * gbn * (1 - asn) - bn = abn * bbn * (1 - asn) - an = abn * (1 - asn) - case SrcAtop: - rn = asn*rsn*abn + (1-asn)*abn*rbn - gn = asn*gsn*abn + (1-asn)*abn*gbn - bn = asn*bsn*abn + (1-asn)*abn*bbn - an = asn*abn + abn*(1-asn) - case DstAtop: - rn = asn*rsn*(1-abn) + abn*rbn*asn - gn = asn*gsn*(1-abn) + abn*gbn*asn - bn = asn*bsn*(1-abn) + abn*bbn*asn - an = asn*(1-abn) + abn*asn - case Xor: - rn = asn*rsn*(1-abn) + abn*rbn*(1-asn) - gn = asn*gsn*(1-abn) + abn*gbn*(1-asn) - bn = asn*bsn*(1-abn) + abn*bbn*(1-asn) - an = asn*(1-abn) + abn*(1-asn) - } - - r = uint32(rn * 255) - g = uint32(gn * 255) - b = uint32(bn * 255) - a = uint32(an * 255) - - bitmap.Img.Set(x, y, color.NRGBA{ - R: uint8(r), - G: uint8(g), - B: uint8(b), - A: uint8(a), - }) - - // applying the blending mode - if blend != nil { - r1, g1, b1, a1 = bitmap.Img.At(x, y).RGBA() - r2, g2, b2, a2 = src.At(x, y).RGBA() - - rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8 - rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8 - - rsn = float64(rs) / 255 - gsn = float64(gs) / 255 - bsn = float64(bs) / 255 - asn = float64(as) / 255 - rbn = float64(rb) / 255 - gbn = float64(gb) / 255 - bbn = float64(bb) / 255 - abn = float64(ab) / 255 - - switch blend.OpType { - case Darken: - rn = utils.Min(rsn, rbn) - gn = utils.Min(gsn, gbn) - bn = utils.Min(bsn, bbn) - an = utils.Min(asn, abn) - case Lighten: - rn = utils.Max(rsn, rbn) - gn = utils.Max(gsn, gbn) - bn = utils.Max(bsn, bbn) - an = utils.Max(asn, abn) - case Screen: - rn = 1 - (1-rsn)*(1-rbn) - gn = 1 - (1-gsn)*(1-gbn) - bn = 1 - (1-bsn)*(1-bbn) - an = 1 - (1-asn)*(1-abn) - case Multiply: - rn = rsn * rbn - gn = gsn * gbn - bn = bsn * bbn - an = asn * abn - case Overlay: - if rsn <= 0.5 { - rn = 2 * rsn * rbn - } else { - rn = 1 - 2*(1-rsn)*(1-rbn) - } - if gsn <= 0.5 { - gn = 2 * gsn * gbn - } else { - gn = 1 - 2*(1-gsn)*(1-gbn) - } - if bsn <= 0.5 { - bn = 2 * bsn * bbn - } else { - bn = 1 - 2*(1-bsn)*(1-bbn) - } - if asn <= 0.5 { - an = 2 * asn * abn - } else { - an = 1 - 2*(1-asn)*(1-abn) - } - } - } - - r = uint32(rn * 255) - g = uint32(gn * 255) - b = uint32(bn * 255) - a = uint32(an * 255) - - bitmap.Img.Set(x, y, color.NRGBA{ - R: uint8(r), - G: uint8(g), - B: uint8(b), - A: uint8(a), - }) + // applying the alpha composition formula + switch op.CurrentOp { + case Clear: + rn, gn, bn, an = 0, 0, 0, 0 + case Copy: + rn = asn * rsn + gn = asn * gsn + bn = asn * bsn + an = asn * asn + case Dst: + rn = abn * rbn + gn = abn * gbn + bn = abn * bbn + an = abn * abn + case SrcOver: + rn = asn*rsn + abn*rbn*(1-asn) + gn = asn*gsn + abn*gbn*(1-asn) + bn = asn*bsn + abn*bbn*(1-asn) + an = asn + abn*(1-asn) + case DstOver: + rn = asn*rsn*(1-abn) + abn*rbn + gn = asn*gsn*(1-abn) + abn*gbn + bn = asn*bsn*(1-abn) + abn*bbn + an = asn*(1-abn) + abn + case SrcIn: + rn = asn * rsn * abn + gn = asn * gsn * abn + bn = asn * bsn * abn + an = asn * abn + case DstIn: + rn = abn * rbn * asn + gn = abn * gbn * asn + bn = abn * bbn * asn + an = abn * asn + case SrcOut: + rn = asn * rsn * (1 - abn) + gn = asn * gsn * (1 - abn) + bn = asn * bsn * (1 - abn) + an = asn * (1 - abn) + case DstOut: + rn = abn * rbn * (1 - asn) + gn = abn * gbn * (1 - asn) + bn = abn * bbn * (1 - asn) + an = abn * (1 - asn) + case SrcAtop: + rn = asn*rsn*abn + (1-asn)*abn*rbn + gn = asn*gsn*abn + (1-asn)*abn*gbn + bn = asn*bsn*abn + (1-asn)*abn*bbn + an = asn*abn + abn*(1-asn) + case DstAtop: + rn = asn*rsn*(1-abn) + abn*rbn*asn + gn = asn*gsn*(1-abn) + abn*gbn*asn + bn = asn*bsn*(1-abn) + abn*bbn*asn + an = asn*(1-abn) + abn*asn + case Xor: + rn = asn*rsn*(1-abn) + abn*rbn*(1-asn) + gn = asn*gsn*(1-abn) + abn*gbn*(1-asn) + bn = asn*bsn*(1-abn) + abn*bbn*(1-asn) + an = asn*(1-abn) + abn*(1-asn) } + + r = uint32(rn * 255) + g = uint32(gn * 255) + b = uint32(bn * 255) + a = uint32(an * 255) + + bitmap.Img.Set(x, y, color.NRGBA{ + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(a), + }) + + // applying the blending mode + if bl != nil { + rn, gn, bn, an = 0, 0, 0, 0 // reset the colors + r1, g1, b1, a1 = src.At(x, y).RGBA() + r2, g2, b2, a2 = dst.At(x, y).RGBA() + + rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8 + rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8 + + rsn = float64(rs) / 255 + gsn = float64(gs) / 255 + bsn = float64(bs) / 255 + asn = float64(as) / 255 + + rbn = float64(rb) / 255 + gbn = float64(gb) / 255 + bbn = float64(bb) / 255 + abn = float64(ab) / 255 + + foreground := Color{R: rsn, G: gsn, B: bsn} + background := Color{R: rbn, G: gbn, B: bbn} + + switch bl.Current { + case Normal: + rn, gn, bn, an = rsn, gsn, bsn, asn + case Darken: + rn = utils.Min(rsn, rbn) + gn = utils.Min(gsn, gbn) + bn = utils.Min(bsn, bbn) + an = utils.Min(asn, abn) + case Lighten: + rn = utils.Max(rsn, rbn) + gn = utils.Max(gsn, gbn) + bn = utils.Max(bsn, bbn) + an = utils.Max(asn, abn) + case Screen: + rn = 1 - (1-rsn)*(1-rbn) + gn = 1 - (1-gsn)*(1-gbn) + bn = 1 - (1-bsn)*(1-bbn) + an = 1 - (1-asn)*(1-abn) + case Multiply: + rn = rsn * rbn + gn = gsn * gbn + bn = bsn * bbn + an = asn * abn + case Overlay: + if rsn <= 0.5 { + rn = 2 * rsn * rbn + } else { + rn = 1 - 2*(1-rsn)*(1-rbn) + } + + if gsn <= 0.5 { + gn = 2 * gsn * gbn + } else { + gn = 1 - 2*(1-gsn)*(1-gbn) + } + + if bsn <= 0.5 { + bn = 2 * bsn * bbn + } else { + bn = 1 - 2*(1-bsn)*(1-bbn) + } + + if asn <= 0.5 { + an = 2 * asn * abn + } else { + an = 1 - 2*(1-asn)*(1-abn) + } + case SoftLight: + if rbn < 0.5 { + rn = rsn - (1-2*rbn)*rsn*(1-rsn) + } else { + var w3r float64 + if rsn < 0.25 { + w3r = ((16*rsn-12)*rsn + 4) * rsn + } else { + w3r = math.Sqrt(rsn) + } + rn = rsn + (2*rbn-1)*(w3r-rsn) + } + + if gbn < 0.5 { + gn = gsn - (1-2*gbn)*gsn*(1-gsn) + } else { + var w3g float64 + if gsn < 0.25 { + w3g = ((16*gsn-12)*gsn + 4) * gsn + } else { + w3g = math.Sqrt(gsn) + } + gn = gsn + (2*gbn-1)*(w3g-gsn) + } + + if bbn < 0.5 { + bn = bsn - (1-2*bbn)*bsn*(1-bsn) + } else { + var w3b float64 + if bsn < 0.25 { + w3b = ((16*bsn-12)*bsn + 4) * bsn + } else { + w3b = math.Sqrt(bsn) + } + bn = bsn + (2*bbn-1)*(w3b-bsn) + } + + if abn < 0.5 { + an = asn - (1-2*abn)*asn*(1-asn) + } else { + var w3a float64 + if asn < 0.25 { + w3a = ((16*asn-12)*asn + 4) * asn + } else { + w3a = math.Sqrt(asn) + } + an = asn + (2*abn-1)*(w3a-asn) + } + case HardLight: + if rbn < 0.5 { + rn = rbn - (1-2*rsn)*rbn*(1-rbn) + } else { + var w3r float64 + if rbn < 0.25 { + w3r = ((16*rbn-12)*rbn + 4) * rbn + } else { + w3r = math.Sqrt(rbn) + } + rn = rbn + (2*rsn-1)*(w3r-rbn) + } + + if gbn < 0.5 { + gn = gbn - (1-2*gsn)*gbn*(1-gbn) + } else { + var w3g float64 + if gbn < 0.25 { + w3g = ((16*gbn-12)*gbn + 4) * gbn + } else { + w3g = math.Sqrt(gbn) + } + gn = gbn + (2*gsn-1)*(w3g-gbn) + } + + if bbn < 0.5 { + bn = bbn - (1-2*bsn)*bbn*(1-bbn) + } else { + var w3b float64 + if bbn < 0.25 { + w3b = ((16*bbn-12)*bbn + 4) * bbn + } else { + w3b = math.Sqrt(bbn) + } + bn = bbn + (2*bsn-1)*(w3b-bbn) + } + + if abn < 0.5 { + an = abn - (1-2*asn)*abn*(1-abn) + } else { + var w3a float64 + if abn < 0.25 { + w3a = ((16*abn-12)*abn + 4) * abn + } else { + w3a = math.Sqrt(abn) + } + an = abn + (2*asn-1)*(w3a-abn) + } + case ColorDodge: + if rsn < 1 { + rn = utils.Min(1, rbn/(1-rsn)) + } else if rsn == 1 { + rn = 1 + } + + if gsn < 1 { + gn = utils.Min(1, gbn/(1-gsn)) + } else if gsn == 1 { + gn = 1 + } + + if bsn < 1 { + bn = utils.Min(1, bbn/(1-bsn)) + } else if bsn == 1 { + bn = 1 + } + + if asn < 1 { + an = utils.Min(1, abn/(1-asn)) + } else if asn == 1 { + an = 1 + } + case ColorBurn: + if rsn > 0 { + rn = 1 - utils.Min(1, (1-rbn)/rsn) + } else if rsn == 0 { + rn = 0 + } + + if gsn > 0 { + gn = 1 - utils.Min(1, (1-gbn)/gsn) + } else if gsn == 0 { + gn = 0 + } + + if bsn > 0 { + bn = 1 - utils.Min(1, (1-bbn)/bsn) + } else if bsn == 0 { + bn = 0 + } + + if asn > 0 { + an = 1 - utils.Min(1, (1-abn)/asn) + } else if asn == 0 { + an = 0 + } + case Difference: + rn = utils.Abs(rbn - rsn) + gn = utils.Abs(gbn - gsn) + bn = utils.Abs(bbn - bsn) + an = 1 + case Exclusion: + rn = rsn + rbn - 2*rsn*rbn + gn = gsn + gbn - 2*gsn*gbn + bn = bsn + bbn - 2*bsn*bbn + an = 1 + + // Non-separable blend modes + // https://www.w3.org/TR/compositing-1/#blendingnonseparable + case Hue: + sat := bl.SetSat(background, bl.Sat(foreground)) + rgb := bl.SetLum(sat, bl.Lum(foreground)) + + a := asn + abn - asn*abn + rn = bl.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = bl.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = bl.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + case Saturation: + sat := bl.SetSat(foreground, bl.Sat(background)) + rgb := bl.SetLum(sat, bl.Lum(foreground)) + + a := asn + abn - asn*abn + rn = bl.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = bl.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = bl.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + case ColorMode: + rgb := bl.SetLum(background, bl.Lum(foreground)) + + a := asn + abn - asn*abn + rn = bl.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = bl.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = bl.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + case Luminosity: + rgb := bl.SetLum(foreground, bl.Lum(background)) + + a := asn + abn - asn*abn + rn = bl.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255) + gn = bl.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255) + bn = bl.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255) + rn, gn, bn = rn/255, gn/255, bn/255 + an = a + } + } + + r = uint32(rn * 255) + g = uint32(gn * 255) + b = uint32(bn * 255) + a = uint32(an * 255) + + bitmap.Img.Set(x, y, color.NRGBA{ + R: uint8(r), + G: uint8(g), + B: uint8(b), + A: uint8(a), + }) } } } diff --git a/imop/composite_test.go b/imop/composite_test.go new file mode 100644 index 0000000..d4581d9 --- /dev/null +++ b/imop/composite_test.go @@ -0,0 +1,210 @@ +package imop + +import ( + "image" + "image/color" + "image/draw" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestComp_Basic(t *testing.T) { + assert := assert.New(t) + + op := InitOp() + err := op.Set("unsupported_composite_operation") + assert.Error(err) + + op.Set(Clear) + assert.Equal(Clear, op.Get()) + assert.NotEqual("unsupported_composite_operation", op.Get()) + + op.Set(Dst) + assert.Equal(Dst, op.Get()) +} + +func TestComp_Ops(t *testing.T) { + assert := assert.New(t) + op := InitOp() + + transparent := color.NRGBA{R: 0, G: 0, B: 0, A: 0} + cyan := color.NRGBA{R: 33, G: 150, B: 243, A: 255} + magenta := color.NRGBA{R: 233, G: 30, B: 99, A: 255} + + rect := image.Rect(0, 0, 10, 10) + bmp := NewBitmap(rect) + source := image.NewNRGBA(rect) + backdrop := image.NewNRGBA(rect) + + // No composition operation applied. The SrcOver is the default one. + draw.Draw(source, image.Rect(0, 4, 6, 10), &image.Uniform{cyan}, image.Point{}, draw.Src) + draw.Draw(backdrop, image.Rect(4, 0, 10, 6), &image.Uniform{magenta}, image.Point{}, draw.Src) + op.Draw(bmp, source, backdrop, nil) + + // Pick three representative points/pixels from the generated image output. + // Depending on the applied composition operation the colors of the + // selected pixels should be the source color, the destination color or transparent. + topRight := bmp.Img.At(9, 0) + bottomLeft := bmp.Img.At(0, 9) + center := bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, cyan) + + // Clear + op.Set(Clear) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, transparent) + + // Copy + op.Set(Copy) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, cyan) + + // Dst + op.Set(Dst) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, magenta) + + // SrcOver + op.Set(SrcOver) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, cyan) + + // DstOver + op.Set(DstOver) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, magenta) + + // SrcIn + op.Set(SrcIn) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, cyan) + + // DstIn + op.Set(DstIn) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, magenta) + + // SrcOut + op.Set(SrcOut) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, transparent) + + // DstOut + op.Set(DstOut) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, transparent) + + // SrcAtop + op.Set(SrcAtop) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, transparent) + assert.EqualValues(center, cyan) + + // DstAtop + op.Set(DstAtop) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, magenta) + + // Xor + op.Set(Xor) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, magenta) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, transparent) + // DstAtop + op.Set(DstAtop) + op.Draw(bmp, source, backdrop, nil) + + topRight = bmp.Img.At(9, 0) + bottomLeft = bmp.Img.At(0, 9) + center = bmp.Img.At(5, 5) + + assert.EqualValues(topRight, transparent) + assert.EqualValues(bottomLeft, cyan) + assert.EqualValues(center, magenta) +}