Skip to main content

iconseed

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

acme nova billing-api staging mercury atlas wren orbit

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

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 authors in pyproject.toml (currently commented out) and the LICENSE copyright 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

iconseed-0.1.0.tar.gz (20.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

iconseed-0.1.0-py3-none-any.whl (16.7 kB view details)

Uploaded Python 3

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

Hashes for iconseed-0.1.0.tar.gz
Algorithm Hash digest
SHA256 78deb7c00051abe90766935a991b7902d04b32cc5e8f6536590fde2d62c3da5a
MD5 e44e5fb3ceb730156a00b7ed0e397012
BLAKE2b-256 465f063d6ea3146b1a4f023969c0e9316c6c4c21d213d95b3d00c36d4687683d

See more details on using hashes here.

Provenance

The following attestation bundles were made for iconseed-0.1.0.tar.gz:

Publisher: release.yml on Canavalny/iconseed

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for iconseed-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 2904e42323a9d5842014bf42e122c12dc68b79ea0c5ab5f82fc68055a6954096
MD5 733343de21faaed512d591bd7902ed49
BLAKE2b-256 aa687bb8b1c58246d8f886ffe3db2afb549e280bccbdd2a84a083f63167c6e53

See more details on using hashes here.

Provenance

The following attestation bundles were made for iconseed-0.1.0-py3-none-any.whl:

Publisher: release.yml on Canavalny/iconseed

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page