Skip to main content

Image Cleanup

Command-line and GTK tools for removing duplicate images and converting images to WebP to save storage.

Installation

The Python environment is managed with uv. The command-line tools use the default dependencies. The optional GTK GUI is in the gui dependency group; its PyCairo dependency is captured in uv.lock. GTK itself and the headers needed to build those Python extensions are native Linux packages.

Install the published CLI package with pip install cleanup-cli. To install the GTK application and its native Python dependencies, use pip install cleanup-gui.

On Fedora, install the native prerequisites first. The Python development package must match the version in .python-version (currently Python 3.12):

sudo dnf install gtk4 python3.12-devel gobject-introspection-devel \
  cairo-devel cairo-gobject-devel gcc pkgconf-pkg-config

For the command-line tools, create/synchronize the project environment from uv.lock:

uv sync

Install the optional GUI dependencies when you want to use the GTK interface:

uv sync --group gui

Developers running the complete test suite should also install the optional gui and scipy groups. The SciPy group is used to compare the built-in NumPy DCT fallback:

uv sync --group gui --group scipy

Do not install PyGObject with the system Python for this project; uv sync --group gui builds and installs PyGObject and pycairo into .venv.

GTK graphical interface

Launch the GNOME-style GTK 4 interface with:

uv run --group gui cleanup-gui

The application provides Duplicate Images and WebP Conversion tabs. Both default to non-destructive dry runs, display scrollable results as each completed item arrives, and update a cumulative space-saved summary while the controller runs in a background thread. Enabling deletion or replacement requires confirmation. The UI uses symbolic icons from the active GNOME icon theme and follows GNOME's light/dark preference, including changes made while the application is open.

Screenshots

Duplicate Images tab
Find duplicate images
WebP Conversion tab
Convert images to WebP

The window uses compact tool navigation beside the title in the header bar and grouped settings beside a results panel. Drag the divider to give either panel more room; each scrolls independently. Narrow, tall windows stack the panels, while short windows keep them side by side. The action button stays visible, and the empty results area provides guidance for the selected tool. Divider positions are shared across tools, and Reset Panel Sizes in the main menu restores their defaults. The settings panel keeps its controls fully visible, with a vertical scrollbar shown whenever its content needs more height.

Live results arrive in batches, and the list reuses widgets for visible rows so large result sets leave the window responsive. The summary uses a single line of monospace text; hover over it to see the full summary when space is limited.

The main view is extensible and accepts zero or any number of tabs. A tab only needs a title, a symbolic icon name, and a method that builds its GTK widget:

import gi

gi.require_version("Gtk", "4.0")
from gi.repository import Gtk

from cleanup_cli.views.gui import GtkGuiView


class InformationTab:
    title = "Information"
    icon_name = "dialog-information-symbolic"

    def build(self) -> Gtk.Widget:
        return Gtk.Label(label="Custom cleanup tool")


GtkGuiView(InformationTab()).run()

Command-line interface

The existing CLI remains available through uv run cleanup-cli. For example:

uv run cleanup-cli deduplicate /path/to/photos
uv run cleanup-cli webp /path/to/photos

The CLI flushes every conversion, skip, or duplicate result as soon as it is available, then prints a final total. WebP totals are bytes actually removed by successful replacements. Deduplication dry runs report bytes that would be reclaimed; --delete reports bytes actually deleted.

Architecture

The package uses Model View Controller boundaries:

  • Models in cleanup_cli/models/ own image policies, filesystem operations, and reusable generic services.
  • Controllers in cleanup_cli/controllers/ accept immutable request dataclasses and return immutable result dataclasses without depending on CLI concerns.
  • Views in cleanup_cli/views/ own presentation and production dependency composition. The argparse CLI registers arbitrary subcommands, while the GTK main view registers arbitrary tabs.

Protocols define structural dependencies such as scanners, analyzers, metrics, removers, and views. Abstract generic base classes define extensible indexer, codec, detector, and controller APIs. Concrete implementations can therefore be replaced in tests or by another UI without changing the domain policies.

Converting images to WebP

The webp subcommand recursively finds decodable images and, by default, performs a non-destructive dry run. Use --replace to opt into replacing each source with a .webp file in the same directory. Quality 80 is lossy, so keep backups before using replacement. Existing WebP images, non-image files, and all existing destination directory entries (including dangling symlinks) are ignored. Pixel dimensions are preserved and a source is replaced only after the output is validated, is smaller, and the source is still the exact file that was analyzed. Destination installation is atomic and no-clobber.

cleanup-cli webp /path/to/photos --replace

The default WebP quality is 80. It can be changed from 0 through 100:

cleanup-cli webp /path/to/photos --quality 90

Before decoding, the converter reads each image header and estimates its peak decoded and encoder memory from its pixel dimensions. Concurrent conversions are admitted only while their combined estimates fit a memory budget. By default, the budget is derived conservatively from currently available memory; it can be set explicitly for a constrained machine or container:

cleanup-cli webp /path/to/photos --replace --memory-limit-mb 512

An image whose individual estimate exceeds the budget is skipped without decoding it. --max-workers remains an upper bound on concurrency, while the memory budget may select fewer workers for large images.

Removing duplicate images

cleanup-cli recursively scans a directory, decodes images with Pillow, and computes a 256-bit perceptual hash (pHash) from the 16-by-16 low-frequency corner of a DCT over a 64-by-64 normalized image. A normalized RGB color signature is also checked because grayscale pHash alone cannot detect a uniform color shift. Files that Pillow cannot decode as images are ignored. Paths are naturally sorted using the rules below. Among matching images, the image with the highest pixel resolution is kept. If resolutions match, the larger file is kept as a quality tie-breaker; if resolution and file size both match, the last naturally sorted path is kept.

The default is a dry run and does not change any files:

cleanup-cli deduplicate /path/to/photos

Before hashing, deduplication reads each image header and estimates its peak decode memory from its pixel dimensions. Concurrent hashes are admitted only while their combined estimates fit a budget derived from available memory. Set an explicit budget for a constrained machine or container:

cleanup-cli deduplicate /path/to/photos --memory-limit-mb 512

Images whose individual estimates exceed the budget are ignored without being decoded. --max-workers remains an upper bound on hashing concurrency, while the memory budget may allow fewer workers for large images.

Successful image signatures are cached in $XDG_CACHE_HOME/cleanup-cli/image-signatures/. If XDG_CACHE_HOME is not set, the cache is stored in ~/.cache/cleanup-cli/image-signatures/. Each scanned directory gets a JSON file named from the SHA-256 hash of its resolved path. On later runs, an image is decoded again only when its device, inode, size, or nanosecond modification time has changed. Cache data from an older signature algorithm is ignored automatically. The cache directory can be deleted at any time; it will be rebuilt on the next scan.

Use --delete to remove the duplicates reported by the dry run:

cleanup-cli deduplicate /path/to/photos --delete

Deletion verifies that each candidate is unchanged since indexing. Changed or missing candidates are refused rather than deleting a different file. The command may still complete partially if an I/O error occurs; inspect its output and keep backups because --delete has no undo.

The threshold is the maximum normalized structural or color distance. The structural value is the Hamming distance between two 256-bit pHashes; average RGB channel differences are mapped to the same 0 through 256 range. 0 requires equal structural and color signatures; larger values tolerate more visual change. A conservative starting point for resized or re-encoded copies is 16:

cleanup-cli deduplicate /path/to/photos --threshold 16

The accepted range is 0 through 256. Perceptual hashes describe low-frequency image structure, not byte equality. Higher thresholds increase both tolerance and the chance of grouping distinct images, so review the dry-run output before using --delete.

Natural sorting of paths

sort_numbered_paths sorts a path by every number it contains, from left to right, instead of sorting the path alphabetically. Every directory and filename component is compared hierarchically:

from cleanup_cli import sort_numbered_paths

paths = ["dir-10", "dir-2", "dir-1.5-xxx", "abc5-5-XYZ", "misc"]
ordered = sort_numbered_paths(paths)
# ["dir-1.5-xxx", "dir-2", "abc5-5-XYZ", "dir-10", "misc"]

1.5 is treated as one exact decimal number, while names such as chapter-2-part-10 produce the numeric tuple (2, 10). Numeric tuples are compared lexicographically, so the first number has priority, then the second, and so on. Names with no numbers are sorted alphabetically after numbered names. Leading zeros compare numerically (dir-02 and dir-2), with spelling used only to break ties. For example, 1/dir-10, 2/misc, and 100/dir-2 sort in that order because the first path component is compared before the filename. Relative and absolute paths retain their path components in the comparison.

Date/time names are validated and sorted chronologically. Supported forms are:

  • year-first separated dates: 2026-08-08, 2026_08_08, 2026.08.08
  • day-first separated dates: 08-08-2026, 08_08_2026, 08.08.2026
  • compact dates: 20260808
  • optional separated times such as 17-30-00, 17_30_00, 17.30.00, or 17:30:00 (seconds may be omitted)
  • optional compact times, for example 20260808_173000

Different representations of the same timestamp compare equally and use the filename only as a deterministic tie-breaker. Invalid dates fall back to the normal numeric rules instead of raising an error.

Publishing

The release workflow builds and publishes both distributions when a v* tag matches the version in each package's pyproject.toml. Keep the two package versions aligned, then push the matching tag (for example, v0.1.0).

Create the following GitHub environments and configure a separate PyPI Trusted Publisher for each project:

PyPI project GitHub environment
cleanup-cli pypi-cleanup-cli
cleanup-gui pypi-cleanup-gui

Both publishers use owner amiralimollaei, repository cleanup-cli, and workflow filename release.yml. The environment field must match the table. For new projects, add pending publishers from your PyPI account's Publishing page. The distinct environments let PyPI identify which project to create.

Pending publishers do not reserve names. To claim the names before pushing the workflow to GitHub, build and upload the first release locally instead:

uv build --no-sources --out-dir dist/cleanup-cli
uv build --no-sources --project packages/cleanup-gui --out-dir dist/cleanup-gui
read -rsp "PyPI API token: " UV_PUBLISH_TOKEN
export UV_PUBLISH_TOKEN
uv publish --trusted-publishing never dist/cleanup-cli/*
uv publish --trusted-publishing never dist/cleanup-gui/*
unset UV_PUBLISH_TOKEN

The first upload requires an account scoped PyPI token because the projects do not exist yet. After uploading, add the publishers above to the existing projects for future releases. PyPI only claims each name after a successful upload.

Metadata

Release files for cleanup-cli 0.1.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for cleanup-cli 0.1.0
File Size Uploaded Provenance
cleanup_cli-0.1.0.tar.gz 45.1 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for cleanup-cli 0.1.0
File Interpreter ABI Platform
cleanup_cli-0.1.0-py3-none-any.whl Python 3 none any Details

Total release size: 105.9 kB

Release files / cleanup_cli-0.1.0.tar.gz

Download URL cleanup_cli-0.1.0.tar.gz
Size 45.1 kB
Tags Source
SHA-256 checksum
How to use checksums
28912bb775213c37f7b4bddaeac1199d2af98b6b474bfea9fa7826dfcd079d2e
BLAKE2b-256 checksum
How to use checksums
9c9b428486047900e3d66a16d23c3652796621740790ed3ec1ee7cd219e5fe3c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Release files / cleanup_cli-0.1.0-py3-none-any.whl

Download URL cleanup_cli-0.1.0-py3-none-any.whl
Size 60.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5e0dee491ac4ff41742ceb5d7b0f38a9614fa0ac87729dca4833bd8418930233
BLAKE2b-256 checksum
How to use checksums
0c9e7733e71633629520c13d2fc77b37520d861f718002bdb0a26d48c2a4a793
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Fedora Linux","version":"44","id":"","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

Security for cleanup-cli 0.1.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page