Skip to content

Commit

Permalink
🚸 Expose arguments
Browse files Browse the repository at this point in the history
  • Loading branch information
dimostenis committed Sep 25, 2023
1 parent 676735c commit 333f62b
Show file tree
Hide file tree
Showing 29 changed files with 355 additions and 53 deletions.
81 changes: 80 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,75 @@ Its hosted on PyPI.
pip install colorhash
```

## Advanced usage

You can influence every aspect of final color. **Default values** are following:

```python
ColorHash(
obj: Any,
lightness: Sequence[float, ...] = (0.35, 0.5, 0.65), # picks 'randomly' one
saturation: Sequence[float, ...] = (0.35, 0.5, 0.65), # picks 'randomly' one
min_h: Optional[int] = None, # hue, min 0
max_h: Optional[int] = None, # hue, max 360
)
```

But be careful, **setting tight conditions may result in very similar colors**. See example tables.

You can fix lightness or saturation to single value(s) by using sequence with 1 element (eg. `[0.5]`).

| code | hex | color |
|:--------------------------------------|:---------:|:-------------------------------:|
| `ColorHash('hey') # default` | `#782d86` | ![#782d86](./docs/782d86.png) |
| `ColorHash('hey', lightness=[0.55])` | `#b453c6` | ![#b453c6](./docs/b453c6.png) |
| `ColorHash('hey', lightness=[0.75])` | `#d69fdf` | ![#d69fdf](./docs/d69fdf.png) |
| `ColorHash('hey', lightness=[0.95])` | `#f7ecf9` | ![#f7ecf9](./docs/f7ecf9.png) |
| `ColorHash('hey', saturation=[0.15])` | `#8d6c93` | ![#8d6c93](./docs/8d6c93.png) |
| `ColorHash('hey', saturation=[0.55])` | `#b139c6` | ![#b139c6](./docs/b139c6.png) |
| `ColorHash('hey', saturation=[0.95])` | `#d406f9` | ![#d406f9](./docs/d406f9.png) |
| `ColorHash('hey', lightness=[0.95], saturation=[0.95])` | `#fbe6fe` | ![#fbe6fe](./docs/fbe6fe.png) |
| `ColorHash('oh', lightness=[0.95], saturation=[0.95])` | `#fef0e6` | ![#fef0e6](./docs/fef0e6.png) |
| `ColorHash('boi', lightness=[0.95], saturation=[0.95])` | `#e6fee7` | ![#e6fee7](./docs/e6fee7.png) |

You can set hue range or even fix it by setting `min_h` = `max_h`.

| code | hex | color |
|:--------------------------------------|:---------:|:-------------------------------:|
| `ColorHash('hey', min_h=150)` | `#2d5886` | ![#2d5886](./docs/2d5886.png) |
| `ColorHash('hey', min_h=300)` | `#862d6c` | ![#862d6c](./docs/862d6c.png) |
| `ColorHash('hey', max_h=150)` | `#866e2d` | ![#866e2d](./docs/866e2d.png) |
| `ColorHash('hey', min_h=150, max_h=360)` | `#2d5886` | ![#2d5886](./docs/2d5886.png) |
| `ColorHash('hey', min_h=150, max_h=150) # fixed hue` | `#2d8659` | ![#2d8659](./docs/2d8659.png) |

Or you can let `ColorHash` decide between combination of many `lightness` and `saturation` options (mind `min_h` and `max_h` are equal in this example).

| code | hex | color |
|:--------------------------------------|:---------:|:-------------------------------:|
| `ColorHash('stick', min_h=65, max_h=65, saturation=[x/10 for x in range(1, 10)], lightness=[x/10 for x in range(1, 10)])` | `#869108` | ![#869108](./docs/869108.png) |
| `ColorHash('with', min_h=65, max_h=65, saturation=[x/10 for x in range(1, 10)], lightness=[x/10 for x in range(1, 10)])` | `#eef5a3` | ![#eef5a3](./docs/eef5a3.png) |
| `ColorHash('one', min_h=65, max_h=65, saturation=[x/10 for x in range(1, 10)], lightness=[x/10 for x in range(1, 10)])` | `#ddeb47` | ![#ddeb47](./docs/ddeb47.png) |

Finally some bad examples. When you set too strict rules, colors may be almost identical.

| code | hex | color |
|:--------------------------------------|:---------:|:-------------------------------:|
| `ColorHash('lets', lightness=[0.95], saturation=[0.95], min_h=300)` | `#fee6f8` | ![#fee6f8](./docs/fee6f8.png) |
| `ColorHash('break', lightness=[0.95], saturation=[0.95], min_h=300)` | `#fee6fb` | ![#fee6fb](./docs/fee6fb.png) |
| `ColorHash('it', lightness=[0.95], saturation=[0.95], min_h=300)` | `#fee6fa` | ![#fee6fa](./docs/fee6fa.png) |
| `ColorHash('here', min_h=150, max_h=150)` | `#6ce0a6` | ![#6ce0a6](./docs/6ce0a6.png) |
| `ColorHash('goes', min_h=150, max_h=150)` | `#79d2a6` | ![#79d2a6](./docs/79d2a6.png) |
| `ColorHash('almost', min_h=150, max_h=150)` | `#6ce0a6` | ![#6ce0a6](./docs/6ce0a6.png) |
| `ColorHash('same', min_h=150, max_h=150)` | `#79d2a6` | ![#79d2a6](./docs/79d2a6.png) |
| `ColorHash('color', min_h=150, max_h=150)` | `#6ce0a6` | ![#6ce0a6](./docs/6ce0a6.png) |


## Changelog

- color-hash **2.0.0** *(2023-09-22)*
- ✨ Expose params to influence colors
- ✨ Runtime validation of input params
- 📝 Update docs for advanced usage
- color-hash **1.3.2** *(2023-09-21)*
- ⚡️ 30%+ speedup on `hsl2rgb()`
- ✅ Add tests for all named colors (1500+ tests)
Expand All @@ -48,7 +115,7 @@ pip install colorhash
- ➕ Add `hatch` (build & test)
- ➕ Add `twine` (publish)
- ✨ Support `python3.11`
- ⚰️ Drop support for `python3.6`
- ⚰️ Drop support for `python3.6` (downloads from PyPI are under 1%)
- color-hash **1.2.2** *(2022-10-17)*
- ✨ Add publish helper script
- color-hash **1.2.1** *(2022-10-17)*
Expand All @@ -71,6 +138,18 @@ pip install colorhash
- color-hash **1.0.0** *(2016-07-07)*
- 🎉 Initial port.

## Speed comparison

Running `pytest` (1600+ tests) on different python versions.

| python | secs |
| :----: | :-----: |
| 3.7 | `1.252` |
| 3.8 | `1.239` |
| 3.9 | `0.720` |
| 3.10 | `0.690` |
| 3.11 | `0.892` |

## License

Copyright (c) 2016 Felix Krull <[email protected]>
Expand Down
89 changes: 56 additions & 33 deletions colorhash/colorhash.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,15 @@
'#2dd24b'
"""
from binascii import crc32
from numbers import Number
from typing import Any
from typing import Optional
from typing import Sequence
from typing import Tuple
from typing import Union

MIN_HUE = 0
MAX_HUE = 360

IntOrFloat = Union[int, float]


Expand Down Expand Up @@ -80,49 +84,46 @@ def rgb2hex(rgb: Tuple[int, int, int]) -> str:

def color_hash(
obj: Any,
hashfunc=crc32_hash,
lightness=(0.35, 0.5, 0.65),
saturation=(0.35, 0.5, 0.65),
min_h=None,
max_h=None,
lightness: Sequence[float] = (0.35, 0.5, 0.65),
saturation: Sequence[float] = (0.35, 0.5, 0.65),
min_h: Optional[int] = None,
max_h: Optional[int] = None,
) -> Tuple[float, float, float]:
"""
Calculate the color for the given object.
Args:
obj: the value.
hashfunc: the hash function to use. Must be a unary function returning
an integer. Defaults to ``crc32_hash``.
lightness: a range of values, one of which will be picked for the
lightness component of the result. Can also be a single
number.
saturation: a range of values, one of which will be picked for the
saturation component of the result. Can also be a single
number.
min_h: if set, limit the hue component to this lower value.
max_h: if set, limit the hue component to this upper value.
This function takes the same arguments as the ``ColorHash`` class.
Returns:
A ``(H, S, L)`` tuple.
"""
if isinstance(lightness, Number):
lightness = [lightness]
if isinstance(saturation, Number):
saturation = [saturation]
# "all([x for x ...])" is actually faster than "all(x for x ...)"
if not all([0.0 <= x <= 1.0 for x in lightness]):
raise ValueError("lightness params must be in range (0.0, 1.0)")
if not all([0.0 <= x <= 1.0 for x in saturation]):
raise ValueError("saturation params must be in range (0.0, 1.0)")

if min_h is None and max_h is not None:
min_h = 0
min_h = MIN_HUE
if min_h is not None and max_h is None:
max_h = 360
max_h = MAX_HUE

hash = hashfunc(obj)
h = hash % 359
hash_val = crc32_hash(obj)
h = hash_val % 359
if min_h is not None and max_h is not None:
if not (
MIN_HUE <= min_h <= MAX_HUE
and MIN_HUE <= max_h <= MAX_HUE
and min_h <= max_h
):
raise ValueError(
"min_h and max_h must be in range [0, 360] with min_h <= max_h"
)
h = (h / 1000) * (max_h - min_h) + min_h
hash //= 360
s = saturation[hash % len(saturation)]
hash //= len(saturation)
l = lightness[hash % len(lightness)] # noqa
hash_val //= 360
s = saturation[hash_val % len(saturation)]
hash_val //= len(saturation)
l = lightness[hash_val % len(lightness)] # noqa

return (h, s, l)

Expand All @@ -131,16 +132,38 @@ class ColorHash:
"""
Generate a color value and provide it in several format.
This class takes the same arguments as the ``color_hash`` function.
Args:
obj: the value.
lightness: a range of values, one of which will be picked for the
lightness component of the result. Can also be a single
number.
saturation: a range of values, one of which will be picked for the
saturation component of the result. Can also be a single
number.
min_h: if set, limit the hue component to this lower value.
max_h: if set, limit the hue component to this upper value.
Attributes:
hsl: HSL representation of the color value.
rgb: RGB representation of the color value.
hex: hex-formatted RGB color value.
"""

def __init__(self, *args, **kwargs):
self.hsl: Tuple[int, float, float] = color_hash(*args, **kwargs)
def __init__(
self,
obj: Any,
lightness: Sequence[float] = (0.35, 0.5, 0.65),
saturation: Sequence[float] = (0.35, 0.5, 0.65),
min_h: Optional[int] = None,
max_h: Optional[int] = None,
):
self.hsl: Tuple[float, float, float] = color_hash(
obj=obj,
lightness=lightness,
saturation=saturation,
min_h=min_h,
max_h=max_h,
)

@property
def rgb(self) -> Tuple[int, int, int]:
Expand Down
Binary file added docs/2d5886.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/2d8659.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/6ce0a6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/782d86.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/79d2a6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/862d6c.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/866e2d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/869108.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/8d6c93.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/b139c6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/b453c6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/d406f9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/d69fdf.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/ddeb47.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/e6fee7.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/eef5a3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/f7ecf9.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/fbe6fe.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/fee6f8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/fee6fa.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/fee6fb.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/fef0e6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions docs/gen.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from pathlib import Path

from PIL import Image

# needed for eval()
from colorhash import ColorHash # noqa

NEW_TABLE_SEPARATOR = object() # sentinel

lst = [
NEW_TABLE_SEPARATOR,
"ColorHash('hey') # default",
"ColorHash('hey', lightness=[0.55])",
"ColorHash('hey', lightness=[0.75])",
"ColorHash('hey', lightness=[0.95])",
"ColorHash('hey', saturation=[0.15])",
"ColorHash('hey', saturation=[0.55])",
"ColorHash('hey', saturation=[0.95])",
"ColorHash('hey', lightness=[0.95], saturation=[0.95])",
"ColorHash('oh', lightness=[0.95], saturation=[0.95])",
"ColorHash('boi', lightness=[0.95], saturation=[0.95])",
NEW_TABLE_SEPARATOR,
"ColorHash('hey', min_h=150)",
"ColorHash('hey', min_h=300)",
"ColorHash('hey', max_h=150)",
"ColorHash('hey', min_h=150, max_h=360)",
"ColorHash('hey', min_h=150, max_h=150) # fixed hue",
NEW_TABLE_SEPARATOR,
"ColorHash('stick', min_h=65, max_h=65, saturation=[x/10 for x in range(1, 10)], lightness=[x/10 for x in range(1, 10)])", # noqa
"ColorHash('with', min_h=65, max_h=65, saturation=[x/10 for x in range(1, 10)], lightness=[x/10 for x in range(1, 10)])", # noqa
"ColorHash('one', min_h=65, max_h=65, saturation=[x/10 for x in range(1, 10)], lightness=[x/10 for x in range(1, 10)])", # noqa
NEW_TABLE_SEPARATOR,
"ColorHash('lets', lightness=[0.95], saturation=[0.95], min_h=300)",
"ColorHash('break', lightness=[0.95], saturation=[0.95], min_h=300)",
"ColorHash('it', lightness=[0.95], saturation=[0.95], min_h=300)",
"ColorHash('here', min_h=150, max_h=150)",
"ColorHash('goes', min_h=150, max_h=150)",
"ColorHash('almost', min_h=150, max_h=150)",
"ColorHash('same', min_h=150, max_h=150)",
"ColorHash('color', min_h=150, max_h=150)",
]

WIDTH = 120
HEIGHT = 25
OUT = Path("docs")
HEADER = "| code | hex | color |\n|:--------------------------------------|:---------:|:-------------------------------:|" # noqa

# delete old tiles
[x.unlink() for x in OUT.glob("*.png")]

# gen tiles + docs table text
for code in lst:
# start new table
if code == NEW_TABLE_SEPARATOR:
print("\n")
print(HEADER)
continue

hex_color = eval(code).hex
tile = Image.new("RGB", (WIDTH, HEIGHT), hex_color)

filename = f"{hex_color[1:]}.png" # eg. "eafbf6.png"
tile.save(OUT / filename)

s: str = f"| `{code}` | `{hex_color}` | ![{hex_color}](./docs/{filename}) |"
print(s)
6 changes: 5 additions & 1 deletion makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: install-pip-tools reqs install-env env test tag build publish
.PHONY: install-pip-tools reqs install-env env test tag build publish docs

VERSION := $(shell python -c 'import colorhash;print(colorhash.__version__)')

Expand All @@ -25,6 +25,10 @@ publish-test: check
publish: check
twine upload --config-file .pypirc --repository pypi dist/**$(VERSION)*

# generates README markdown tables and color tiles
docs:
python -m docs.gen

# ===================
# --- SUB TARGETS ---
# ===================
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ requires-python = ">=3.6"
name = "colorhash"
description = "Generate color based on any object"
license = { text = "MIT" }
version = "1.3.2"
version = "2.0.0"
readme = "README.md"
authors = [
{ name = "dimostenis", email = "[email protected]" },
Expand All @@ -15,7 +15,7 @@ dependencies = []
Homepage = "https://github.com/dimostenis/color-hash-python"
"Bug Tracker" = "https://github.com/dimostenis/color-hash-python/issues"
[project.optional-dependencies]
dev = ["black", "pre-commit", "pytest"]
dev = ["black", "pre-commit", "pytest", "pillow"]

[tool.isort]
profile = "black"
Expand Down
Loading

0 comments on commit 333f62b

Please sign in to comment.