Metadata-Version: 2.4
Name: iconseed
Version: 0.1.0
Summary: Deterministic PWA icon sets generated from a string. Pure Python, zero dependencies, no Pillow.
License: MIT
Project-URL: Homepage, https://github.com/Canavalny/iconseed
Project-URL: Source, https://github.com/Canavalny/iconseed
Project-URL: Issues, https://github.com/Canavalny/iconseed/issues
Keywords: pwa,icon,favicon,manifest,png,maskable,apple-touch-icon,identicon,avatar,generator,zero-dependency,pure-python
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.8
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: Topic :: Multimedia :: Graphics
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.8
Description-Content-Type: text/markdown
License-File: LICENSE
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# iconseed

**Deterministic PWA icon sets, grown from a string. Pure Python, zero dependencies, no Pillow.**

<p>
<img src="examples/seeds/acme.png" width="72" alt="acme">
<img src="examples/seeds/nova.png" width="72" alt="nova">
<img src="examples/seeds/billing-api.png" width="72" alt="billing-api">
<img src="examples/seeds/staging.png" width="72" alt="staging">
<img src="examples/seeds/mercury.png" width="72" alt="mercury">
<img src="examples/seeds/atlas.png" width="72" alt="atlas">
<img src="examples/seeds/wren.png" width="72" alt="wren">
<img src="examples/seeds/orbit.png" width="72" alt="orbit">
</p>

Those are the strings `acme`, `nova`, `billing-api`, `staging`, `mercury`, `atlas`, `wren`, `orbit` — nothing else was supplied. No source image, no palette, no design file.

```python
import iconseed

iconseed.write_icons("billing-api", "static/icons")
# static/icons/icon-32.png, icon-180.png, icon-192.png, icon-512.png
```

---

## Why this exists

Every other way to produce a PWA icon set starts from an image you already have and a native imaging library you have to install. That is the wrong shape for a whole class of applications:

- **Multi-tenant apps** where every workspace, project, or environment should be visually distinct on a home screen, and nobody is going to draw a thousand icons.
- **Self-hosted tools** where each install wants to look like its own app rather than a clone of everyone else's.
- **Slim containers and serverless bundles** where pulling Pillow in to draw two shapes is a bad trade.

`iconseed` needs a string. It derives a stable colour, draws a glyph, writes the PNG bytes itself, and hands you the manifest entries to declare them.

## Install

```bash
pip install iconseed
```

Requires Python 3.8+. That is the entire dependency list — the package imports nothing outside the standard library: `hashlib`, `struct`, `zlib`, `math`, `json`, `functools`, `os`, `re`, plus `argparse` and `sys` in the CLI.

## Use

```python
import iconseed

# One icon, as bytes.
png = iconseed.icon_png("billing-api", size=192)

# The whole set.
icons = iconseed.icon_set("billing-api")          # {32: b"...", 180: ..., 192: ..., 512: ...}

# Or straight to disk.
paths = iconseed.write_icons("billing-api", "static/icons")

# The colour on its own, for theme-color and CSS.
iconseed.color_for("billing-api")                  # '#3090a6'

# Manifest and markup that match what you just rendered.
print(iconseed.manifest_json("Billing API", theme_color=iconseed.color_for("billing-api")))
print(iconseed.html_head(theme_color=iconseed.color_for("billing-api")))
```

Pick a different glyph, or supply your own as polylines in units of the icon's side, measured from the centre:

```python
iconseed.icon_png("billing-api", glyph="bolt")
iconseed.icon_png("billing-api", glyph=[[(-0.15, 0.1), (0.0, -0.15), (0.15, 0.1)]])
```

Override the colour when the brand is already decided and only the glyph should vary:

```python
iconseed.icon_png("billing-api", color="#5b2ea8", glyph="ring")
```

## Command line

```bash
iconseed billing-api --out static/icons
iconseed billing-api --glyph bolt --sizes 192,512
iconseed billing-api --manifest --name "Billing API" > static/manifest.webmanifest
iconseed billing-api --head
iconseed --list-glyphs
```

## Glyphs

<p>
<img src="examples/glyphs/check.png" width="56" alt="check">
<img src="examples/glyphs/plus.png" width="56" alt="plus">
<img src="examples/glyphs/minus.png" width="56" alt="minus">
<img src="examples/glyphs/cross.png" width="56" alt="cross">
<img src="examples/glyphs/arrow.png" width="56" alt="arrow">
<img src="examples/glyphs/bolt.png" width="56" alt="bolt">
<img src="examples/glyphs/wave.png" width="56" alt="wave">
<img src="examples/glyphs/bars.png" width="56" alt="bars">
<img src="examples/glyphs/dot.png" width="56" alt="dot">
<img src="examples/glyphs/ring.png" width="56" alt="ring">
</p>

`check` · `plus` · `minus` · `cross` · `arrow` · `bolt` · `wave` · `bars` · `dot` · `ring`

## The two decisions that matter

Most of this library is arithmetic. Two choices in it are worth explaining, because they are the difference between icons that work and icons that merely render.

### Colours are darkened until the glyph is actually readable

A hue is hashed out of the seed — one of 360, not one of eight, because a small palette collides embarrassingly fast once a user has three or four of your instances on one home screen. Then lightness is stepped down until the white glyph clears **3:1 contrast against the background**, the WCAG 2.1 threshold for non-text graphics.

That loop is why the icons above look like a set despite nobody choosing the colours. Without it, the blues come out fine and the yellows and limes come out illegible, because equal HSL lightness is nowhere near equal perceived brightness. The test suite checks the ratio across 400 generated seeds.

`hashlib`, not the built-in `hash()`: the latter is salted per interpreter, so it would hand you a different icon after every restart and quietly poison every cache downstream.

### One file is valid as both `any` and `maskable`

Android masks adaptive icons to whatever shape the launcher prefers, and the only region guaranteed to survive is the centred circle covering 80% of the width. Every built-in glyph is defined to sit inside that circle, stroke included, so a single PNG can be declared:

```json
{ "src": "icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }
```

instead of shipping two sets and getting the second one wrong. This is enforced twice in CI — analytically against the glyph geometry, and by scanning the rendered pixels of every built-in glyph for anything that escapes the safe radius. `fits_safe_zone()` is public, so custom glyphs can be checked the same way:

```python
iconseed.fits_safe_zone([[(-0.5, 0.0), (0.5, 0.0)]], stroke=0.063)   # False — too wide
```

## Serving icons from a web app

The bytes are a pure function of the arguments, so an icon can be generated per request and cached by content. `etag_for()` computes the identity without rendering anything, which is enough to answer a conditional request for free:

```python
@app.route("/t/<slug>/icon-<int:size>.png")
def icon(slug, size):
    etag = iconseed.etag_for(slug, size)
    if request.headers.get("If-None-Match") == etag:
        return "", 304
    return Response(
        iconseed.icon_png(slug, size),
        mimetype="image/png",
        headers={"ETag": etag, "Cache-Control": "public, max-age=31536000, immutable"},
    )
```

`icon_png` is memoised, so a warm process answers from memory.

## What it does not do

- **No raster input.** If you have a logo, use [`favicons`](https://pypi.org/project/favicons/) — it resizes a source image properly, and that is a different job.
- **No text or initials.** Drawing letters means shipping a font or hand-encoded outlines; the glyph set is deliberately geometric instead.
- **No `.ico`.** Browsers have accepted PNG favicons for a decade, and the sizes here cover it.
- **Not a general imaging library.** It writes 8-bit truecolour PNGs with filter type 0 and nothing else, because that is all an icon needs.

## Why no dependencies

A PNG is a signature followed by length-prefixed, CRC-checked chunks. The three that matter — `IHDR`, `IDAT`, `IEND` — take about forty lines with `struct` and `zlib`, both of which are in the standard library. Antialiasing a polyline is a distance field, which is arithmetic. Neither justifies a compiled imaging dependency in a container, a Lambda bundle, or a `requirements.txt` that someone will have to audit.

The test suite includes a hand-written PNG *reader* for the same reason: verifying a zero-dependency encoder with someone else's decoder would stop proving the claim.

## Development

```bash
git clone https://github.com/Canavalny/iconseed
cd iconseed
python3 -m pytest -q            # 60 tests, no dependencies beyond pytest
python3 examples/generate.py    # regenerate the images in this README
```

> **Optional before publishing to PyPI:** decide whether to fill in `authors`
> in `pyproject.toml` (currently commented out) and the `LICENSE` copyright line
> (currently "iconseed contributors"). The repository URLs are already set.

## License

MIT.
