# Modern Colorthief

Rust-based Python extension for extracting dominant colors and color palettes from images.
~100x faster than pure Python colorthief. Uses all available CPU cores via rayon parallelism.

Version: 0.2.1 | License: MIT | Python: >3.10 | Rust Edition: 2024
Docs: https://modern-colorthief.readthedocs.io
Repo: https://github.com/baseplate-admin/modern_colorthief

---

## Installation

Requires Python 3.10+ and a supported platform (Linux, macOS, Windows -- prebuilt wheels available).

```bash
# pip
pip install modern_colorthief

# uv (fastest)
uv pip install modern_colorthief

# Poetry
poetry add modern_colorthief
```

Verify:
```python
import modern_colorthief
print(modern_colorthief.__version__)  # "0.2.1"
```

---

## Public API

Modern Colorthief exposes two functions. Both accept the same input types and are backed by a Rust implementation of the Median Cut Color Quantization algorithm.

### `get_color(image, quality=10) -> tuple[int, int, int]`

Extract the single dominant color from an image.

Internally extracts a small palette (5 colors) and returns the top entry.

**Parameters:**
- `image` -- File path (`str`), raw image bytes (`bytes`), or a `io.BytesIO` stream.
- `quality` (int, default 10) -- Downsample factor. Every Nth pixel is skipped. Higher = faster but less accurate. Range: 1-100.

**Returns:** An RGB tuple `(R, G, B)` with each channel in `0..=255`.

**Raises:** `ValueError` if the image is invalid or contains no colors. `TypeError` for unsupported input types.

```python
from modern_colorthief import get_color

color = get_color("photo.jpg")
print(color)  # (139, 69, 19)

# From bytes
with open("photo.jpg", "rb") as f:
    color = get_color(f.read())

# From BytesIO
import io
color = get_color(io.BytesIO(open("photo.jpg", "rb").read()))
```

### `get_palette(image, color_count=10, quality=10) -> list[tuple[int, int, int]]`

Extract a palette of dominant colors from an image.

Results are deduplicated -- if the algorithm produces duplicate colors, only unique entries are returned.

**Parameters:**
- `image` -- File path (`str`), raw image bytes (`bytes`), or a `io.BytesIO` stream.
- `color_count` (int, default 10) -- Number of colors to extract.
- `quality` (int, default 10) -- Downsample factor. Higher = faster but less accurate.

**Returns:** A list of unique RGB tuples. Actual length may be less than `color_count` if duplicates are removed.

**Raises:** `ValueError` if the image is invalid. `TypeError` for unsupported input types.

```python
from modern_colorthief import get_palette

palette = get_palette("photo.jpg", color_count=5)
print(palette)
# [(139, 69, 19), (220, 20, 60), (255, 250, 240), (34, 139, 34), (70, 130, 180)]
```

---

## CLI

The `modern-colorthief` command is installed alongside the package.

```bash
modern-colorthief FILE [--palette] [--quality N] [--count N]
```

**Arguments:**
- `FILE` -- Path to the image file.

**Options:**
- `--palette` -- Extract a palette instead of a single dominant color.
- `--quality N` -- Quality factor (default: 10). Lower = more accurate.
- `--count N` -- Number of palette colors (default: 5). Only used with `--palette`.

**Output:** Hex color strings, one per line (e.g., `#8b4513`).

```bash
# Dominant color
modern-colorthief photo.jpg
# Output: #8b4513

# Palette of 8 colors
modern-colorthief photo.jpg --palette --count 8 --quality 5
# Output:
# #8b4513
# #dc143c
# #faf0be
# ...
```

Exit code 0 on success, 1 on error (invalid file, decode failure, etc.).

---

## Usage Patterns

### File path (recommended for performance)

```python
from modern_colorthief import get_color, get_palette

color = get_color("photo.jpg")
palette = get_palette("photo.jpg", color_count=6)
```

### Raw bytes

```python
with open("photo.jpg", "rb") as f:
    color = get_color(f.read())
```

### BytesIO

```python
import io

buf = io.BytesIO(open("photo.jpg", "rb").read())
color = get_color(buf)
```

### With Pillow

```python
import io
from PIL import Image

img = Image.open("photo.jpg").convert("RGB")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)

color = get_color(buf)
```

### With NumPy arrays

```python
import io
import numpy as np
from PIL import Image

arr = np.random.randint(0, 256, (480, 640, 3), dtype=np.uint8)
img = Image.fromarray(arr)
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)

color = get_color(buf)
```

---

## Parity With Other Libraries

Modern Colorthief uses the same Median Cut algorithm as the original colorthief, but image decoders differ between Rust (`image` crate) and Python (`Pillow`). To get identical output:

```python
import io
from PIL import Image
from modern_colorthief import get_color, get_palette

img = Image.open("photo.jpg")
buf = io.BytesIO()
img.save(buf, format="PNG")
buf.seek(0)

# Now output matches colorthief / fast-colorthief
color = get_color(buf)
palette = get_palette(buf)
```

Typical deviation without parity: 1-3 units per channel per color.

---

## Performance Benchmarks

On a sample JPEG image (approximate timings):

| Task             | Python (colorthief) | C++ (fast_colorthief) | Rust (modern_colorthief) |
|------------------|---------------------|-----------------------|--------------------------|
| Extracting Color | 0.219895 s          | 0.021180 s            | **0.019645 s**           |
| Extracting Palette | 0.202956 s        | 0.023626 s            | **0.018661 s**           |

~100x speedup over pure Python. Matches or exceeds C++ implementation.
Pixel processing is parallelized across all available CPU cores using rayon.
On WASM, parallelism degrades gracefully to sequential execution.

---

## Comparison With Alternatives

### vs fast-colorthief (C++/pybind11)
- Broader architecture support via PyO3 (vs pybind11)
- No hard NumPy dependency
- Simpler, more maintainable codebase
- Smaller automated build via maturin + GitHub Actions
- Larger binary (500-700 KB vs 52-60 KB)

### vs color-thief-py (pure Python)
- ~100x faster execution
- No hard Pillow dependency
- Fully compatible with modern Python versions

---

## Algorithm

Modern Colorthief uses **Median Cut Color Quantization** (MMCQ):

1. Collect all pixels from the image (downsampled by `quality` factor).
2. Place them into a single 3D RGB box.
3. Find the channel (R, G, or B) with the largest value range.
4. Sort pixels along that channel and split the box at the median.
5. Repeat until the desired number of color boxes is reached.
6. Compute the average color of each box to form the palette.
7. Deduplicate the result.

The algorithm is simple, efficient, and produces visually representative palettes.

---

## Supported Image Formats

The Rust `image` crate supports: JPEG, PNG, WebP, GIF, BMP, TIFF, and others.
Exact support depends on the features enabled in the `image` crate dependency.

---

## Roadmap

Completed:
- BytesIO support (PR #47)

Planned:
- Profile-Guided Optimization (PGO) when PyO3/maturin support it (tracking: maturin#1840)

---

## License

MIT License. See LICENSE file in repository for full text.
