active tool

chop

Unix-philosophy image manipulation CLI with lazy evaluation, JSON piping, and multi-image composition

Started 2026 Python

Resources & Distribution

Package Registries

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-img

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. PipelineState stores 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 in OPERATIONS dict (geometric, color, trim/alpha/mask), multi-image ops in COMPOSITION_OPS set with --gap/--gap-color support.
  • cli.py — Argparse CLI. Single handlers dict dispatches all 34 commands. --on targets transforms, --as names source/composition results.
  • output.py — One function: write JSON to stdout. Always. 20 lines total.

License

MIT

Discussion