Why was this a conflict?
continuous-integration/drone/push Build is passing Details

main
unknown 3 weeks ago
commit 12db5d3e70

@ -13,7 +13,7 @@ steps:
- name: publish_prod - name: publish_prod
depends_on: depends_on:
- hugo - hugo
image: drillster/drone-rsync image: code.ndumas.com/ndumas/drone-rsync
settings: settings:
key: key:
from_secret: BLOG_DEPLOY_KEY from_secret: BLOG_DEPLOY_KEY
@ -31,7 +31,7 @@ steps:
- name: publish_dev - name: publish_dev
depends_on: depends_on:
- hugo - hugo
image: drillster/drone-rsync image: code.ndumas.com/ndumas/drone-rsync
settings: settings:
key: key:
from_secret: BLOG_DEPLOY_KEY from_secret: BLOG_DEPLOY_KEY
@ -49,7 +49,7 @@ steps:
- name: publish_drafts - name: publish_drafts
depends_on: depends_on:
- hugo - hugo
image: drillster/drone-rsync image: code.ndumas.com/ndumas/drone-rsync
settings: settings:
key: key:
from_secret: BLOG_DEPLOY_KEY from_secret: BLOG_DEPLOY_KEY

@ -0,0 +1,90 @@
---
draft: false
title: "Deterministically Mapping Strings to RGB Colors"
aliases: ["Deterministically Mapping Strings to RGB Colors"]
date: "2025-01-25"
series: ["strings-to-colors"]
series_order: 1
author: "Nick Dumas"
authorTwitter: ""
cover: ""
tags: ["golang"]
keywords: ["golang"]
summary: "Using Go so I don't have to think about color palettes."
showFullContent: false
---
## Inspiration
The XMPP site has a document describing a process for deterministically mapping strings to colors with [XEP-0392](https://xmpp.org/extensions/xep-0392.html#algorithm-genpalette). I've had this document saved in my notes for two years now and I haven't really had a good excuse to apply it until now.
I won't get too detailed here but the short version is
- I've got a spreadsheet of addresses.
- Each entry has a category and I'm turning that spreadsheet into a KML document.
- I want each KML placemark to be colored according to its category but I don't want to manually assign colors to categories. I don't have much of an eye for design and categories might change over time.
All of this adds up to finding a way to programmatically turn a string into a color.
## The Plan
I made an attempt at implementing XEP-0392 verbatim but I got walled by two details:
1. Go doesn't have any HSL handling in the standard library.
2. I don't understand HSLuv well enough to parse the instructions for converting the SHA1 hash to a color. HSL is comprised of 3 components ( hue, saturation, and lightness ) but the XEP-0392 document only talks about generating an angle.
I do understand RGB and how hashes work so I was able to synthesize that into a close-enough solution:
- Take an arbitrary UTF-8 string
- SHA1 the string
- Interpret equally sized chunks of the hash as unsigned 64-bit integers: 3 chunks for RGB, 4 chunks for RGBA.
- Downscale those integers into 8-bit unsigned integers.
## The Implementation
`ToRGB` is pretty straightforward, implementing the algorithm described in the previous section. We take the SHA1 of the string, break it into chunks, convert those into `uint64`s to account for the 7 bytes ( 56 bits total ) we're turning into an integer. The blue value overlaps the green by a few bits but that this isn't a security system so that doesn't matter much.
```go
func ToRGB(s string) color.RGBA {
h := sha1.New()
io.WriteString(h, s)
sum := h.Sum(nil)
var rgb color.RGBA
r := binary.LittleEndian.Uint64(sum[:8])
g := binary.LittleEndian.Uint64(sum[8:16])
b := binary.LittleEndian.Uint64(sum[12:])
rgb.R = uint8(ScaleUint64(r, 0, uint64(math.Pow(2, 56)), 0, 64384))
rgb.G = uint8(ScaleUint64(g, 0, uint64(math.Pow(2, 56)), 0, 64384))
rgb.B = uint8(ScaleUint64(b, 0, uint64(math.Pow(2, 56)), 0, 64384))
rgb.A = uint8(255)
return rgb
}
```
`ToRGBA` functions almost identically, with a few small changes. Instead of 7 byte chunks, we use 5 byte chunks and adjust the `Scale` call accordingly for the 40 bit chunks.
```go
func ToRGBA(s string) color.RGBA {
// ...
// We use Uint32 here because the chunks are smaller and the Uint64 function panics if the input []byte is too short.
a := binary.LittleEndian.Uint32(sum[15:])
// ...
rgba.A = uint8(ScaleUint64(a, 0, uint64(math.Pow(2, 40)), 0, 64384))
// ...
return rgb
}
```
### Scaling
The final bit of magic we need is a way to scale these 64 bit integers down to between 0 and 255 to represent the RGB ( and sometimes A ) values that make up the `image.Color`. Enter the `Scale` function:
```go
func Scale[T constraints.Unsigned | constraints.Float | constraints.Signed](f, inputMin, inputMax, outputMin, outputMax T) T {
return (f-(inputMin))*(outputMax-outputMin)/(inputMax-inputMin) + outputMin
}
var ScaleUint64 = Scale[uint64]
```
This does a bit of relatively straightforward math to smoothly scale the input into the desired range. Not a lot to say about it, it's just math and I don't know much about where it came from. I've had this floating around in various repos for years and I don't have any notes on where I found it.
## The Results
Below is a sample of "lorem ipsum" with each word colored using this algorithm.
<p><span style="color:#FD9FAB;">Lorem</span> <span style="color:#D35EC5;">ipsum</span> <span style="color:#45EEDD;">dolor</span> <span style="color:#3A2FE8;">sit</span> <span style="color:#04B55A;">amet</span>, <span style="color:#883581;">consectetur</span> <span style="color:#31CA93;">adipiscing</span> <span style="color:#99461B;">elit</span>, <span style="color:#000AF1;">sed</span> <span style="color:#899099;">do</span> <span style="color:#7B05AB;">eiusmod</span> <span style="color:#F0B186;">tempor</span> <span style="color:#3D1519;">incididunt</span> <span style="color:#93B1CD;">ut</span> <span style="color:#8333C5;">labore</span> <span style="color:#846002;">et</span> <span style="color:#81BB0B;">dolore</span> <span style="color:#E3DDD1;">magna</span> <span style="color:#877C09;">aliqua</span>. <span style="color:#CB6967;">Ut</span> <span style="color:#B38347;">enim</span> <span style="color:#4B8A53;">ad</span> <span style="color:#078AC6;">minim</span> <span style="color:#D2F7E3;">veniam</span>, <span style="color:#6BD5A2;">quis</span> <span style="color:#2B37D2;">nostrud</span> <span style="color:#C592D6;">exercitation</span> <span style="color:#0EB9D1;">ullamco</span> <span style="color:#9BC026;">laboris</span> <span style="color:#872CC2;">nisi</span> <span style="color:#93B1CD;">ut</span> <span style="color:#7DB82B;">aliquip</span> <span style="color:#1CED95;">ex</span> <span style="color:#C0504C;">ea</span> <span style="color:#5DA8B5;">commodo</span> <span style="color:#B762A6;">consequat</span>. <span style="color:#2FCB06;">Duis</span> <span style="color:#EADF81;">aute</span> <span style="color:#0602B8;">irure</span> <span style="color:#45EEDD;">dolor</span> <span style="color:#5E0D47;">in</span> <span style="color:#8B2DEF;">reprehenderit</span> <span style="color:#5E0D47;">in</span> <span style="color:#790B62;">voluptate</span> <span style="color:#7486E8;">velit</span> <span style="color:#33E7F2;">esse</span> <span style="color:#DE099F;">cillum</span> <span style="color:#81BB0B;">dolore</span> <span style="color:#C514CD;">eu</span> <span style="color:#B4B15A;">fugiat</span> <span style="color:#93AA9C;">nulla</span> <span style="color:#83967C;">pariatur</span>. <span style="color:#EDD318;">Excepteur</span> <span style="color:#580A2A;">sint</span> <span style="color:#7DC184;">occaecat</span> <span style="color:#BCEEAB;">cupidatat</span> <span style="color:#EA0503;">non</span> <span style="color:#0AC6DC;">proident</span>, <span style="color:#90C0A3;">sunt</span> <span style="color:#5E0D47;">in</span> <span style="color:#0C0B4B;">culpa</span> <span style="color:#B0E1D5;">qui</span> <span style="color:#F8F0B6;">officia</span> <span style="color:#FFF0B6;">deserunt</span> <span style="color:#115AE8;">mollit</span> <span style="color:#6EC31F;">anim</span> <span style="color:#10B00E;">id</span> <span style="color:#8ADFA4;">est</span> <span style="color:#7EBB61;">laborum</span>. </p>
## Possible Improvements
There's a few places where this code could be meaningfully improved.
- It'd be nice to have HSL/HSV colors as an option. I've personally never been in a situation where RGB wasn't enough to do what I needed, but they exist for a reason. [HSLuv](https://www.hsluv.org/) claims to better capture colors as perceived by the human eye and that sounds pretty neat.
- Accessibility and consistency. Sometimes the color for a word will be almost identical to its background making portions of the text hard or impossible to read. It'd be cool to be able to limit color generation to values within certain ranges of a color palette. This is probably doable with another application of `Scale`.
Loading…
Cancel
Save