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.
Top comments (0)