diff --git a/Gopkg.lock b/Gopkg.lock index 3a8e7062..e4ee747f 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -106,17 +106,17 @@ ] revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8" -[[projects]] - name = "github.com/imdario/mergo" - packages = ["."] - revision = "6ce7536fed6623eff47b95768f858a0f8d1ac57b" - [[projects]] name = "github.com/json-iterator/go" packages = ["."] revision = "0ac74bba4a81211b28e32ef260c0f16ae41f1377" version = "1.1.1" +[[projects]] + name = "github.com/lucasb-eyer/go-colorful" + packages = ["."] + revision = "c7842319cf3ac2eff253e8b3ebe15fcc56b6414a" + [[projects]] name = "github.com/magiconair/properties" packages = ["."] @@ -331,6 +331,6 @@ [solve-meta] analyzer-name = "dep" analyzer-version = 1 - inputs-digest = "f8aabb9e62bc4c0dc3fcc9f74d3880cb6c8d96025fb50610b02d0fc02dec281d" + inputs-digest = "7510fd7021bc0b09307edb3ed5e34495556854b9eb9a5f29d06da3bca2fc87e1" solver-name = "gps-cdcl" solver-version = 1 diff --git a/Gopkg.toml b/Gopkg.toml index d6d015ae..0c90e6ee 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -103,3 +103,7 @@ [[constraint]] name = "github.com/rwcarlsen/goexif" revision = "8d986c03457a2057c7b0fb0a48113f7dd48f9619" + +[[constraint]] + name = "github.com/lucasb-eyer/go-colorful" + revision = "c7842319cf3ac2eff253e8b3ebe15fcc56b6414a" diff --git a/README.rst b/README.rst index 765051a1..e782b199 100644 --- a/README.rst +++ b/README.rst @@ -434,6 +434,18 @@ Rotate rotates the image to the desired degree and returns the transformed image You have to pass the ``rotate`` value to the ``op`` parameter to use this operation. +Flat +---- + +Flat draws a given image on the image resulted by the previous operation. +Flat can be used only with the [multiple operation system]. + +- **path** - the foreground image path +- **color** - the foreground color in Hex (without ``#``), default is transparent +- **pos** - the destination rectange + +In order to undersand the Flat operation, please read the following `docs `_. + Methods ======= diff --git a/application/application.go b/application/application.go index 9cb8e1de..493863a7 100644 --- a/application/application.go +++ b/application/application.go @@ -256,13 +256,13 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) qs := c.MustGet("parameters").(map[string]interface{}) var err error + s := storage.SourceFromContext(c) + u, exists := c.Get("url") if exists { file, err = image.FromURL(u.(*url.URL), cfg.Options.DefaultUserAgent) } else { // URL provided we use http protocol to retrieve it - s := storage.SourceFromContext(c) - filepath = qs["path"].(string) if !s.Exists(filepath) { return nil, errs.ErrFileNotExists @@ -275,7 +275,7 @@ func processImage(c *gin.Context, l logger.Logger, storeKey string, async bool) } e := engine.FromContext(c) - parameters, err := NewParameters(e, file, qs) + parameters, err := NewParameters(e, s, file, qs) if err != nil { return nil, err } diff --git a/application/parameters.go b/application/parameters.go index a3c5dc37..c2e20e7d 100644 --- a/application/parameters.go +++ b/application/parameters.go @@ -6,8 +6,11 @@ import ( "strings" "github.com/disintegration/imaging" + "github.com/ulule/gostorages" + "github.com/thoas/picfit/engine" "github.com/thoas/picfit/engine/backend" + "github.com/thoas/picfit/errs" "github.com/thoas/picfit/image" ) @@ -31,7 +34,8 @@ type Parameters struct { Operations []engine.EngineOperation } -func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) { +// NewParameters returns Parameters for engine. +func NewParameters(e *engine.Engine, s gostorages.Storage, input *image.ImageFile, qs map[string]interface{}) (*Parameters, error) { format, ok := qs["fmt"].(string) filepath := input.Filepath @@ -101,7 +105,7 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter return nil, err } } else { - engineOperation, err = newEngineOperationFromQuery(e, ops[i]) + engineOperation, err = newEngineOperationFromQuery(e, s, ops[i]) if err != nil { return nil, err } @@ -120,12 +124,17 @@ func NewParameters(e *engine.Engine, input *image.ImageFile, qs map[string]inter }, nil } -func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOperation, error) { +func newEngineOperationFromQuery(e *engine.Engine, s gostorages.Storage, op string) (*engine.EngineOperation, error) { params := make(map[string]interface{}) + var imagePaths []string for _, p := range strings.Split(op, " ") { l := strings.Split(p, ":") if len(l) > 1 { - params[l[0]] = l[1] + if l[0] == "path" { + imagePaths = append(imagePaths, l[1]) + } else { + params[l[0]] = l[1] + } } } @@ -140,6 +149,18 @@ func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOpe return nil, err } + for i := range imagePaths { + if !s.Exists(imagePaths[i]) { + return nil, errs.ErrFileNotExists + } + + file, err := image.FromStorage(s, imagePaths[i]) + if err != nil { + return nil, err + } + opts.Images = append(opts.Images, *file) + } + return &engine.EngineOperation{ Options: opts, Operation: operation, @@ -149,7 +170,7 @@ func newEngineOperationFromQuery(e *engine.Engine, op string) (*engine.EngineOpe func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operation, qs map[string]interface{}) (*backend.Options, error) { var ( err error - quality int + quality = e.DefaultQuality upscale = defaultUpscale height = defaultHeight width = defaultWidth @@ -166,8 +187,6 @@ func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operatio if quality > 100 { return nil, fmt.Errorf("Quality should be <= 100") } - } else { - quality = e.DefaultQuality } position, ok := qs["pos"].(string) @@ -175,6 +194,8 @@ func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operatio return nil, fmt.Errorf("Parameter \"pos\" not found in query string") } + color, _ := qs["color"].(string) + if deg, ok := qs["deg"].(string); ok { degree, err = strconv.Atoi(deg) if err != nil { @@ -210,5 +231,6 @@ func newBackendOptionsFromParameters(e *engine.Engine, operation engine.Operatio Position: position, Quality: quality, Degree: degree, + Color: color, }, nil } diff --git a/application/parameters_test.go b/application/parameters_test.go index edbe78eb..fe652127 100644 --- a/application/parameters_test.go +++ b/application/parameters_test.go @@ -10,7 +10,7 @@ import ( func TestEngineOperationFromQuery(t *testing.T) { op := "op:resize w:123 h:321 upscale:true pos:top q:99" - operation, err := newEngineOperationFromQuery(&engine.Engine{}, op) + operation, err := newEngineOperationFromQuery(&engine.Engine{}, nil, op) assert.Nil(t, err) assert.Equal(t, operation.Operation.String(), "resize") diff --git a/docs/flat.md b/docs/flat.md new file mode 100644 index 00000000..c5b64758 --- /dev/null +++ b/docs/flat.md @@ -0,0 +1,48 @@ +# Flat + +Flat is a method implemented to the engine goimage in order to draw +images on a background image. + +This method can be used only with the multiple operation parameter `op`, +in the URL. + + ## Parameters: + +* `path`: the foreground image, can be multiple. +* `pos`: the foreground destination as a rectangle +* `color`: the foreground color in Hex (without `#`), default is transparent. + + +## Usage + +The background is defined by the image transformed by the previous +operation. In order to draw an image on the background a position must +be given in the sub-parameter `pos` and the image path in the +sub-parameter `path`. + +example: +``` +/display?path=path/to/background.png&op=resize&w=100&h=100 + &op=op:flat+path:path/to/foreground.png+pos:60.10.80.30 +``` + + +The value of `pos` must be the coordinates of the rectangle defined +according to the [go image package](https://blog.golang.org/go-image-package): +an axis-aligned rectangle on the integer grid, defined by its top-left and +bottom-right Point. + +![Rectangle position](https://github.com/thoas/picfit/blob/superpose-images/docs/picfit-dst-position.png) + +The foreground image is resized in order to fit in the given rectangle +and centered inside. + +If several images are given in the same flat operation with the +subparameters path. The rectangle is cut in equal parts, **horizontally** if +the rectangle width `Dx` is superior to its height `Dy` and +**vertically** if it is not the case. Each images are then resized in +order to fit in each parts and centered inside. The order follow the +given order of `path` parameters in the URL. + +![Flat multiple images](https://github.com/thoas/picfit/blob/superpose-images/docs/picfit-flat.png) + diff --git a/docs/picfit-dst-position.png b/docs/picfit-dst-position.png new file mode 100644 index 00000000..892b81ee Binary files /dev/null and b/docs/picfit-dst-position.png differ diff --git a/docs/picfit-flat.png b/docs/picfit-flat.png new file mode 100644 index 00000000..cea554c6 Binary files /dev/null and b/docs/picfit-flat.png differ diff --git a/engine/backend/backend.go b/engine/backend/backend.go index 07053a41..2f3f5df4 100644 --- a/engine/backend/backend.go +++ b/engine/backend/backend.go @@ -17,7 +17,9 @@ type Options struct { Width int Height int Position string + Color string Degree int + Images []image.ImageFile } // Engine is an interface to define an image engine @@ -28,4 +30,5 @@ type Backend interface { Flip(img *image.ImageFile, options *Options) ([]byte, error) Rotate(img *image.ImageFile, options *Options) ([]byte, error) Fit(img *image.ImageFile, options *Options) ([]byte, error) + Flat(background *image.ImageFile, options *Options) ([]byte, error) } diff --git a/engine/backend/goimage_flat.go b/engine/backend/goimage_flat.go new file mode 100644 index 00000000..27fcb81f --- /dev/null +++ b/engine/backend/goimage_flat.go @@ -0,0 +1,138 @@ +package backend + +import ( + "fmt" + "image" + "image/draw" + "strconv" + "strings" + + "github.com/disintegration/imaging" + colorful "github.com/lucasb-eyer/go-colorful" + + imagefile "github.com/thoas/picfit/image" +) + +func (e *GoImage) Flat(backgroundFile *imagefile.ImageFile, options *Options) ([]byte, error) { + if options.Format == imaging.GIF { + return e.TransformGIF(backgroundFile, options, imaging.Resize) + } + + background, err := e.Source(backgroundFile) + if err != nil { + return nil, err + } + + images := make([]image.Image, len(options.Images)) + for i := range options.Images { + images[i], err = e.Source(&options.Images[i]) + if err != nil { + return nil, err + } + } + + bg := image.NewRGBA(image.Rectangle{image.Point{}, background.Bounds().Size()}) + draw.Draw(bg, background.Bounds(), background, image.Point{}, draw.Src) + + dst := positionForeground(bg, options.Position) + fg := foregroundImage(dst, options.Color) + fg = drawForeground(fg, images, options) + + draw.Draw(bg, dst, fg, fg.Bounds().Min, draw.Over) + + return e.ToBytes(bg, options.Format, options.Quality) +} + +func positionForeground(bg image.Image, pos string) image.Rectangle { + ratios := []int{100, 100, 100, 100} + val := strings.Split(pos, ".") + for i := range val { + if i+1 > len(ratios) { + break + } + ratios[i], _ = strconv.Atoi(val[i]) + } + b := bg.Bounds() + return image.Rectangle{ + image.Point{b.Dx() * ratios[0], b.Dy() * ratios[1]}.Div(100), + image.Point{b.Dx() * ratios[2], b.Dy() * ratios[3]}.Div(100), + } +} + +func foregroundImage(rec image.Rectangle, c string) *image.RGBA { + fg := image.NewRGBA(image.Rectangle{image.ZP, rec.Size()}) + if c == "" { + return fg + } + + col, err := colorful.Hex(fmt.Sprintf("#%s", c)) + if err != nil { + return fg + } + + draw.Draw(fg, fg.Bounds(), &image.Uniform{col}, fg.Bounds().Min, draw.Src) + return fg +} + +func drawForeground(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA { + n := len(images) + if n == 0 { + return fg + } + + // resize images for foreground + b := fg.Bounds() + opts := &Options{Upscale: true} + + if b.Dx() > b.Dy() { + opts.Width = b.Dx() / n + opts.Height = b.Dy() + } else { + opts.Width = b.Dx() + opts.Height = b.Dy() / n + } + + for i := range images { + images[i] = scale(images[i], opts, imaging.Fit) + } + + if b.Dx() > b.Dy() { + return foregroundHorizontal(fg, images, options) + } else { + return foregroundVertical(fg, images, options) + } +} + +func foregroundHorizontal(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA { + position := fg.Bounds().Min + totalHeight := fg.Bounds().Dy() + cellWidth := fg.Bounds().Dx() / len(images) + for i := range images { + bounds := images[i].Bounds() + position.Y = (totalHeight - bounds.Dy()) / 2 + position.X = fg.Bounds().Min.X + i*cellWidth + (cellWidth-bounds.Dx())/2 + r := image.Rectangle{ + position, + position.Add(fg.Bounds().Size()), + } + draw.Draw(fg, r, images[i], bounds.Min, draw.Over) + } + return fg +} + +func foregroundVertical(fg *image.RGBA, images []image.Image, options *Options) *image.RGBA { + position := fg.Bounds().Min + cellHeight := fg.Bounds().Dy() / len(images) + totalWidth := fg.Bounds().Dx() + for i := range images { + bounds := images[i].Bounds() + position.Y = fg.Bounds().Min.Y + i*cellHeight + (cellHeight-bounds.Dy())/2 + position.X = fg.Bounds().Min.X + (totalWidth-bounds.Dx())/2 + r := image.Rectangle{ + position, + position.Add(image.Point{bounds.Dx(), bounds.Dy()}), + } + draw.Draw(fg, r, images[i], bounds.Min, draw.Over) + } + return fg +} diff --git a/engine/backend/lilliput.go b/engine/backend/lilliput.go index 7cd960e9..ae97490c 100644 --- a/engine/backend/lilliput.go +++ b/engine/backend/lilliput.go @@ -151,3 +151,7 @@ func (e *Lilliput) transform(img *imagefile.ImageFile, options *lilliput.ImageOp func (e *Lilliput) String() string { return "lilliput" } + +func (e *Lilliput) Flat(background *imagefile.ImageFile, options *Options) ([]byte, error) { + return nil, MethodNotImplementedError +} diff --git a/engine/engine.go b/engine/engine.go index eb1ff598..a85eb23d 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -37,10 +37,15 @@ func New(cfg config.Config) *Engine { b = append(b, &backend.GoImage{}) } + quality := config.DefaultQuality + if cfg.Quality != 0 { + quality = cfg.Quality + } + return &Engine{ DefaultFormat: cfg.DefaultFormat, Format: cfg.Format, - DefaultQuality: cfg.Quality, + DefaultQuality: quality, backends: b, } } @@ -93,6 +98,8 @@ func operate(b backend.Backend, img *image.ImageFile, operation Operation, optio return b.Thumbnail(img, options) case Fit: return b.Fit(img, options) + case Flat: + return b.Flat(img, options) default: return nil, fmt.Errorf("Operation not found for %s", operation) } diff --git a/engine/operations.go b/engine/operations.go index 8b9a2614..0536fe03 100644 --- a/engine/operations.go +++ b/engine/operations.go @@ -15,6 +15,7 @@ const ( Flip = Operation("flip") Fit = Operation("fit") Noop = Operation("noop") + Flat = Operation("flat") ) var Operations = map[string]Operation{ @@ -24,6 +25,7 @@ var Operations = map[string]Operation{ Rotate.String(): Rotate, Fit.String(): Fit, Noop.String(): Noop, + Flat.String(): Flat, } type EngineOperation struct { diff --git a/vendor/github.com/imdario/mergo/LICENSE b/vendor/github.com/imdario/mergo/LICENSE deleted file mode 100644 index 68668029..00000000 --- a/vendor/github.com/imdario/mergo/LICENSE +++ /dev/null @@ -1,28 +0,0 @@ -Copyright (c) 2013 Dario Castañé. All rights reserved. -Copyright (c) 2012 The Go Authors. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/imdario/mergo/doc.go b/vendor/github.com/imdario/mergo/doc.go deleted file mode 100644 index 6e9aa7ba..00000000 --- a/vendor/github.com/imdario/mergo/doc.go +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2013 Dario Castañé. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package mergo merges same-type structs and maps by setting default values in zero-value fields. - -Mergo won't merge unexported (private) fields but will do recursively any exported one. It also won't merge structs inside maps (because they are not addressable using Go reflection). - -Usage - -From my own work-in-progress project: - - type networkConfig struct { - Protocol string - Address string - ServerType string `json: "server_type"` - Port uint16 - } - - type FssnConfig struct { - Network networkConfig - } - - var fssnDefault = FssnConfig { - networkConfig { - "tcp", - "127.0.0.1", - "http", - 31560, - }, - } - - // Inside a function [...] - - if err := mergo.Merge(&config, fssnDefault); err != nil { - log.Fatal(err) - } - - // More code [...] - -*/ -package mergo diff --git a/vendor/github.com/imdario/mergo/map.go b/vendor/github.com/imdario/mergo/map.go deleted file mode 100644 index 44361e88..00000000 --- a/vendor/github.com/imdario/mergo/map.go +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2014 Dario Castañé. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Based on src/pkg/reflect/deepequal.go from official -// golang's stdlib. - -package mergo - -import ( - "fmt" - "reflect" - "unicode" - "unicode/utf8" -) - -func changeInitialCase(s string, mapper func(rune) rune) string { - if s == "" { - return s - } - r, n := utf8.DecodeRuneInString(s) - return string(mapper(r)) + s[n:] -} - -func isExported(field reflect.StructField) bool { - r, _ := utf8.DecodeRuneInString(field.Name) - return r >= 'A' && r <= 'Z' -} - -// Traverses recursively both values, assigning src's fields values to dst. -// The map argument tracks comparisons that have already been seen, which allows -// short circuiting on recursive types. -func deepMap(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) { - if dst.CanAddr() { - addr := dst.UnsafeAddr() - h := 17 * addr - seen := visited[h] - typ := dst.Type() - for p := seen; p != nil; p = p.next { - if p.ptr == addr && p.typ == typ { - return nil - } - } - // Remember, remember... - visited[h] = &visit{addr, typ, seen} - } - zeroValue := reflect.Value{} - switch dst.Kind() { - case reflect.Map: - dstMap := dst.Interface().(map[string]interface{}) - for i, n := 0, src.NumField(); i < n; i++ { - srcType := src.Type() - field := srcType.Field(i) - if !isExported(field) { - continue - } - fieldName := field.Name - fieldName = changeInitialCase(fieldName, unicode.ToLower) - if v, ok := dstMap[fieldName]; !ok || isEmptyValue(reflect.ValueOf(v)) { - dstMap[fieldName] = src.Field(i).Interface() - } - } - case reflect.Struct: - srcMap := src.Interface().(map[string]interface{}) - for key := range srcMap { - srcValue := srcMap[key] - fieldName := changeInitialCase(key, unicode.ToUpper) - dstElement := dst.FieldByName(fieldName) - if dstElement == zeroValue { - // We discard it because the field doesn't exist. - continue - } - srcElement := reflect.ValueOf(srcValue) - dstKind := dstElement.Kind() - srcKind := srcElement.Kind() - if srcKind == reflect.Ptr && dstKind != reflect.Ptr { - srcElement = srcElement.Elem() - srcKind = reflect.TypeOf(srcElement.Interface()).Kind() - } else if dstKind == reflect.Ptr { - // Can this work? I guess it can't. - if srcKind != reflect.Ptr && srcElement.CanAddr() { - srcPtr := srcElement.Addr() - srcElement = reflect.ValueOf(srcPtr) - srcKind = reflect.Ptr - } - } - if !srcElement.IsValid() { - continue - } - if srcKind == dstKind { - if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil { - return - } - } else { - if srcKind == reflect.Map { - if err = deepMap(dstElement, srcElement, visited, depth+1); err != nil { - return - } - } else { - return fmt.Errorf("type mismatch on %s field: found %v, expected %v", fieldName, srcKind, dstKind) - } - } - } - } - return -} - -// Map sets fields' values in dst from src. -// src can be a map with string keys or a struct. dst must be the opposite: -// if src is a map, dst must be a valid pointer to struct. If src is a struct, -// dst must be map[string]interface{}. -// It won't merge unexported (private) fields and will do recursively -// any exported field. -// If dst is a map, keys will be src fields' names in lower camel case. -// Missing key in src that doesn't match a field in dst will be skipped. This -// doesn't apply if dst is a map. -// This is separated method from Merge because it is cleaner and it keeps sane -// semantics: merging equal types, mapping different (restricted) types. -func Map(dst, src interface{}) error { - var ( - vDst, vSrc reflect.Value - err error - ) - if vDst, vSrc, err = resolveValues(dst, src); err != nil { - return err - } - // To be friction-less, we redirect equal-type arguments - // to deepMerge. Only because arguments can be anything. - if vSrc.Kind() == vDst.Kind() { - return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0) - } - switch vSrc.Kind() { - case reflect.Struct: - if vDst.Kind() != reflect.Map { - return ErrExpectedMapAsDestination - } - case reflect.Map: - if vDst.Kind() != reflect.Struct { - return ErrExpectedStructAsDestination - } - default: - return ErrNotSupported - } - return deepMap(vDst, vSrc, make(map[uintptr]*visit), 0) -} diff --git a/vendor/github.com/imdario/mergo/merge.go b/vendor/github.com/imdario/mergo/merge.go deleted file mode 100644 index 5d328b1f..00000000 --- a/vendor/github.com/imdario/mergo/merge.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2013 Dario Castañé. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Based on src/pkg/reflect/deepequal.go from official -// golang's stdlib. - -package mergo - -import ( - "reflect" -) - -// Traverses recursively both values, assigning src's fields values to dst. -// The map argument tracks comparisons that have already been seen, which allows -// short circuiting on recursive types. -func deepMerge(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) { - if !src.IsValid() { - return - } - if dst.CanAddr() { - addr := dst.UnsafeAddr() - h := 17 * addr - seen := visited[h] - typ := dst.Type() - for p := seen; p != nil; p = p.next { - if p.ptr == addr && p.typ == typ { - return nil - } - } - // Remember, remember... - visited[h] = &visit{addr, typ, seen} - } - switch dst.Kind() { - case reflect.Struct: - for i, n := 0, dst.NumField(); i < n; i++ { - if err = deepMerge(dst.Field(i), src.Field(i), visited, depth+1); err != nil { - return - } - } - case reflect.Map: - for _, key := range src.MapKeys() { - srcElement := src.MapIndex(key) - if !srcElement.IsValid() { - continue - } - dstElement := dst.MapIndex(key) - switch reflect.TypeOf(srcElement.Interface()).Kind() { - case reflect.Struct: - fallthrough - case reflect.Map: - if err = deepMerge(dstElement, srcElement, visited, depth+1); err != nil { - return - } - } - if !dstElement.IsValid() { - dst.SetMapIndex(key, srcElement) - } - } - case reflect.Ptr: - fallthrough - case reflect.Interface: - if src.IsNil() { - break - } else if dst.IsNil() { - if dst.CanSet() && isEmptyValue(dst) { - dst.Set(src) - } - } else if err = deepMerge(dst.Elem(), src.Elem(), visited, depth+1); err != nil { - return - } - default: - if dst.CanSet() && !isEmptyValue(src) { - dst.Set(src) - } - } - return -} - -// Merge sets fields' values in dst from src if they have a zero -// value of their type. -// dst and src must be valid same-type structs and dst must be -// a pointer to struct. -// It won't merge unexported (private) fields and will do recursively -// any exported field. -func Merge(dst, src interface{}) error { - var ( - vDst, vSrc reflect.Value - err error - ) - if vDst, vSrc, err = resolveValues(dst, src); err != nil { - return err - } - if vDst.Type() != vSrc.Type() { - return ErrDifferentArgumentsTypes - } - return deepMerge(vDst, vSrc, make(map[uintptr]*visit), 0) -} diff --git a/vendor/github.com/imdario/mergo/mergo.go b/vendor/github.com/imdario/mergo/mergo.go deleted file mode 100644 index f8a0991e..00000000 --- a/vendor/github.com/imdario/mergo/mergo.go +++ /dev/null @@ -1,90 +0,0 @@ -// Copyright 2013 Dario Castañé. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -// Based on src/pkg/reflect/deepequal.go from official -// golang's stdlib. - -package mergo - -import ( - "errors" - "reflect" -) - -// Errors reported by Mergo when it finds invalid arguments. -var ( - ErrNilArguments = errors.New("src and dst must not be nil") - ErrDifferentArgumentsTypes = errors.New("src and dst must be of same type") - ErrNotSupported = errors.New("only structs and maps are supported") - ErrExpectedMapAsDestination = errors.New("dst was expected to be a map") - ErrExpectedStructAsDestination = errors.New("dst was expected to be a struct") -) - -// During deepMerge, must keep track of checks that are -// in progress. The comparison algorithm assumes that all -// checks in progress are true when it reencounters them. -// Visited are stored in a map indexed by 17 * a1 + a2; -type visit struct { - ptr uintptr - typ reflect.Type - next *visit -} - -// From src/pkg/encoding/json. -func isEmptyValue(v reflect.Value) bool { - switch v.Kind() { - case reflect.Array, reflect.Map, reflect.Slice, reflect.String: - return v.Len() == 0 - case reflect.Bool: - return !v.Bool() - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - return v.Int() == 0 - case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr: - return v.Uint() == 0 - case reflect.Float32, reflect.Float64: - return v.Float() == 0 - case reflect.Interface, reflect.Ptr: - return v.IsNil() - } - return false -} - -func resolveValues(dst, src interface{}) (vDst, vSrc reflect.Value, err error) { - if dst == nil || src == nil { - err = ErrNilArguments - return - } - vDst = reflect.ValueOf(dst).Elem() - if vDst.Kind() != reflect.Struct && vDst.Kind() != reflect.Map { - err = ErrNotSupported - return - } - vSrc = reflect.ValueOf(src) - // We check if vSrc is a pointer to dereference it. - if vSrc.Kind() == reflect.Ptr { - vSrc = vSrc.Elem() - } - return -} - -// Traverses recursively both values, assigning src's fields values to dst. -// The map argument tracks comparisons that have already been seen, which allows -// short circuiting on recursive types. -func deeper(dst, src reflect.Value, visited map[uintptr]*visit, depth int) (err error) { - if dst.CanAddr() { - addr := dst.UnsafeAddr() - h := 17 * addr - seen := visited[h] - typ := dst.Type() - for p := seen; p != nil; p = p.next { - if p.ptr == addr && p.typ == typ { - return nil - } - } - // Remember, remember... - visited[h] = &visit{addr, typ, seen} - } - return // TODO refactor -} diff --git a/vendor/github.com/imdario/mergo/mergo_test.go b/vendor/github.com/imdario/mergo/mergo_test.go deleted file mode 100644 index 072bddb7..00000000 --- a/vendor/github.com/imdario/mergo/mergo_test.go +++ /dev/null @@ -1,288 +0,0 @@ -// Copyright 2013 Dario Castañé. All rights reserved. -// Copyright 2009 The Go Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package mergo - -import ( - "gopkg.in/yaml.v1" - "io/ioutil" - "reflect" - "testing" -) - -type simpleTest struct { - Value int -} - -type complexTest struct { - St simpleTest - sz int - Id string -} - -type moreComplextText struct { - Ct complexTest - St simpleTest - Nt simpleTest -} - -type pointerTest struct { - C *simpleTest -} - -type sliceTest struct { - S []int -} - -func TestNil(t *testing.T) { - if err := Merge(nil, nil); err != ErrNilArguments { - t.Fail() - } -} - -func TestDifferentTypes(t *testing.T) { - a := simpleTest{42} - b := 42 - if err := Merge(&a, b); err != ErrDifferentArgumentsTypes { - t.Fail() - } -} - -func TestSimpleStruct(t *testing.T) { - a := simpleTest{} - b := simpleTest{42} - if err := Merge(&a, b); err != nil { - t.FailNow() - } - if a.Value != 42 { - t.Fatalf("b not merged in a properly: a.Value(%d) != b.Value(%d)", a.Value, b.Value) - } - if !reflect.DeepEqual(a, b) { - t.FailNow() - } -} - -func TestComplexStruct(t *testing.T) { - a := complexTest{} - a.Id = "athing" - b := complexTest{simpleTest{42}, 1, "bthing"} - if err := Merge(&a, b); err != nil { - t.FailNow() - } - if a.St.Value != 42 { - t.Fatalf("b not merged in a properly: a.St.Value(%d) != b.St.Value(%d)", a.St.Value, b.St.Value) - } - if a.sz == 1 { - t.Fatalf("a's private field sz not preserved from merge: a.sz(%d) == b.sz(%d)", a.sz, b.sz) - } - if a.Id != b.Id { - t.Fatalf("a's field Id not merged properly: a.Id(%s) != b.Id(%s)", a.Id, b.Id) - } -} - -func TestPointerStruct(t *testing.T) { - s1 := simpleTest{} - s2 := simpleTest{19} - a := pointerTest{&s1} - b := pointerTest{&s2} - if err := Merge(&a, b); err != nil { - t.FailNow() - } - if a.C.Value != b.C.Value { - //t.Fatalf("b not merged in a properly: a.C.Value(%d) != b.C.Value(%d)", a.C.Value, b.C.Value) - } -} - -func TestPointerStructNil(t *testing.T) { - a := pointerTest{nil} - b := pointerTest{&simpleTest{19}} - if err := Merge(&a, b); err != nil { - t.FailNow() - } - if a.C.Value != b.C.Value { - t.Fatalf("b not merged in a properly: a.C.Value(%d) != b.C.Value(%d)", a.C.Value, b.C.Value) - } -} - -func TestSliceStruct(t *testing.T) { - a := sliceTest{} - b := sliceTest{[]int{1, 2, 3}} - if err := Merge(&a, b); err != nil { - t.FailNow() - } - if len(b.S) != 3 { - t.FailNow() - } - if len(a.S) != len(b.S) { - t.Fatalf("b not merged in a properly %d != %d", len(a.S), len(b.S)) - } - - a = sliceTest{[]int{1}} - b = sliceTest{[]int{1, 2, 3}} - if err := Merge(&a, b); err != nil { - t.FailNow() - } - if len(b.S) != 3 { - t.FailNow() - } - if len(a.S) != len(b.S) { - t.Fatalf("b not merged in a properly %d != %d", len(a.S), len(b.S)) - } -} - -func TestMaps(t *testing.T) { - m := map[string]simpleTest{ - "a": simpleTest{}, - "b": simpleTest{42}, - } - n := map[string]simpleTest{ - "a": simpleTest{16}, - "b": simpleTest{}, - "c": simpleTest{12}, - } - if err := Merge(&m, n); err != nil { - t.Fatalf(err.Error()) - } - if len(m) != 3 { - t.Fatalf(`n not merged in m properly, m must have 3 elements instead of %d`, len(m)) - } - if m["a"].Value != 0 { - t.Fatalf(`n merged in m because I solved non-addressable map values TODO: m["a"].Value(%d) != n["a"].Value(%d)`, m["a"].Value, n["a"].Value) - } - if m["b"].Value != 42 { - t.Fatalf(`n wrongly merged in m: m["b"].Value(%d) != n["b"].Value(%d)`, m["b"].Value, n["b"].Value) - } - if m["c"].Value != 12 { - t.Fatalf(`n not merged in m: m["c"].Value(%d) != n["c"].Value(%d)`, m["c"].Value, n["c"].Value) - } -} - -func TestYAMLMaps(t *testing.T) { - thing := loadYAML("testdata/thing.yml") - license := loadYAML("testdata/license.yml") - ft := thing["fields"].(map[interface{}]interface{}) - fl := license["fields"].(map[interface{}]interface{}) - expectedLength := len(ft) + len(fl) - if err := Merge(&license, thing); err != nil { - t.Fatal(err.Error()) - } - currentLength := len(license["fields"].(map[interface{}]interface{})) - if currentLength != expectedLength { - t.Fatalf(`thing not merged in license properly, license must have %d elements instead of %d`, expectedLength, currentLength) - } - fields := license["fields"].(map[interface{}]interface{}) - if _, ok := fields["id"]; !ok { - t.Fatalf(`thing not merged in license properly, license must have a new id field from thing`) - } -} - -func TestTwoPointerValues(t *testing.T) { - a := &simpleTest{} - b := &simpleTest{42} - if err := Merge(a, b); err != nil { - t.Fatalf(`Boom. You crossed the streams: %s`, err) - } -} - -func TestMap(t *testing.T) { - a := complexTest{} - a.Id = "athing" - c := moreComplextText{a, simpleTest{}, simpleTest{}} - b := map[string]interface{}{ - "ct": map[string]interface{}{ - "st": map[string]interface{}{ - "value": 42, - }, - "sz": 1, - "id": "bthing", - }, - "st": &simpleTest{144}, // Mapping a reference - "zt": simpleTest{299}, // Mapping a missing field (zt doesn't exist) - "nt": simpleTest{3}, - } - if err := Map(&c, b); err != nil { - t.FailNow() - } - m := b["ct"].(map[string]interface{}) - n := m["st"].(map[string]interface{}) - o := b["st"].(*simpleTest) - p := b["nt"].(simpleTest) - if c.Ct.St.Value != 42 { - t.Fatalf("b not merged in a properly: c.Ct.St.Value(%d) != b.Ct.St.Value(%d)", c.Ct.St.Value, n["value"]) - } - if c.St.Value != 144 { - t.Fatalf("b not merged in a properly: c.St.Value(%d) != b.St.Value(%d)", c.St.Value, o.Value) - } - if c.Nt.Value != 3 { - t.Fatalf("b not merged in a properly: c.Nt.Value(%d) != b.Nt.Value(%d)", c.St.Value, p.Value) - } - if c.Ct.sz == 1 { - t.Fatalf("a's private field sz not preserved from merge: c.Ct.sz(%d) == b.Ct.sz(%d)", c.Ct.sz, m["sz"]) - } - if c.Ct.Id != m["id"] { - t.Fatalf("a's field Id not merged properly: c.Ct.Id(%s) != b.Ct.Id(%s)", c.Ct.Id, m["id"]) - } -} - -func TestSimpleMap(t *testing.T) { - a := simpleTest{} - b := map[string]interface{}{ - "value": 42, - } - if err := Map(&a, b); err != nil { - t.FailNow() - } - if a.Value != 42 { - t.Fatalf("b not merged in a properly: a.Value(%d) != b.Value(%v)", a.Value, b["value"]) - } -} - -type pointerMapTest struct { - A int - hidden int - B *simpleTest -} - -func TestBackAndForth(t *testing.T) { - pt := pointerMapTest{42, 1, &simpleTest{66}} - m := make(map[string]interface{}) - if err := Map(&m, pt); err != nil { - t.FailNow() - } - var ( - v interface{} - ok bool - ) - if v, ok = m["a"]; v.(int) != pt.A || !ok { - t.Fatalf("pt not merged properly: m[`a`](%d) != pt.A(%d)", v, pt.A) - } - if v, ok = m["b"]; !ok { - t.Fatalf("pt not merged properly: B is missing in m") - } - var st *simpleTest - if st = v.(*simpleTest); st.Value != 66 { - t.Fatalf("something went wrong while mapping pt on m, B wasn't copied") - } - bpt := pointerMapTest{} - if err := Map(&bpt, m); err != nil { - t.Fatal(err) - } - if bpt.A != pt.A { - t.Fatalf("pt not merged properly: bpt.A(%d) != pt.A(%d)", bpt.A, pt.A) - } - if bpt.hidden == pt.hidden { - t.Fatalf("pt unexpectedly merged: bpt.hidden(%d) == pt.hidden(%d)", bpt.hidden, pt.hidden) - } - if bpt.B.Value != pt.B.Value { - t.Fatalf("pt not merged properly: bpt.B.Value(%d) != pt.B.Value(%d)", bpt.B.Value, pt.B.Value) - } -} - -func loadYAML(path string) (m map[string]interface{}) { - m = make(map[string]interface{}) - raw, _ := ioutil.ReadFile(path) - _ = yaml.Unmarshal(raw, &m) - return -} diff --git a/vendor/github.com/imdario/mergo/testdata/license.yml b/vendor/github.com/imdario/mergo/testdata/license.yml deleted file mode 100644 index 62fdb61e..00000000 --- a/vendor/github.com/imdario/mergo/testdata/license.yml +++ /dev/null @@ -1,3 +0,0 @@ -import: ../../../../fossene/db/schema/thing.yml -fields: - site: string diff --git a/vendor/github.com/lucasb-eyer/go-colorful/LICENSE b/vendor/github.com/lucasb-eyer/go-colorful/LICENSE new file mode 100644 index 00000000..4e402a00 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/LICENSE @@ -0,0 +1,7 @@ +Copyright (c) 2013 Lucas Beyer + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go b/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go new file mode 100644 index 00000000..2e2e49e1 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/colorgens.go @@ -0,0 +1,55 @@ +// Various ways to generate single random colors + +package colorful + +import ( + "math/rand" +) + +// Creates a random dark, "warm" color through a restricted HSV space. +func FastWarmColor() Color { + return Hsv( + rand.Float64()*360.0, + 0.5+rand.Float64()*0.3, + 0.3+rand.Float64()*0.3) +} + +// Creates a random dark, "warm" color through restricted HCL space. +// This is slower than FastWarmColor but will likely give you colors which have +// the same "warmness" if you run it many times. +func WarmColor() (c Color) { + for c = randomWarm(); !c.IsValid(); c = randomWarm() { + } + return +} + +func randomWarm() Color { + return Hcl( + rand.Float64()*360.0, + 0.1+rand.Float64()*0.3, + 0.2+rand.Float64()*0.3) +} + +// Creates a random bright, "pimpy" color through a restricted HSV space. +func FastHappyColor() Color { + return Hsv( + rand.Float64()*360.0, + 0.7+rand.Float64()*0.3, + 0.6+rand.Float64()*0.3) +} + +// Creates a random bright, "pimpy" color through restricted HCL space. +// This is slower than FastHappyColor but will likely give you colors which +// have the same "brightness" if you run it many times. +func HappyColor() (c Color) { + for c = randomPimp(); !c.IsValid(); c = randomPimp() { + } + return +} + +func randomPimp() Color { + return Hcl( + rand.Float64()*360.0, + 0.5+rand.Float64()*0.3, + 0.5+rand.Float64()*0.3) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/colorgens_test.go b/vendor/github.com/lucasb-eyer/go-colorful/colorgens_test.go new file mode 100644 index 00000000..564e7a85 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/colorgens_test.go @@ -0,0 +1,33 @@ +package colorful + +import ( + "math/rand" + "testing" + "time" +) + +// This is really difficult to test, if you've got a good idea, pull request! + +// Check if it returns all valid colors. +func TestColorValidity(t *testing.T) { + seed := time.Now().UTC().UnixNano() + rand.Seed(seed) + + for i := 0; i < 100; i++ { + if col := WarmColor(); !col.IsValid() { + t.Errorf("Warm color %v is not valid! Seed was: %v", col, seed) + } + + if col := FastWarmColor(); !col.IsValid() { + t.Errorf("Fast warm color %v is not valid! Seed was: %v", col, seed) + } + + if col := HappyColor(); !col.IsValid() { + t.Errorf("Happy color %v is not valid! Seed was: %v", col, seed) + } + + if col := FastHappyColor(); !col.IsValid() { + t.Errorf("Fast happy color %v is not valid! Seed was: %v", col, seed) + } + } +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/colors.go b/vendor/github.com/lucasb-eyer/go-colorful/colors.go new file mode 100644 index 00000000..febf94c7 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/colors.go @@ -0,0 +1,819 @@ +// The colorful package provides all kinds of functions for working with colors. +package colorful + +import ( + "fmt" + "image/color" + "math" +) + +// A color is stored internally using sRGB (standard RGB) values in the range 0-1 +type Color struct { + R, G, B float64 +} + +// Implement the Go color.Color interface. +func (col Color) RGBA() (r, g, b, a uint32) { + r = uint32(col.R*65535.0 + 0.5) + g = uint32(col.G*65535.0 + 0.5) + b = uint32(col.B*65535.0 + 0.5) + a = 0xFFFF + return +} + +// Constructs a colorful.Color from something implementing color.Color +func MakeColor(col color.Color) (Color, bool) { + r, g, b, a := col.RGBA() + if a == 0 { + return Color{0, 0, 0}, false + } + + // Since color.Color is alpha pre-multiplied, we need to divide the + // RGB values by alpha again in order to get back the original RGB. + r *= 0xffff + r /= a + g *= 0xffff + g /= a + b *= 0xffff + b /= a + + return Color{float64(r) / 65535.0, float64(g) / 65535.0, float64(b) / 65535.0}, true +} + +// Might come in handy sometimes to reduce boilerplate code. +func (col Color) RGB255() (r, g, b uint8) { + r = uint8(col.R*255.0 + 0.5) + g = uint8(col.G*255.0 + 0.5) + b = uint8(col.B*255.0 + 0.5) + return +} + +// This is the tolerance used when comparing colors using AlmostEqualRgb. +const Delta = 1.0 / 255.0 + +// This is the default reference white point. +var D65 = [3]float64{0.95047, 1.00000, 1.08883} + +// And another one. +var D50 = [3]float64{0.96422, 1.00000, 0.82521} + +// Checks whether the color exists in RGB space, i.e. all values are in [0..1] +func (c Color) IsValid() bool { + return 0.0 <= c.R && c.R <= 1.0 && + 0.0 <= c.G && c.G <= 1.0 && + 0.0 <= c.B && c.B <= 1.0 +} + +func clamp01(v float64) float64 { + return math.Max(0.0, math.Min(v, 1.0)) +} + +// Returns Clamps the color into valid range, clamping each value to [0..1] +// If the color is valid already, this is a no-op. +func (c Color) Clamped() Color { + return Color{clamp01(c.R), clamp01(c.G), clamp01(c.B)} +} + +func sq(v float64) float64 { + return v * v +} + +func cub(v float64) float64 { + return v * v * v +} + +// DistanceRgb computes the distance between two colors in RGB space. +// This is not a good measure! Rather do it in Lab space. +func (c1 Color) DistanceRgb(c2 Color) float64 { + return math.Sqrt(sq(c1.R-c2.R) + sq(c1.G-c2.G) + sq(c1.B-c2.B)) +} + +// Check for equality between colors within the tolerance Delta (1/255). +func (c1 Color) AlmostEqualRgb(c2 Color) bool { + return math.Abs(c1.R-c2.R)+ + math.Abs(c1.G-c2.G)+ + math.Abs(c1.B-c2.B) < 3.0*Delta +} + +// You don't really want to use this, do you? Go for BlendLab, BlendLuv or BlendHcl. +func (c1 Color) BlendRgb(c2 Color, t float64) Color { + return Color{c1.R + t*(c2.R-c1.R), + c1.G + t*(c2.G-c1.G), + c1.B + t*(c2.B-c1.B)} +} + +// Utility used by Hxx color-spaces for interpolating between two angles in [0,360]. +func interp_angle(a0, a1, t float64) float64 { + // Based on the answer here: http://stackoverflow.com/a/14498790/2366315 + // With potential proof that it works here: http://math.stackexchange.com/a/2144499 + delta := math.Mod(math.Mod(a1-a0, 360.0)+540, 360.0) - 180.0 + return math.Mod(a0+t*delta+360.0, 360.0) +} + +/// HSV /// +/////////// +// From http://en.wikipedia.org/wiki/HSL_and_HSV +// Note that h is in [0..360] and s,v in [0..1] + +// Hsv returns the Hue [0..360], Saturation and Value [0..1] of the color. +func (col Color) Hsv() (h, s, v float64) { + min := math.Min(math.Min(col.R, col.G), col.B) + v = math.Max(math.Max(col.R, col.G), col.B) + C := v - min + + s = 0.0 + if v != 0.0 { + s = C / v + } + + h = 0.0 // We use 0 instead of undefined as in wp. + if min != v { + if v == col.R { + h = math.Mod((col.G-col.B)/C, 6.0) + } + if v == col.G { + h = (col.B-col.R)/C + 2.0 + } + if v == col.B { + h = (col.R-col.G)/C + 4.0 + } + h *= 60.0 + if h < 0.0 { + h += 360.0 + } + } + return +} + +// Hsv creates a new Color given a Hue in [0..360], a Saturation and a Value in [0..1] +func Hsv(H, S, V float64) Color { + Hp := H / 60.0 + C := V * S + X := C * (1.0 - math.Abs(math.Mod(Hp, 2.0)-1.0)) + + m := V - C + r, g, b := 0.0, 0.0, 0.0 + + switch { + case 0.0 <= Hp && Hp < 1.0: + r = C + g = X + case 1.0 <= Hp && Hp < 2.0: + r = X + g = C + case 2.0 <= Hp && Hp < 3.0: + g = C + b = X + case 3.0 <= Hp && Hp < 4.0: + g = X + b = C + case 4.0 <= Hp && Hp < 5.0: + r = X + b = C + case 5.0 <= Hp && Hp < 6.0: + r = C + b = X + } + + return Color{m + r, m + g, m + b} +} + +// You don't really want to use this, do you? Go for BlendLab, BlendLuv or BlendHcl. +func (c1 Color) BlendHsv(c2 Color, t float64) Color { + h1, s1, v1 := c1.Hsv() + h2, s2, v2 := c2.Hsv() + + // We know that h are both in [0..360] + return Hsv(interp_angle(h1, h2, t), s1+t*(s2-s1), v1+t*(v2-v1)) +} + +/// HSL /// +/////////// + +// Hsl returns the Hue [0..360], Saturation [0..1], and Luminance (lightness) [0..1] of the color. +func (col Color) Hsl() (h, s, l float64) { + min := math.Min(math.Min(col.R, col.G), col.B) + max := math.Max(math.Max(col.R, col.G), col.B) + + l = (max + min) / 2 + + if min == max { + s = 0 + h = 0 + } else { + if l < 0.5 { + s = (max - min) / (max + min) + } else { + s = (max - min) / (2.0 - max - min) + } + + if max == col.R { + h = (col.G - col.B) / (max - min) + } else if max == col.G { + h = 2.0 + (col.B-col.R)/(max-min) + } else { + h = 4.0 + (col.R-col.G)/(max-min) + } + + h *= 60 + + if h < 0 { + h += 360 + } + } + + return +} + +// Hsl creates a new Color given a Hue in [0..360], a Saturation [0..1], and a Luminance (lightness) in [0..1] +func Hsl(h, s, l float64) Color { + if s == 0 { + return Color{l, l, l} + } + + var r, g, b float64 + var t1 float64 + var t2 float64 + var tr float64 + var tg float64 + var tb float64 + + if l < 0.5 { + t1 = l * (1.0 + s) + } else { + t1 = l + s - l*s + } + + t2 = 2*l - t1 + h = h / 360 + tr = h + 1.0/3.0 + tg = h + tb = h - 1.0/3.0 + + if tr < 0 { + tr++ + } + if tr > 1 { + tr-- + } + if tg < 0 { + tg++ + } + if tg > 1 { + tg-- + } + if tb < 0 { + tb++ + } + if tb > 1 { + tb-- + } + + // Red + if 6*tr < 1 { + r = t2 + (t1-t2)*6*tr + } else if 2*tr < 1 { + r = t1 + } else if 3*tr < 2 { + r = t2 + (t1-t2)*(2.0/3.0-tr)*6 + } else { + r = t2 + } + + // Green + if 6*tg < 1 { + g = t2 + (t1-t2)*6*tg + } else if 2*tg < 1 { + g = t1 + } else if 3*tg < 2 { + g = t2 + (t1-t2)*(2.0/3.0-tg)*6 + } else { + g = t2 + } + + // Blue + if 6*tb < 1 { + b = t2 + (t1-t2)*6*tb + } else if 2*tb < 1 { + b = t1 + } else if 3*tb < 2 { + b = t2 + (t1-t2)*(2.0/3.0-tb)*6 + } else { + b = t2 + } + + return Color{r, g, b} +} + +/// Hex /// +/////////// + +// Hex returns the hex "html" representation of the color, as in #ff0080. +func (col Color) Hex() string { + // Add 0.5 for rounding + return fmt.Sprintf("#%02x%02x%02x", uint8(col.R*255.0+0.5), uint8(col.G*255.0+0.5), uint8(col.B*255.0+0.5)) +} + +// Hex parses a "html" hex color-string, either in the 3 "#f0c" or 6 "#ff1034" digits form. +func Hex(scol string) (Color, error) { + format := "#%02x%02x%02x" + factor := 1.0 / 255.0 + if len(scol) == 4 { + format = "#%1x%1x%1x" + factor = 1.0 / 15.0 + } + + var r, g, b uint8 + n, err := fmt.Sscanf(scol, format, &r, &g, &b) + if err != nil { + return Color{}, err + } + if n != 3 { + return Color{}, fmt.Errorf("color: %v is not a hex-color", scol) + } + + return Color{float64(r) * factor, float64(g) * factor, float64(b) * factor}, nil +} + +/// Linear /// +////////////// +// http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/ +// http://www.brucelindbloom.com/Eqn_RGB_to_XYZ.html + +func linearize(v float64) float64 { + if v <= 0.04045 { + return v / 12.92 + } + return math.Pow((v+0.055)/1.055, 2.4) +} + +// LinearRgb converts the color into the linear RGB space (see http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/). +func (col Color) LinearRgb() (r, g, b float64) { + r = linearize(col.R) + g = linearize(col.G) + b = linearize(col.B) + return +} + +// A much faster and still quite precise linearization using a 6th-order Taylor approximation. +// See the accompanying Jupyter notebook for derivation of the constants. +func linearize_fast(v float64) float64 { + v1 := v - 0.5 + v2 := v1 * v1 + v3 := v2 * v1 + v4 := v2 * v2 + //v5 := v3*v2 + return -0.248750514614486 + 0.925583310193438*v + 1.16740237321695*v2 + 0.280457026598666*v3 - 0.0757991963780179*v4 //+ 0.0437040411548932*v5 +} + +// FastLinearRgb is much faster than and almost as accurate as LinearRgb. +// BUT it is important to NOTE that they only produce good results for valid colors r,g,b in [0,1]. +func (col Color) FastLinearRgb() (r, g, b float64) { + r = linearize_fast(col.R) + g = linearize_fast(col.G) + b = linearize_fast(col.B) + return +} + +func delinearize(v float64) float64 { + if v <= 0.0031308 { + return 12.92 * v + } + return 1.055*math.Pow(v, 1.0/2.4) - 0.055 +} + +// LinearRgb creates an sRGB color out of the given linear RGB color (see http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/). +func LinearRgb(r, g, b float64) Color { + return Color{delinearize(r), delinearize(g), delinearize(b)} +} + +func delinearize_fast(v float64) float64 { + // This function (fractional root) is much harder to linearize, so we need to split. + if v > 0.2 { + v1 := v - 0.6 + v2 := v1 * v1 + v3 := v2 * v1 + v4 := v2 * v2 + v5 := v3 * v2 + return 0.442430344268235 + 0.592178981271708*v - 0.287864782562636*v2 + 0.253214392068985*v3 - 0.272557158129811*v4 + 0.325554383321718*v5 + } else if v > 0.03 { + v1 := v - 0.115 + v2 := v1 * v1 + v3 := v2 * v1 + v4 := v2 * v2 + v5 := v3 * v2 + return 0.194915592891669 + 1.55227076330229*v - 3.93691860257828*v2 + 18.0679839248761*v3 - 101.468750302746*v4 + 632.341487393927*v5 + } else { + v1 := v - 0.015 + v2 := v1 * v1 + v3 := v2 * v1 + v4 := v2 * v2 + v5 := v3 * v2 + // You can clearly see from the involved constants that the low-end is highly nonlinear. + return 0.0519565234928877 + 5.09316778537561*v - 99.0338180489702*v2 + 3484.52322764895*v3 - 150028.083412663*v4 + 7168008.42971613*v5 + } +} + +// FastLinearRgb is much faster than and almost as accurate as LinearRgb. +// BUT it is important to NOTE that they only produce good results for valid inputs r,g,b in [0,1]. +func FastLinearRgb(r, g, b float64) Color { + return Color{delinearize_fast(r), delinearize_fast(g), delinearize_fast(b)} +} + +// XyzToLinearRgb converts from CIE XYZ-space to Linear RGB space. +func XyzToLinearRgb(x, y, z float64) (r, g, b float64) { + r = 3.2404542*x - 1.5371385*y - 0.4985314*z + g = -0.9692660*x + 1.8760108*y + 0.0415560*z + b = 0.0556434*x - 0.2040259*y + 1.0572252*z + return +} + +func LinearRgbToXyz(r, g, b float64) (x, y, z float64) { + x = 0.4124564*r + 0.3575761*g + 0.1804375*b + y = 0.2126729*r + 0.7151522*g + 0.0721750*b + z = 0.0193339*r + 0.1191920*g + 0.9503041*b + return +} + +/// XYZ /// +/////////// +// http://www.sjbrown.co.uk/2004/05/14/gamma-correct-rendering/ + +func (col Color) Xyz() (x, y, z float64) { + return LinearRgbToXyz(col.LinearRgb()) +} + +func Xyz(x, y, z float64) Color { + return LinearRgb(XyzToLinearRgb(x, y, z)) +} + +/// xyY /// +/////////// +// http://www.brucelindbloom.com/Eqn_XYZ_to_xyY.html + +// Well, the name is bad, since it's xyY but Golang needs me to start with a +// capital letter to make the method public. +func XyzToXyy(X, Y, Z float64) (x, y, Yout float64) { + return XyzToXyyWhiteRef(X, Y, Z, D65) +} + +func XyzToXyyWhiteRef(X, Y, Z float64, wref [3]float64) (x, y, Yout float64) { + Yout = Y + N := X + Y + Z + if math.Abs(N) < 1e-14 { + // When we have black, Bruce Lindbloom recommends to use + // the reference white's chromacity for x and y. + x = wref[0] / (wref[0] + wref[1] + wref[2]) + y = wref[1] / (wref[0] + wref[1] + wref[2]) + } else { + x = X / N + y = Y / N + } + return +} + +func XyyToXyz(x, y, Y float64) (X, Yout, Z float64) { + Yout = Y + + if -1e-14 < y && y < 1e-14 { + X = 0.0 + Z = 0.0 + } else { + X = Y / y * x + Z = Y / y * (1.0 - x - y) + } + + return +} + +// Converts the given color to CIE xyY space using D65 as reference white. +// (Note that the reference white is only used for black input.) +// x, y and Y are in [0..1] +func (col Color) Xyy() (x, y, Y float64) { + return XyzToXyy(col.Xyz()) +} + +// Converts the given color to CIE xyY space, taking into account +// a given reference white. (i.e. the monitor's white) +// (Note that the reference white is only used for black input.) +// x, y and Y are in [0..1] +func (col Color) XyyWhiteRef(wref [3]float64) (x, y, Y float64) { + X, Y2, Z := col.Xyz() + return XyzToXyyWhiteRef(X, Y2, Z, wref) +} + +// Generates a color by using data given in CIE xyY space. +// x, y and Y are in [0..1] +func Xyy(x, y, Y float64) Color { + return Xyz(XyyToXyz(x, y, Y)) +} + +/// L*a*b* /// +////////////// +// http://en.wikipedia.org/wiki/Lab_color_space#CIELAB-CIEXYZ_conversions +// For L*a*b*, we need to L*a*b*<->XYZ->RGB and the first one is device dependent. + +func lab_f(t float64) float64 { + if t > 6.0/29.0*6.0/29.0*6.0/29.0 { + return math.Cbrt(t) + } + return t/3.0*29.0/6.0*29.0/6.0 + 4.0/29.0 +} + +func XyzToLab(x, y, z float64) (l, a, b float64) { + // Use D65 white as reference point by default. + // http://www.fredmiranda.com/forum/topic/1035332 + // http://en.wikipedia.org/wiki/Standard_illuminant + return XyzToLabWhiteRef(x, y, z, D65) +} + +func XyzToLabWhiteRef(x, y, z float64, wref [3]float64) (l, a, b float64) { + fy := lab_f(y / wref[1]) + l = 1.16*fy - 0.16 + a = 5.0 * (lab_f(x/wref[0]) - fy) + b = 2.0 * (fy - lab_f(z/wref[2])) + return +} + +func lab_finv(t float64) float64 { + if t > 6.0/29.0 { + return t * t * t + } + return 3.0 * 6.0 / 29.0 * 6.0 / 29.0 * (t - 4.0/29.0) +} + +func LabToXyz(l, a, b float64) (x, y, z float64) { + // D65 white (see above). + return LabToXyzWhiteRef(l, a, b, D65) +} + +func LabToXyzWhiteRef(l, a, b float64, wref [3]float64) (x, y, z float64) { + l2 := (l + 0.16) / 1.16 + x = wref[0] * lab_finv(l2+a/5.0) + y = wref[1] * lab_finv(l2) + z = wref[2] * lab_finv(l2-b/2.0) + return +} + +// Converts the given color to CIE L*a*b* space using D65 as reference white. +func (col Color) Lab() (l, a, b float64) { + return XyzToLab(col.Xyz()) +} + +// Converts the given color to CIE L*a*b* space, taking into account +// a given reference white. (i.e. the monitor's white) +func (col Color) LabWhiteRef(wref [3]float64) (l, a, b float64) { + x, y, z := col.Xyz() + return XyzToLabWhiteRef(x, y, z, wref) +} + +// Generates a color by using data given in CIE L*a*b* space using D65 as reference white. +// WARNING: many combinations of `l`, `a`, and `b` values do not have corresponding +// valid RGB values, check the FAQ in the README if you're unsure. +func Lab(l, a, b float64) Color { + return Xyz(LabToXyz(l, a, b)) +} + +// Generates a color by using data given in CIE L*a*b* space, taking +// into account a given reference white. (i.e. the monitor's white) +func LabWhiteRef(l, a, b float64, wref [3]float64) Color { + return Xyz(LabToXyzWhiteRef(l, a, b, wref)) +} + +// DistanceLab is a good measure of visual similarity between two colors! +// A result of 0 would mean identical colors, while a result of 1 or higher +// means the colors differ a lot. +func (c1 Color) DistanceLab(c2 Color) float64 { + l1, a1, b1 := c1.Lab() + l2, a2, b2 := c2.Lab() + return math.Sqrt(sq(l1-l2) + sq(a1-a2) + sq(b1-b2)) +} + +// That's actually the same, but I don't want to break code. +func (c1 Color) DistanceCIE76(c2 Color) float64 { + return c1.DistanceLab(c2) +} + +// Uses the CIE94 formula to calculate color distance. More accurate than +// DistanceLab, but also more work. +func (cl Color) DistanceCIE94(cr Color) float64 { + l1, a1, b1 := cl.Lab() + l2, a2, b2 := cr.Lab() + + // NOTE: Since all those formulas expect L,a,b values 100x larger than we + // have them in this library, we either need to adjust all constants + // in the formula, or convert the ranges of L,a,b before, and then + // scale the distances down again. The latter is less error-prone. + l1, a1, b1 = l1*100.0, a1*100.0, b1*100.0 + l2, a2, b2 = l2*100.0, a2*100.0, b2*100.0 + + kl := 1.0 // 2.0 for textiles + kc := 1.0 + kh := 1.0 + k1 := 0.045 // 0.048 for textiles + k2 := 0.015 // 0.014 for textiles. + + deltaL := l1 - l2 + c1 := math.Sqrt(sq(a1) + sq(b1)) + c2 := math.Sqrt(sq(a2) + sq(b2)) + deltaCab := c1 - c2 + + // Not taking Sqrt here for stability, and it's unnecessary. + deltaHab2 := sq(a1-a2) + sq(b1-b2) - sq(deltaCab) + sl := 1.0 + sc := 1.0 + k1*c1 + sh := 1.0 + k2*c1 + + vL2 := sq(deltaL / (kl * sl)) + vC2 := sq(deltaCab / (kc * sc)) + vH2 := deltaHab2 / sq(kh*sh) + + return math.Sqrt(vL2+vC2+vH2) * 0.01 // See above. +} + +// BlendLab blends two colors in the L*a*b* color-space, which should result in a smoother blend. +// t == 0 results in c1, t == 1 results in c2 +func (c1 Color) BlendLab(c2 Color, t float64) Color { + l1, a1, b1 := c1.Lab() + l2, a2, b2 := c2.Lab() + return Lab(l1+t*(l2-l1), + a1+t*(a2-a1), + b1+t*(b2-b1)) +} + +/// L*u*v* /// +////////////// +// http://en.wikipedia.org/wiki/CIELUV#XYZ_.E2.86.92_CIELUV_and_CIELUV_.E2.86.92_XYZ_conversions +// For L*u*v*, we need to L*u*v*<->XYZ<->RGB and the first one is device dependent. + +func XyzToLuv(x, y, z float64) (l, a, b float64) { + // Use D65 white as reference point by default. + // http://www.fredmiranda.com/forum/topic/1035332 + // http://en.wikipedia.org/wiki/Standard_illuminant + return XyzToLuvWhiteRef(x, y, z, D65) +} + +func XyzToLuvWhiteRef(x, y, z float64, wref [3]float64) (l, u, v float64) { + if y/wref[1] <= 6.0/29.0*6.0/29.0*6.0/29.0 { + l = y / wref[1] * 29.0 / 3.0 * 29.0 / 3.0 * 29.0 / 3.0 + } else { + l = 1.16*math.Cbrt(y/wref[1]) - 0.16 + } + ubis, vbis := xyz_to_uv(x, y, z) + un, vn := xyz_to_uv(wref[0], wref[1], wref[2]) + u = 13.0 * l * (ubis - un) + v = 13.0 * l * (vbis - vn) + return +} + +// For this part, we do as R's graphics.hcl does, not as wikipedia does. +// Or is it the same? +func xyz_to_uv(x, y, z float64) (u, v float64) { + denom := x + 15.0*y + 3.0*z + if denom == 0.0 { + u, v = 0.0, 0.0 + } else { + u = 4.0 * x / denom + v = 9.0 * y / denom + } + return +} + +func LuvToXyz(l, u, v float64) (x, y, z float64) { + // D65 white (see above). + return LuvToXyzWhiteRef(l, u, v, D65) +} + +func LuvToXyzWhiteRef(l, u, v float64, wref [3]float64) (x, y, z float64) { + //y = wref[1] * lab_finv((l + 0.16) / 1.16) + if l <= 0.08 { + y = wref[1] * l * 100.0 * 3.0 / 29.0 * 3.0 / 29.0 * 3.0 / 29.0 + } else { + y = wref[1] * cub((l+0.16)/1.16) + } + un, vn := xyz_to_uv(wref[0], wref[1], wref[2]) + if l != 0.0 { + ubis := u/(13.0*l) + un + vbis := v/(13.0*l) + vn + x = y * 9.0 * ubis / (4.0 * vbis) + z = y * (12.0 - 3.0*ubis - 20.0*vbis) / (4.0 * vbis) + } else { + x, y = 0.0, 0.0 + } + return +} + +// Converts the given color to CIE L*u*v* space using D65 as reference white. +// L* is in [0..1] and both u* and v* are in about [-1..1] +func (col Color) Luv() (l, u, v float64) { + return XyzToLuv(col.Xyz()) +} + +// Converts the given color to CIE L*u*v* space, taking into account +// a given reference white. (i.e. the monitor's white) +// L* is in [0..1] and both u* and v* are in about [-1..1] +func (col Color) LuvWhiteRef(wref [3]float64) (l, u, v float64) { + x, y, z := col.Xyz() + return XyzToLuvWhiteRef(x, y, z, wref) +} + +// Generates a color by using data given in CIE L*u*v* space using D65 as reference white. +// L* is in [0..1] and both u* and v* are in about [-1..1] +// WARNING: many combinations of `l`, `a`, and `b` values do not have corresponding +// valid RGB values, check the FAQ in the README if you're unsure. +func Luv(l, u, v float64) Color { + return Xyz(LuvToXyz(l, u, v)) +} + +// Generates a color by using data given in CIE L*u*v* space, taking +// into account a given reference white. (i.e. the monitor's white) +// L* is in [0..1] and both u* and v* are in about [-1..1] +func LuvWhiteRef(l, u, v float64, wref [3]float64) Color { + return Xyz(LuvToXyzWhiteRef(l, u, v, wref)) +} + +// DistanceLuv is a good measure of visual similarity between two colors! +// A result of 0 would mean identical colors, while a result of 1 or higher +// means the colors differ a lot. +func (c1 Color) DistanceLuv(c2 Color) float64 { + l1, u1, v1 := c1.Luv() + l2, u2, v2 := c2.Luv() + return math.Sqrt(sq(l1-l2) + sq(u1-u2) + sq(v1-v2)) +} + +// BlendLuv blends two colors in the CIE-L*u*v* color-space, which should result in a smoother blend. +// t == 0 results in c1, t == 1 results in c2 +func (c1 Color) BlendLuv(c2 Color, t float64) Color { + l1, u1, v1 := c1.Luv() + l2, u2, v2 := c2.Luv() + return Luv(l1+t*(l2-l1), + u1+t*(u2-u1), + v1+t*(v2-v1)) +} + +/// HCL /// +/////////// +// HCL is nothing else than L*a*b* in cylindrical coordinates! +// (this was wrong on English wikipedia, I fixed it, let's hope the fix stays.) +// But it is widely popular since it is a "correct HSV" +// http://www.hunterlab.com/appnotes/an09_96a.pdf + +// Converts the given color to HCL space using D65 as reference white. +// H values are in [0..360], C and L values are in [0..1] although C can overshoot 1.0 +func (col Color) Hcl() (h, c, l float64) { + return col.HclWhiteRef(D65) +} + +func LabToHcl(L, a, b float64) (h, c, l float64) { + // Oops, floating point workaround necessary if a ~= b and both are very small (i.e. almost zero). + if math.Abs(b-a) > 1e-4 && math.Abs(a) > 1e-4 { + h = math.Mod(57.29577951308232087721*math.Atan2(b, a)+360.0, 360.0) // Rad2Deg + } else { + h = 0.0 + } + c = math.Sqrt(sq(a) + sq(b)) + l = L + return +} + +// Converts the given color to HCL space, taking into account +// a given reference white. (i.e. the monitor's white) +// H values are in [0..360], C and L values are in [0..1] +func (col Color) HclWhiteRef(wref [3]float64) (h, c, l float64) { + L, a, b := col.LabWhiteRef(wref) + return LabToHcl(L, a, b) +} + +// Generates a color by using data given in HCL space using D65 as reference white. +// H values are in [0..360], C and L values are in [0..1] +// WARNING: many combinations of `l`, `a`, and `b` values do not have corresponding +// valid RGB values, check the FAQ in the README if you're unsure. +func Hcl(h, c, l float64) Color { + return HclWhiteRef(h, c, l, D65) +} + +func HclToLab(h, c, l float64) (L, a, b float64) { + H := 0.01745329251994329576 * h // Deg2Rad + a = c * math.Cos(H) + b = c * math.Sin(H) + L = l + return +} + +// Generates a color by using data given in HCL space, taking +// into account a given reference white. (i.e. the monitor's white) +// H values are in [0..360], C and L values are in [0..1] +func HclWhiteRef(h, c, l float64, wref [3]float64) Color { + L, a, b := HclToLab(h, c, l) + return LabWhiteRef(L, a, b, wref) +} + +// BlendHcl blends two colors in the CIE-L*C*h° color-space, which should result in a smoother blend. +// t == 0 results in c1, t == 1 results in c2 +func (col1 Color) BlendHcl(col2 Color, t float64) Color { + h1, c1, l1 := col1.Hcl() + h2, c2, l2 := col2.Hcl() + + // We know that h are both in [0..360] + return Hcl(interp_angle(h1, h2, t), c1+t*(c2-c1), l1+t*(l2-l1)) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/colors_test.go b/vendor/github.com/lucasb-eyer/go-colorful/colors_test.go new file mode 100644 index 00000000..3cd9beff --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/colors_test.go @@ -0,0 +1,619 @@ +package colorful + +import ( + "image/color" + "math" + "math/rand" + "strings" + "testing" +) + +var bench_result float64 // Dummy for benchmarks to avoid optimization + +// Checks whether the relative error is below eps +func almosteq_eps(v1, v2, eps float64) bool { + if math.Abs(v1) > delta { + return math.Abs((v1-v2)/v1) < eps + } + return true +} + +// Checks whether the relative error is below the 8bit RGB delta, which should be good enough. +const delta = 1.0 / 256.0 + +func almosteq(v1, v2 float64) bool { + return almosteq_eps(v1, v2, delta) +} + +// Note: the XYZ, L*a*b*, etc. are using D65 white and D50 white if postfixed by "50". +// See http://www.brucelindbloom.com/index.html?ColorCalcHelp.html +// For d50 white, no "adaptation" and the sRGB model are used in colorful +// HCL values form http://www.easyrgb.com/index.php?X=CALC and missing ones hand-computed from lab ones +var vals = []struct { + c Color + hsl [3]float64 + hsv [3]float64 + hex string + xyz [3]float64 + xyy [3]float64 + lab [3]float64 + lab50 [3]float64 + luv [3]float64 + luv50 [3]float64 + hcl [3]float64 + hcl50 [3]float64 + rgba [4]uint32 + rgb255 [3]uint8 +}{ + {Color{1.0, 1.0, 1.0}, [3]float64{ 0.0, 0.0, 1.00}, [3]float64{ 0.0, 0.0, 1.0}, "#ffffff", [3]float64{0.950470, 1.000000, 1.088830}, [3]float64{0.312727, 0.329023, 1.000000}, [3]float64{1.000000, 0.000000, 0.000000}, [3]float64{1.000000,-0.023881,-0.193622}, [3]float64{1.00000, 0.00000, 0.00000}, [3]float64{1.00000,-0.14716,-0.25658}, [3]float64{ 0.0000, 0.000000, 1.000000}, [3]float64{262.9688, 0.195089, 1.000000}, [4]uint32{65535, 65535, 65535, 65535}, [3]uint8{255, 255, 255}}, + {Color{0.5, 1.0, 1.0}, [3]float64{180.0, 1.0, 0.75}, [3]float64{180.0, 0.5, 1.0}, "#80ffff", [3]float64{0.626296, 0.832848, 1.073634}, [3]float64{0.247276, 0.328828, 0.832848}, [3]float64{0.931390,-0.353319,-0.108946}, [3]float64{0.931390,-0.374100,-0.301663}, [3]float64{0.93139,-0.53909,-0.11630}, [3]float64{0.93139,-0.67615,-0.35528}, [3]float64{197.1371, 0.369735, 0.931390}, [3]float64{218.8817, 0.480574, 0.931390}, [4]uint32{32768, 65535, 65535, 65535}, [3]uint8{128, 255, 255}}, + {Color{1.0, 0.5, 1.0}, [3]float64{300.0, 1.0, 0.75}, [3]float64{300.0, 0.5, 1.0}, "#ff80ff", [3]float64{0.669430, 0.437920, 0.995150}, [3]float64{0.318397, 0.208285, 0.437920}, [3]float64{0.720892, 0.651673,-0.422133}, [3]float64{0.720892, 0.630425,-0.610035}, [3]float64{0.72089, 0.60047,-0.77626}, [3]float64{0.72089, 0.49438,-0.96123}, [3]float64{327.0661, 0.776450, 0.720892}, [3]float64{315.9417, 0.877257, 0.720892}, [4]uint32{65535, 32768, 65535, 65535}, [3]uint8{255, 128, 255}}, + {Color{1.0, 1.0, 0.5}, [3]float64{ 60.0, 1.0, 0.75}, [3]float64{ 60.0, 0.5, 1.0}, "#ffff80", [3]float64{0.808654, 0.943273, 0.341930}, [3]float64{0.386203, 0.450496, 0.943273}, [3]float64{0.977637,-0.165795, 0.602017}, [3]float64{0.977637,-0.188424, 0.470410}, [3]float64{0.97764, 0.05759, 0.79816}, [3]float64{0.97764,-0.08628, 0.54731}, [3]float64{105.3975, 0.624430, 0.977637}, [3]float64{111.8287, 0.506743, 0.977637}, [4]uint32{65535, 65535, 32768, 65535}, [3]uint8{255, 255, 128}}, + {Color{0.5, 0.5, 1.0}, [3]float64{240.0, 1.0, 0.75}, [3]float64{240.0, 0.5, 1.0}, "#8080ff", [3]float64{0.345256, 0.270768, 0.979954}, [3]float64{0.216329, 0.169656, 0.270768}, [3]float64{0.590453, 0.332846,-0.637099}, [3]float64{0.590453, 0.315806,-0.824040}, [3]float64{0.59045,-0.07568,-1.04877}, [3]float64{0.59045,-0.16257,-1.20027}, [3]float64{297.5843, 0.718805, 0.590453}, [3]float64{290.9689, 0.882482, 0.590453}, [4]uint32{32768, 32768, 65535, 65535}, [3]uint8{128, 128, 255}}, + {Color{1.0, 0.5, 0.5}, [3]float64{ 0.0, 1.0, 0.75}, [3]float64{ 0.0, 0.5, 1.0}, "#ff8080", [3]float64{0.527613, 0.381193, 0.248250}, [3]float64{0.455996, 0.329451, 0.381193}, [3]float64{0.681085, 0.483884, 0.228328}, [3]float64{0.681085, 0.464258, 0.110043}, [3]float64{0.68108, 0.92148, 0.19879}, [3]float64{0.68108, 0.82125, 0.02404}, [3]float64{ 25.2610, 0.535049, 0.681085}, [3]float64{ 13.3347, 0.477121, 0.681085}, [4]uint32{65535, 32768, 32768, 65535}, [3]uint8{255, 128, 128}}, + {Color{0.5, 1.0, 0.5}, [3]float64{120.0, 1.0, 0.75}, [3]float64{120.0, 0.5, 1.0}, "#80ff80", [3]float64{0.484480, 0.776121, 0.326734}, [3]float64{0.305216, 0.488946, 0.776121}, [3]float64{0.906026,-0.600870, 0.498993}, [3]float64{0.906026,-0.619946, 0.369365}, [3]float64{0.90603,-0.58869, 0.76102}, [3]float64{0.90603,-0.72202, 0.52855}, [3]float64{140.2920, 0.781050, 0.906026}, [3]float64{149.2134, 0.721640, 0.906026}, [4]uint32{32768, 65535, 32768, 65535}, [3]uint8{128, 255, 128}}, + {Color{0.5, 0.5, 0.5}, [3]float64{ 0.0, 0.0, 0.50}, [3]float64{ 0.0, 0.0, 0.5}, "#808080", [3]float64{0.203440, 0.214041, 0.233054}, [3]float64{0.312727, 0.329023, 0.214041}, [3]float64{0.533890, 0.000000, 0.000000}, [3]float64{0.533890,-0.014285,-0.115821}, [3]float64{0.53389, 0.00000, 0.00000}, [3]float64{0.53389,-0.07857,-0.13699}, [3]float64{ 0.0000, 0.000000, 0.533890}, [3]float64{262.9688, 0.116699, 0.533890}, [4]uint32{32768, 32768, 32768, 65535}, [3]uint8{128, 128, 128}}, + {Color{0.0, 1.0, 1.0}, [3]float64{180.0, 1.0, 0.50}, [3]float64{180.0, 1.0, 1.0}, "#00ffff", [3]float64{0.538014, 0.787327, 1.069496}, [3]float64{0.224656, 0.328760, 0.787327}, [3]float64{0.911132,-0.480875,-0.141312}, [3]float64{0.911132,-0.500630,-0.333781}, [3]float64{0.91113,-0.70477,-0.15204}, [3]float64{0.91113,-0.83886,-0.38582}, [3]float64{196.3762, 0.501209, 0.911132}, [3]float64{213.6923, 0.601698, 0.911132}, [4]uint32{ 0, 65535, 65535, 65535}, [3]uint8{ 0, 255, 255}}, + {Color{1.0, 0.0, 1.0}, [3]float64{300.0, 1.0, 0.50}, [3]float64{300.0, 1.0, 1.0}, "#ff00ff", [3]float64{0.592894, 0.284848, 0.969638}, [3]float64{0.320938, 0.154190, 0.284848}, [3]float64{0.603242, 0.982343,-0.608249}, [3]float64{0.603242, 0.961939,-0.794531}, [3]float64{0.60324, 0.84071,-1.08683}, [3]float64{0.60324, 0.75194,-1.24161}, [3]float64{328.2350, 1.155407, 0.603242}, [3]float64{320.4444, 1.247640, 0.603242}, [4]uint32{65535, 0, 65535, 65535}, [3]uint8{255, 0, 255}}, + {Color{1.0, 1.0, 0.0}, [3]float64{ 60.0, 1.0, 0.50}, [3]float64{ 60.0, 1.0, 1.0}, "#ffff00", [3]float64{0.770033, 0.927825, 0.138526}, [3]float64{0.419320, 0.505246, 0.927825}, [3]float64{0.971393,-0.215537, 0.944780}, [3]float64{0.971393,-0.237800, 0.847398}, [3]float64{0.97139, 0.07706, 1.06787}, [3]float64{0.97139,-0.06590, 0.81862}, [3]float64{102.8512, 0.969054, 0.971393}, [3]float64{105.6754, 0.880131, 0.971393}, [4]uint32{65535, 65535, 0, 65535}, [3]uint8{255, 255, 0}}, + {Color{0.0, 0.0, 1.0}, [3]float64{240.0, 1.0, 0.50}, [3]float64{240.0, 1.0, 1.0}, "#0000ff", [3]float64{0.180437, 0.072175, 0.950304}, [3]float64{0.150000, 0.060000, 0.072175}, [3]float64{0.322970, 0.791875,-1.078602}, [3]float64{0.322970, 0.778150,-1.263638}, [3]float64{0.32297,-0.09405,-1.30342}, [3]float64{0.32297,-0.14158,-1.38629}, [3]float64{306.2849, 1.338076, 0.322970}, [3]float64{301.6248, 1.484014, 0.322970}, [4]uint32{ 0, 0, 65535, 65535}, [3]uint8{ 0, 0, 255}}, + {Color{0.0, 1.0, 0.0}, [3]float64{120.0, 1.0, 0.50}, [3]float64{120.0, 1.0, 1.0}, "#00ff00", [3]float64{0.357576, 0.715152, 0.119192}, [3]float64{0.300000, 0.600000, 0.715152}, [3]float64{0.877347,-0.861827, 0.831793}, [3]float64{0.877347,-0.879067, 0.739170}, [3]float64{0.87735,-0.83078, 1.07398}, [3]float64{0.87735,-0.95989, 0.84887}, [3]float64{136.0160, 1.197759, 0.877347}, [3]float64{139.9409, 1.148534, 0.877347}, [4]uint32{ 0, 65535, 0, 65535}, [3]uint8{ 0, 255, 0}}, + {Color{1.0, 0.0, 0.0}, [3]float64{ 0.0, 1.0, 0.50}, [3]float64{ 0.0, 1.0, 1.0}, "#ff0000", [3]float64{0.412456, 0.212673, 0.019334}, [3]float64{0.640000, 0.330000, 0.212673}, [3]float64{0.532408, 0.800925, 0.672032}, [3]float64{0.532408, 0.782845, 0.621518}, [3]float64{0.53241, 1.75015, 0.37756}, [3]float64{0.53241, 1.67180, 0.24096}, [3]float64{ 39.9990, 1.045518, 0.532408}, [3]float64{ 38.4469, 0.999566, 0.532408}, [4]uint32{65535, 0, 0, 65535}, [3]uint8{255, 0, 0}}, + {Color{0.0, 0.0, 0.0}, [3]float64{ 0.0, 0.0, 0.00}, [3]float64{ 0.0, 0.0, 0.0}, "#000000", [3]float64{0.000000, 0.000000, 0.000000}, [3]float64{0.312727, 0.329023, 0.000000}, [3]float64{0.000000, 0.000000, 0.000000}, [3]float64{0.000000, 0.000000, 0.000000}, [3]float64{0.00000, 0.00000, 0.00000}, [3]float64{0.00000, 0.00000, 0.00000}, [3]float64{ 0.0000, 0.000000, 0.000000}, [3]float64{ 0.0000, 0.000000, 0.000000}, [4]uint32{ 0, 0, 0, 65535}, [3]uint8{ 0, 0, 0}}, +} + +// For testing short-hex values, since the above contains colors which don't +// have corresponding short hexes. +var shorthexvals = []struct { + c Color + hex string +}{ + {Color{1.0, 1.0, 1.0}, "#fff"}, + {Color{0.6, 1.0, 1.0}, "#9ff"}, + {Color{1.0, 0.6, 1.0}, "#f9f"}, + {Color{1.0, 1.0, 0.6}, "#ff9"}, + {Color{0.6, 0.6, 1.0}, "#99f"}, + {Color{1.0, 0.6, 0.6}, "#f99"}, + {Color{0.6, 1.0, 0.6}, "#9f9"}, + {Color{0.6, 0.6, 0.6}, "#999"}, + {Color{0.0, 1.0, 1.0}, "#0ff"}, + {Color{1.0, 0.0, 1.0}, "#f0f"}, + {Color{1.0, 1.0, 0.0}, "#ff0"}, + {Color{0.0, 0.0, 1.0}, "#00f"}, + {Color{0.0, 1.0, 0.0}, "#0f0"}, + {Color{1.0, 0.0, 0.0}, "#f00"}, + {Color{0.0, 0.0, 0.0}, "#000"}, +} + +/// RGBA /// +//////////// + +func TestRGBAConversion(t *testing.T) { + for i, tt := range vals { + r, g, b, a := tt.c.RGBA() + if r != tt.rgba[0] || g != tt.rgba[1] || b != tt.rgba[2] || a != tt.rgba[3] { + t.Errorf("%v. %v.RGBA() => (%v), want %v (delta %v)", i, tt.c, []uint32{r, g, b, a}, tt.rgba, delta) + } + } +} + +/// RGB255 /// +//////////// + +func TestRGB255Conversion(t *testing.T) { + for i, tt := range vals { + r, g, b := tt.c.RGB255() + if r != tt.rgb255[0] || g != tt.rgb255[1] || b != tt.rgb255[2] { + t.Errorf("%v. %v.RGB255() => (%v), want %v (delta %v)", i, tt.c, []uint8{r, g, b}, tt.rgb255, delta) + } + } +} + +/// HSV /// +/////////// + +func TestHsvCreation(t *testing.T) { + for i, tt := range vals { + c := Hsv(tt.hsv[0], tt.hsv[1], tt.hsv[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Hsv(%v) => (%v), want %v (delta %v)", i, tt.hsv, c, tt.c, delta) + } + } +} + +func TestHsvConversion(t *testing.T) { + for i, tt := range vals { + h, s, v := tt.c.Hsv() + if !almosteq(h, tt.hsv[0]) || !almosteq(s, tt.hsv[1]) || !almosteq(v, tt.hsv[2]) { + t.Errorf("%v. %v.Hsv() => (%v), want %v (delta %v)", i, tt.c, []float64{h, s, v}, tt.hsv, delta) + } + } +} + +/// HSL /// +/////////// + +func TestHslCreation(t *testing.T) { + for i, tt := range vals { + c := Hsl(tt.hsl[0], tt.hsl[1], tt.hsl[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Hsl(%v) => (%v), want %v (delta %v)", i, tt.hsl, c, tt.c, delta) + } + } +} + +func TestHslConversion(t *testing.T) { + for i, tt := range vals { + h, s, l := tt.c.Hsl() + if !almosteq(h, tt.hsl[0]) || !almosteq(s, tt.hsl[1]) || !almosteq(l, tt.hsl[2]) { + t.Errorf("%v. %v.Hsl() => (%v), want %v (delta %v)", i, tt.c, []float64{h, s, l}, tt.hsl, delta) + } + } +} + +/// Hex /// +/////////// + +func TestHexCreation(t *testing.T) { + for i, tt := range vals { + c, err := Hex(tt.hex) + if err != nil || !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Hex(%v) => (%v), want %v (delta %v)", i, tt.hex, c, tt.c, delta) + } + } +} + +func TestHEXCreation(t *testing.T) { + for i, tt := range vals { + c, err := Hex(strings.ToUpper(tt.hex)) + if err != nil || !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. HEX(%v) => (%v), want %v (delta %v)", i, strings.ToUpper(tt.hex), c, tt.c, delta) + } + } +} + +func TestShortHexCreation(t *testing.T) { + for i, tt := range shorthexvals { + c, err := Hex(tt.hex) + if err != nil || !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Hex(%v) => (%v), want %v (delta %v)", i, tt.hex, c, tt.c, delta) + } + } +} + +func TestShortHEXCreation(t *testing.T) { + for i, tt := range shorthexvals { + c, err := Hex(strings.ToUpper(tt.hex)) + if err != nil || !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Hex(%v) => (%v), want %v (delta %v)", i, strings.ToUpper(tt.hex), c, tt.c, delta) + } + } +} + +func TestHexConversion(t *testing.T) { + for i, tt := range vals { + hex := tt.c.Hex() + if hex != tt.hex { + t.Errorf("%v. %v.Hex() => (%v), want %v (delta %v)", i, tt.c, hex, tt.hex, delta) + } + } +} + +/// Linear /// +////////////// + +// LinearRgb itself is implicitly tested by XYZ conversions below (they use it). +// So what we do here is just test that the FastLinearRgb approximation is "good enough" +func TestFastLinearRgb(t *testing.T) { + const eps = 6.0 / 255.0 // We want that "within 6 RGB values total" is "good enough". + + for r := 0.0; r < 256.0; r++ { + for g := 0.0; g < 256.0; g++ { + for b := 0.0; b < 256.0; b++ { + c := Color{r / 255.0, g / 255.0, b / 255.0} + r_want, g_want, b_want := c.LinearRgb() + r_appr, g_appr, b_appr := c.FastLinearRgb() + dr, dg, db := math.Abs(r_want-r_appr), math.Abs(g_want-g_appr), math.Abs(b_want-b_appr) + if dr+dg+db > eps { + t.Errorf("FastLinearRgb not precise enough for %v: differences are (%v, %v, %v), allowed total difference is %v", c, dr, dg, db, eps) + return + } + + c_want := LinearRgb(r/255.0, g/255.0, b/255.0) + c_appr := FastLinearRgb(r/255.0, g/255.0, b/255.0) + dr, dg, db = math.Abs(c_want.R-c_appr.R), math.Abs(c_want.G-c_appr.G), math.Abs(c_want.B-c_appr.B) + if dr+dg+db > eps { + t.Errorf("FastLinearRgb not precise enough for (%v, %v, %v): differences are (%v, %v, %v), allowed total difference is %v", r, g, b, dr, dg, db, eps) + return + } + } + } + } +} + +// Also include some benchmarks to make sure the `Fast` versions are actually significantly faster! +// (Sounds silly, but the original ones weren't!) + +func BenchmarkColorToLinear(bench *testing.B) { + var r, g, b float64 + for n := 0; n < bench.N; n++ { + r, g, b = Color{rand.Float64(), rand.Float64(), rand.Float64()}.LinearRgb() + } + bench_result = r + g + b +} + +func BenchmarkFastColorToLinear(bench *testing.B) { + var r, g, b float64 + for n := 0; n < bench.N; n++ { + r, g, b = Color{rand.Float64(), rand.Float64(), rand.Float64()}.FastLinearRgb() + } + bench_result = r + g + b +} + +func BenchmarkLinearToColor(bench *testing.B) { + var c Color + for n := 0; n < bench.N; n++ { + c = LinearRgb(rand.Float64(), rand.Float64(), rand.Float64()) + } + bench_result = c.R + c.G + c.B +} + +func BenchmarkFastLinearToColor(bench *testing.B) { + var c Color + for n := 0; n < bench.N; n++ { + c = FastLinearRgb(rand.Float64(), rand.Float64(), rand.Float64()) + } + bench_result = c.R + c.G + c.B +} + +/// XYZ /// +/////////// +func TestXyzCreation(t *testing.T) { + for i, tt := range vals { + c := Xyz(tt.xyz[0], tt.xyz[1], tt.xyz[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Xyz(%v) => (%v), want %v (delta %v)", i, tt.xyz, c, tt.c, delta) + } + } +} + +func TestXyzConversion(t *testing.T) { + for i, tt := range vals { + x, y, z := tt.c.Xyz() + if !almosteq(x, tt.xyz[0]) || !almosteq(y, tt.xyz[1]) || !almosteq(z, tt.xyz[2]) { + t.Errorf("%v. %v.Xyz() => (%v), want %v (delta %v)", i, tt.c, [3]float64{x, y, z}, tt.xyz, delta) + } + } +} + +/// xyY /// +/////////// +func TestXyyCreation(t *testing.T) { + for i, tt := range vals { + c := Xyy(tt.xyy[0], tt.xyy[1], tt.xyy[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Xyy(%v) => (%v), want %v (delta %v)", i, tt.xyy, c, tt.c, delta) + } + } +} + +func TestXyyConversion(t *testing.T) { + for i, tt := range vals { + x, y, Y := tt.c.Xyy() + if !almosteq(x, tt.xyy[0]) || !almosteq(y, tt.xyy[1]) || !almosteq(Y, tt.xyy[2]) { + t.Errorf("%v. %v.Xyy() => (%v), want %v (delta %v)", i, tt.c, [3]float64{x, y, Y}, tt.xyy, delta) + } + } +} + +/// L*a*b* /// +////////////// +func TestLabCreation(t *testing.T) { + for i, tt := range vals { + c := Lab(tt.lab[0], tt.lab[1], tt.lab[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Lab(%v) => (%v), want %v (delta %v)", i, tt.lab, c, tt.c, delta) + } + } +} + +func TestLabConversion(t *testing.T) { + for i, tt := range vals { + l, a, b := tt.c.Lab() + if !almosteq(l, tt.lab[0]) || !almosteq(a, tt.lab[1]) || !almosteq(b, tt.lab[2]) { + t.Errorf("%v. %v.Lab() => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, a, b}, tt.lab, delta) + } + } +} + +func TestLabWhiteRefCreation(t *testing.T) { + for i, tt := range vals { + c := LabWhiteRef(tt.lab50[0], tt.lab50[1], tt.lab50[2], D50) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. LabWhiteRef(%v, D50) => (%v), want %v (delta %v)", i, tt.lab50, c, tt.c, delta) + } + } +} + +func TestLabWhiteRefConversion(t *testing.T) { + for i, tt := range vals { + l, a, b := tt.c.LabWhiteRef(D50) + if !almosteq(l, tt.lab50[0]) || !almosteq(a, tt.lab50[1]) || !almosteq(b, tt.lab50[2]) { + t.Errorf("%v. %v.LabWhiteRef(D50) => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, a, b}, tt.lab50, delta) + } + } +} + +/// L*u*v* /// +////////////// +func TestLuvCreation(t *testing.T) { + for i, tt := range vals { + c := Luv(tt.luv[0], tt.luv[1], tt.luv[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Luv(%v) => (%v), want %v (delta %v)", i, tt.luv, c, tt.c, delta) + } + } +} + +func TestLuvConversion(t *testing.T) { + for i, tt := range vals { + l, u, v := tt.c.Luv() + if !almosteq(l, tt.luv[0]) || !almosteq(u, tt.luv[1]) || !almosteq(v, tt.luv[2]) { + t.Errorf("%v. %v.Luv() => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, u, v}, tt.luv, delta) + } + } +} + +func TestLuvWhiteRefCreation(t *testing.T) { + for i, tt := range vals { + c := LuvWhiteRef(tt.luv50[0], tt.luv50[1], tt.luv50[2], D50) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. LuvWhiteRef(%v, D50) => (%v), want %v (delta %v)", i, tt.luv50, c, tt.c, delta) + } + } +} + +func TestLuvWhiteRefConversion(t *testing.T) { + for i, tt := range vals { + l, u, v := tt.c.LuvWhiteRef(D50) + if !almosteq(l, tt.luv50[0]) || !almosteq(u, tt.luv50[1]) || !almosteq(v, tt.luv50[2]) { + t.Errorf("%v. %v.LuvWhiteRef(D50) => (%v), want %v (delta %v)", i, tt.c, [3]float64{l, u, v}, tt.luv50, delta) + } + } +} + +/// HCL /// +/////////// +// CIE-L*a*b* in polar coordinates. +func TestHclCreation(t *testing.T) { + for i, tt := range vals { + c := Hcl(tt.hcl[0], tt.hcl[1], tt.hcl[2]) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. Hcl(%v) => (%v), want %v (delta %v)", i, tt.hcl, c, tt.c, delta) + } + } +} + +func TestHclConversion(t *testing.T) { + for i, tt := range vals { + h, c, l := tt.c.Hcl() + if !almosteq(h, tt.hcl[0]) || !almosteq(c, tt.hcl[1]) || !almosteq(l, tt.hcl[2]) { + t.Errorf("%v. %v.Hcl() => (%v), want %v (delta %v)", i, tt.c, [3]float64{h, c, l}, tt.hcl, delta) + } + } +} + +func TestHclWhiteRefCreation(t *testing.T) { + for i, tt := range vals { + c := HclWhiteRef(tt.hcl50[0], tt.hcl50[1], tt.hcl50[2], D50) + if !c.AlmostEqualRgb(tt.c) { + t.Errorf("%v. HclWhiteRef(%v, D50) => (%v), want %v (delta %v)", i, tt.hcl50, c, tt.c, delta) + } + } +} + +func TestHclWhiteRefConversion(t *testing.T) { + for i, tt := range vals { + h, c, l := tt.c.HclWhiteRef(D50) + if !almosteq(h, tt.hcl50[0]) || !almosteq(c, tt.hcl50[1]) || !almosteq(l, tt.hcl50[2]) { + t.Errorf("%v. %v.HclWhiteRef(D50) => (%v), want %v (delta %v)", i, tt.c, [3]float64{h, c, l}, tt.hcl50, delta) + } + } +} + +/// Test distances /// +////////////////////// + +// Ground-truth from http://www.brucelindbloom.com/index.html?ColorDifferenceCalcHelp.html +var dists = []struct { + c1 Color + c2 Color + d76 float64 // That's also dLab + d94 float64 +}{ + {Color{1.0, 1.0, 1.0}, Color{1.0, 1.0, 1.0}, 0.0, 0.0}, + {Color{0.0, 0.0, 0.0}, Color{0.0, 0.0, 0.0}, 0.0, 0.0}, + + // Just pairs of values of the table way above. + {Lab(1.000000, 0.000000, 0.000000), Lab(0.931390, -0.353319, -0.108946), 0.37604638, 0.37604638}, + {Lab(0.720892, 0.651673, -0.422133), Lab(0.977637, -0.165795, 0.602017), 1.33531088, 0.65466377}, + {Lab(0.590453, 0.332846, -0.637099), Lab(0.681085, 0.483884, 0.228328), 0.88317072, 0.42541075}, + {Lab(0.906026, -0.600870, 0.498993), Lab(0.533890, 0.000000, 0.000000), 0.86517280, 0.41038323}, + {Lab(0.911132, -0.480875, -0.141312), Lab(0.603242, 0.982343, -0.608249), 1.56647162, 0.87431457}, + {Lab(0.971393, -0.215537, 0.944780), Lab(0.322970, 0.791875, -1.078602), 2.35146891, 1.11858192}, + {Lab(0.877347, -0.861827, 0.831793), Lab(0.532408, 0.800925, 0.672032), 1.70565338, 0.68800270}, +} + +func TestLabDistance(t *testing.T) { + for i, tt := range dists { + d := tt.c1.DistanceCIE76(tt.c2) + if !almosteq(d, tt.d76) { + t.Errorf("%v. %v.DistanceCIE76(%v) => (%v), want %v (delta %v)", i, tt.c1, tt.c2, d, tt.d76, delta) + } + } +} + +func TestCIE94Distance(t *testing.T) { + for i, tt := range dists { + d := tt.c1.DistanceCIE94(tt.c2) + if !almosteq(d, tt.d94) { + t.Errorf("%v. %v.DistanceCIE94(%v) => (%v), want %v (delta %v)", i, tt.c1, tt.c2, d, tt.d94, delta) + } + } +} + +/// Test utilities /// +////////////////////// + +func TestClamp(t *testing.T) { + c_orig := Color{1.1, -0.1, 0.5} + c_want := Color{1.0, 0.0, 0.5} + if c_orig.Clamped() != c_want { + t.Errorf("%v.Clamped() => %v, want %v", c_orig, c_orig.Clamped(), c_want) + } +} + +func TestMakeColor(t *testing.T) { + c_orig_nrgba := color.NRGBA{123, 45, 67, 255} + c_ours, ok := MakeColor(c_orig_nrgba) + r, g, b := c_ours.RGB255() + if r != 123 || g != 45 || b != 67 || !ok { + t.Errorf("NRGBA->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_nrgba, r, g, b, ok) + } + + c_orig_nrgba64 := color.NRGBA64{123 << 8, 45 << 8, 67 << 8, 0xffff} + c_ours, ok = MakeColor(c_orig_nrgba64) + r, g, b = c_ours.RGB255() + if r != 123 || g != 45 || b != 67 || !ok { + t.Errorf("NRGBA64->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_nrgba64, r, g, b, ok) + } + + c_orig_gray := color.Gray{123} + c_ours, ok = MakeColor(c_orig_gray) + r, g, b = c_ours.RGB255() + if r != 123 || g != 123 || b != 123 || !ok { + t.Errorf("Gray->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_gray, r, g, b, ok) + } + + c_orig_gray16 := color.Gray16{123 << 8} + c_ours, ok = MakeColor(c_orig_gray16) + r, g, b = c_ours.RGB255() + if r != 123 || g != 123 || b != 123 || !ok { + t.Errorf("Gray16->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_gray16, r, g, b, ok) + } + + c_orig_rgba := color.RGBA{255, 255, 255, 0} + c_ours, ok = MakeColor(c_orig_rgba) + r, g, b = c_ours.RGB255() + if r != 0 || g != 0 || b != 0 || ok { + t.Errorf("RGBA->Colorful->RGB255 error: %v became (%v, %v, %v, %t)", c_orig_rgba, r, g, b, ok) + } +} + +/// Issues raised on github /// +/////////////////////////////// + +// https://github.com/lucasb-eyer/go-colorful/issues/11 +func TestIssue11(t *testing.T) { + c1hex := "#1a1a46" + c2hex := "#666666" + + c1, _ := Hex(c1hex) + c2, _ := Hex(c2hex) + + blend := c1.BlendHsv(c2, 0).Hex() + if blend != c1hex { + t.Errorf("Issue11: %v --Hsv-> %v = %v, want %v", c1hex, c2hex, blend, c1hex) + } + blend = c1.BlendHsv(c2, 1).Hex() + if blend != c2hex { + t.Errorf("Issue11: %v --Hsv-> %v = %v, want %v", c1hex, c2hex, blend, c2hex) + } + + blend = c1.BlendLuv(c2, 0).Hex() + if blend != c1hex { + t.Errorf("Issue11: %v --Luv-> %v = %v, want %v", c1hex, c2hex, blend, c1hex) + } + blend = c1.BlendLuv(c2, 1).Hex() + if blend != c2hex { + t.Errorf("Issue11: %v --Luv-> %v = %v, want %v", c1hex, c2hex, blend, c2hex) + } + + blend = c1.BlendRgb(c2, 0).Hex() + if blend != c1hex { + t.Errorf("Issue11: %v --Rgb-> %v = %v, want %v", c1hex, c2hex, blend, c1hex) + } + blend = c1.BlendRgb(c2, 1).Hex() + if blend != c2hex { + t.Errorf("Issue11: %v --Rgb-> %v = %v, want %v", c1hex, c2hex, blend, c2hex) + } + + blend = c1.BlendLab(c2, 0).Hex() + if blend != c1hex { + t.Errorf("Issue11: %v --Lab-> %v = %v, want %v", c1hex, c2hex, blend, c1hex) + } + blend = c1.BlendLab(c2, 1).Hex() + if blend != c2hex { + t.Errorf("Issue11: %v --Lab-> %v = %v, want %v", c1hex, c2hex, blend, c2hex) + } + + blend = c1.BlendHcl(c2, 0).Hex() + if blend != c1hex { + t.Errorf("Issue11: %v --Hcl-> %v = %v, want %v", c1hex, c2hex, blend, c1hex) + } + blend = c1.BlendHcl(c2, 1).Hex() + if blend != c2hex { + t.Errorf("Issue11: %v --Hcl-> %v = %v, want %v", c1hex, c2hex, blend, c2hex) + } +} + +// For testing angular interpolation internal function +// NOTE: They are being tested in both directions. +var anglevals = []struct { + a0 float64 + a1 float64 + t float64 + at float64 +}{ + {0.0, 1.0, 0.0, 0.0}, + {0.0, 1.0, 0.25, 0.25}, + {0.0, 1.0, 0.5, 0.5}, + {0.0, 1.0, 1.0, 1.0}, + {0.0, 90.0, 0.0, 0.0}, + {0.0, 90.0, 0.25, 22.5}, + {0.0, 90.0, 0.5, 45.0}, + {0.0, 90.0, 1.0, 90.0}, + {0.0, 178.0, 0.0, 0.0}, // Exact 0-180 is ambiguous. + {0.0, 178.0, 0.25, 44.5}, + {0.0, 178.0, 0.5, 89.0}, + {0.0, 178.0, 1.0, 178.0}, + {0.0, 182.0, 0.0, 0.0}, // Exact 0-180 is ambiguous. + {0.0, 182.0, 0.25, 315.5}, + {0.0, 182.0, 0.5, 271.0}, + {0.0, 182.0, 1.0, 182.0}, + {0.0, 270.0, 0.0, 0.0}, + {0.0, 270.0, 0.25, 337.5}, + {0.0, 270.0, 0.5, 315.0}, + {0.0, 270.0, 1.0, 270.0}, + {0.0, 359.0, 0.0, 0.0}, + {0.0, 359.0, 0.25, 359.75}, + {0.0, 359.0, 0.5, 359.5}, + {0.0, 359.0, 1.0, 359.0}, +} + +func TestInterpolation(t *testing.T) { + // Forward + for i, tt := range anglevals { + res := interp_angle(tt.a0, tt.a1, tt.t) + if !almosteq_eps(res, tt.at, 1e-15) { + t.Errorf("%v. interp_angle(%v, %v, %v) => (%v), want %v", i, tt.a0, tt.a1, tt.t, res, tt.at) + } + } + // Backward + for i, tt := range anglevals { + res := interp_angle(tt.a1, tt.a0, 1.0-tt.t) + if !almosteq_eps(res, tt.at, 1e-15) { + t.Errorf("%v. interp_angle(%v, %v, %v) => (%v), want %v", i, tt.a1, tt.a0, 1.0-tt.t, res, tt.at) + } + } +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/doc/colorblend/colorblend.go b/vendor/github.com/lucasb-eyer/go-colorful/doc/colorblend/colorblend.go new file mode 100644 index 00000000..a4b17876 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/doc/colorblend/colorblend.go @@ -0,0 +1,41 @@ +package main + +import "fmt" +import "github.com/lucasb-eyer/go-colorful" +import "image" +import "image/draw" +import "image/png" +import "os" + +func main() { + blocks := 10 + blockw := 40 + img := image.NewRGBA(image.Rect(0, 0, blocks*blockw, 200)) + + c1, _ := colorful.Hex("#fdffcc") + c2, _ := colorful.Hex("#242a42") + + // Use these colors to get invalid RGB in the gradient. + //c1, _ := colorful.Hex("#EEEF61") + //c2, _ := colorful.Hex("#1E3140") + + for i := 0; i < blocks; i++ { + draw.Draw(img, image.Rect(i*blockw, 0, (i+1)*blockw, 40), &image.Uniform{c1.BlendHsv(c2, float64(i)/float64(blocks-1))}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*blockw, 40, (i+1)*blockw, 80), &image.Uniform{c1.BlendLuv(c2, float64(i)/float64(blocks-1))}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*blockw, 80, (i+1)*blockw, 120), &image.Uniform{c1.BlendRgb(c2, float64(i)/float64(blocks-1))}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*blockw, 120, (i+1)*blockw, 160), &image.Uniform{c1.BlendLab(c2, float64(i)/float64(blocks-1))}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*blockw, 160, (i+1)*blockw, 200), &image.Uniform{c1.BlendHcl(c2, float64(i)/float64(blocks-1))}, image.ZP, draw.Src) + + // This can be used to "fix" invalid colors in the gradient. + //draw.Draw(img, image.Rect(i*blockw,160,(i+1)*blockw,200), &image.Uniform{c1.BlendHcl(c2, float64(i)/float64(blocks-1)).Clamped()}, image.ZP, draw.Src) + } + + toimg, err := os.Create("colorblend.png") + if err != nil { + fmt.Printf("Error: %v", err) + return + } + defer toimg.Close() + + png.Encode(toimg, img) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/doc/colordist/colordist.go b/vendor/github.com/lucasb-eyer/go-colorful/doc/colordist/colordist.go new file mode 100644 index 00000000..7d8c558e --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/doc/colordist/colordist.go @@ -0,0 +1,17 @@ +package main + +import "fmt" +import "github.com/lucasb-eyer/go-colorful" + +func main() { + c1a := colorful.Color{150.0 / 255.0, 10.0 / 255.0, 150.0 / 255.0} + c1b := colorful.Color{53.0 / 255.0, 10.0 / 255.0, 150.0 / 255.0} + c2a := colorful.Color{10.0 / 255.0, 150.0 / 255.0, 50.0 / 255.0} + c2b := colorful.Color{99.9 / 255.0, 150.0 / 255.0, 10.0 / 255.0} + + fmt.Printf("DistanceRgb: c1: %v\tand c2: %v\n", c1a.DistanceRgb(c1b), c2a.DistanceRgb(c2b)) + fmt.Printf("DistanceLab: c1: %v\tand c2: %v\n", c1a.DistanceLab(c1b), c2a.DistanceLab(c2b)) + fmt.Printf("DistanceLuv: c1: %v\tand c2: %v\n", c1a.DistanceLuv(c1b), c2a.DistanceLuv(c2b)) + fmt.Printf("DistanceCIE76: c1: %v\tand c2: %v\n", c1a.DistanceCIE76(c1b), c2a.DistanceCIE76(c2b)) + fmt.Printf("DistanceCIE94: c1: %v\tand c2: %v\n", c1a.DistanceCIE94(c1b), c2a.DistanceCIE94(c2b)) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/doc/colorgens/colorgens.go b/vendor/github.com/lucasb-eyer/go-colorful/doc/colorgens/colorgens.go new file mode 100644 index 00000000..f13aca59 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/doc/colorgens/colorgens.go @@ -0,0 +1,39 @@ +package main + +import "fmt" +import "github.com/lucasb-eyer/go-colorful" +import "image" +import "image/draw" +import "image/png" +import "math/rand" +import "os" +import "time" + +func main() { + blocks := 10 + blockw := 40 + space := 5 + + rand.Seed(time.Now().UTC().UnixNano()) + img := image.NewRGBA(image.Rect(0, 0, blocks*blockw+space*(blocks-1), 4*(blockw+space))) + + for i := 0; i < blocks; i++ { + warm := colorful.WarmColor() + fwarm := colorful.FastWarmColor() + happy := colorful.HappyColor() + fhappy := colorful.FastHappyColor() + draw.Draw(img, image.Rect(i*(blockw+space), 0, i*(blockw+space)+blockw, blockw), &image.Uniform{warm}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), blockw+space, i*(blockw+space)+blockw, 2*blockw+space), &image.Uniform{fwarm}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 2*blockw+3*space, i*(blockw+space)+blockw, 3*blockw+3*space), &image.Uniform{happy}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 3*blockw+4*space, i*(blockw+space)+blockw, 4*blockw+4*space), &image.Uniform{fhappy}, image.ZP, draw.Src) + } + + toimg, err := os.Create("colorgens.png") + if err != nil { + fmt.Printf("Error: %v", err) + return + } + defer toimg.Close() + + png.Encode(toimg, img) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/doc/gradientgen/gradientgen.go b/vendor/github.com/lucasb-eyer/go-colorful/doc/gradientgen/gradientgen.go new file mode 100644 index 00000000..4180986c --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/doc/gradientgen/gradientgen.go @@ -0,0 +1,84 @@ +package main + +import "github.com/lucasb-eyer/go-colorful" +import "image" +import "image/draw" +import "image/png" +import "os" +import "strconv" + +// This table contains the "keypoints" of the colorgradient you want to generate. +// The position of each keypoint has to live in the range [0,1] +type GradientTable []struct { + Col colorful.Color + Pos float64 +} + +// This is the meat of the gradient computation. It returns a HCL-blend between +// the two colors around `t`. +// Note: It relies heavily on the fact that the gradient keypoints are sorted. +func (self GradientTable) GetInterpolatedColorFor(t float64) colorful.Color { + for i := 0; i < len(self)-1; i++ { + c1 := self[i] + c2 := self[i+1] + if c1.Pos <= t && t <= c2.Pos { + // We are in between c1 and c2. Go blend them! + t := (t - c1.Pos) / (c2.Pos - c1.Pos) + return c1.Col.BlendHcl(c2.Col, t).Clamped() + } + } + + // Nothing found? Means we're at (or past) the last gradient keypoint. + return self[len(self)-1].Col +} + +// This is a very nice thing Golang forces you to do! +// It is necessary so that we can write out the literal of the colortable below. +func MustParseHex(s string) colorful.Color { + c, err := colorful.Hex(s) + if err != nil { + panic("MustParseHex: " + err.Error()) + } + return c +} + +func main() { + // The "keypoints" of the gradient. + keypoints := GradientTable{ + {MustParseHex("#9e0142"), 0.0}, + {MustParseHex("#d53e4f"), 0.1}, + {MustParseHex("#f46d43"), 0.2}, + {MustParseHex("#fdae61"), 0.3}, + {MustParseHex("#fee090"), 0.4}, + {MustParseHex("#ffffbf"), 0.5}, + {MustParseHex("#e6f598"), 0.6}, + {MustParseHex("#abdda4"), 0.7}, + {MustParseHex("#66c2a5"), 0.8}, + {MustParseHex("#3288bd"), 0.9}, + {MustParseHex("#5e4fa2"), 1.0}, + } + + h := 1024 + w := 40 + + if len(os.Args) == 3 { + // Meh, I'm being lazy... + w, _ = strconv.Atoi(os.Args[1]) + h, _ = strconv.Atoi(os.Args[2]) + } + + img := image.NewRGBA(image.Rect(0, 0, w, h)) + + for y := h - 1; y >= 0; y-- { + c := keypoints.GetInterpolatedColorFor(float64(y) / float64(h)) + draw.Draw(img, image.Rect(0, y, w, y+1), &image.Uniform{c}, image.ZP, draw.Src) + } + + outpng, err := os.Create("gradientgen.png") + if err != nil { + panic("Error storing png: " + err.Error()) + } + defer outpng.Close() + + png.Encode(outpng, img) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/doc/palettegens/palettegens.go b/vendor/github.com/lucasb-eyer/go-colorful/doc/palettegens/palettegens.go new file mode 100644 index 00000000..16e606be --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/doc/palettegens/palettegens.go @@ -0,0 +1,64 @@ +package main + +import "fmt" +import "github.com/lucasb-eyer/go-colorful" +import "image" +import "image/draw" +import "image/png" +import "math/rand" +import "os" +import "time" + +func main() { + colors := 10 + blockw := 40 + space := 5 + + rand.Seed(time.Now().UTC().UnixNano()) + img := image.NewRGBA(image.Rect(0, 0, colors*blockw+space*(colors-1), 6*blockw+8*space)) + + warm, err := colorful.WarmPalette(colors) + if err != nil { + fmt.Printf("Error generating warm palette: %v", err) + return + } + fwarm := colorful.FastWarmPalette(colors) + happy, err := colorful.HappyPalette(colors) + if err != nil { + fmt.Printf("Error generating happy palette: %v", err) + return + } + fhappy := colorful.FastHappyPalette(colors) + soft, err := colorful.SoftPalette(colors) + if err != nil { + fmt.Printf("Error generating soft palette: %v", err) + return + } + brownies, err := colorful.SoftPaletteEx(colors, colorful.SoftPaletteSettings{isbrowny, 50, true}) + if err != nil { + fmt.Printf("Error generating brownies: %v", err) + return + } + for i := 0; i < colors; i++ { + draw.Draw(img, image.Rect(i*(blockw+space), 0, i*(blockw+space)+blockw, blockw), &image.Uniform{warm[i]}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 1*blockw+1*space, i*(blockw+space)+blockw, 2*blockw+1*space), &image.Uniform{fwarm[i]}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 2*blockw+3*space, i*(blockw+space)+blockw, 3*blockw+3*space), &image.Uniform{happy[i]}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 3*blockw+4*space, i*(blockw+space)+blockw, 4*blockw+4*space), &image.Uniform{fhappy[i]}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 4*blockw+6*space, i*(blockw+space)+blockw, 5*blockw+6*space), &image.Uniform{soft[i]}, image.ZP, draw.Src) + draw.Draw(img, image.Rect(i*(blockw+space), 5*blockw+8*space, i*(blockw+space)+blockw, 6*blockw+8*space), &image.Uniform{brownies[i]}, image.ZP, draw.Src) + } + + toimg, err := os.Create("palettegens.png") + if err != nil { + fmt.Printf("Error: %v", err) + return + } + defer toimg.Close() + + png.Encode(toimg, img) +} + +func isbrowny(l, a, b float64) bool { + h, c, L := colorful.LabToHcl(l, a, b) + return 10.0 < h && h < 50.0 && 0.1 < c && c < 0.5 && L < 0.5 +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go b/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go new file mode 100644 index 00000000..bb66dfa4 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/happy_palettegen.go @@ -0,0 +1,25 @@ +package colorful + +import ( + "math/rand" +) + +// Uses the HSV color space to generate colors with similar S,V but distributed +// evenly along their Hue. This is fast but not always pretty. +// If you've got time to spare, use Lab (the non-fast below). +func FastHappyPalette(colorsCount int) (colors []Color) { + colors = make([]Color, colorsCount) + + for i := 0; i < colorsCount; i++ { + colors[i] = Hsv(float64(i)*(360.0/float64(colorsCount)), 0.8+rand.Float64()*0.2, 0.65+rand.Float64()*0.2) + } + return +} + +func HappyPalette(colorsCount int) ([]Color, error) { + pimpy := func(l, a, b float64) bool { + _, c, _ := LabToHcl(l, a, b) + return 0.3 <= c && 0.4 <= l && l <= 0.8 + } + return SoftPaletteEx(colorsCount, SoftPaletteSettings{pimpy, 50, true}) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go b/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go new file mode 100644 index 00000000..86a5ed98 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/hexcolor.go @@ -0,0 +1,37 @@ +package colorful + +import ( + "database/sql/driver" + "fmt" + "reflect" +) + +// A HexColor is a Color stored as a hex string "#rrggbb". It implements the +// database/sql.Scanner and database/sql/driver.Value interfaces. +type HexColor Color + +type errUnsupportedType struct { + got interface{} + want reflect.Type +} + +func (hc *HexColor) Scan(value interface{}) error { + s, ok := value.(string) + if !ok { + return errUnsupportedType{got: reflect.TypeOf(value), want: reflect.TypeOf("")} + } + c, err := Hex(s) + if err != nil { + return err + } + *hc = HexColor(c) + return nil +} + +func (hc *HexColor) Value() (driver.Value, error) { + return Color(*hc).Hex(), nil +} + +func (e errUnsupportedType) Error() string { + return fmt.Sprintf("unsupported type: got %v, want a %s", e.got, e.want) +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/hexcolor_test.go b/vendor/github.com/lucasb-eyer/go-colorful/hexcolor_test.go new file mode 100644 index 00000000..041d3ea9 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/hexcolor_test.go @@ -0,0 +1,58 @@ +package colorful + +import ( + "fmt" + "log" + "reflect" + "testing" + + "gopkg.in/DATA-DOG/go-sqlmock.v1" +) + +func TestHexColor(t *testing.T) { + for _, tc := range []struct { + hc HexColor + s string + }{ + {HexColor{R: 0, G: 0, B: 0}, "#000000"}, + {HexColor{R: 1, G: 0, B: 0}, "#ff0000"}, + {HexColor{R: 0, G: 1, B: 0}, "#00ff00"}, + {HexColor{R: 0, G: 0, B: 1}, "#0000ff"}, + {HexColor{R: 1, G: 1, B: 1}, "#ffffff"}, + } { + var gotHC HexColor + if err := gotHC.Scan(tc.s); err != nil { + t.Errorf("_.Scan(%q) == %v, want ", tc.s, err) + } + if !reflect.DeepEqual(gotHC, tc.hc) { + t.Errorf("_.Scan(%q) wrote %v, want %v", tc.s, gotHC, tc.hc) + } + if gotValue, err := tc.hc.Value(); err != nil || !reflect.DeepEqual(gotValue, tc.s) { + t.Errorf("%v.Value() == %v, %v, want %v, ", tc.hc, gotValue, err, tc.s) + } + } +} + +func Example_HexColor_Scan() { + db, mock, err := sqlmock.New() + if err != nil { + log.Fatal(err) + } + defer db.Close() + + mock.ExpectQuery("SELECT '#ff0000' AS color;"). + WillReturnRows( + sqlmock.NewRows([]string{"color"}). + AddRow("#ff0000"), + ) + + var hc HexColor + if err := db.QueryRow("SELECT '#ff0000' AS color;").Scan(&hc); err != nil { + log.Fatal(err) + } + + fmt.Printf("hc = %+v\n", hc) + + // Output: + // hc = {R:1 G:0 B:0} +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go b/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go new file mode 100644 index 00000000..0154ac9b --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen.go @@ -0,0 +1,185 @@ +// Largely inspired by the descriptions in http://lab.medialab.sciences-po.fr/iwanthue/ +// but written from scratch. + +package colorful + +import ( + "fmt" + "math" + "math/rand" +) + +// The algorithm works in L*a*b* color space and converts to RGB in the end. +// L* in [0..1], a* and b* in [-1..1] +type lab_t struct { + L, A, B float64 +} + +type SoftPaletteSettings struct { + // A function which can be used to restrict the allowed color-space. + CheckColor func(l, a, b float64) bool + + // The higher, the better quality but the slower. Usually two figures. + Iterations int + + // Use up to 160000 or 8000 samples of the L*a*b* space (and thus calls to CheckColor). + // Set this to true only if your CheckColor shapes the Lab space weirdly. + ManySamples bool +} + +// Yeah, windows-stype Foo, FooEx, screw you golang... +// Uses K-means to cluster the color-space and return the means of the clusters +// as a new palette of distinctive colors. Falls back to K-medoid if the mean +// happens to fall outside of the color-space, which can only happen if you +// specify a CheckColor function. +func SoftPaletteEx(colorsCount int, settings SoftPaletteSettings) ([]Color, error) { + + // Checks whether it's a valid RGB and also fulfills the potentially provided constraint. + check := func(col lab_t) bool { + c := Lab(col.L, col.A, col.B) + return c.IsValid() && (settings.CheckColor == nil || settings.CheckColor(col.L, col.A, col.B)) + } + + // Sample the color space. These will be the points k-means is run on. + dl := 0.05 + dab := 0.1 + if settings.ManySamples { + dl = 0.01 + dab = 0.05 + } + + samples := make([]lab_t, 0, int(1.0/dl*2.0/dab*2.0/dab)) + for l := 0.0; l <= 1.0; l += dl { + for a := -1.0; a <= 1.0; a += dab { + for b := -1.0; b <= 1.0; b += dab { + if check(lab_t{l, a, b}) { + samples = append(samples, lab_t{l, a, b}) + } + } + } + } + + // That would cause some infinite loops down there... + if len(samples) < colorsCount { + return nil, fmt.Errorf("palettegen: more colors requested (%v) than samples available (%v). Your requested color count may be wrong, you might want to use many samples or your constraint function makes the valid color space too small.", colorsCount, len(samples)) + } else if len(samples) == colorsCount { + return labs2cols(samples), nil // Oops? + } + + // We take the initial means out of the samples, so they are in fact medoids. + // This helps us avoid infinite loops or arbitrary cutoffs with too restrictive constraints. + means := make([]lab_t, colorsCount) + for i := 0; i < colorsCount; i++ { + for means[i] = samples[rand.Intn(len(samples))]; in(means, i, means[i]); means[i] = samples[rand.Intn(len(samples))] { + } + } + + clusters := make([]int, len(samples)) + samples_used := make([]bool, len(samples)) + + // The actual k-means/medoid iterations + for i := 0; i < settings.Iterations; i++ { + // Reassing the samples to clusters, i.e. to their closest mean. + // By the way, also check if any sample is used as a medoid and if so, mark that. + for isample, sample := range samples { + samples_used[isample] = false + mindist := math.Inf(+1) + for imean, mean := range means { + dist := lab_dist(sample, mean) + if dist < mindist { + mindist = dist + clusters[isample] = imean + } + + // Mark samples which are used as a medoid. + if lab_eq(sample, mean) { + samples_used[isample] = true + } + } + } + + // Compute new means according to the samples. + for imean := range means { + // The new mean is the average of all samples belonging to it.. + nsamples := 0 + newmean := lab_t{0.0, 0.0, 0.0} + for isample, sample := range samples { + if clusters[isample] == imean { + nsamples++ + newmean.L += sample.L + newmean.A += sample.A + newmean.B += sample.B + } + } + if nsamples > 0 { + newmean.L /= float64(nsamples) + newmean.A /= float64(nsamples) + newmean.B /= float64(nsamples) + } else { + // That mean doesn't have any samples? Get a new mean from the sample list! + var inewmean int + for inewmean = rand.Intn(len(samples_used)); samples_used[inewmean]; inewmean = rand.Intn(len(samples_used)) { + } + newmean = samples[inewmean] + samples_used[inewmean] = true + } + + // But now we still need to check whether the new mean is an allowed color. + if nsamples > 0 && check(newmean) { + // It does, life's good (TM) + means[imean] = newmean + } else { + // New mean isn't an allowed color or doesn't have any samples! + // Switch to medoid mode and pick the closest (unused) sample. + // This should always find something thanks to len(samples) >= colorsCount + mindist := math.Inf(+1) + for isample, sample := range samples { + if !samples_used[isample] { + dist := lab_dist(sample, newmean) + if dist < mindist { + mindist = dist + newmean = sample + } + } + } + } + } + } + return labs2cols(means), nil +} + +// A wrapper which uses common parameters. +func SoftPalette(colorsCount int) ([]Color, error) { + return SoftPaletteEx(colorsCount, SoftPaletteSettings{nil, 50, false}) +} + +func in(haystack []lab_t, upto int, needle lab_t) bool { + for i := 0; i < upto && i < len(haystack); i++ { + if haystack[i] == needle { + return true + } + } + return false +} + +const LAB_DELTA = 1e-6 + +func lab_eq(lab1, lab2 lab_t) bool { + return math.Abs(lab1.L-lab2.L) < LAB_DELTA && + math.Abs(lab1.A-lab2.A) < LAB_DELTA && + math.Abs(lab1.B-lab2.B) < LAB_DELTA +} + +// That's faster than using colorful's DistanceLab since we would have to +// convert back and forth for that. Here is no conversion. +func lab_dist(lab1, lab2 lab_t) float64 { + return math.Sqrt(sq(lab1.L-lab2.L) + sq(lab1.A-lab2.A) + sq(lab1.B-lab2.B)) +} + +func labs2cols(labs []lab_t) (cols []Color) { + cols = make([]Color, len(labs)) + for k, v := range labs { + cols[k] = Lab(v.L, v.A, v.B) + } + return cols +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen_test.go b/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen_test.go new file mode 100644 index 00000000..b748d7b0 --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/soft_palettegen_test.go @@ -0,0 +1,65 @@ +package colorful + +import ( + "fmt" + "testing" +) + +// This is really difficult to test, if you've got a good idea, pull request! + +// Check if it returns all valid and enough colors. +func TestColorCount(t *testing.T) { + fmt.Printf("Testing up to %v palettes may take a while...\n", 100) + for i := 0; i < 100; i++ { + //pal, err := SoftPaletteEx(i, SoftPaletteGenSettings{nil, 50, true}) + pal, err := SoftPalette(i) + if err != nil { + t.Errorf("Error: %v", err) + } + + // Check the color count of the palette + if len(pal) != i { + t.Errorf("Requested %v colors but got %v", i, len(pal)) + } + + // Also check whether all colors exist in RGB space. + for icol, col := range pal { + if !col.IsValid() { + t.Errorf("Color %v in palette of %v is invalid: %v", icol, len(pal), col) + } + } + } + fmt.Println("Done with that, but more tests to run.") +} + +// Check if it errors-out on an impossible constraint +func TestImpossibleConstraint(t *testing.T) { + never := func(l, a, b float64) bool { return false } + + pal, err := SoftPaletteEx(10, SoftPaletteSettings{never, 50, true}) + if err == nil || pal != nil { + t.Error("Should error-out on impossible constraint!") + } +} + +// Check whether the constraint is respected +func TestConstraint(t *testing.T) { + octant := func(l, a, b float64) bool { return l <= 0.5 && a <= 0.0 && b <= 0.0 } + + pal, err := SoftPaletteEx(100, SoftPaletteSettings{octant, 50, true}) + if err != nil { + t.Errorf("Error: %v", err) + } + + // Check ALL the colors! + for icol, col := range pal { + if !col.IsValid() { + t.Errorf("Color %v in constrained palette is invalid: %v", icol, col) + } + + l, a, b := col.Lab() + if l > 0.5 || a > 0.0 || b > 0.0 { + t.Errorf("Color %v in constrained palette violates the constraint: %v (lab: %v)", icol, col, [3]float64{l, a, b}) + } + } +} diff --git a/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go b/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go new file mode 100644 index 00000000..00f42a5c --- /dev/null +++ b/vendor/github.com/lucasb-eyer/go-colorful/warm_palettegen.go @@ -0,0 +1,25 @@ +package colorful + +import ( + "math/rand" +) + +// Uses the HSV color space to generate colors with similar S,V but distributed +// evenly along their Hue. This is fast but not always pretty. +// If you've got time to spare, use Lab (the non-fast below). +func FastWarmPalette(colorsCount int) (colors []Color) { + colors = make([]Color, colorsCount) + + for i := 0; i < colorsCount; i++ { + colors[i] = Hsv(float64(i)*(360.0/float64(colorsCount)), 0.55+rand.Float64()*0.2, 0.35+rand.Float64()*0.2) + } + return +} + +func WarmPalette(colorsCount int) ([]Color, error) { + warmy := func(l, a, b float64) bool { + _, c, _ := LabToHcl(l, a, b) + return 0.1 <= c && c <= 0.4 && 0.2 <= l && l <= 0.5 + } + return SoftPaletteEx(colorsCount, SoftPaletteSettings{warmy, 50, true}) +}