Support RGBA for recolor

Fixes #1
This commit is contained in:
makeworld
2021-05-09 23:29:18 -04:00
parent 7593701803
commit 849df35258
5 changed files with 73 additions and 24 deletions

View File

@@ -3,7 +3,7 @@
title: DIDDER title: DIDDER
section: 1 section: 1
header: User Manual header: User Manual
footer: didder v1.0.0-3-g2404e20 footer: didder v1.0.0-4-g7593701
date: May 09, 2021 date: May 09, 2021
--- ---
@@ -16,6 +16,8 @@ didder — dither images
# DESCRIPTION # DESCRIPTION
Dither images with a variety of algorithms and processing options. 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). 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! 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* **-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: <https://en.wikipedia.org/wiki/List_of_video_game_console_palettes#Game_Boy>. 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. 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: <https://en.wikipedia.org/wiki/List_of_video_game_console_palettes#Game_Boy>. 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.

View File

@@ -1,6 +1,6 @@
.\" Automatically generated by Pandoc 2.13 .\" 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 .hy
.SH NAME .SH NAME
.PP .PP
@@ -13,6 +13,9 @@ didder \[em] dither images
.PP .PP
Dither images with a variety of algorithms and processing options. Dither images with a variety of algorithms and processing options.
.PP .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 Mandatory global flags are \f[B]--palette\f[R], \f[B]--in\f[R], and
\f[B]--out\f[R], all others are optional. \f[B]--out\f[R], all others are optional.
Each command applies a dithering algorithm or set of algorithms to the 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] \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 Set the color palette used for replacing the dithered color palette
after dithering. 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 .RS
.PP .PP
The \f[B]--recolor\f[R] flag exists because when palettes that are The \f[B]--recolor\f[R] flag exists because when palettes that are

View File

@@ -15,6 +15,8 @@ didder — dither images
# DESCRIPTION # DESCRIPTION
Dither images with a variety of algorithms and processing options. 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). 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! The most important parts of this manual are highlighted in the **TIPS** section, make sure you check it out!
@@ -44,7 +46,7 @@ Homepage: <https://github.com/makeworld-the-better-one/didder>
Here's an example of all color formats being used: **\--palette \'23,230,100 D24242 135 forestGreen'** Here's an example of all color formats being used: **\--palette \'23,230,100 D24242 135 forestGreen'**
**-r**, **\--recolor** *COLORS* **-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: <https://en.wikipedia.org/wiki/List_of_video_game_console_palettes#Game_Boy>. 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. 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: <https://en.wikipedia.org/wiki/List_of_video_game_console_palettes#Game_Boy>. 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.

View File

@@ -88,7 +88,7 @@ func parseArgs(args []string, splitRunes string) []string {
return finalArgs 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 // Modified from https://github.com/lucasb-eyer/go-colorful/blob/v1.2.0/colors.go#L333
hex = strings.TrimPrefix(hex, "#") hex = strings.TrimPrefix(hex, "#")
@@ -97,35 +97,50 @@ func hexToColor(hex string) (color.RGBA, error) {
var r, g, b uint8 var r, g, b uint8
n, err := fmt.Sscanf(strings.ToLower(hex), format, &r, &g, &b) n, err := fmt.Sscanf(strings.ToLower(hex), format, &r, &g, &b)
if err != nil { if err != nil {
return color.RGBA{}, err return color.NRGBA{}, err
} }
if n != 3 { 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" format := "%d,%d,%d"
var r, g, b uint8 var r, g, b uint8
n, err := fmt.Sscanf(s, format, &r, &g, &b) n, err := fmt.Sscanf(s, format, &r, &g, &b)
if err != nil { if err != nil {
return color.RGBA{}, err return color.NRGBA{}, err
} }
if n != 3 { 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 // 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) { func parseColors(flag string, c *cli.Context) ([]color.Color, error) {
args := parseArgs([]string{globalFlag(flag, c).(string)}, " ") args := parseArgs([]string{globalFlag(flag, c).(string)}, " ")
colors := make([]color.Color, len(args)) colors := make([]color.Color, len(args))
for i, arg := range args { for i, arg := range args {
// Try to parse as RGB numbers, then hex, then grayscale, then SVG colors, then fail // 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 { if strings.Count(arg, ",") == 2 {
rgbColor, err := rgbToColor(arg) rgbColor, err := rgbToColor(arg)
@@ -136,6 +151,15 @@ func parseColors(flag string, c *cli.Context) ([]color.Color, error) {
continue 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) hexColor, err := hexToColor(arg)
if err == nil { if err == nil {
colors[i] = hexColor colors[i] = hexColor
@@ -147,13 +171,13 @@ func parseColors(flag string, c *cli.Context) ([]color.Color, error) {
if n > 255 || n < 0 { if n > 255 || n < 0 {
return nil, fmt.Errorf("%s: single numbers like %d must be in the range 0-255", flag, n) 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 continue
} }
htmlColor, ok := colornames.Map[strings.ToLower(arg)] htmlColor, ok := colornames.Map[strings.ToLower(arg)]
if ok { if ok {
colors[i] = htmlColor colors[i] = color.NRGBAModel.Convert(htmlColor).(color.NRGBA)
continue continue
} }
@@ -229,10 +253,22 @@ func recolor(src image.Image) image.Image {
// Modified and returned value // Modified and returned value
var img draw.Image var img draw.Image
// Map of original palette colors to recolor colors // getRecolor takes an image color and returns the recolor one
paletteToRecolor := make(map[color.Color]color.Color) getRecolor := func(a color.Color) color.Color {
for i, c := range palette { // palette and recolorPalette are both NRGBA, so use that here too
paletteToRecolor[c] = recolorPalette[i] 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 // 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 // For each color in the image palette, replace it with the equivalent
// recolor palette color // recolor palette color
for i, c := range p.Palette { for i, c := range p.Palette {
p.Palette[i] = paletteToRecolor[color.RGBAModel.Convert(c)] p.Palette[i] = getRecolor(c)
} }
return p return p
} }
@@ -259,7 +295,7 @@ func recolor(src image.Image) image.Image {
for x := b.Min.X; x < b.Max.X; x++ { for x := b.Min.X; x < b.Max.X; x++ {
// Image pixel -> convert to RGBA -> find recolor palette color using map // Image pixel -> convert to RGBA -> find recolor palette color using map
// -> set color // -> 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 return img

View File

@@ -26,11 +26,11 @@ const (
var ( var (
// palette stores the palette colors. It's set after pre-processing. // 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 palette []color.Color
// recolorPalette stores the recolor palette colors. It's set after pre-processing. // 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 recolorPalette []color.Color
grayscale bool grayscale bool