Commits
aca7335
.forgejo
assets
docs
examples
presets
scripts
src
tests
tools
.gitignore 27 B
Cargo.lock 116.0 KiB
Cargo.toml 3.8 KiB
icon-studio.desktop 398 B
LICENSE.md 33.9 KiB
README.md 10.7 KiB

Icon Studio

CI status audit status packages status license install neiam-icon-studio (deb) install neiam-icon-studio (rpm)

Designs application icons from the house palettes: pick a Material Symbols glyph or some text, a foreground colour and a background treatment, and see every combination your choices allow side by side.

all ten background generators

Provenance

The original icon studio was lost in the data-loss incident, and unlike the Android clients there was no artefact to decompile — an egui binary was not sitting on a phone somewhere. So this is a rebuild from two things that did survive:

  • its inputs — the palettes, which are still live (see below);
  • its outputs — the icons it produced, which ship inside the five cbz* Android apps.

That second point is what keeps this honest. tests/reproduce.rs renders a background with the reconstructed code and compares it against ic_icon_background.png taken from the installed cbzauth APK. The originals turn out to be an even two-tone diagonal split of the Casabeza brand colours with a hairline boundary — roughly one blended pixel per row — and the tests assert exactly that, so a change that turns the split into a soft gradient fails rather than quietly drifting.

What is attested, and what is not

Feature Evidence
Diagonal split background Attested — all five shipped cbz icons use it
Material Symbols foreground Attested — shipped icons are Symbols Rounded, outlined
Text foreground Attested — cbznumerics ships “1234”
Casabeza + neiam palettes Attested — recovered from the APK and live in dms
Solid, gradients, stripes, checks, dots, rays, wedge Invented — described as “gradients, patterns and other things”, but no shipped icon uses them
Sub-banner Invented — described, but no shipped icon has one, so the layout is a guess

The invented parts are reasonable, not recovered. Treat them as a starting point rather than as the tool you remember.

Documents

Configurations save as TOML, so they are readable and hand-editable:

version = 1
palette = "Casabeza"

[foreground]
kind = "icon"
codepoint = "e899"
name = "lock"        # informational; the codepoint wins

[colors.background_a]
role = "primary"
rgb = [0, 201, 212]

[background]
style = "diagonal-split"
angle = 45.0

Colours carry both a palette role and literal RGB. The role wins where the named palette still defines it, so retuning a theme in dms updates every document that references it; the RGB is the fallback that keeps old documents openable when a role is gone. A document from a newer version is refused rather than half-read, and an unknown background.style is an error rather than a silent default — but an unknown grid preference is dropped, since that should not block opening a file.

New / Open / Save / Save as are on the left panel, with Ctrl+N, Ctrl+O, Ctrl+S and Ctrl+Shift+S. The title bar shows the filename and a bullet for unsaved changes, and New/Open prompt before discarding them.

The cbz icon set

presets/ holds a document per shipped app — cbz-auth, cbz-lists, cbz-numerics, cbz-scanner, cbz-inventory — so each icon opens as an editable configuration instead of having to be rebuilt by hand.

These are reconstructions. The palette, the diagonal split and its orientation are measured from the shipped PNGs, and the scales are taken from each one’s ink extents; the glyph identifications are mine by eye, so cbz-scanner being satellite_alt is a judgement, not a recovered fact. A test loads all five and asserts they still reproduce the shipped background orientation.

Palettes

Nothing is retyped by hand. tools/regen_palettes.py writes src/palette/generated.rs from:

  • neiam — the daisyUI themes carried by the sites, converted from OKLCH to sRGB with the same transform the Android clients use. Several sites ship the same set, so more than one stylesheet is read and the results merged by name: dms provides her, afterdark, forest, sky, clays, stones, dark and light; waxx additionally provides blueprint. The shared themes are identical between sources — where they ever diverge, the earlier source wins and the script says so rather than resolving it quietly;
  • Casabeza — the exact values recovered from the cbzauth APK after the loss, carried in the script.
tools/regen_palettes.py                    # refresh after a site's themes change
tools/regen_palettes.py --source path.css  # read a specific stylesheet, repeatable
tools/regen_palettes.py --check            # non-zero if stale, for CI

The Android themes and the dms themes agree exactly on primary and accent; Android’s backgrounds are deliberately darker (L 0.18 against 0.35). Both are offered.

Fonts

Vendored in assets/:

  • Material Symbols Rounded, outlined and filled, from egui_material_icons (MIT), with the .codepoints table parsed at startup so the icon list and the rasteriser cannot disagree about what a name means;
  • B612 regular and bold, the face the cbz apps ship in res/font, used for text marks and sub-banners. Rendering text through the Symbols font lands on .notdef, which is why text needs its own face.

Installing

Packages are published to this forge’s registries on every push to master, x86_64 only.

# Debian / Ubuntu
curl -fsSL $FORGE/pkg/_keys/deb/neiam | sudo tee /etc/apt/keyrings/neiam.asc >/dev/null
echo "deb [signed-by=/etc/apt/keyrings/neiam.asc] $FORGE/pkg/deb/neiam/_all stable main" \
  | sudo tee /etc/apt/sources.list.d/neiam.list
sudo apt update && sudo apt install neiam-icon-studio

For Fedora / RHEL / openSUSE the workflow prints a matching .repo stanza at the end of each run.

The package installs the binary, a desktop entry with the application icon, and the preset documents under /usr/share/neiam-icon-studio/presets/.

Builds off a tag publish the bare Cargo version; builds off master publish <version>+dev.<timestamp>, which sorts after the release — so a machine tracking this repository gets the latest build, and two runs never collide on one filename.

Running

cargo run              # the studio
cargo test             # including the comparison against shipped icons
cargo run --example reproduce_cbz   # re-render the five cbz icons
cargo run --example showcase        # every background generator
cargo run --example make_presets    # regenerate presets/
cargo run --example make_app_icon   # regenerate the desktop icon

The application’s own icon is made by the application, from presets/icon-studio.toml — change it in the studio and regenerate.

Export

Two forms, both writing ic_icon, ic_icon_background and ic_icon_foreground across mipmap-mdpi through mipmap-xxxhdpi (108–432px), the layout the cbz apps use.

To a folder. The chosen directory is interpreted rather than taken literally, because mipmap-* only means anything inside an Android res/ — dropping those beside build.gradle.kts writes fifteen files, reports success, and leaves the app with its old icon. So:

You pick It writes to
a project root app/src/main/res
a repo whose client is under android/ android/app/src/main/res
a module root src/main/res
a res/ directory itself
a mipmap-* bucket the res/ above it
anything else the folder itself, and says so

The resolved path and the reason are shown before you commit to it, along with how many files would be written and how many already exist and would be replaced. Destinations you have used before are offered in a dropdown and remembered between sessions in ~/.config/icon-studio/config.toml — that is a property of the machine, not of the icon, so it never travels in a document.

An export writes three things, because images alone are not enough:

  1. the PNGs, across every density. The flat <name>.png and the two adaptive layers are different sizes and different shapes: the layers are 108dp full-bleed squares for the launcher to mask itself, while the flat icon is 48dp, cropped to the adaptive safe zone, and masked to a rounded square with its corners cut away. A full-bleed square there is what makes an app show up as a hard-edged tile in a launcher or an F-Droid index beside everyone else’s rounded ones;
  2. mipmap-anydpi-v26/<name>.xml, the adaptive-icon descriptor. Without it Android has no idea the background and foreground are related, falls back to the flat PNG, and the layers are dead weight;
  3. optionally, android:icon and android:roundIcon on <application> in AndroidManifest.xml. A manifest that names no icon shows the platform default while the build succeeds and nothing reports a problem.

The manifest is patched as a targeted text edit, not by parsing and re-serialising: a manifest is hand-maintained and carries comments, attribute ordering and indentation that a round-trip through a generic XML writer would quietly destroy. Inserted attributes match the surrounding indentation, running it twice changes nothing the second time, and a manifest that already says the right thing is not rewritten at all.

As a zip bundle. One archive holding the same mipmap tree plus the source .toml. A bundle of bare PNGs is a dead end — whoever receives it cannot change the icon without rebuilding the configuration by eye, which is the problem this program exists to avoid.

Design notes

Rendering is tiny-skia and ab_glyph, deliberately independent of egui: the preview, the grid and the exported PNG all go through render::render_composed, so what you see is what lands on disk, and the renderer can be tested headlessly. Grid cells are cached by content hash — the cartesian product gets large quickly, and it is capped at 240 cells for that reason.