iconseed
Deterministic PWA icon sets, grown from a string. Pure Python, zero dependencies, no Pillow.
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.
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
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
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:
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:
iconseed.icon_png("billing-api", color="#5b2ea8", glyph="ring")
Command line
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
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:
{ "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:
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:
@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— 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
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
authorsinpyproject.toml(currently commented out) and theLICENSEcopyright line (currently "iconseed contributors"). The repository URLs are already set.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file iconseed-0.1.0.tar.gz.
File metadata
- Download URL: iconseed-0.1.0.tar.gz
- Upload date:
- Size: 20.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
78deb7c00051abe90766935a991b7902d04b32cc5e8f6536590fde2d62c3da5a
|
|
| MD5 |
e44e5fb3ceb730156a00b7ed0e397012
|
|
| BLAKE2b-256 |
465f063d6ea3146b1a4f023969c0e9316c6c4c21d213d95b3d00c36d4687683d
|
Provenance
The following attestation bundles were made for iconseed-0.1.0.tar.gz:
Publisher:
release.yml on Canavalny/iconseed
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
iconseed-0.1.0.tar.gz -
Subject digest:
78deb7c00051abe90766935a991b7902d04b32cc5e8f6536590fde2d62c3da5a - Sigstore transparency entry: 2367604233
- Sigstore integration time:
-
Permalink:
Canavalny/iconseed@2f897c62fede64e3e2915e1c850e6f7514c97185 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Canavalny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2f897c62fede64e3e2915e1c850e6f7514c97185 -
Trigger Event:
push
-
Statement type:
File details
Details for the file iconseed-0.1.0-py3-none-any.whl.
File metadata
- Download URL: iconseed-0.1.0-py3-none-any.whl
- Upload date:
- Size: 16.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2904e42323a9d5842014bf42e122c12dc68b79ea0c5ab5f82fc68055a6954096
|
|
| MD5 |
733343de21faaed512d591bd7902ed49
|
|
| BLAKE2b-256 |
aa687bb8b1c58246d8f886ffe3db2afb549e280bccbdd2a84a083f63167c6e53
|
Provenance
The following attestation bundles were made for iconseed-0.1.0-py3-none-any.whl:
Publisher:
release.yml on Canavalny/iconseed
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
iconseed-0.1.0-py3-none-any.whl -
Subject digest:
2904e42323a9d5842014bf42e122c12dc68b79ea0c5ab5f82fc68055a6954096 - Sigstore transparency entry: 2367604763
- Sigstore integration time:
-
Permalink:
Canavalny/iconseed@2f897c62fede64e3e2915e1c850e6f7514c97185 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/Canavalny
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@2f897c62fede64e3e2915e1c850e6f7514c97185 -
Trigger Event:
push
-
Statement type: