// Package quantize offers an implementation of the draw.Quantize interface using an optimized Median Cut method, // including advanced functionality for fine-grained control of color priority
package quantize import ( ) type bucketPool struct { sync.Pool maxCap int m sync.Mutex } func ( *bucketPool) ( int) colorBucket { .m.Lock() if .maxCap > { .maxCap = .maxCap * 99 / 100 } if .maxCap < { .maxCap = } := .maxCap .m.Unlock() := .Pool.Get() if == nil || cap(.(colorBucket)) < { return make(colorBucket, )[0:] } := .(colorBucket) = [0:] for := range { [] = colorPriority{} } return } var bpool bucketPool // AggregationType specifies the type of aggregation to be done type AggregationType uint8 const ( // Mode - pick the highest priority value Mode AggregationType = iota // Mean - weighted average all values Mean ) // MedianCutQuantizer implements the go draw.Quantizer interface using the Median Cut method type MedianCutQuantizer struct { // The type of aggregation to be used to find final colors Aggregation AggregationType // The weighting function to use on each pixel Weighting func(image.Image, int, int) uint32 // Whether to create a transparent entry AddTransparent bool } //bucketize takes a bucket and performs median cut on it to obtain the target number of grouped buckets func bucketize( colorBucket, int) ( []colorBucket) { if len() == 0 || == 0 { return nil } := = make([]colorBucket, 1, *2) [0] = for len() < && len() < len() { // Limit to palette capacity or number of colors , = [0], [1:] if len() < 2 { = append(, ) continue } else if len() == 2 { = append(, [:1], [1:]) continue } , := .partition() = append(, , ) } return } // palettize finds a single color to represent a set of color buckets func ( MedianCutQuantizer) ( color.Palette, []colorBucket) color.Palette { for , := range { switch .Aggregation { case Mean: := .mean() = append(, ) case Mode: var colorPriority for , := range { if .p > .p { = } } = append(, .RGBA) } } return } // quantizeSlice expands the provided bucket and then palettizes the result func ( MedianCutQuantizer) ( color.Palette, []colorPriority) color.Palette { := cap() - len() := .AddTransparent if { for , := range { if , , , := .RGBA(); == 0 { = false } } if { -- } } := bucketize(, ) = .palettize(, ) if { = append(, color.RGBA{0, 0, 0, 0}) } return } func colorAt( image.Image, int, int) color.RGBA { switch i := .(type) { case *image.YCbCr: := .YOffset(, ) := .COffset(, ) := color.YCbCr{ .Y[], .Cb[], .Cr[], } return color.RGBA{.Y, .Cb, .Cr, 255} case *image.RGBA: := .PixOffset(, ) return color.RGBA{.Pix[+0], .Pix[+1], .Pix[+2], .Pix[+3]} default: return color.RGBAModel.Convert(.At(, )).(color.RGBA) } } // buildBucket creates a prioritized color slice with all the colors in the image func ( MedianCutQuantizer) ( image.Image) ( colorBucket) { := .Bounds() := (.Max.X - .Min.X) * (.Max.Y - .Min.Y) * 2 := bpool.getBucket() for := .Min.Y; < .Max.Y; ++ { for := .Min.X; < .Max.X; ++ { := uint32(1) if .Weighting != nil { = .Weighting(, , ) } if != 0 { := colorAt(, , ) := int(.R)<<16 | int(.G)<<8 | int(.B) for := 1; ; ++ { := &[%] if .p == 0 || .RGBA == { * = colorPriority{.p + , } break } += 1 + } } } } = [:0] switch .(type) { case *image.YCbCr: for , := range { if .p != 0 { , , := color.YCbCrToRGB(.R, .G, .B) = append(, colorPriority{.p, color.RGBA{, , , .A}}) } } default: for , := range { if .p != 0 { = append(, ) } } } return } // Quantize quantizes an image to a palette and returns the palette func ( MedianCutQuantizer) ( color.Palette, image.Image) color.Palette { := .buildBucket() defer bpool.Put() return .quantizeSlice(, ) }