From 8b0a3f3e215d8dd0de9cd92a8f384b30bad61f03 Mon Sep 17 00:00:00 2001 From: Raph Levien Date: Fri, 8 Nov 2024 21:42:58 -0800 Subject: [PATCH] Add x11 named colors (#12) Add a lookup for x11 named colors to the parser. The implementation is a hand-rolled minimal perfect hash. --- .github/check_autogen.sh | 12 ++ .github/workflows/ci.yml | 3 + color/make_x11_colors.py | 275 ++++++++++++++++++++++++++++++++ color/src/lib.rs | 3 +- color/src/parse.rs | 38 ++++- color/src/x11_colors.rs | 327 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 654 insertions(+), 4 deletions(-) create mode 100644 .github/check_autogen.sh create mode 100644 color/make_x11_colors.py create mode 100644 color/src/x11_colors.rs diff --git a/.github/check_autogen.sh b/.github/check_autogen.sh new file mode 100644 index 0000000..dd76c01 --- /dev/null +++ b/.github/check_autogen.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +# Check that the autogenerated file matches the repo (protect against hand-editing). + +set -e + +python3 color/make_x11_colors.py > x11_colors.rs +diff color/src/x11_colors.rs x11_colors.rs + +rm x11_colors.rs +echo "Autogenerated file matches." +exit 0 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c0bf647..fba86b1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,6 +81,9 @@ jobs: - name: check copyright headers run: bash .github/copyright.sh + - name: check autogenerated files + run: bash .github/check_autogen.sh + clippy-stable: name: cargo clippy runs-on: ${{ matrix.os }} diff --git a/color/make_x11_colors.py b/color/make_x11_colors.py new file mode 100644 index 0000000..9e2f5ce --- /dev/null +++ b/color/make_x11_colors.py @@ -0,0 +1,275 @@ +# Copyright 2024 the Color Authors +# SPDX-License-Identifier: Apache-2.0 OR MIT + +# A utility to create a minimal perfect hash lookup table for +# the x11 palette colors. +# +# This utility has been adapted from . +# +# See Steve Hanov's blog +# [Throw away the keys: Easy, Minimal Perfect Hashing](https://stevehanov.ca/blog/?id=119) +# for the basic technique. + +colors = [ + ("aliceblue", (240, 248, 255, 255)), + ("antiquewhite", (250, 235, 215, 255)), + ("aqua", (0, 255, 255, 255)), + ("aquamarine", (127, 255, 212, 255)), + ("azure", (240, 255, 255, 255)), + ("beige", (245, 245, 220, 255)), + ("bisque", (255, 228, 196, 255)), + ("black", (0, 0, 0, 255)), + ("blanchedalmond", (255, 235, 205, 255)), + ("blue", (0, 0, 255, 255)), + ("blueviolet", (138, 43, 226, 255)), + ("brown", (165, 42, 42, 255)), + ("burlywood", (222, 184, 135, 255)), + ("cadetblue", (95, 158, 160, 255)), + ("chartreuse", (127, 255, 0, 255)), + ("chocolate", (210, 105, 30, 255)), + ("coral", (255, 127, 80, 255)), + ("cornflowerblue", (100, 149, 237, 255)), + ("cornsilk", (255, 248, 220, 255)), + ("crimson", (220, 20, 60, 255)), + ("cyan", (0, 255, 255, 255)), + ("darkblue", (0, 0, 139, 255)), + ("darkcyan", (0, 139, 139, 255)), + ("darkgoldenrod", (184, 134, 11, 255)), + ("darkgray", (169, 169, 169, 255)), + ("darkgreen", (0, 100, 0, 255)), + ("darkkhaki", (189, 183, 107, 255)), + ("darkmagenta", (139, 0, 139, 255)), + ("darkolivegreen", (85, 107, 47, 255)), + ("darkorange", (255, 140, 0, 255)), + ("darkorchid", (153, 50, 204, 255)), + ("darkred", (139, 0, 0, 255)), + ("darksalmon", (233, 150, 122, 255)), + ("darkseagreen", (143, 188, 143, 255)), + ("darkslateblue", (72, 61, 139, 255)), + ("darkslategray", (47, 79, 79, 255)), + ("darkturquoise", (0, 206, 209, 255)), + ("darkviolet", (148, 0, 211, 255)), + ("deeppink", (255, 20, 147, 255)), + ("deepskyblue", (0, 191, 255, 255)), + ("dimgray", (105, 105, 105, 255)), + ("dodgerblue", (30, 144, 255, 255)), + ("firebrick", (178, 34, 34, 255)), + ("floralwhite", (255, 250, 240, 255)), + ("forestgreen", (34, 139, 34, 255)), + ("fuchsia", (255, 0, 255, 255)), + ("gainsboro", (220, 220, 220, 255)), + ("ghostwhite", (248, 248, 255, 255)), + ("gold", (255, 215, 0, 255)), + ("goldenrod", (218, 165, 32, 255)), + ("gray", (128, 128, 128, 255)), + ("green", (0, 128, 0, 255)), + ("greenyellow", (173, 255, 47, 255)), + ("honeydew", (240, 255, 240, 255)), + ("hotpink", (255, 105, 180, 255)), + ("indianred", (205, 92, 92, 255)), + ("indigo", (75, 0, 130, 255)), + ("ivory", (255, 255, 240, 255)), + ("khaki", (240, 230, 140, 255)), + ("lavender", (230, 230, 250, 255)), + ("lavenderblush", (255, 240, 245, 255)), + ("lawngreen", (124, 252, 0, 255)), + ("lemonchiffon", (255, 250, 205, 255)), + ("lightblue", (173, 216, 230, 255)), + ("lightcoral", (240, 128, 128, 255)), + ("lightcyan", (224, 255, 255, 255)), + ("lightgoldenrodyellow", (250, 250, 210, 255)), + ("lightgray", (211, 211, 211, 255)), + ("lightgreen", (144, 238, 144, 255)), + ("lightpink", (255, 182, 193, 255)), + ("lightsalmon", (255, 160, 122, 255)), + ("lightseagreen", (32, 178, 170, 255)), + ("lightskyblue", (135, 206, 250, 255)), + ("lightslategray", (119, 136, 153, 255)), + ("lightsteelblue", (176, 196, 222, 255)), + ("lightyellow", (255, 255, 224, 255)), + ("lime", (0, 255, 0, 255)), + ("limegreen", (50, 205, 50, 255)), + ("linen", (250, 240, 230, 255)), + ("magenta", (255, 0, 255, 255)), + ("maroon", (128, 0, 0, 255)), + ("mediumaquamarine", (102, 205, 170, 255)), + ("mediumblue", (0, 0, 205, 255)), + ("mediumorchid", (186, 85, 211, 255)), + ("mediumpurple", (147, 112, 219, 255)), + ("mediumseagreen", (60, 179, 113, 255)), + ("mediumslateblue", (123, 104, 238, 255)), + ("mediumspringgreen", (0, 250, 154, 255)), + ("mediumturquoise", (72, 209, 204, 255)), + ("mediumvioletred", (199, 21, 133, 255)), + ("midnightblue", (25, 25, 112, 255)), + ("mintcream", (245, 255, 250, 255)), + ("mistyrose", (255, 228, 225, 255)), + ("moccasin", (255, 228, 181, 255)), + ("navajowhite", (255, 222, 173, 255)), + ("navy", (0, 0, 128, 255)), + ("oldlace", (253, 245, 230, 255)), + ("olive", (128, 128, 0, 255)), + ("olivedrab", (107, 142, 35, 255)), + ("orange", (255, 165, 0, 255)), + ("orangered", (255, 69, 0, 255)), + ("orchid", (218, 112, 214, 255)), + ("palegoldenrod", (238, 232, 170, 255)), + ("palegreen", (152, 251, 152, 255)), + ("paleturquoise", (175, 238, 238, 255)), + ("palevioletred", (219, 112, 147, 255)), + ("papayawhip", (255, 239, 213, 255)), + ("peachpuff", (255, 218, 185, 255)), + ("peru", (205, 133, 63, 255)), + ("pink", (255, 192, 203, 255)), + ("plum", (221, 160, 221, 255)), + ("powderblue", (176, 224, 230, 255)), + ("purple", (128, 0, 128, 255)), + ("rebeccapurple", (102, 51, 153, 255)), + ("red", (255, 0, 0, 255)), + ("rosybrown", (188, 143, 143, 255)), + ("royalblue", (65, 105, 225, 255)), + ("saddlebrown", (139, 69, 19, 255)), + ("salmon", (250, 128, 114, 255)), + ("sandybrown", (244, 164, 96, 255)), + ("seagreen", (46, 139, 87, 255)), + ("seashell", (255, 245, 238, 255)), + ("sienna", (160, 82, 45, 255)), + ("silver", (192, 192, 192, 255)), + ("skyblue", (135, 206, 235, 255)), + ("slateblue", (106, 90, 205, 255)), + ("slategray", (112, 128, 144, 255)), + ("snow", (255, 250, 250, 255)), + ("springgreen", (0, 255, 127, 255)), + ("steelblue", (70, 130, 180, 255)), + ("tan", (210, 180, 140, 255)), + ("teal", (0, 128, 128, 255)), + ("thistle", (216, 191, 216, 255)), + ("tomato", (255, 99, 71, 255)), + ("transparent", (0, 0, 0, 0)), + ("turquoise", (64, 224, 208, 255)), + ("violet", (238, 130, 238, 255)), + ("wheat", (245, 222, 179, 255)), + ("white", (255, 255, 255, 255)), + ("whitesmoke", (245, 245, 245, 255)), + ("yellow", (255, 255, 0, 255)), + ("yellowgreen", (154, 205, 50, 255)), +] + +def weak_hash_string(s): + mask_32 = 0xffffffff + h = 0 + for char in s: + h = (9 * h + ord(char)) & mask_32 + return h + +# Guaranteed to be less than n. +def weak_hash(s, salt, n): + x = weak_hash_string(s[0]) + # This is hash based on the theory that multiplication is efficient + mask_32 = 0xffffffff + y = ((x + salt) * 2654435769) & mask_32 + y ^= x + return (y * n) >> 32 + +# Compute minimal perfect hash function, d can be either a dict or list of keys. +def minimal_perfect_hash(d): + n = len(d) + buckets = dict((h, []) for h in range(n)) + for key in d: + h = weak_hash(key, 0, n) + buckets[h].append(key) + bsorted = [(len(buckets[h]), h) for h in range(n)] + bsorted.sort(reverse = True) + claimed = [False] * n + salts = [0] * n + keys = [0] * n + for (bucket_size, h) in bsorted: + # Note: the traditional perfect hashing approach would also special-case + # bucket_size == 1 here and assign any empty slot, rather than iterating + # until rehash finds an empty slot. But we're not doing that so we can + # avoid the branch. + if bucket_size == 0: + break + else: + for salt in range(1, 32768): + rehashes = [weak_hash(key, salt, n) for key in buckets[h]] + # Make sure there are no rehash collisions within this bucket. + if all(not claimed[hash] for hash in rehashes): + if len(set(rehashes)) < bucket_size: + continue + salts[h] = salt + for key in buckets[h]: + rehash = weak_hash(key, salt, n) + claimed[rehash] = True + keys[rehash] = key + break + if salts[h] == 0: + print("minimal perfect hashing failed") + # Note: if this happens (because of unfortunate data), then there are + # a few things that could be done. First, the hash function could be + # tweaked. Second, the bucket order could be scrambled (especially the + # singletons). Right now, the buckets are sorted, which has the advantage + # of being deterministic. + # + # As a more extreme approach, the singleton bucket optimization could be + # applied (give the direct address for singleton buckets, rather than + # relying on a rehash). That is definitely the more standard approach in + # the minimal perfect hashing literature, but in testing the branch was a + # significant slowdown. + exit(1) + return (salts, keys) + +(salts, keys) = minimal_perfect_hash(colors) +n = len(colors) +print("""// Copyright 2024 the Color Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// This file was auto-generated by make_x11_colors.py. Do not hand-edit. +""") +print(f"const SALTS: [u8; {n}] = [") +obuf = " " +for salt in salts: + word = f" {salt}," + if len(obuf) + len(word) >= 100: + print(obuf) + obuf = " " + obuf += word +if len(obuf) > 3: + print(obuf) +print("];") +print() + +print(f"const NAMES: [&str; {n}] = [") +for (name, rgba) in keys: + print(f' "{name}",') +print("];") +print() +print(f"const COLORS: [[u8; 4]; {n}] = [") +for (name, rgba) in keys: + print(f' {list(rgba)},') +print("];") +print(""" +/// Hash the 32 bit key into a value less than `n`, adding salt. +/// +/// This is basically the weakest hash we can get away with that +/// still distinguishes all the values. +#[inline] +fn weak_hash(key: u32, salt: u32, n: usize) -> usize { + let y = key.wrapping_add(salt).wrapping_mul(2654435769); + let y = y ^ key; + (((y as u64) * (n as u64)) >> 32) as usize +} + +pub(crate) fn lookup_palette(s: &str) -> Option<[u8; 4]> { + let mut key = 0_u32; + for b in s.as_bytes() { + key = key.wrapping_mul(9).wrapping_add(*b as u32); + } + let salt = SALTS[weak_hash(key, 0, SALTS.len())] as u32; + let ix = weak_hash(key, salt, SALTS.len()); + if s == NAMES[ix] { + Some(COLORS[ix]) + } else { + None + } +}""") diff --git a/color/src/lib.rs b/color/src/lib.rs index 3ed86b3..e1ac2f5 100644 --- a/color/src/lib.rs +++ b/color/src/lib.rs @@ -25,10 +25,11 @@ mod color; mod colorspace; mod css; mod gradient; -// Note: this will be feature-gated, but not bothering for now +// Note: this may become feature-gated; we'll decide this soon mod parse; mod serialize; mod tagged; +mod x11_colors; #[cfg(all(not(feature = "std"), not(test)))] mod floatfuncs; diff --git a/color/src/parse.rs b/color/src/parse.rs index bab7d85..57b3fe5 100644 --- a/color/src/parse.rs +++ b/color/src/parse.rs @@ -6,7 +6,7 @@ use core::f64; use core::str::FromStr; -use crate::{AlphaColor, Bitset, ColorSpaceTag, CssColor, Srgb, TaggedColor}; +use crate::{AlphaColor, Bitset, ColorSpaceTag, CssColor, Srgb}; // TODO: proper error type, maybe include string offset pub type Error = &'static str; @@ -368,7 +368,7 @@ impl<'a> Parser<'a> { pub fn parse_color(s: &str) -> Result { if let Some(stripped) = s.strip_prefix('#') { let color = color_from_4bit_hex(get_4bit_hex_channels(stripped)?); - return Ok(TaggedColor::from_alpha_color(color).into()); + return Ok(CssColor::from_alpha_color(color)); } // TODO: the named x11 colors (steal from peniko) let mut parser = Parser::new(s); @@ -379,7 +379,14 @@ pub fn parse_color(s: &str) -> Result { "oklch" => parser.oklch(), "transparent" => Ok(color_from_components([Some(0.); 4], ColorSpaceTag::Srgb)), "color" => parser.color(), - _ => Err("unknown identifier"), + _ => { + if let Some([r, g, b, a]) = crate::x11_colors::lookup_palette(id) { + let color = AlphaColor::from_rgba8(r, g, b, a); + Ok(CssColor::from_alpha_color(color)) + } else { + Err("unknown color identifier") + } + } } // TODO: should we validate that the parser is at eof? } else { @@ -442,3 +449,28 @@ impl FromStr for ColorSpaceTag { } } } + +#[cfg(test)] +mod tests { + use crate::CssColor; + + use super::parse_color; + + fn assert_close_color(c1: CssColor, c2: CssColor) { + const EPSILON: f32 = 1e-4; + assert_eq!(c1.cs, c2.cs); + for i in 0..4 { + assert!((c1.components[i] - c2.components[i]).abs() < EPSILON); + } + } + + #[test] + fn x11_color_names() { + let red = parse_color("red").unwrap(); + assert_close_color(red, parse_color("rgb(255, 0, 0)").unwrap()); + let lgy = parse_color("lightgoldenrodyellow").unwrap(); + assert_close_color(lgy, parse_color("rgb(250, 250, 210)").unwrap()); + let transparent = parse_color("transparent").unwrap(); + assert_close_color(transparent, parse_color("rgba(0, 0, 0, 0)").unwrap()); + } +} diff --git a/color/src/x11_colors.rs b/color/src/x11_colors.rs new file mode 100644 index 0000000..e3fe1fe --- /dev/null +++ b/color/src/x11_colors.rs @@ -0,0 +1,327 @@ +// Copyright 2024 the Color Authors +// SPDX-License-Identifier: Apache-2.0 OR MIT + +// This file was auto-generated by make_x11_colors.py. Do not hand-edit. + +const SALTS: [u8; 142] = [ + 3, 8, 0, 5, 0, 0, 0, 0, 3, 0, 1, 0, 9, 0, 112, 7, 3, 0, 1, 19, 1, 0, 0, 0, 7, 3, 5, 2, 23, 38, + 9, 0, 4, 28, 5, 3, 0, 0, 1, 0, 0, 18, 7, 0, 3, 2, 8, 2, 13, 16, 12, 1, 3, 0, 11, 0, 14, 0, 1, + 0, 2, 4, 1, 0, 3, 6, 1, 0, 3, 3, 0, 0, 3, 0, 6, 17, 0, 10, 0, 0, 1, 1, 7, 12, 1, 7, 0, 2, 0, 0, + 1, 1, 1, 2, 1, 0, 1, 0, 3, 6, 5, 1, 0, 1, 5, 0, 2, 0, 0, 1, 0, 1, 0, 1, 0, 1, 6, 6, 2, 2, 0, 5, + 0, 0, 1, 4, 2, 0, 0, 2, 2, 0, 1, 0, 0, 1, 5, 0, 0, 1, 0, 0, +]; + +const NAMES: [&str; 142] = [ + "rosybrown", + "forestgreen", + "cornsilk", + "silver", + "purple", + "thistle", + "antiquewhite", + "mediumspringgreen", + "dimgray", + "olivedrab", + "wheat", + "mediumturquoise", + "darkseagreen", + "burlywood", + "magenta", + "lemonchiffon", + "darkkhaki", + "lightyellow", + "mediumslateblue", + "mediumorchid", + "rebeccapurple", + "transparent", + "springgreen", + "maroon", + "lightseagreen", + "yellow", + "gray", + "tan", + "gainsboro", + "chartreuse", + "gold", + "orchid", + "red", + "snow", + "greenyellow", + "limegreen", + "aqua", + "salmon", + "sienna", + "slateblue", + "aquamarine", + "azure", + "saddlebrown", + "lightskyblue", + "cornflowerblue", + "darkorange", + "dodgerblue", + "midnightblue", + "mediumpurple", + "white", + "darkslateblue", + "darkturquoise", + "paleturquoise", + "linen", + "slategray", + "lightpink", + "lightcyan", + "peru", + "beige", + "indianred", + "navajowhite", + "green", + "lightblue", + "mediumaquamarine", + "palegreen", + "skyblue", + "darkviolet", + "deeppink", + "darkcyan", + "peachpuff", + "navy", + "darkolivegreen", + "lavender", + "lightgoldenrodyellow", + "moccasin", + "deepskyblue", + "firebrick", + "ghostwhite", + "honeydew", + "turquoise", + "darkorchid", + "plum", + "lightgray", + "chocolate", + "lightsteelblue", + "bisque", + "mediumblue", + "aliceblue", + "lightcoral", + "lightslategray", + "darkgreen", + "mintcream", + "coral", + "darkmagenta", + "mistyrose", + "whitesmoke", + "lightsalmon", + "mediumvioletred", + "tomato", + "indigo", + "brown", + "lawngreen", + "cadetblue", + "violet", + "fuchsia", + "darkblue", + "orange", + "mediumseagreen", + "lime", + "blue", + "darkred", + "blueviolet", + "papayawhip", + "blanchedalmond", + "seagreen", + "steelblue", + "seashell", + "lavenderblush", + "floralwhite", + "lightgreen", + "darksalmon", + "black", + "crimson", + "hotpink", + "darkslategray", + "palevioletred", + "oldlace", + "darkgray", + "olive", + "palegoldenrod", + "powderblue", + "pink", + "orangered", + "ivory", + "teal", + "cyan", + "darkgoldenrod", + "royalblue", + "sandybrown", + "khaki", + "goldenrod", + "yellowgreen", +]; + +const COLORS: [[u8; 4]; 142] = [ + [188, 143, 143, 255], + [34, 139, 34, 255], + [255, 248, 220, 255], + [192, 192, 192, 255], + [128, 0, 128, 255], + [216, 191, 216, 255], + [250, 235, 215, 255], + [0, 250, 154, 255], + [105, 105, 105, 255], + [107, 142, 35, 255], + [245, 222, 179, 255], + [72, 209, 204, 255], + [143, 188, 143, 255], + [222, 184, 135, 255], + [255, 0, 255, 255], + [255, 250, 205, 255], + [189, 183, 107, 255], + [255, 255, 224, 255], + [123, 104, 238, 255], + [186, 85, 211, 255], + [102, 51, 153, 255], + [0, 0, 0, 0], + [0, 255, 127, 255], + [128, 0, 0, 255], + [32, 178, 170, 255], + [255, 255, 0, 255], + [128, 128, 128, 255], + [210, 180, 140, 255], + [220, 220, 220, 255], + [127, 255, 0, 255], + [255, 215, 0, 255], + [218, 112, 214, 255], + [255, 0, 0, 255], + [255, 250, 250, 255], + [173, 255, 47, 255], + [50, 205, 50, 255], + [0, 255, 255, 255], + [250, 128, 114, 255], + [160, 82, 45, 255], + [106, 90, 205, 255], + [127, 255, 212, 255], + [240, 255, 255, 255], + [139, 69, 19, 255], + [135, 206, 250, 255], + [100, 149, 237, 255], + [255, 140, 0, 255], + [30, 144, 255, 255], + [25, 25, 112, 255], + [147, 112, 219, 255], + [255, 255, 255, 255], + [72, 61, 139, 255], + [0, 206, 209, 255], + [175, 238, 238, 255], + [250, 240, 230, 255], + [112, 128, 144, 255], + [255, 182, 193, 255], + [224, 255, 255, 255], + [205, 133, 63, 255], + [245, 245, 220, 255], + [205, 92, 92, 255], + [255, 222, 173, 255], + [0, 128, 0, 255], + [173, 216, 230, 255], + [102, 205, 170, 255], + [152, 251, 152, 255], + [135, 206, 235, 255], + [148, 0, 211, 255], + [255, 20, 147, 255], + [0, 139, 139, 255], + [255, 218, 185, 255], + [0, 0, 128, 255], + [85, 107, 47, 255], + [230, 230, 250, 255], + [250, 250, 210, 255], + [255, 228, 181, 255], + [0, 191, 255, 255], + [178, 34, 34, 255], + [248, 248, 255, 255], + [240, 255, 240, 255], + [64, 224, 208, 255], + [153, 50, 204, 255], + [221, 160, 221, 255], + [211, 211, 211, 255], + [210, 105, 30, 255], + [176, 196, 222, 255], + [255, 228, 196, 255], + [0, 0, 205, 255], + [240, 248, 255, 255], + [240, 128, 128, 255], + [119, 136, 153, 255], + [0, 100, 0, 255], + [245, 255, 250, 255], + [255, 127, 80, 255], + [139, 0, 139, 255], + [255, 228, 225, 255], + [245, 245, 245, 255], + [255, 160, 122, 255], + [199, 21, 133, 255], + [255, 99, 71, 255], + [75, 0, 130, 255], + [165, 42, 42, 255], + [124, 252, 0, 255], + [95, 158, 160, 255], + [238, 130, 238, 255], + [255, 0, 255, 255], + [0, 0, 139, 255], + [255, 165, 0, 255], + [60, 179, 113, 255], + [0, 255, 0, 255], + [0, 0, 255, 255], + [139, 0, 0, 255], + [138, 43, 226, 255], + [255, 239, 213, 255], + [255, 235, 205, 255], + [46, 139, 87, 255], + [70, 130, 180, 255], + [255, 245, 238, 255], + [255, 240, 245, 255], + [255, 250, 240, 255], + [144, 238, 144, 255], + [233, 150, 122, 255], + [0, 0, 0, 255], + [220, 20, 60, 255], + [255, 105, 180, 255], + [47, 79, 79, 255], + [219, 112, 147, 255], + [253, 245, 230, 255], + [169, 169, 169, 255], + [128, 128, 0, 255], + [238, 232, 170, 255], + [176, 224, 230, 255], + [255, 192, 203, 255], + [255, 69, 0, 255], + [255, 255, 240, 255], + [0, 128, 128, 255], + [0, 255, 255, 255], + [184, 134, 11, 255], + [65, 105, 225, 255], + [244, 164, 96, 255], + [240, 230, 140, 255], + [218, 165, 32, 255], + [154, 205, 50, 255], +]; + +/// Hash the 32 bit key into a value less than `n`, adding salt. +/// +/// This is basically the weakest hash we can get away with that +/// still distinguishes all the values. +#[inline] +fn weak_hash(key: u32, salt: u32, n: usize) -> usize { + let y = key.wrapping_add(salt).wrapping_mul(2654435769); + let y = y ^ key; + (((y as u64) * (n as u64)) >> 32) as usize +} + +pub(crate) fn lookup_palette(s: &str) -> Option<[u8; 4]> { + let mut key = 0_u32; + for b in s.as_bytes() { + key = key.wrapping_mul(9).wrapping_add(*b as u32); + } + let salt = SALTS[weak_hash(key, 0, SALTS.len())] as u32; + let ix = weak_hash(key, salt, SALTS.len()); + if s == NAMES[ix] { + Some(COLORS[ix]) + } else { + None + } +}