Diving deep(er) into Web Development
What started as a small project to add comments on my static blog, ballooned in scope big time.
Tuesday, May 5, 2026 at 18:23 • 6 min readI have a habit of saving random images from the internet on my computer, but the modern internet is filled with different image types. Here's how I solved my problem.
For the longest time, I used a browser extension called "Save Image as Type", to save my images, that's because I wanted to make it as easy as possible. The only thing it did was adding a couple of buttons to the context menu (right click / thumb hold menu), that allowed you to download an image in different formats. Instead of clicking "Save as...", you clicked on "Save as PNG...", and boom, you got a PNG image, no matter what format the site saved it as.
It worked great! I pretty much used it to save all my images. Every meme that I found even remotely funny in Reddit. Until one, when my browser (Vivaldi, btw) decided to auto-disable the extension, and warn me that it was malware. Cool stuff, but now I was stuck downloading images in different formats again. And then I thought... why not make my own extension?
Here is what I needed the extension to do:
The problem was, I didn't actually know how it all worked, but I remembered a while back someone saying that all those resources phone home to a webserver, that does the actual image processing, and then it sends the bytes back for you to download. So I tried this approach.
The idea is simple. Send POST request to the server, with the URL (or file data) that you want to convert, and the target format that you want. If it, works, then the API will send you back the bytes for you to download. If not, it will send you the error message.
So, I made 2 API points.
HTTP1.1 GET /convert
Content-Type: application/json
{
imageUrl: string,
targetFormat: string
}
Returns 200 image/format, with the image in the body as ReadableStream, or 400 application/json, with the errors.
------
HTTP1.1 GET /health
Returns 200 OK, if the server is alive and ready to serve.
The webserver uses
sharpfor the image processor, and keeps everything in memory. No files saved. Should be more than enough.
Easy enough, but not completely what I want. The current implementation of the server only allows for internet-facing image URLs. So no local images (which means I cannot use my browser as an image converter software). It shouldn't be hard to implement a byte stream, but for now I don't fully know how to do it. A project for later, maybe.
Right now, the webserver simply fetches the image URLs, caches it as-is, converts it to the format that you want, and caches the converted image as well (but not as much). This means that if you want to convert an image to multiple formats (for some reason), then the server will only need to fetch that image once. Same thing if you (or multiple clients) need to convert the same image. It will fetch once and use more. And it will convert once and serve more (thank you for your service, server!).
I also implemented a simple rate limiter for the server. 20 convert requests / minute (cached responses also count). That's 1 request every 3 seconds. Plenty for a normal user.
The /health API is used to make sure you setup the API correctly in the browser extension, and to not try to convert an image, if the server is not reachable. I use the same API to show an error message in the browser extension, for the same reason. I want to make sure the user knows if the server is reachable or not.
There are still some things left to do here:
I was completely out of my element here, as I never wrote a browser extension before this. Luckily, it is easy* to make them compatible between browser, and to follow the documentation.
The extension should be as out-of-the-way as possible for the user. Simple buttons added to the context menu, and a simple extension options that allows you to change the webserver URL that's gonna convert your images. Since this resource is fully open source, I want to allow users to setup webservers for themselves, friends, or for public (actually... I am not sure about public tbh).
One thing that annoyed me with other extensions like this, is that you don't know if the image is being converted, or if it failed, because there is no visual feedback. So in my extension, I setup a simple HTML dialog that opens up on the webpage, when you initialize an image download. If it successes, then the dialog dissappears, and you get the "Save As..." view from the browser. If it fails, for any reason, an error will appear on the same dialog. It's not a pretty dialog, but is more than enough for my needs.
The download part was a bit annoying to set up. I used a background worker, with "chrome.downloads" to send the POST request to /convert. This (i think) means, I don't really get the browser confirmation for the download (I am not even sure if that's a thing for downloads like this). I wanted to use content scripts for everything, but since I wanted to be able to use both HTTP: and HTTPS for the webserver (I keep it on my local network, for example), that means I need to be able to fetch on both protocols, not only on the protocol that the website is.
If the download request is in a MIME type that is recognized by the extension, it will continue to download it. If not, it will cancel the download and show you an error.
Just like the webserver, there are still some things that need to be addressed with the extension:
From the beginning, I wanted to release this as free and open source. You can find the code for both the extension and the webserver on Codeberg.
The webserver is a simple node app (so it requires you having Node installed). No dockerfiles, or anything. Just npm install in the node-app folder, and start it with npm run prod (or npm start, if you want to watch the files). Additionally, you can setup a .env file, add it as a param in the NPM command, and add the PORT as an environment variable. Default PORT is 4200.
For the browser extension, you will have to go to browser://extensions (or vivaldi:extensions in my case), enable Developer Mode, and then click on "Load Unpacked". Right now, I decided not to distribute a packed version. After you loaded it, click on "Details > Extension Options" and add the correct webserver hostname (http://localhost:4200 for example. This is also the default). And you're done!
You can now right click on any image, then go to "Save as Type > Save as [FORMAT]", and you will be able to get a converted image!
I really enjoy writing software like this. Simple, rough around the edges, most likely not secure and definitely not finished. But it gives me the opportunity to just sit down and learn new stuff. Munching through the Google documentation for extension thought me a lot about what you can and cannot do with them. And honestly, it thought me not to trust browser extensions. Like... what do you mean extensions can just send random data to webservers that you don't know???