Skip to main content

pix

Batch AVIF shrink and image-host sync CLI.

pix incrementally compresses local image directories to AVIF, mirrors originals to cloud storage, and publishes compressed images to an image host. Compression uses pyvips; sync is delegated to rclone.

中文 · Architecture · Development · Decisions · Roadmap

Status: core commands are implemented and tested. Real R2 and Windows smoke tests are still pending; see the roadmap.

What pix does

Command Purpose
pix shrink Recursively compress images to AVIF, re-encoding only changed files
pix mirror Bidirectionally mirror an original photo directory to a cloud drive
pix publish Bidirectionally mirror AVIF output to an image host; optionally emit public URLs
pix doctor Report Python, libvips and rclone versions, plus the config path
pix config init Write a commented default configuration file

pix is deliberately narrow:

  • It is not an rclone replacement and does not manage credentials.
  • It does not read repository config or care about version control.
  • It has no one-shot pix run; you chain the commands yourself.

How it works

photos/ ── shrink (pyvips) ──> out/*.avif + out/.pix/state.json
                                     │
                                     └── publish (rclone bisync) ──> r2:images
photos/ ── mirror (rclone bisync) ────────────────────────────────> onedrive:photos
Command What actually runs Who does the work
shrink pyvips thumbnail + AVIF encode pix
mirror rclone bisync SRC REMOTE pix composes arguments, rclone executes
publish rclone bisync OUT REMOTE, excluding .pix/** pix + rclone
doctor / config init Runtime report / config scaffold pix

Sync is rclone. mirror and publish only compose rclone bisync arguments; rclone handles authentication, transfers and conflict handling. Configure remotes with rclone config before using them. pix never reads or writes rclone.conf.

mirror / publish are bidirectional mirrors, not one-way copies. Deletions propagate in both directions.

Requirements

  • Python 3.11+
  • uv
  • rclone >= 1.66 for mirror / publish (and for pix doctor to report a clean environment)
  • At least one remote already configured in rclone

pyvips[binary] bundles libvips, so no separate vips installation is required.

Install

Install from PyPI:

uv tool install pix-cli
pix --version

Upgrade with uv tool upgrade pix-cli; remove with uv tool uninstall pix-cli. Linux installs ~/.local/bin/pix; Windows installs %USERPROFILE%\.local\bin\pix.exe.

To work on pix itself, install the checkout in editable mode:

git clone https://github.com/uzvg/pix.git
cd pix
uv tool install --editable .
pix --version

An editable install picks up source changes immediately. Reinstall with uv tool install --force --editable ..

Configure rclone

Only needed for mirror / publish:

rclone config         # create a remote, for example onedrive or r2
rclone lsd onedrive:  # verify that it works

rclone stores credentials in ~/.config/rclone/rclone.conf (Windows: %APPDATA%\rclone\rclone.conf). pix passes the remote name through and does not store or validate credentials.

Configure pix (optional)

Default configuration paths:

  • Linux/macOS: $XDG_CONFIG_HOME/pix/config.toml, or ~/.config/pix/config.toml
  • Windows: %APPDATA%\pix\config.toml

If the file is missing, built-in defaults are used. Generate a commented file with:

pix config init

Existing files are not overwritten; pass --force to replace one. Use --config PATH before the subcommand to point at another file; an explicit path must exist, while a missing default path is fine.

Common keys:

Section Key Default Meaning
[shrink] width 1920 Longest edge in pixels
quality 85 AVIF quality, 1–100
effort 5 libvips effort, 0–9
min_size 0 Skip sources smaller than this; accepts 500K, 1M, 2G
include jpg, jpeg, png, webp Extensions to scan, without dots
[mirror] include common image extensions Extensions to mirror, without dots; [] disables the whitelist
exclude [] rclone exclude globs; command-line values are appended
max_delete 10 Delete limit as a percentage; -1 disables, 0 forbids
[publish] base_url "" Public URL prefix; must start with http:// or https://
urls false Emit a JSON URL manifest
exclude [] Same as mirror; .pix/** is always excluded first
max_delete 10 Same as mirror
[rclone] binary rclone rclone executable name or path

Precedence is command line > pix config > built-in defaults. --exclude is appended to config excludes rather than replacing them. -q means quality; --quiet is errors-only; -v / --verbose adds per-file detail and rclone progress. --no-urls overrides [publish].urls = true for one run.

The default [mirror].include is jpg, jpeg, png, webp, gif, bmp, tif, tiff, heic, heif, avif, jxl, svg, ico, cr2, cr3, nef, arw, dng, orf, raf, rw2. Set it to [] to mirror every file.

Quick start

  1. Check the environment:

    pix doctor
    
  2. Compress images:

    pix shrink -s ~/Pictures -d ~/Pictures-avif --dry-run
    pix shrink -s ~/Pictures -d ~/Pictures-avif -w 1920 -q 85
    

    The first run encodes everything; later runs skip unchanged files. State lives in OUT/.pix/state.json. -w is a maximum long edge, so smaller images are not upscaled; relative directories are preserved under OUT. Change detection uses source size/mtime and output size, not content hashes. Changing width, quality or effort triggers a full re-encode. --dry-run only prints the plan. --prune removes only outputs recorded in state; extra files in OUT are left alone.

  3. Mirror originals:

    pix mirror -s ~/Pictures -d onedrive:photos --dry-run
    pix mirror -s ~/Pictures -d onedrive:photos
    

    The destination is created automatically. Only files matching [mirror].include are mirrored; other source files are ignored, and non-matching files already on the destination are left untouched. If no source file matches the filter, the source counts as empty and is refused unless you pass --allow-empty --resync. An empty destination initializes its own baseline, so no --resync is needed on the first run. Use --resync to rebuild the baseline when the destination already has matching content, or after changing include/exclude; --dry-run may create an empty destination but does not transfer or delete. After upgrading from a version without include filtering, the first run also needs --resync; preview with --dry-run --resync first (the preview does not write the baseline).

  4. Publish the AVIF output:

    pix publish -s ~/Pictures-avif -d r2:images \
      --base-url https://img.example.com --urls > urls.json
    

    publish requires at least one .avif in the source unless you pass --allow-empty --resync; it syncs only .avif files and always excludes .pix/**. The URL manifest is printed to stdout as JSON only after a successful, non-dry-run sync. It lists every .avif under OUT, not just files changed in this run, and is not filtered by [publish].exclude or --exclude; excluded files can still appear in it. If you rely on excludes to hide files, do not publish the manifest until this behavior is fixed.

  5. Re-run later:

    pix shrink -s ~/Pictures -d ~/Pictures-avif
    pix mirror -s ~/Pictures -d onedrive:photos
    pix publish -s ~/Pictures-avif -d r2:images \
      --base-url https://img.example.com --urls
    

    Only changes are encoded and transferred. --base-url is not persisted between runs; set [publish].base_url in config to avoid repeating it.

Command cheat sheet

Task Command
Preview compression pix shrink -s SRC -d OUT --dry-run
Re-encode everything pix shrink -s SRC -d OUT --force
Remove tracked outputs whose source is gone pix shrink -s SRC -d OUT --prune
Preview a sync pix mirror -s SRC -d REMOTE --dry-run
First sync (new or empty destination) pix mirror -s SRC -d REMOTE
Rebuild the baseline (non-empty destination, or after changing include/exclude) pix mirror -s SRC -d REMOTE --resync
Emit a URL manifest pix publish -s OUT -d REMOTE --base-url https://img.example.com --urls
Diagnose the environment pix doctor
Generate a config file pix config init

Safety rules

Situation What happens Protection
Deletions on either side Propagated in both directions; accidental deletion is irreversible Preview with --dry-run; default --max-delete 10 (percentage)
First sync (destination empty) pix creates the destination and initializes the baseline Preview with --dry-run; it may create an empty destination but does not transfer or delete. No manual rclone needed
Destination has matching content but no baseline pix does not auto-resync; bisync fails instead of overwriting Confirm which side is authoritative, then add --resync
Include/exclude filters changed When filters are active, rclone detects the changed filters file and exits 7 Run with --resync after changing include or exclude; --dry-run --resync previews but does not write the baseline
Non-image files in the source mirror ignores them by default; publish ignores anything that is not .avif. If no file matches, the source counts as empty and is refused Add extensions to [mirror].include if they should be mirrored; use --allow-empty --resync only for an intentional empty sync
Empty source mirror / publish refuse. shrink --prune refuses only while state still has entries; empty state is a no-op Use --allow-empty (plus --resync for sync) only when intentional
a.jpg and a.jpeg both exist Both map to the same output Detected before encoding; exits 2
Two concurrent shrinks They would write the same state and outputs OUT/.pix/lock blocks the second process
State missing or corrupt Full rebuild; corrupt state logs a warning Only causes re-encoding; outputs stay valid

Output and exit codes

  • Logs and progress go to stderr.
  • stdout carries doctor reports, shrink --dry-run plans, config init's wrote <path>, and --urls JSON. mirror / publish --dry-run output comes from rclone.
  • Exit codes: 0 success, 1 runtime failure, 2 argument or configuration error.
  • mirror / publish return rclone's bisync exit code unchanged; destination preflight failures return 1.

URL manifest example:

{
  "2024/a.avif": "https://img.example.com/2024/a.avif"
}

Troubleshooting

Symptom Fix
pyvips/libvips unavailable Reinstall to bring libvips: uv tool install --force --editable /path/to/pix; or install libvips manually (Linux: apt install libvips42, Windows: libvips release zip on PATH)
rclone is older than 1.66 Upgrade rclone
bisync reports a missing baseline or changed filters file The destination has matching content, or the include/exclude rules changed (including the first run after upgrading). Confirm local should win, then add --resync; an empty destination initializes automatically. --dry-run --resync previews but does not write the baseline.
OUT/.pix/lock already exists Check for another shrink, then remove a stale lock
Configuration fails to parse The error names the file and location; fix the TOML or remove the file to fall back to defaults. pix doctor cannot run until the config parses
publish says the source has no .avif Run pix shrink first
URL-related error base_url must start with http:// or https://, and --urls requires it

Development

uv sync
uv run pytest
uv run pix --version
uv build

The code must stay compatible with Python 3.11. See development.md for tests, packaging and release details. Behavior is specified in architecture.md; rationale lives in decisions.md.

The detailed docs are currently written in Chinese.

Metadata

Release files for pix-cli 0.1.4

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

Source distribution (sdist)

Source distribution for pix-cli 0.1.4
File Size Uploaded Provenance Actions
pix_cli-0.1.4.tar.gz 61.4 kB With provenance Details for pix_cli-0.1.4.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for pix-cli 0.1.4
File Interpreter ABI Platform Actions
pix_cli-0.1.4-py3-none-any.whl Python 3 none any Details for pix_cli-0.1.4-py3-none-any.whl

Total release size: 82.3 kB

Release files / pix_cli-0.1.4.tar.gz

Download URL pix_cli-0.1.4.tar.gz
Size 61.4 kB
Tags Source
SHA-256 checksum
How to use checksums
e1fbf7c939cb38588e405cfbdd1eaffdfea2591589591b02a48d03b915a39680
BLAKE2b-256 checksum
How to use checksums
58b91954dd3c68606a698be65b5b6029196ac5482fef234fd21e30ee95243dde
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

Release files / pix_cli-0.1.4-py3-none-any.whl

Download URL pix_cli-0.1.4-py3-none-any.whl
Size 20.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
25a05a061f45e0931a6191d26421fea9cc0c4363a21b914bf3d0ed4677683e44
BLAKE2b-256 checksum
How to use checksums
feeb02b4edc9f4e93da14b3d820b7a0f0b79683e2b65a69608f08857b6c5e4d9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via uv/0.13.7 {"installer":{"name":"uv","version":"0.13.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

Security for pix-cli 0.1.4

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.

Full provenance

Both files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pix_cli-0.1.4.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Nov 6, 2026.

Transparency log

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.

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