feat: extended imop package with new features, test cases

This commit is contained in:
esimov
2022-12-10 07:37:56 +02:00
parent a099a985d1
commit 35d7d0d00f
4 changed files with 956 additions and 200 deletions
+155 -21
View File
@@ -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))
}
+204
View File
@@ -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)
}
+387 -179
View File
@@ -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),
})
}
}
}
+210
View File
@@ -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)
}