From 849df3525818fa79f55def23112965ed4521420a Mon Sep 17 00:00:00 2001 From: makeworld Date: Sun, 9 May 2021 23:29:18 -0400 Subject: [PATCH] Support RGBA for recolor Fixes #1 --- MANPAGE.md | 6 ++-- didder.1 | 13 ++++++-- didder.1.md | 4 ++- subcommand_helpers.go | 70 ++++++++++++++++++++++++++++++++----------- subcommands.go | 4 +-- 5 files changed, 73 insertions(+), 24 deletions(-) diff --git a/MANPAGE.md b/MANPAGE.md index 692f903..de83b8e 100644 --- a/MANPAGE.md +++ b/MANPAGE.md @@ -3,7 +3,7 @@ title: DIDDER section: 1 header: User Manual -footer: didder v1.0.0-3-g2404e20 +footer: didder v1.0.0-4-g7593701 date: May 09, 2021 --- @@ -16,6 +16,8 @@ didder — dither images # DESCRIPTION Dither images with a variety of algorithms and processing options. +Images with transparency are supported, and their alpha channel is kept the way it was to begin with. + Mandatory global flags are **\--palette**, **\--in**, and **\--out**, all others are optional. Each command applies a dithering algorithm or set of algorithms to the input image(s). The most important parts of this manual are highlighted in the **TIPS** section, make sure you check it out! @@ -49,7 +51,7 @@ Here's an example of all color formats being used: **\--palette \'23,230,100 D24 **-r**, **\--recolor** *COLORS* -Set the color palette used for replacing the dithered color palette after dithering. The argument syntax is the same as **\--palette**. +Set the color palette used for replacing the dithered color palette after dithering. The argument syntax is the same as **\--palette**, with one exception. It also supports RGB*A* tuples, so 4 values. This means you can also choose to change the opacity of a palette color after dithering. The values are not premultiplied, so set the RGB to the color you want as you'd expect. The **\--recolor** flag exists because when palettes that are severely limited in terms of RGB spread are used, accurately representing the image colors with the desired palette is impossible. Instead of accuracy of color, the new goal is accuracy of luminance, or even just accuracy of contrast. For example, the original Nintendo Game Boy used a solely green palette: . By setting **\--palette** to shades of gray and then **\--recolor**-ing to the desired shades of green, input images will be converted to grayscale automatically and then dithered in one dimension (gray), rather than trying to dither a color image (three dimensions, RGB) into a one dimensional green palette. This is similar to "hue shifting" or "colorizing" an image in image editing software. diff --git a/didder.1 b/didder.1 index aa24104..745adb8 100644 --- a/didder.1 +++ b/didder.1 @@ -1,6 +1,6 @@ .\" Automatically generated by Pandoc 2.13 .\" -.TH "DIDDER" "1" "May 09, 2021" "didder v1.0.0-3-g2404e20" "User Manual" +.TH "DIDDER" "1" "May 09, 2021" "didder v1.0.0-4-g7593701" "User Manual" .hy .SH NAME .PP @@ -13,6 +13,9 @@ didder \[em] dither images .PP Dither images with a variety of algorithms and processing options. .PP +Images with transparency are supported, and their alpha channel is kept +the way it was to begin with. +.PP Mandatory global flags are \f[B]--palette\f[R], \f[B]--in\f[R], and \f[B]--out\f[R], all others are optional. Each command applies a dithering algorithm or set of algorithms to the @@ -78,7 +81,13 @@ Here\[cq]s an example of all color formats being used: \f[B]--palette \f[B]-r\f[R], \f[B]--recolor\f[R] \f[I]COLORS\f[R] Set the color palette used for replacing the dithered color palette after dithering. -The argument syntax is the same as \f[B]--palette\f[R]. +The argument syntax is the same as \f[B]--palette\f[R], with one +exception. +It also supports RGB\f[I]A\f[R] tuples, so 4 values. +This means you can also choose to change the opacity of a palette color +after dithering. +The values are not premultiplied, so set the RGB to the color you want +as you\[cq]d expect. .RS .PP The \f[B]--recolor\f[R] flag exists because when palettes that are diff --git a/didder.1.md b/didder.1.md index b36d0ba..5d1087b 100644 --- a/didder.1.md +++ b/didder.1.md @@ -15,6 +15,8 @@ didder — dither images # DESCRIPTION Dither images with a variety of algorithms and processing options. +Images with transparency are supported, and their alpha channel is kept the way it was to begin with. + Mandatory global flags are **\--palette**, **\--in**, and **\--out**, all others are optional. Each command applies a dithering algorithm or set of algorithms to the input image(s). The most important parts of this manual are highlighted in the **TIPS** section, make sure you check it out! @@ -44,7 +46,7 @@ Homepage: Here's an example of all color formats being used: **\--palette \'23,230,100 D24242 135 forestGreen'** **-r**, **\--recolor** *COLORS* -: Set the color palette used for replacing the dithered color palette after dithering. The argument syntax is the same as **\--palette**. +: Set the color palette used for replacing the dithered color palette after dithering. The argument syntax is the same as **\--palette**, with one exception. It also supports RGB*A* tuples, so 4 values. This means you can also choose to change the opacity of a palette color after dithering. The values are not premultiplied, so set the RGB to the color you want as you'd expect. The **\--recolor** flag exists because when palettes that are severely limited in terms of RGB spread are used, accurately representing the image colors with the desired palette is impossible. Instead of accuracy of color, the new goal is accuracy of luminance, or even just accuracy of contrast. For example, the original Nintendo Game Boy used a solely green palette: . By setting **\--palette** to shades of gray and then **\--recolor**-ing to the desired shades of green, input images will be converted to grayscale automatically and then dithered in one dimension (gray), rather than trying to dither a color image (three dimensions, RGB) into a one dimensional green palette. This is similar to "hue shifting" or "colorizing" an image in image editing software. diff --git a/subcommand_helpers.go b/subcommand_helpers.go index 1019c35..2aee494 100644 --- a/subcommand_helpers.go +++ b/subcommand_helpers.go @@ -88,7 +88,7 @@ func parseArgs(args []string, splitRunes string) []string { return finalArgs } -func hexToColor(hex string) (color.RGBA, error) { +func hexToColor(hex string) (color.NRGBA, error) { // Modified from https://github.com/lucasb-eyer/go-colorful/blob/v1.2.0/colors.go#L333 hex = strings.TrimPrefix(hex, "#") @@ -97,35 +97,50 @@ func hexToColor(hex string) (color.RGBA, error) { var r, g, b uint8 n, err := fmt.Sscanf(strings.ToLower(hex), format, &r, &g, &b) if err != nil { - return color.RGBA{}, err + return color.NRGBA{}, err } if n != 3 { - return color.RGBA{}, fmt.Errorf("%s is not a hex color", hex) + return color.NRGBA{}, fmt.Errorf("%s is not a hex color", hex) } - return color.RGBA{r, g, b, 255}, nil + return color.NRGBA{r, g, b, 255}, nil } -func rgbToColor(s string) (color.RGBA, error) { +func rgbToColor(s string) (color.NRGBA, error) { format := "%d,%d,%d" var r, g, b uint8 n, err := fmt.Sscanf(s, format, &r, &g, &b) if err != nil { - return color.RGBA{}, err + return color.NRGBA{}, err } if n != 3 { - return color.RGBA{}, fmt.Errorf("%s is not an RGB tuple", s) + return color.NRGBA{}, fmt.Errorf("%s is not an RGB tuple", s) } - return color.RGBA{r, g, b, 255}, nil + return color.NRGBA{r, g, b, 255}, nil +} + +func rgbaToColor(s string) (color.NRGBA, error) { + format := "%d,%d,%d,%d" + var r, g, b, a uint8 + n, err := fmt.Sscanf(s, format, &r, &g, &b, &a) + if err != nil { + return color.NRGBA{}, err + } + if n != 4 { + return color.NRGBA{}, fmt.Errorf("%s is not an RGBA tuple", s) + } + // Parse as non-premult, as that's more user-friendly + return color.NRGBA{r, g, b, a}, nil } // parseColors takes args and turns them into a color slice. All returned -// colors are guaranteed to only be color.RGBA. +// 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)}, " ") colors := make([]color.Color, len(args)) for i, arg := range args { // Try to parse as RGB numbers, then hex, then grayscale, then SVG colors, then fail + // Optionally try for RGBA if it's recolor, see #1 if strings.Count(arg, ",") == 2 { rgbColor, err := rgbToColor(arg) @@ -136,6 +151,15 @@ func parseColors(flag string, c *cli.Context) ([]color.Color, error) { continue } + if flag == "recolor" && strings.Count(arg, ",") == 3 { + rgbaColor, err := rgbaToColor(arg) + if err != nil { + return nil, fmt.Errorf("%s: %s is not a valid RGBA tuple. Example: 25,200,150,100", flag, arg) + } + colors[i] = rgbaColor + continue + } + hexColor, err := hexToColor(arg) if err == nil { colors[i] = hexColor @@ -147,13 +171,13 @@ func parseColors(flag string, c *cli.Context) ([]color.Color, error) { if n > 255 || n < 0 { return nil, fmt.Errorf("%s: single numbers like %d must be in the range 0-255", flag, n) } - colors[i] = color.RGBA{uint8(n), uint8(n), uint8(n), 255} + colors[i] = color.NRGBA{uint8(n), uint8(n), uint8(n), 255} continue } htmlColor, ok := colornames.Map[strings.ToLower(arg)] if ok { - colors[i] = htmlColor + colors[i] = color.NRGBAModel.Convert(htmlColor).(color.NRGBA) continue } @@ -229,10 +253,22 @@ func recolor(src image.Image) image.Image { // Modified and returned value var img draw.Image - // Map of original palette colors to recolor colors - paletteToRecolor := make(map[color.Color]color.Color) - for i, c := range palette { - paletteToRecolor[c] = recolorPalette[i] + // getRecolor takes an image color and returns the recolor one + getRecolor := func(a color.Color) color.Color { + // palette and recolorPalette are both NRGBA, so use that here too + c := color.NRGBAModel.Convert(a).(color.NRGBA) + + for i := range palette { + pc := palette[i].(color.NRGBA) + if pc.R == c.R && pc.G == c.G && pc.B == c.B { + // Colors match. Alpha is ignored because palette colors aren't + // allowed alpha, so theirs will always be 255. While the image + // might have a different alpha at that point + return recolorPalette[i] + } + } + // This should never happen + return recolorPalette[0] } // Fast path for paletted images @@ -240,7 +276,7 @@ func recolor(src image.Image) image.Image { // For each color in the image palette, replace it with the equivalent // recolor palette color for i, c := range p.Palette { - p.Palette[i] = paletteToRecolor[color.RGBAModel.Convert(c)] + p.Palette[i] = getRecolor(c) } return p } @@ -259,7 +295,7 @@ func recolor(src image.Image) image.Image { for x := b.Min.X; x < b.Max.X; x++ { // Image pixel -> convert to RGBA -> find recolor palette color using map // -> set color - img.Set(x, y, paletteToRecolor[color.RGBAModel.Convert(img.At(x, y))]) + img.Set(x, y, getRecolor(img.At(x, y))) } } return img diff --git a/subcommands.go b/subcommands.go index 30db0f6..7ce7235 100644 --- a/subcommands.go +++ b/subcommands.go @@ -26,11 +26,11 @@ const ( var ( // palette stores the palette colors. It's set after pre-processing. - // Guaranteed to only hold color.RGBA. + // Guaranteed to only hold color.NRGBA. palette []color.Color // recolorPalette stores the recolor palette colors. It's set after pre-processing. - // Guaranteed to only hold color.RGBA. + // Guaranteed to only hold color.NRGBA. recolorPalette []color.Color grayscale bool