ImageMagick for VS Code 0.5.0: cutting 100 lines and a bug I did not know I had
A cleanup release for my ImageMagick VS Code extension. No new features, 98 fewer lines, and one real bug that fell out when I deleted the code that was hiding it.
0.5.0 of my ImageMagick VS Code extension ships no new features. It deletes 223 lines and adds 125, for a net loss of 98, and behaves identically. I want to write about it anyway, because one of those deletions turned out to be a bug fix, and I only found it by trying to remove the code.
The extension was guessing
When you bulk-save a folder of images, the work splits across two processes. The webview holds the ImageMagick engine and does the encoding. The extension host reads files off disk and writes the results back. They talk over a message channel.
So the host receives a pile of encoded bytes and has to name the output file. It never decoded the image, so it does not know whether those bytes are a WebP or a JPEG. My solution, written months ago and never questioned: read the first twelve bytes and match them against the magic numbers of every format the extension can produce.
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return "jpg";
if (b[0] === 0x89 && b[1] === 0x50 && b[2] === 0x4e) return "png";
// ...and so on, for WebP, GIF, BMP, TIFF, AVIF
return "img";
Forty-two lines. Look at the last one. If none of the signatures matched, the file was saved as photo.optimized.img, a file extension that does not exist, on an image that was encoded perfectly well.
Here is the part that stings: the webview knew the format the whole time. It had just chosen it. It encoded with it. It simply never put it in the message. The save path in the same codebase already sends the format across; the bulk path just did not.
The fix was to add one field to a message type and delete the other forty-two lines. No more sniffing, no more .img.
Everything was written three times
The rest of the cleanup is less dramatic and more common. The host and the webview each need to know which ImageMagick format enum matches which of my format strings, which formats ignore the quality setting because they are lossless, and how to clamp a quality value into 1 to 100.
Each of those existed in two or three copies. I added a third copy of the lossless-format list myself, an hour before the audit, without noticing the other two. That is how this happens. Nobody sits down and decides to maintain three copies of a list. You just need it in a file that does not have it yet, and the version you need is four lines, and typing it is faster than working out where the other one lives.
They now live in one module that both sides import. The two processes already shared a message-type module, so the place to put it had existed all along.
Two smaller cuts in the same spirit. A format-to-extension lookup table that mapped jpg to "jpg" and png to "png", an identity function wearing a costume, in two files. And a security nonce generator: thirty-two characters picked one at a time out of an alphabet string with Math.random, which is eight lines doing what crypto.randomBytes(16).toString("base64url") does in one, and doing it worse, since Math.random is not a cryptographic source.
What I take from it
The bug did not survive because it was subtle. It survived because it was buried in forty-two lines of plausible-looking code that nobody had a reason to read. The sniffer worked, mostly. It never crashed. It just quietly produced a wrong filename in the case its author had already prepared a fallback for, which is a strong signal that the author knew this could not always work and shipped it anyway.
Deleting code is the cheapest way I know to find that kind of thing. You cannot delete a function without understanding what it is for, and understanding what it is for is exactly the step that was skipped when it was written.
Next
- Apply a preset from inside the panel, not just from the Explorer
- Before and after size with percent saved
- Compare slider in the preview
- Responsive size set from one source
- Auto-optimize on save
More soon.
Building scalable systems and developer-first tools. Lead Software Engineer at DSRPT.
Frequently asked
-
No. There are no new settings, no new commands, and no changes to how anything works. Your saved presets carry over untouched.
-
One thing. When bulk-saving, an image whose encoded bytes did not match a known file signature could be written with a .img extension instead of its real one. The extension no longer guesses the format, so that cannot happen.
-
Because the code that was deleted was hiding a bug, and because three copies of the same format table will eventually drift apart and produce a real failure. Paying that down while it is cheap is the point.
-
The image engine runs in the webview, sandboxed, while file writing happens in the extension host. The host never decodes the image, so it had no way of knowing what format the bytes were. It could simply have been told, and now it is.
-
Neither, measurably. This was a clarity change, not a performance one. The encode path is identical.