DEV Community

shekhar
shekhar

Posted on

I built a CLI so I'd stop typing ImageMagick flags wrong.

I can never remember ImageMagick's flags. Something like this:

magick mogrify -path out -format webp -quality 80 -resize 1600x1600\> -unsharp 0x1 photos/*.jpg
Enter fullscreen mode Exit fullscreen mode

I don't think I've ever typed that correctly from memory. So I made photu, where the same pipeline is just normal shell pipes:

photu read "photos/*.jpg" | photu resize 1600 | photu sharpen | photu write "out/{name}.webp" quality=80
Enter fullscreen mode Exit fullscreen mode

You can chain as many of these as you want:

photu read "photos/*.jpg" \
| photu resize 1600 \
| photu crop 1200x630 gravity=center \
| photu rotate -3 background=white \
| photu adjust saturation=1.3 hue=15 \
| photu sharpen \
| photu overlay logo.png gravity=southeast opacity=0.35 \
| photu pad 24 color=white \
| photu write "social/{name}.webp" quality=82
Enter fullscreen mode Exit fullscreen mode

The pipe between stages isn't actually carrying images though. Each command just adds one op to a small JSON plan and passes it along, and only the last stage (write) hands the whole thing to libvips 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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Needs Node 22+, nothing to compile since libvips ships prebuilt with the sharp dependency.

There's a WASM build in the browser too, at tryphotu.vercel.app, if you want to try it without installing anything - images stay local, nothing gets uploaded anywhere.

Repo's at github.com/u84u/photu.
Show HN thread

Top comments (1)

Collapse
 
ronak_parmar_033c50d168b5 profile image
Ronak Parmar

I saw you showhn post, can you tell more about applications of photu or libvips compiled binary?