<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: shekhar</title>
    <description>The latest articles on DEV Community by shekhar (@u84u).</description>
    <link>https://dev.to/u84u</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3887397%2Fd31d0a3b-3646-4e6d-a0af-f388523f40e2.jpeg</url>
      <title>DEV Community: shekhar</title>
      <link>https://dev.to/u84u</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/u84u"/>
    <language>en</language>
    <item>
      <title>I built a CLI so I'd stop typing ImageMagick flags wrong.</title>
      <dc:creator>shekhar</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:33:40 +0000</pubDate>
      <link>https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61</link>
      <guid>https://dev.to/u84u/i-built-a-cli-so-id-stop-typing-imagemagick-flags-wrong-e61</guid>
      <description>&lt;p&gt;I can never remember ImageMagick's flags. Something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;magick mogrify &lt;span class="nt"&gt;-path&lt;/span&gt; out &lt;span class="nt"&gt;-format&lt;/span&gt; webp &lt;span class="nt"&gt;-quality&lt;/span&gt; 80 &lt;span class="nt"&gt;-resize&lt;/span&gt; 1600x1600&lt;span class="se"&gt;\&amp;gt;&lt;/span&gt; &lt;span class="nt"&gt;-unsharp&lt;/span&gt; 0x1 photos/&lt;span class="k"&gt;*&lt;/span&gt;.jpg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I don't think I've ever typed that correctly from memory. So I made &lt;a href="https://github.com/u84u/photu" rel="noopener noreferrer"&gt;photu&lt;/a&gt;, where the same pipeline is just normal shell pipes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;photu &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="s2"&gt;"photos/*.jpg"&lt;/span&gt; | photu resize 1600 | photu sharpen | photu write &lt;span class="s2"&gt;"out/{name}.webp"&lt;/span&gt; &lt;span class="nv"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can chain as many of these as you want:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;photu &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="s2"&gt;"photos/*.jpg"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
| photu resize 1600 &lt;span class="se"&gt;\&lt;/span&gt;
| photu crop 1200x630 &lt;span class="nv"&gt;gravity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;center &lt;span class="se"&gt;\&lt;/span&gt;
| photu rotate &lt;span class="nt"&gt;-3&lt;/span&gt; &lt;span class="nv"&gt;background&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;white &lt;span class="se"&gt;\&lt;/span&gt;
| photu adjust &lt;span class="nv"&gt;saturation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1.3 &lt;span class="nv"&gt;hue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;15 &lt;span class="se"&gt;\&lt;/span&gt;
| photu sharpen &lt;span class="se"&gt;\&lt;/span&gt;
| photu overlay logo.png &lt;span class="nv"&gt;gravity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;southeast &lt;span class="nv"&gt;opacity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0.35 &lt;span class="se"&gt;\&lt;/span&gt;
| photu pad 24 &lt;span class="nv"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;white &lt;span class="se"&gt;\&lt;/span&gt;
| photu write &lt;span class="s2"&gt;"social/{name}.webp"&lt;/span&gt; &lt;span class="nv"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;82
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;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 (&lt;code&gt;write&lt;/code&gt;) hands the whole thing to &lt;a href="https://www.libvips.org/" rel="noopener noreferrer"&gt;libvips&lt;/a&gt; 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.&lt;/p&gt;

&lt;p&gt;You can see the plan mid-pipeline with &lt;code&gt;photu explain&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;photu &lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="s2"&gt;"photos/*.jpg"&lt;/span&gt; | photu resize 800x600 &lt;span class="nv"&gt;fit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cover | photu explain
&lt;span class="go"&gt;photu plan (protocol 1)
files (50):
&lt;/span&gt;&lt;span class="c"&gt;  ...
&lt;/span&gt;&lt;span class="go"&gt;ops (1):
  1. resize  width=800 height=600 fit="cover" upscale=false
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Since it's just text, this also works unchanged in PowerShell and cmd, not just bash/zsh.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;vipsthumbnail&lt;/code&gt; already covers what you need, use that.&lt;/p&gt;

&lt;p&gt;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:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;photu&lt;/td&gt;
&lt;td&gt;2.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImageMagick Q8 with &lt;code&gt;-define jpeg:size=1600x1200&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;12.6 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImageMagick Q8&lt;/td&gt;
&lt;td&gt;22.2 s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ImageMagick Q16-HDRI (the default download)&lt;/td&gt;
&lt;td&gt;23.0 s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;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.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i &lt;span class="nt"&gt;-g&lt;/span&gt; photu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Needs Node 22+, nothing to compile since libvips ships prebuilt with the sharp dependency.&lt;/p&gt;

&lt;p&gt;There's a WASM build in the browser too, at &lt;a href="https://tryphotu.vercel.app" rel="noopener noreferrer"&gt;tryphotu.vercel.app&lt;/a&gt;, if you want to try it without installing anything - images stay local, nothing gets uploaded anywhere.&lt;/p&gt;

&lt;p&gt;Repo's at &lt;a href="https://github.com/u84u/photu" rel="noopener noreferrer"&gt;github.com/u84u/photu&lt;/a&gt;.&lt;br&gt;
&lt;a href="https://news.ycombinator.com/item?id=49006128" rel="noopener noreferrer"&gt;Show HN thread&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>javascript</category>
      <category>opensource</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
