~kota/colorswap

dd8108063502bf6d04d2243613b4e178214619a5 — Dakota Walsh 6 months ago 7d00e39 main
add support for rgba, vec3, and vec4

This is essentially a rewrite of the program. We now require a cli flag
indicating your output format, but we accept any of the formats on
input.
7 files changed, 343 insertions(+), 73 deletions(-)

M README.md
M go.mod
D go.sum
M main.go
A parse.go
A parse_test.go
D sample.txt
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)",
}