A professional-grade desktop application for creating multi-resolution ICO files from PNG, ICO, and SVG sources. Built with PyQt6 and Pillow, featuring a polished three-theme UI, cross-platform icon exports, batch processing, and a full command-line interface.
Part of the RNVizion toolkit — a suite of professional desktop tools for developers and designers.
- Multi-resolution ICO — Build ICO files with standard sizes (256, 128, 64, 48, 32, 16px)
- PNG compression — Smaller files using PNG encoding for the 256 and 128 px sizes (Vista+)
- Autofill — Automatically generate missing smaller sizes from the largest source
- Size detection — A square PNG goes into the slot its pixel dimensions match, if that is a standard size; an ICO adds every standard size it holds, and an SVG is rendered at all six
- Transform — Rotate 90° either way or 180°, flip horizontal/vertical
- Crop & Resize — Auto-crop, add padding, center resize
- Fill & Borders — Fill transparent areas with a color; colored borders with configurable width
- Color — Brightness, contrast, saturation sliders (-100 to +100), grayscale
- Undo/Redo — One shared 20-step history; each adjustment applies to every loaded size
- Windows ICO — Multi-resolution with optional PNG compression
- macOS ICNS — Apple icon format with Retina support
- PNG Set — Individual PNG files for each size, named by the output filename template (Settings > Export):
{name}is the source file's name,{size}the size,{date}and{time}the moment of the export. The default,icon_{size}, writesicon_256.pngand so on; a template without{size}gets_{size}added, and characters a file name cannot hold become_ - Favicon Package — Complete web set with ICO, PNGs, and manifest
- Android Icons —
res/mipmap-*folders (mdpi through xxxhdpi) - iOS App Icons — An
AppIcon.appiconset/folder with Contents.json
- Batch processing — Queue multiple files for bulk conversion
- Folder watching — Monitor a folder and auto-process new images
- Size presets — Save and load custom size configurations, beside six built-in ones
- Project files — Save the loaded images and build options as
.rnviconfiles - Session recovery — The session is saved every 5 minutes; after an unexpected close, the next start offers to recover it: the loaded files are loaded again from where they were, and the window's size and position and the autofill and PNG compression options come back. A file moved or deleted since is skipped. A normal close leaves nothing to recover
- Restore last session — With Restore last session on startup on (Settings > Auto; it starts off), closing the app keeps the loaded images and the autofill and PNG compression options, and the next start loads them without asking. After an unexpected close, the offer to recover comes instead, and only it
- Project auto-save — With Auto-save project periodically on (the default), an open
.rnviconproject that has changed is saved to its own file every 5 minutes, and the status bar says so. A project never saved has no file, so it waits until you save it
- Live preview — All sizes with configurable zoom (50%–400%) and backgrounds
- Context preview — See icons in OS mockups (taskbar, folder, browser, dock)
- ICO analyzer — Examine existing ICO file structure, extract to PNGs
- Metadata panel — Beside the thumbnails: the file name, folder, file size and format of the size you click in the thumbnails or the size list, with its dimensions, color mode, bit depth and transparency. It shows the largest loaded size until you click another; an autofilled size is an in-memory image
- Color palette — Extract and display dominant colors from loaded images
- Size estimate and build statistics — The estimated ICO size before a build; the last build's compression and a history of ICO builds after
- Dark mode — Default dark UI
- Light mode — Full light theme
- Image mode — Background image with translucent panels
- Cycle with
Ctrl+Tor the theme button (Dark → Light → Image)
The gear button or Ctrl+, opens Settings. Its tabs:
- Sizes — which sizes go into the output, with quick buttons for All Sizes, Favicon, Windows, macOS and Clear All
- Export — PNG set,
.icns, favicon, Android and iOS exports, the ICO analyzer, the estimated file size, and the output filename template for the PNG set, with a preview - Adjust — the image adjustments above, with Undo and Redo
- Preview — preview background (checkerboard, white, black or a custom color), zoom, the color palette, and Icon in Context
- Info — the last build's statistics, the export history and overall export statistics
- Auto — the batch queue, watch folder, presets (the built-ins include Large Only and Small Only), project files, and the session settings: Restore last session on startup and Auto-save project periodically, kept from one start to the next
- Recent — recent files and folders; double-click one to open it again
The main window has three options of its own: Autofill missing smaller sizes, Scan subfolders when selecting folder, and Use PNG compression.
- Python 3.13
- PyQt6
- Pillow
The version ranges are in requirements.txt.
Clone the repository and install dependencies:
git clone https://github.com/RNVizion/rnv-icon-builder.git
cd rnv-icon-builder
pip install -r requirements.txtpython RNV_Icon_Builder.pyInstalling from the checkout adds two commands:
pip install . # a regular install: a copy, with resources/ in it
pip install -e . # or an editable one, for working on the code
rnv-icon-builder # the app, the same as python RNV_Icon_Builder.py
rnv-icon-cli --help # the CLI, the same as python cli.pyA regular install copies the app and its resources/ (font, icons, button images and
the Image-mode background) into the environment, so the checkout can go afterwards. An
editable install runs the code in the checkout and reads resources/ from there; keep
the checkout where it is.
The repository includes platform-specific build scripts that clean caches, remove stale outputs, run PyInstaller, and verify the result.
Windows:
pip install pyinstaller
build_windows.batLinux:
pip install pyinstaller
chmod +x build_linux.sh # First time only
./build_linux.shManual (any platform):
pyinstaller --clean --noconfirm RNV_Icon_Builder.specThe built executable appears in dist/. The included .spec file bundles all resources (fonts, button images, background images, and the app icon) into the binary.
Two scripts at the repository root set up a checkout. Both install the same two files:
requirements.txt, which is what the app needs to run, and tests/requirements-dev.txt,
which adds the test tools and rnv-brand at the commit this repository pins.
# Windows (PowerShell 7)
.\in.ps1 # makes .venv with Python 3.13 if there is none, then installs
.\.venv\Scripts\Activate.ps1# Linux or Codespaces
./in.sh # also installs the Qt system libraries with apt-getrnv-brand is installed from GitHub at a pinned commit, so the second file needs git and
network access. Run the script again after any change that moves the pin; the
register-pin test fails while the installed brand is not the pinned one.
The CLI provides full access to icon building without the GUI:
# Basic ICO creation
python cli.py input.png -o output.ico
# Custom sizes
python cli.py input.png -o output.ico --sizes 256,48,32,16
# Use a preset
python cli.py input.png -o output.ico --preset favicon
# Batch process a folder
python cli.py folder/ -o icons/ --batch
# Batch process a folder and its subfolders
python cli.py folder/ -o icons/ --batch -r
# Platform exports
python cli.py input.png --favicon-package -o web/
python cli.py input.png --android -o android/
python cli.py input.png --ios -o ios/
# Analyze an existing ICO file
python cli.py app.ico --analyze
# List available presets
python cli.py --list-presetsA build needs the input and -o; --analyze needs only the input, and --list-presets needs neither. Preset names ignore case, so favicon finds Favicon; an unknown name is reported and the build uses all six sizes. --preset sets the sizes only; autofill and PNG compression follow the options below.
Other options: --no-autofill and --no-png-compression turn those off (both are on by default), -q prints errors only, and --version prints the version. python cli.py --help lists them all.
The platform exports write:
--favicon-package—favicon.ico,favicon-16x16.png,favicon-32x32.png,favicon-48x48.png,site.webmanifestandfavicon-html.txt(the<link>tags to paste)--android—mipmap-<density>/ic_launcher.pngandic_launcher_round.pngfor mdpi through xxxhdpi--ios—AppIcon.appiconset/with its PNGs andContents.json
| Shortcut | Action |
|---|---|
Ctrl+O |
Open files |
Ctrl+Shift+O |
Open folder |
Ctrl+N |
Clear all files |
Ctrl+B |
Build ICO |
Ctrl+T |
Cycle theme |
F5 |
Refresh preview |
Escape |
Clear selection |
Ctrl+S |
Save project |
Ctrl+Shift+S |
Save project as |
Ctrl+Shift+N |
New project |
Ctrl+, |
Settings |
F11 |
Toggle tooltips |
Ctrl+/ |
About |
| Gesture | Action |
|---|---|
| Hover a thumbnail | Show it enlarged |
| Double-click a thumbnail | Open the full-size preview |
| Click a thumbnail, or a row of the size list | Show that size's details in the metadata panel |
| Right-click a thumbnail | Preview Full Size, Compare with Original (for an autofilled size), Replace this size, Remove this size |
| Drop PNG, ICO or SVG files on the window | Load them |
| Drop a folder on the window | Load its images, as Select Folder does: with its subfolders when Scan subfolders when selecting folder is checked |
RNV_Icon_Builder/
├── RNV_Icon_Builder.py # Main application entry point
├── cli.py # Command-line interface
├── __init__.py # Lazy-loader for package/module dual access
├── py.typed # PEP 561 type-hint marker
├── RNV_Icon_Builder.spec # PyInstaller build spec
├── RNVIconBuilder.pyproj # Visual Studio project file
├── pyproject.toml # Package metadata & entry points
├── requirements.txt # Runtime dependencies
├── in.sh # Linux/Codespaces setup (pip + Qt system libraries)
├── in.ps1 # Windows setup (.venv + pip)
├── brand_map.json # Where each color comes from: rnv-brand or the app
├── COLORS.md # This app's color chart, written by the sync
├── ACCENT_IMAGES.md # Every image with the gold drawn in, written by tools/accent_images.py
├── README.md # This file
├── CHANGELOG.md # What changed
├── LICENSE # MIT license
├── .gitignore
├── clean_python_cache.bat # Windows cache cleanup
├── clean_python_cache.sh # Unix cache cleanup
├── build_windows.bat # Windows build (clean + PyInstaller + verify)
├── build_linux.sh # Linux build (clean + PyInstaller + verify)
│
├── .github/
│ └── workflows/
│ ├── brand-sync.yml # Daily: colors from rnv-brand; a pull request only if one moved
│ ├── tests-linux.yml # CI: Ubuntu, Python 3.13, xvfb
│ └── tests-windows.yml # CI: Windows, Python 3.13
│
│ # Test infrastructure (root-level)
├── test_rnv_icon_builder.py # Legacy unittest suite
├── run_tests.py # Dual-suite runner (unittest + pytest + coverage)
├── pytest.ini # pytest configuration
├── setup.cfg # mutmut mutation-testing config
├── .coveragerc # coverage.py config (branch mode, parallel)
├── coveragerc # The same settings as .coveragerc
├── coverage_report.txt # The last full coverage report, written by run_tests.py
├── sitecustomize.py # Subprocess-coverage bootstrap shim
├── TEST_SYSTEM.md # Test architecture reference
│
├── core/
│ ├── __init__.py
│ ├── py.typed # PEP 561 type-hint marker
│ ├── icon_builder_core.py # ICO generation, format exports
│ ├── image_processor.py # Image loading, validation, undo/redo
│ ├── batch_processor.py # Batch file processing queue
│ ├── folder_watcher.py # Auto-process watched folders
│ ├── preset_manager.py # Size preset management
│ ├── project_manager.py # .rnvicon project files
│ ├── recent_files.py # Recent file/folder history
│ ├── session_manager.py # Auto-save, crash recovery and the session settings
│ └── export_history.py # Export log with timestamps
├── ui/
│ ├── __init__.py
│ ├── py.typed
│ ├── base_dialog.py # Base dialog with signal/move mixins
│ ├── theme_manager.py # Dark/Light/Image theme cycling
│ ├── settings_dialog.py # Tabbed settings (Sizes, Export, Adjust, etc.)
│ ├── about_dialog.py # About dialog with features and shortcuts
│ ├── preview_utils.py # Checkerboard, zoom, color palette widgets
│ ├── context_preview.py # OS context mockup previews
│ ├── ico_analyzer.py # ICO file analysis dialog
│ ├── metadata_panel.py # File details beside the thumbnails
│ ├── image_adjustments.py # Transform and color adjustment functions
│ └── debug_button.py # DebugButton: the main window's image-state action buttons
├── utils/
│ ├── __init__.py
│ ├── py.typed
│ ├── colors.py # Brand colors, theme palettes
│ ├── config.py # Constants, paths, feature flags
│ ├── logger.py # Colored console + file logging
│ ├── error_handler.py # Centralized error handling
│ ├── dialog_helper.py # Themed message dialog helpers
│ ├── file_utils.py # File I/O utilities and the output filename template
│ ├── atomic_write.py # Atomic writes for the files the app keeps
│ ├── font_loader.py # Custom font loading with fallback
│ ├── pil_compat.py # Reads pixels with or without Pillow's getdata()
│ ├── pixmap_cache.py # QPixmap caching layer
│ ├── signal_manager.py # Qt signal connection tracking
│ └── async_file_ops.py # Non-blocking file operations
├── tools/
│ ├── sync_colors.py # Writes the colors in utils/colors.py, and COLORS.md
│ └── accent_images.py # Writes ACCENT_IMAGES.md
├── tests/
│ ├── requirements-dev.txt # Test-only dependencies
│ ├── conftest.py # Shared fixtures + Qt/font/theme bootstrapping
│ ├── snapshots.json # Reference data for test_snapshots.py
│ ├── color_values.py # Every color value a test needs that is not the app's, named once
│ └── test_*.py # The pytest suite (see TEST_SYSTEM.md)
├── docs/
│ └── INTERNALS.md # Architectural reference
└── resources/
├── button_images/ # Button state images (base, hover, pressed)
├── background_images/ # Image mode background
├── fonts/ # Embedded Montserrat font
├── icons/ # Application icon
└── screenshots/ # Project screenshots for README
The app's colors live in utils/colors.py, but their values are not edited there.
brand_map.json, at the repository root, says where each one comes from: a name in
rnv-brand (such as APP.accent) or a value this app owns, with the reason.
tools/sync_colors.py writes the value on each of those lines, and writes COLORS.md, the
chart of this app's colors.
- A color this app owns: change its value in
brand_map.json, then runpython tools/sync_colors.py. - A brand color: it changes in rnv-brand first. Move the pin in
tests/requirements-dev.txt, reinstall (above), then runpython tools/sync_colors.py. The diff ofutils/colors.pyshows exactly what moved. Thebrand-syncworkflow does this every day and opens a pull request only when a color this app uses has moved. python tools/sync_colors.py --checkwrites nothing and exits 1 if the color file orCOLORS.mdis out of date.tests/test_colors_in_sync.pyruns it on every test run, so a value edited by hand inutils/colors.pyfails the suite until the map says the same.
A derived color, such as a lightened or translucent step of the gold, stays code in
utils/colors.py and follows its base.
The images do not follow. Button art, backgrounds and icons have the gold drawn into
their pixels. ACCENT_IMAGES.md lists every image that carries it; tools/accent_images.py
writes that list (--check writes nothing) and never changes an image.
tests/test_accent_images.py keeps the list current.
Session recovery data, the session settings, the last session, export history, presets
and recent files (all under ~/.rnv_icon_builder/), and .rnvicon projects, auto-saved
or not, are written atomically through
utils/atomic_write.py: the new content goes to a temporary file beside the real one, is
flushed to disk, and replaces the original in one step. A crash or a full disk during a
save leaves the old file or the new one, never half of each. Exports you ask for, such as
an ICO file or a PNG set, are not state and keep their own writers.
tests/test_atomic_state_writes.py lists every writer and fails if one writes in place.
The project ships with two complementary test suites totaling over 1,100 tests, measured for branch coverage across all source modules. Run both via the unified runner:
python run_tests.py # Run unittest + pytest + coverage report
python run_tests.py --summary # The same, but the report leaves out fully covered modules
python run_tests.py --html # Also generate htmlcov/ report
python run_tests.py --report # Re-render report from existing data (no re-run)
python run_tests.py --fail-under=N # Exit non-zero if total coverage is under N%
python run_tests.py --benchmark # Run pytest-benchmark suite onlyCoverage is measured on every full run. python run_tests.py --report prints the current
figure for every module; it differs a little between Windows and Linux.
| Suite | Location | Count | Purpose |
|---|---|---|---|
| Legacy unittest | test_rnv_icon_builder.py |
over 400 | Original test suite, run via unittest |
| pytest | tests/ |
over 600 | Modern fixtures, qtbot, hypothesis, subprocess, snapshots |
The pytest suite includes property-based tests (Hypothesis), structural snapshot tests against tests/snapshots.json, performance benchmarks (pytest-benchmark), subprocess-isolated CLI tests in tests/test_cli.py that participate in coverage via a sitecustomize.py bootstrap, and the guard tests listed in TEST_SYSTEM.md.
python -m unittest test_rnv_icon_builder # unittest only
python -m pytest tests/ # pytest only
python -m pytest tests/ -v # verbose
python -m pytest tests/ -m integration # integration-marked tests
python -m pytest tests/ --benchmark-only # benchmarks onlyInstall test-only dependencies separately:
pip install -r tests/requirements-dev.txtThis pulls in pytest, pytest-qt, pytest-cov, pytest-benchmark, pytest-timeout, hypothesis, coverage, mutmut, and rnv-brand at the pinned commit. The setup scripts above install it for you.
Configured in setup.cfg for mutmut 3. It mutates one module, ui/image_adjustments.py, and runs the unittest class that tests it:
python -m mutmut run # Mutate, then run the tests against each mutant
python -m mutmut results # List the mutants that survived
python -m mutmut show <name> # Show one mutant's diff
python -m mutmut browse # Browse the results interactivelymutmut works in a mutants/ folder at the repository root (git ignores it). To audit another module, change source_paths and the tests named in pytest_add_cli_args_test_selection. See TEST_SYSTEM.md.
Coverage data lives in .coverage (canonical) plus parallel-mode subprocess files merged automatically at the end of each run. Each report is also written to coverage_report.txt. With --html, the HTML report renders to htmlcov/index.html. CI runs python run_tests.py --html --fail-under=66 on Linux and Windows: the run fails if total coverage falls under 66%, and htmlcov/ is uploaded as an artifact.
- docs/INTERNALS.md — Architectural reference covering package structure, ICO format implementation,
.rnviconproject files, theme system, signal management, and design decisions - TEST_SYSTEM.md — Test architecture: runner internals, fixture system, subprocess coverage, parallel-mode merging, guard tests
- CHANGELOG.md — What changed, newest first
- Python 3.13 — Modern syntax with type hints throughout
- PyQt6 — UI framework with Fusion style for cross-platform consistency
- Pillow — Image processing and format conversion
- Custom ICO encoder — Direct binary ICO generation with PNG compression
- pytest + Hypothesis + pytest-benchmark — Property-based, snapshot, and benchmark testing
- coverage.py — Branch coverage with parallel-mode subprocess tracking
- mutmut — Mutation testing for assertion strength validation
Built by RNVizion