Unix-philosophy image manipulation CLI with JSON piping
Project description
chop
A Unix-philosophy image manipulation CLI. Every command reads JSON, writes JSON, does one thing.
# Single image
chop load photo.jpg | chop resize 50% | chop save out.png
# Color adjustments
chop load photo.jpg | chop brightness 1.5 | chop grayscale | chop save out.png
# Multi-image composition
chop load --as bg photo.jpg | chop load --as fg logo.png \
| chop resize 50% --on fg | chop overlay bg fg | chop save out.png
The Design
Unix Philosophy
Each chop invocation is a filter. It reads pipeline state as JSON from stdin, appends its operation, and writes the updated state as JSON to stdout. Composition happens through pipes, not flags.
# Every intermediate step is inspectable
chop load photo.jpg | tee step1.json | chop resize 50% | tee step2.json | chop save out.png
# Integrate with jq
chop load photo.jpg | chop info | jq '.metadata.width'
# Shell scripting
for f in *.jpg; do
chop load "$f" | chop resize 50% | chop pad 10 | chop save "thumb_$f"
done
Side effects — file writes, diagnostic messages — go to stderr. Stdout is reserved exclusively for JSON pipeline state. No modes, no configuration files, no global state.
Uniform Output (SICP Principle)
Every command behaves the same way: read pipeline state from stdin, write pipeline state as JSON to stdout. No exceptions by category — load, resize, save, info, print all follow the same contract.
This means save writes a file to disk (side effect) and outputs JSON (so the pipeline continues). info prints diagnostics to stderr and outputs JSON. print writes a message to stderr and outputs JSON.
# Multi-save: save is non-terminal
chop load photo.jpg | chop save full.png | chop resize 50% | chop save thumb.png
# Info in the middle of a pipeline
chop load photo.jpg | chop resize 50% | chop info | chop pad 10 | chop save out.png
# Debug with print
chop load photo.jpg | chop print "before resize" | chop resize 50% | chop save out.png
The single exception: save - writes binary image data to stdout (because binary bytes and JSON can't share a stream). This is the only command that terminates the pipeline.
Lazy Evaluation
Operations are recorded, not executed. The pipeline state is a list of [name, args, kwargs] triples — no pixels are touched until save or info triggers materialization.
This enables unbound programs: pipelines without a load that can be saved as JSON and applied later.
# Create a reusable recipe
chop resize 50% | chop pad 10 | chop border 2 > recipe.json
# Apply it to any image
chop load photo.jpg | chop apply recipe.json | chop save out.png
chop load other.jpg | chop apply recipe.json | chop save out2.png
Installation
pip install chop
Development:
git clone https://github.com/queelius/chop.git
cd chop
pip install -e .
Requires Python 3.10+, numpy, and Pillow.
Usage
Single Image Pipeline
chop load photo.jpg | chop resize 800x600 | chop save out.png
chop load photo.jpg | chop resize w800 | chop save out.png # width-only, keep aspect
chop load photo.jpg | chop resize h600 | chop save out.png # height-only, keep aspect
chop load photo.jpg | chop fit 800x600 | chop save out.png # fit within bounds
chop load photo.jpg | chop fill 800x600 | chop save out.png # fill bounds, crop excess
Color Operations
All color operations take a factor argument (1.0 = original).
chop load photo.jpg | chop brightness 1.5 | chop save bright.png
chop load photo.jpg | chop contrast 1.3 | chop save crisp.png
chop load photo.jpg | chop saturation 0.5 | chop save muted.png
chop load photo.jpg | chop sharpen 2.0 | chop save sharp.png
chop load photo.jpg | chop blur 3.0 | chop save soft.png
chop load photo.jpg | chop grayscale | chop save gray.png
chop load photo.jpg | chop invert | chop save inverted.png
# Sepia toning
chop load photo.jpg | chop grayscale | chop colorize '#704214' | chop save sepia.png
# Partial color tint
chop load photo.jpg | chop colorize blue --strength 0.3 | chop save cool.png
# Set opacity (for watermarks, overlays)
chop load watermark.png | chop opacity 0.3 | chop save faded.png
# Flatten transparency onto solid color
chop load logo.png | chop background white | chop save logo.jpg
# Auto-crop uniform borders
chop load scan.png | chop trim | chop save clean.png
chop load scan.png | chop trim --fuzz 10 | chop save clean.png
# Shape masks
chop load photo.jpg | chop mask circle | chop save avatar.png
chop load photo.jpg | chop mask roundrect 20 | chop save rounded.png
chop load photo.jpg | chop mask ellipse | chop save oval.png
Multi-Image Composition
Label images with --as, target transforms with --on:
# Side-by-side comparison
chop load --as left before.jpg | chop load --as right after.jpg \
| chop hstack | chop save comparison.png
# Watermark overlay
chop load --as bg photo.jpg | chop load --as wm watermark.png \
| chop overlay bg wm -x 10 -y 10 --opacity 0.5 | chop save watermarked.png
# Vertical stack
chop load --as a img1.jpg | chop load --as b img2.jpg \
| chop vstack --align left | chop save stacked.png
# Grid layout
chop load --as a img1.jpg | chop load --as b img2.jpg \
| chop load --as c img3.jpg | chop load --as d img4.jpg \
| chop grid --cols 2 | chop save grid.png
Spacing between composed images:
# Gapped side-by-side
chop load --as a img1.jpg | chop load --as b img2.jpg \
| chop hstack --gap 10 --gap-color white | chop save comparison.png
# Gapped grid
chop load a.jpg | chop load b.jpg | chop load c.jpg | chop load d.jpg \
| chop grid --cols 2 --gap 5 --gap-color white | chop save grid.png
Without label arguments, composition ops use all loaded images in insertion order:
chop load a.jpg | chop load b.jpg | chop load c.jpg | chop hstack | chop save row.png
Canvas (Blank Image Source)
Create blank canvases for composition backgrounds:
# Colored background with overlay
chop canvas 800x600 --color '#2d5016' --as bg \
| chop load logo.png --as fg | chop fit 200x200 --on fg \
| chop overlay bg fg -x 300 -y 200 | chop save composed.png
# Transparent canvas
chop canvas 400x400 --as bg | chop load icon.png --as fg \
| chop overlay bg fg -x 100 -y 100 | chop save padded-icon.png
Unbound Programs (Recipes)
A pipeline without load is an unbound program — a reusable recipe:
# Save a recipe
chop resize 50% | chop pad 10 | chop grayscale > recipe.json
# Apply to images
chop load photo.jpg | chop apply recipe.json | chop save out.png
The JSON file contains only the ops list — no image data, no paths.
Pipeline Inspection
# info: materializes the pipeline, prints dimensions to stderr, enriches metadata
chop load photo.jpg | chop resize 50% | chop info | jq '.metadata'
# print: no materialization, just prints a message to stderr
chop load photo.jpg | chop print "checkpoint" | chop resize 50% | chop save out.png
# print without message: shows pipeline summary
chop load photo.jpg | chop resize 50% | chop print
Additional Operations
chop load photo.jpg | chop crop 10 10 200 150 | chop save cropped.png # x y w h
chop load photo.jpg | chop crop 10% 10% 80% 80% | chop save cropped.png # percentage
chop load photo.jpg | chop rotate 90 | chop save rotated.png
chop load photo.jpg | chop flip h | chop save flipped.png # h or v
chop load photo.jpg | chop pad 20 | chop save padded.png # uniform
chop load photo.jpg | chop pad 10 20 | chop save padded.png # vert horiz
chop load photo.jpg | chop pad 10 20 30 40 | chop save padded.png # CSS order
chop load photo.jpg | chop border 3 --color red | chop save bordered.png
chop load photo.jpg | chop tile 3 2 | chop save tiled.png # cols rows
Pipeline Management
# Select a different image as cursor
chop load --as a img1.jpg | chop load --as b img2.jpg | chop select a | chop save out.png
# Duplicate a labeled image
chop load --as orig photo.jpg | chop dup orig copy | chop resize 50% --on copy | chop save out.png
# Save to stdout (binary — terminates pipeline)
chop load photo.jpg | chop resize 50% | chop save - --format png > out.png
Command Reference
Source Operations
| Command | Args | Flags | Description |
|---|---|---|---|
load |
source | --as |
Load image from file, URL, or stdin (-) |
canvas |
WxH | --as, --color |
Create a blank canvas image |
apply |
program | Apply a saved JSON recipe |
Geometric Transforms
| Command | Args | Flags | Description |
|---|---|---|---|
resize |
size | --on |
Resize (50%, 800x600, w800, h600) |
crop |
x y w h | --on |
Crop region (pixels or %) |
rotate |
degrees | --on |
Rotate counter-clockwise |
flip |
direction | --on |
Flip (h horizontal, v vertical) |
fit |
WxH | --on |
Fit within bounds, preserve aspect ratio |
fill |
WxH | --on |
Fill bounds, center-crop excess |
pad |
values | --on, --color |
Add padding (1, 2, or 4 values, CSS order) |
border |
width | --on, --color |
Add colored border |
tile |
cols rows | --on |
Tile image NxM times |
Color Operations
| Command | Args | Flags | Description |
|---|---|---|---|
brightness |
factor | --on |
Adjust brightness (0=black, 1=original, 2=double) |
contrast |
factor | --on |
Adjust contrast (0=grey, 1=original, 2=double) |
saturation |
factor | --on |
Adjust saturation (0=grayscale, 1=original, 2=double) |
sharpen |
factor | --on |
Adjust sharpness (0=blurred, 1=original, 2=sharp) |
blur |
radius | --on |
Gaussian blur (radius in pixels) |
grayscale |
--on |
Convert to grayscale | |
invert |
--on |
Invert colors | |
colorize |
color | --on, --strength |
Tint image with a color (preserves luminance) |
opacity |
factor | --on |
Set uniform opacity (0.0=transparent, 1.0=opaque) |
background |
color | --on |
Flatten transparency onto solid color |
trim |
--on, --fuzz |
Auto-crop uniform borders | |
mask |
shape [radius] | --on, --invert |
Apply shape mask (roundrect, circle, ellipse) |
Composition
| Command | Args | Flags | Description |
|---|---|---|---|
hstack |
[labels...] | --as, --align, --gap, --gap-color |
Stack horizontally (top/center/bottom) |
vstack |
[labels...] | --as, --align, --gap, --gap-color |
Stack vertically (left/center/right) |
overlay |
[labels...] | --as, -x, -y, --opacity, --paste |
Overlay images |
grid |
[labels...] | --as, --cols, --gap, --gap-color |
Arrange in grid |
Pipeline Management
| Command | Args | Flags | Description |
|---|---|---|---|
select |
label | Switch cursor to a labeled image | |
dup |
source dest | Duplicate a labeled image |
Output
| Command | Args | Flags | Description |
|---|---|---|---|
save |
path | --format |
Save to file or stdout (-) |
info |
Materialize and show dimensions (stderr), enrich metadata | ||
print |
[message] | Print message or summary to stderr (no materialization) |
JSON Wire Format
The JSON flowing between commands:
{
"version": 3,
"ops": [
["load", ["photo.jpg"], {}],
["resize", ["50%"], {}],
["pad", [10], {"color": "transparent"}]
],
"metadata": {}
}
Each operation is a [name, args, kwargs] triple. The metadata dict is enriched by info with width, height, mode, and images_loaded.
No image data travels through the pipe — only the recipe. Materialization (actually loading and processing pixels) happens at save or info time.
Architecture
Four modules:
pipeline.py— Multi-image composition engine.PipelineStatestores ops + metadata.materialize()executes ops against a labeled image context (dict[str, Image]) with cursor tracking. Source ops (load,canvas) add images and set cursor.operations.py— Pure image functions (Image → Image). 21 transform ops inOPERATIONSdict (geometric, color, trim/alpha/mask), multi-image ops inCOMPOSITION_OPSset with--gap/--gap-colorsupport.cli.py— Argparse CLI. Singlehandlersdict dispatches all 34 commands.--ontargets transforms,--asnames source/composition results.output.py— One function: write JSON to stdout. Always. 20 lines total.
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file chop_img-0.5.0.tar.gz.
File metadata
- Download URL: chop_img-0.5.0.tar.gz
- Upload date:
- Size: 36.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6fa937d0e9af949c9b0fd888b2d648457e0675bcbc48c6328e7dc6ab7a82bc01
|
|
| MD5 |
2bc0b1996be50211d07f212f2e2b8d5f
|
|
| BLAKE2b-256 |
80972d9d5ad393be2c4e06a418a9966776f02f83e0854bad430dbf2b1ff8f081
|
File details
Details for the file chop_img-0.5.0-py3-none-any.whl.
File metadata
- Download URL: chop_img-0.5.0-py3-none-any.whl
- Upload date:
- Size: 34.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.1.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
907eac377f1c02bd35f5ad75fc94794a4aeee53536399ccede5830b3a756e159
|
|
| MD5 |
6ff526a16ea7262efdb917c79bc62a45
|
|
| BLAKE2b-256 |
17f440970fb639765394a8ae0bb11ed61c36ab263caba965e46c51adf66f44c7
|