
Rebuilding BICO from scratch
BICO is a desktop app that converts and optimises images in bulk. Everything runs on your machine and nothing is uploaded.
Version 2 worked, and I used it. It also had a problem I could not design around: the interface froze during every batch. Version 3 is a full rewrite, and this is what the rewrite actually involved, including the two bugs that taught me the most.
The freeze was architectural
In version 2, sharp ran on the renderer thread. Every conversion competed with the thing drawing the window, so the app stopped repainting for the length of a batch. No amount of spinners fixes that, because there is no thread left to animate the spinner.
Version 3 moves encoding into a pool of native worker threads sized to the machine, off both the UI thread and the main process. The window now stays at full frame rate through a run, and the queue shows per file progress because there is a thread free to report it.
That single change is most of why the app feels different, and it is not a feature anybody sees in a screenshot.
Putting the GPU to work without pretending
Image codecs are CPU bound. A GPU cannot encode a JPEG for you. What it can do is the parts that genuinely parallelise: resize, sharpen, blur, colour adjustments, watermark compositing.
BICO runs those as WebGPU compute shaders. Machines with a discrete and an integrated adapter can drive both at once, each on its own lane. On an NVIDIA card that work executes on the CUDA cores.
Two decisions matter more than the shaders:
Fallback is silent and automatic. Any device loss, driver reset or unsupported operation moves that image to the CPU pipeline. A converter that fails because a driver hiccupped is worse than one that is briefly slower.
The app tells you where each file ran. Every row in the queue carries a GPU or CPU badge, and the run summary shows the split. If I claim acceleration, you get to check it.
Shipping codecs that libvips does not have
I wanted JPEG XL and JPEG 2000. No prebuilt copy of libvips includes either, and asking users to compile a custom libvips is not a real answer for a desktop app.
So BICO carries its own WebAssembly builds: @jsquash/jxl for JPEG XL and ImageMagick compiled to WASM for JPEG 2000. They cost a few megabytes and behave identically on Windows, macOS and Linux.
I tested ImageMagick for JPEG XL too, since it was already there. It produced larger files at higher quality settings, which is inverted, so JPEG XL kept its own encoder. Measuring beats assuming, and it took one afternoon to find out.
The About screen reports what your build actually resolved at startup rather than what the formats support in theory, and anything unavailable is greyed out in the picker instead of failing mid run.
The contrast formula that got it wrong
Here is my favourite bug.
BICO lets you pick any accent colour, so it has to choose black or white text to sit on top. I did the responsible thing and used the WCAG 2 contrast ratio, picking whichever scored higher.
On the default blue, #4c8dff, it chose black. It looked awful.
The maths was not wrong, it was just measuring the wrong thing:
black on #4c8dff 6.56 : 1
white on #4c8dff 3.20 : 1
WCAG 2 relative luminance weights green at 0.7152 and blue at 0.0722. A saturated blue therefore scores as a light colour, and black wins the ratio test on it. Every mainstream design system puts white on a blue button, and every one of them is right.
APCA, the perceptual model drafted for WCAG 3, gets it right:
white on #4c8dff Lc 64.2
black on #4c8dff Lc 45.2
So the foreground is now chosen perceptually. But fixing that surfaced a second, better bug.
Ant Design paints all three button states with one text colour and three different backgrounds, and its hover shade is lighter while its pressed shade is darker. On the Nord and Dracula themes the pressed shade landed dark enough that the black text chosen for the resting colour was stranded on a mid tone.
The fix was to stop letting the shades drift: hover and pressed are now generated in Oklab, moving lightness while keeping hue and chroma, and always away from the text colour. Contrast rises as you interact with a button rather than falling.
Then a third thing. The rendered button was not even the colour I was checking. Ant Design's dark
algorithm regenerates the seed against a dark background, so #4c8dff goes in and #447bdc is
painted. I had been judging a colour that never reached the screen, which is what left Nord and
Dracula at a genuinely poor Lc 52 and 45.
I verified the final version across every accent the colour picker can produce: 24,389 colours by three states, 73,167 measurements, zero states where the text colour disagrees with the background it is painted on.
Three bugs stacked on one wrong assumption, and the only reason I found the second and third is that I measured the rendered DOM instead of trusting the theme config.
The installer that installed twice
The other bug worth writing down.
Version 3 changed the application id. Windows keys its uninstall entry on a GUID derived from that id, so as far as Windows was concerned, version 3 was an unrelated product. Installing it left two BICOs: two entries in Apps and Features, two Start Menu shortcuts, two program folders, neither aware of the other.
The tempting fix is a custom uninstall script. The better fix was smaller: electron-builder already has a tested routine that finds a previous install, runs its uninstaller with retries, handles the app being open, and only then writes the new files. It just never fired, because it looks up the current id's GUID.
Pinning the installer's GUID back to the one version 2 registered under makes that existing machinery do the work. One line of configuration instead of a hundred lines of NSIS I would have had to debug on a virtual machine.
The lesson I keep relearning: before writing the thing, check whether the tool already does it and you have simply not given it what it needs.
Making the interface speak three languages
BICO ships in English, Turkish and Arabic, and Arabic flips the whole layout to right to left. Not a mirrored font, an actual mirror: panels, tables, sliders and drawers all change side.
The part I would recommend to anyone doing this: the dictionaries are typed. Every language is
declared as Record<TranslationKey, string>, so a string added in English fails the build until
Turkish and Arabic supply it. Nothing can quietly fall back.
That caught a real bug months later. The format descriptions were being rendered straight from a capability table in shared code rather than from the dictionaries, so they stayed English in both translations and sat outside the contract entirely. The moment I moved those words into the dictionaries, the build broke with missing 18 properties, which is exactly what should happen.
Cutting the words back out
Late on, I read the settings sidebar as a user rather than as its author, and it was exhausting. Every slider carried a paragraph explaining why the setting existed.
Higher effort means smaller files and slower encoding. The image itself is unchanged, the encoder just searches harder for a cheaper way to store it.
That is 149 characters for one slider. It is now:
Higher effort means smaller files and slower encoding.
Across the interface the helper text went from roughly 26,000 characters to 15,800, and the sidebar average dropped from 117 characters a hint to 69. The facts and the warnings stayed. The reasoning went, because a hint is read while you are trying to do something else, not studied.
What version 3 is made of
Electron 43, React 19, TypeScript 5.9, Ant Design 6, sharp on libvips 8.18, built with electron-vite. Node integration is gone from the renderer, context isolation is on, and every path the interface touches crosses a typed IPC contract compiled by all four processes.
The interface is set in IBM Plex, bundled rather than fetched, chosen because its Arabic is a designed companion to the Latin rather than an unrelated fallback face.
Try it
- Download: releases for Windows, macOS and Linux
- Source and issues: github.com/shehari007/BICO
- Me: github.com/shehari007
It is MIT licensed. If you convert images in bulk and something annoys you about it, open an issue. The bugs above were all found by being annoyed at my own software and then actually measuring why.
