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
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: <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
.\"
.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

View File

@@ -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: <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'**
**-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.

View File

@@ -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

View File

@@ -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