I can never remember ImageMagick's flags. Something like this:
magick mogrify -path out -format webp -quality 80 -resize 1600x1600\> -unsharp 0x1 photos/*.jpg
I don't think I've ever typed that correctly from memory. So I made and runs it. So it doesn't matter how many stages you chain, each image still only gets decoded and encoded once, at the end.
You can see the plan mid-pipeline with photu explain:
$ photu read "photos/*.jpg" | photu resize 800x600 fit=cover | photu explain
photu plan (protocol 1)
files (50):
...
ops (1):
1. resize width=800 height=600 fit="cover" upscale=false
Since it's just text, this also works unchanged in PowerShell and cmd, not just bash/zsh.
Is this just a libvips wrapper? Pretty much, yeah. libvips does the actual pixel work and gets all the credit for the speed. What photu adds is that the vips CLI runs one operation per process, so chaining means writing intermediate files or piping raw pixels yourself - photu just passes a plan instead and fuses the whole thing. Also adds globs, URLs as inputs, output templates, that kind of glue. If vipsthumbnail already covers what you need, use that.
On speed: I grabbed 50 public-domain images from the Met Museum (1,130px to 4,000px, 116MB of JPEGs total), resized them to 800px wide and wrote out as WebP q80, timed with hyperfine:
| time | |
|---|---|
| photu | 2.0 s |
ImageMagick Q8 with -define jpeg:size=1600x1200 | 12.6 s |
| ImageMagick Q8 | 22.2 s |
| ImageMagick Q16-HDRI (the default download) | 23.0 s |
Most of that is just libvips being libvips - it decodes JPEGs at reduced scale when it can, and threads well. photu's number here still includes spinning up four separate Node processes.
npm i -g photu
Needs Node 22+, nothing to compile since libvips ships prebuilt with the sharp dependency.
There's a WASM build in the browser too, at .
Show HN thread
SOCIAL SHARE CARD GENERATOR