M README.md => README.md +19 -8
@@ 1,12 1,23 @@
# colorswap
-Reads from STDIN, looking for HEX color codes or RGB color codes. It will swap
-all occurences in each line to the other type. It prints the modified files to
-STDOUT.
-
+Finds color codes from STDIN and replaces them with a new format:
```sh
-colorswap < sample.txt
+$ echo 'rgb(155,112,255)' | colorswap -hex
+#9b70ff
+```
+
+You can pass in a huge file intermingled with text, code, and colors. The
+output (and detectable input) formats are:
+```
+hex: #9b70ff
+rgb: rgb(155,112,255)
+rgba: rgba(155,112,255,128)
+vec3: vec3(0.607843,0.439216,1.000000)
+vec4: vec4(0.607843,0.439216,0.500000)
```
+Capitalization doesn't matter for hex inputs, and the shorthand form `#EEE` is
+accepted. For the other formats, spaces are accepted after the commas and you
+can use less precision in your vecs.
# Install
```
@@ 20,9 31,9 @@ sudo make uninstall
```
# Author
-Written and maintained by Dakota Walsh.
-Up-to-date sources can be found at https://git.sr.ht/~kota/colorswap/
+Written and maintained by Dakota Walsh.\
+Up-to-date sources can be found at https://git.sr.ht/~kota/colorswap/.
# License
+Copyright 2023 Dakota Walsh\
GNU GPL version 3 or later, see LICENSE.
-Copyright 2022 Dakota Walsh
M go.mod => go.mod +0 -2
@@ 1,5 1,3 @@
module git.sr.ht/~kota/colorswap
go 1.18
-
-require gopkg.in/go-playground/colors.v1 v1.2.0
D go.sum => go.sum +0 -2
@@ 1,2 0,0 @@
-gopkg.in/go-playground/colors.v1 v1.2.0 h1:SPweMUve+ywPrfwao+UvfD5Ah78aOLUkT5RlJiZn52c=
-gopkg.in/go-playground/colors.v1 v1.2.0/go.mod h1:AvbqcMpNXVl5gBrM20jBm3VjjKBbH/kI5UnqjU7lxFI=
M main.go => main.go +29 -34
@@ 2,50 2,45 @@ package main
import (
"bufio"
+ "flag"
"fmt"
+ "log"
"os"
- "regexp"
-
- "gopkg.in/go-playground/colors.v1"
)
-var validHEX = regexp.MustCompile(`(?i)#[0-9a-f]{6}`)
-var validRGB = regexp.MustCompile(`rgb\((?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]))\)`)
-
func main() {
- scanner := bufio.NewScanner(os.Stdin)
- for scanner.Scan() {
- fmt.Printf("%s\n", swap(scanner.Bytes()))
- }
-}
+ log.SetPrefix("")
+ log.SetFlags(0)
-// swap reads a []byte representing a line of text and replaces any HEX color
-// codes with RGB color codes or vice versa.
-//
-// A HEX color code is in the form #F8F8F8 (lowercase is accepted). An RGB code
-// is in the form 235, 100, 0 with or without spaces, though no more than one
-// space and optionally with 0 padded numbers.
-func swap(src []byte) []byte {
- if validHEX.Match(src) {
- return validHEX.ReplaceAllFunc(src, swapHEX)
- }
- return validRGB.ReplaceAllFunc(src, swapRGB)
-}
+ hexPtr := flag.Bool("hex", false, "convert to hex")
+ rgbPtr := flag.Bool("rgb", false, "convert to rgb")
+ rgbaPtr := flag.Bool("rgba", false, "convert to rgba")
+ vec3Ptr := flag.Bool("vec3", false, "convert to vec3")
+ vec4Ptr := flag.Bool("vec4", false, "convert to vec4")
+ flag.Parse()
-// swapHEX replaces all HEX color codes with RGB color codes in a []byte.
-func swapHEX(src []byte) []byte {
- color, err := colors.ParseHEX(string(src))
+ format, err := getFormat(
+ hexPtr,
+ rgbPtr,
+ rgbaPtr,
+ vec3Ptr,
+ vec4Ptr,
+ )
if err != nil {
- return src
+ log.Fatalln(err)
+ }
+
+ scanner := bufio.NewScanner(os.Stdin)
+ for scanner.Scan() {
+ fmt.Printf("%s\n", Swap(scanner.Bytes(), format))
}
- return []byte(color.ToRGB().String())
}
-// swapRGB replaces all RGB color codes with HEX color codes in a []byte.
-func swapRGB(src []byte) []byte {
- color, err := colors.ParseRGB(string(src))
- if err != nil {
- return src
+func getFormat(opts ...*bool) (Format, error) {
+ for i, op := range opts {
+ if *op {
+ return Format(i), nil
+ }
}
- return []byte(color.ToHEX().String())
+ return 0, fmt.Errorf("no output format selected")
}
A parse.go => parse.go +164 -0
@@ 0,0 1,164 @@
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "image/color"
+ "math"
+ "regexp"
+)
+
+type Format uint8
+
+const (
+ Hex Format = iota
+ RGB
+ RGBA
+ Vec3
+ Vec4
+)
+
+const (
+ hexFormat = "#%02x%02x%02x"
+ hexShortFormat = "#%1x%1x%1x"
+ rgbFormat = "rgb(%d,%d,%d)"
+ rgbaFormat = "rgba(%d,%d,%d,%d)"
+ vec3Format = "vec3(%f,%f,%f)"
+ vec4Format = "vec4(%f,%f,%f,%f)"
+)
+
+var matchColor = regexp.MustCompile(`(?i)#(?:([0-9a-f]{1,2})([0-9a-f]{1,2})([0-9a-f]{1,2}))|rgba\((?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]))\)|rgb\((?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]), ?)(?:([0-9]{1,2}|1[0-9]{1,2}|2[0-4][0-9]|25[0-5]))\)|vec4\((?:([0-1]\.?\d*), ?([0-1]\.?\d*), ?([0-1]\.?\d*), ?([0-1]\.?\d*)\))|vec3\((?:([0-1]\.?\d*), ?([0-1]\.?\d*), ?([0-1]\.?\d*)\))`)
+
+// Swap reads a []byte representing a block of text and replaces any color
+// codes with color codes in the specified Format.
+func Swap(src []byte, format Format) []byte {
+ switch format {
+ case Hex:
+ return matchColor.ReplaceAllFunc(src, toHex)
+ case RGB:
+ return matchColor.ReplaceAllFunc(src, toRGB)
+ case RGBA:
+ return matchColor.ReplaceAllFunc(src, toRGBA)
+ case Vec3:
+ return matchColor.ReplaceAllFunc(src, toVec3)
+ case Vec4:
+ return matchColor.ReplaceAllFunc(src, toVec4)
+ default:
+ return matchColor.ReplaceAllFunc(src, toHex)
+ }
+}
+
+// toHex replaces all color codes with Hex color codes in a []byte.
+func toHex(src []byte) []byte {
+ c := Parse(src)
+ r, g, b, _ := c.RGBA()
+ return []byte(fmt.Sprintf(hexFormat, uint8(r), uint8(g), uint8(b)))
+}
+
+// toRGB replaces all color codes with NRGB color codes in a []byte.
+func toRGB(src []byte) []byte {
+ c := Parse(src)
+ r, g, b := c.R, c.G, c.B
+ return []byte(fmt.Sprintf(rgbFormat, r, g, b))
+}
+
+// toRGBA replaces all color codes with NRGBA color codes in a []byte.
+func toRGBA(src []byte) []byte {
+ c := Parse(src)
+ r, g, b, a := c.R, c.G, c.B, c.A
+ return []byte(fmt.Sprintf(rgbaFormat, r, g, b, a))
+}
+
+// toVec3 replaces all color codes with Vec3 color codes in a []byte.
+func toVec3(src []byte) []byte {
+ c := Parse(src)
+ r, g, b := c.R, c.G, c.B
+ rf := float64(r) / 255
+ gf := float64(g) / 255
+ bf := float64(b) / 255
+ return []byte(fmt.Sprintf(vec3Format, rf, gf, bf))
+}
+
+// toVec4 replaces all color codes with Vec4 color codes in a []byte.
+func toVec4(src []byte) []byte {
+ c := Parse(src)
+ r, g, b, a := c.R, c.G, c.B, c.A
+ rf := float64(r) / 255
+ gf := float64(g) / 255
+ bf := float64(b) / 255
+ af := float64(a) / 255
+ return []byte(fmt.Sprintf(vec4Format, rf, gf, bf, af))
+}
+
+func Parse(src []byte) color.NRGBA {
+ if len(src) < 4 {
+ // Invalid color!
+ // If this happens the matchColor regex above is incorrect.
+ panic("invalid color: less than 4 bytes")
+ }
+
+ s := string(bytes.ToLower(src))
+ switch {
+ case s[:1] == "#":
+ return parseHex(s)
+ case s[:4] == "rgba":
+ return parseRGBA(s)
+ case s[:3] == "rgb":
+ return parseRGB(s)
+ case s[:4] == "vec3":
+ return parseVec3(s)
+ case s[:4] == "vec4":
+ return parseVec4(s)
+ default:
+ panic("invalid color: unknown color prefix")
+ }
+}
+
+func parseHex(s string) color.NRGBA {
+ var r, g, b uint8
+ switch len(s) {
+ case 4:
+ fmt.Sscanf(s, hexShortFormat, &r, &g, &b)
+ r *= 17
+ g *= 17
+ b *= 17
+ case 7:
+ fmt.Sscanf(s, hexFormat, &r, &g, &b)
+ default:
+ panic("invalid hex color: was not 4 or 7 bytes")
+ }
+ return color.NRGBA{R: r, G: g, B: b, A: 255}
+}
+
+func parseRGB(s string) color.NRGBA {
+ var r, g, b uint8
+ fmt.Sscanf(s, rgbFormat, &r, &g, &b)
+ return color.NRGBA{R: r, G: g, B: b, A: 255}
+}
+
+func parseRGBA(s string) color.NRGBA {
+ var r, g, b, a uint8
+ fmt.Sscanf(s, rgbaFormat, &r, &g, &b, &a)
+ return color.NRGBA{R: r, G: g, B: b, A: a}
+}
+
+func parseVec3(s string) color.NRGBA {
+ fmt.Println(s)
+ var r, g, b float64
+ fmt.Sscanf(s, vec3Format, &r, &g, &b)
+ r = math.Round(r * 255)
+ g = math.Round(g * 255)
+ b = math.Round(b * 255)
+ return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: 255}
+}
+
+func parseVec4(s string) color.NRGBA {
+ fmt.Println(s)
+ var r, g, b, a float64
+ fmt.Sscanf(s, vec4Format, &r, &g, &b, &a)
+ r = math.Round(r * 255)
+ g = math.Round(g * 255)
+ b = math.Round(b * 255)
+ a = math.Round(a * 255)
+ return color.NRGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}
+}
A parse_test.go => parse_test.go +131 -0
@@ 0,0 1,131 @@
+package main
+
+import (
+ "image/color"
+ "reflect"
+ "testing"
+)
+
+func TestSwap(t *testing.T) {
+ type test struct {
+ input string
+ want string
+ format Format
+ }
+
+ tests := []test{
+ {
+ input: "rgb(18, 52, 86)",
+ want: "#123456",
+ format: Hex,
+ },
+ {
+ input: "#123456",
+ want: "rgb(18,52,86)",
+ format: RGB,
+ },
+ {
+ input: "rgba(20,20,20)",
+ want: "rgba(20,20,20)",
+ format: RGB,
+ },
+ {
+ input: "rgb(20,20,20)",
+ want: "rgba(20,20,20,255)",
+ format: RGBA,
+ },
+ {
+ input: "rgba(20,20,20,20)",
+ want: "rgba(20,20,20,20)",
+ format: RGBA,
+ },
+ {
+ input: "rgb(20,20,20)",
+ want: "vec3(0.078431,0.078431,0.078431)",
+ format: Vec3,
+ },
+ {
+ input: "rgb(20,40,60)",
+ want: "vec3(0.078431,0.156863,0.235294)",
+ format: Vec3,
+ },
+ {
+ input: "rgb(20,40,60)",
+ want: "vec4(0.078431,0.156863,0.235294,1.000000)",
+ format: Vec4,
+ },
+ }
+
+ for _, tc := range tests {
+ got := Swap([]byte(tc.input), tc.format)
+ if string(got) != tc.want {
+ t.Fatalf("expected: %s, got: %s", tc.want, got)
+ }
+ }
+}
+
+func TestParse(t *testing.T) {
+ type test struct {
+ input string
+ want color.NRGBA
+ format Format
+ }
+
+ tests := []test{
+ {
+ input: "#123456",
+ want: color.NRGBA{18, 52, 86, 255},
+ },
+ {
+ input: "#eee",
+ want: color.NRGBA{238, 238, 238, 255},
+ },
+ {
+ input: "#EEE",
+ want: color.NRGBA{238, 238, 238, 255},
+ },
+ {
+ input: "#eeeeee",
+ want: color.NRGBA{238, 238, 238, 255},
+ },
+ {
+ input: "#EEEEEE",
+ want: color.NRGBA{238, 238, 238, 255},
+ },
+ {
+ input: "rgb(18,52,86)",
+ want: color.NRGBA{18, 52, 86, 255},
+ },
+ {
+ input: "rgb(0,0,0)",
+ want: color.NRGBA{0, 0, 0, 255},
+ },
+ {
+ input: "rgb(255, 0, 255)",
+ want: color.NRGBA{255, 0, 255, 255},
+ },
+ {
+ input: "rgb(10, 0, 255)",
+ want: color.NRGBA{10, 0, 255, 255},
+ },
+ {
+ input: "rgba(10, 0, 255, 32)",
+ want: color.NRGBA{10, 0, 255, 32},
+ },
+ {
+ input: "vec3(0.224, 0.0, 1.0)",
+ want: color.NRGBA{57, 0, 255, 255},
+ },
+ {
+ input: "vec4(0.224, 0.0, 0.75, 0.60)",
+ want: color.NRGBA{57, 0, 191, 153},
+ },
+ }
+
+ for _, tc := range tests {
+ got := Parse([]byte(tc.input))
+ if !reflect.DeepEqual(got, tc.want) {
+ t.Fatalf("expected: %v, got: %v", tc.want, got)
+ }
+ }
+}
D sample.txt => sample.txt +0 -27
@@ 1,27 0,0 @@
-local M = {
- black = "#000000",
- dark_grey = "#767676",
- grey = "#AAAAAA",
- light_grey = "#CCCCCC",
- lighter_grey = "#EEEEEE",
- lightest_grey = "#F8F8F8",
- white = "#FFFFFF",
- red = "#eb3232",
- mint = "#57bda0",
- orange = "#ffb35b",
- purple = "#888aca",
-}
-
-local M = {
- black = "rgb(0,0,0)",
- dark_grey = "rgb(118,118,118)",
- grey = "rgb(170,170,170)",
- light_grey = "rgb(204,204,204)",
- lighter_grey = "rgb(238,238,238)",
- lightest_grey = "rgb(248,248,248)",
- white = "rgb(255,255,255)",
- red = "rgb(235,50,50)",
- mint = "rgb(87,189,160)",
- orange = "rgb(255,179,91)",
- purple = "rgb(136,138,202)",
-}