diff --git a/go.mod b/go.mod index a31c613..ab90bc7 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/disintegration/imaging v1.6.2 github.com/makeworld-the-better-one/dither/v2 v2.3.0 + github.com/mccutchen/palettor v1.0.0 // indirect github.com/urfave/cli/v2 v2.3.0 golang.org/x/image v0.0.0-20210220032944-ac19c3e999fb ) diff --git a/go.sum b/go.sum index bc67db6..de9028d 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,9 @@ github.com/disintegration/imaging v1.6.2 h1:w1LecBlG2Lnp8B3jk5zSuNqd7b4DXhcjwek1 github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4= github.com/makeworld-the-better-one/dither/v2 v2.3.0 h1:s9wgm88KFZSzvZh9gL79tPayp5sDUGIku/1aJewxlB4= github.com/makeworld-the-better-one/dither/v2 v2.3.0/go.mod h1:VBtN8DXO7SNtyGmLiGA7IsFeKrBkQPze1/iAeM95arc= +github.com/mccutchen/palettor v1.0.0 h1:YRNAzEZlRBnu8qP/9siuNTJiAj7VhL0dEQ1AQmu9jew= +github.com/mccutchen/palettor v1.0.0/go.mod h1:5ZFq9YwI0o5zRpmAuEsm+0B7divaVds1dvTAznEnd6g= +github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= diff --git a/subcommand_helpers.go b/subcommand_helpers.go index 2aee494..7b107fa 100644 --- a/subcommand_helpers.go +++ b/subcommand_helpers.go @@ -9,6 +9,7 @@ import ( "image/gif" "image/png" "io" + "log" "math" "os" "path/filepath" @@ -17,6 +18,7 @@ import ( "github.com/disintegration/imaging" "github.com/makeworld-the-better-one/dither/v2" + "github.com/mccutchen/palettor" "github.com/urfave/cli/v2" "golang.org/x/image/colornames" ) @@ -49,7 +51,9 @@ func parsePercentArg(arg string, maxOne bool) (float64, error) { // globalFlag returns the value of flag at the top level of the command. // For example, with the command: -// dither --threads 1 edm -s Simple2D +// +// dither --threads 1 edm -s Simple2D +// // "threads" is a global flag, and "s" is a flag local to the edm subcommand. func globalFlag(flag string, c *cli.Context) interface{} { ancestor := c.Lineage()[len(c.Lineage())-1] @@ -132,10 +136,38 @@ func rgbaToColor(s string) (color.NRGBA, error) { return color.NRGBA{r, g, b, a}, nil } +// extractInputPalette extracts a 5-color palette from the first input image +// using palettor. +func extractInputPalette(flag string, c *cli.Context) ([]color.Color, error) { + img, err := getInputImage(inputImages[0], c) + if err != nil { + return nil, fmt.Errorf("error loading image for palette extraction '%v': %w", inputImages, err) + } + + // Resize: keep palettor.Extract fast. See the palettor CLI source: + // https://github.com/mccutchen/palettor/blob/3eaed180/cmd/palettor/palettor.go#L57 + thumbnail := imaging.Resize(img, 200, 200, imaging.NearestNeighbor) + + // TODO: make these settings configurable, particularly the number of colors + // in the palette. That means threading the argument through the CLI. + palette, err := palettor.Extract(5, 500, thumbnail) + if err != nil { + return nil, fmt.Errorf("error extracting image palette: %w", err) + } + + log.Printf("Extracted palette: %v", palette.Colors()) + return palette.Colors(), nil +} + // parseColors takes args and turns them into a color slice. All returned // colors are guaranteed to only be color.NRGBA. func parseColors(flag string, c *cli.Context) ([]color.Color, error) { args := parseArgs([]string{globalFlag(flag, c).(string)}, " ") + + if len(args) == 1 && args[0] == "sample" { + return extractInputPalette(flag, c) + } + colors := make([]color.Color, len(args)) for i, arg := range args { diff --git a/subcommands.go b/subcommands.go index 7ce7235..57784a0 100644 --- a/subcommands.go +++ b/subcommands.go @@ -71,39 +71,6 @@ func preProcess(c *cli.Context) error { runtime.GOMAXPROCS(int(c.Uint("threads"))) var err error - palette, err = parseColors("palette", c) - if err != nil { - return err - } - if len(palette) < 2 { - return errors.New("the palette must have at least two colors") - } - - if c.String("recolor") != "" { - recolorPalette, err = parseColors("recolor", c) - if err != nil { - return err - } - if len(recolorPalette) != len(palette) { - return errors.New("recolor palette must have the same number of colors as the initial palette") - } - } - - // Check if palette is grayscale and make image grayscale - // Or if the user forces it - - grayscale = true - if !c.Bool("grayscale") { - // Grayscale isn't specified by the user - // So check to see if palette is grayscale - for _, c := range palette { - r, g, b, _ := c.RGBA() - if r != g || g != b { - grayscale = false - break - } - } - } saturation, err = parsePercentArg(c.String("saturation"), false) if err != nil { @@ -138,6 +105,40 @@ func preProcess(c *cli.Context) error { } } + palette, err = parseColors("palette", c) + if err != nil { + return err + } + if len(palette) < 2 { + return errors.New("the palette must have at least two colors") + } + + if c.String("recolor") != "" { + recolorPalette, err = parseColors("recolor", c) + if err != nil { + return err + } + if len(recolorPalette) != len(palette) { + return errors.New("recolor palette must have the same number of colors as the initial palette") + } + } + + // Check if palette is grayscale and make image grayscale + // Or if the user forces it + + grayscale = true + if !c.Bool("grayscale") { + // Grayscale isn't specified by the user + // So check to see if palette is grayscale + for _, c := range palette { + r, g, b, _ := c.RGBA() + if r != g || g != b { + grayscale = false + break + } + } + } + formatVal := c.String("format") if formatVal != "png" && formatVal != "gif" { return fmt.Errorf(unsupportedFormat, formatVal)