For the purpose of transparency: this little coding exercise was completed in conjunction with a job application to join Fly.io's DevRel team. They rejected my application. They made no claim to this code, so I am sharing it as my intellectual property. I had a lot of fun writing it up, so I hope you enjoy it.
Perhaps you've been walking in a park or on a trail on a warm summer's day and thought, "What if I wrote a single page PNG to JPEG converter with Svelte..." Well, I haven't either, but it's a more apealing experiment than you may think. The complexities run deeper than surface-level: handling image upload, sorting out image conversion, providing a download method, error handling, and list goes on. If you're like me, you might be imagining it'll take several files, a REST API, and a bunch of fiddling with non-toxic glue. What if I told you we could fit that into <50 lines of readable code with zero dependencies and no server-side code? Let's walk through it.
Here's the game plan:
This recipe has no dependencies and uses the default SvelteKit configuration, so initializing the project is as easy as npm create svelte@latest png2jpeg-converter
.
Once you've gotten comfy in your brand-new SvelteKit project, we can make a new page and get started:
<script>
let error;
let files;
let jpeg;
</script>
<center>
<h2>PNG to JPEG Image Converter</h2>
<label for="pngFile">Upload a png file:</label>
<input bind:files type="file" id="pngFile" accept="image/png" />
<h4>Result:</h4>
{#if jpeg}
<img src={jpeg} alt="PNG Converted to JPEG" />
{/if}
{#if error}
<h4 style="color: red;">Error encountered!</h4>
<code>{error}</code>
{/if}
<canvas hidden id="canvas"></canvas>
</center>
The majority of this is standard (metadata, data modeling, visual elements), but let's first dig into the input
.
<input bind:files type="file" id="pngFile" accept="image/png" />
Since it is a file type input it will bind files
to a FileList
. This allows us to access the data that is uploaded in the form of a File
:
<script>
let files;
...
$: if (files) {
let pngFile = files[0];
}
</script>
Note: File inputs only support uploading a single file if you do not mark them with the
multiple
attribute. It would entirely be possible to adapt the code shared here to support multiple files, but for the sake of brevity we will keep it to one.
Wonderful! Now how can we transmorgrify the File
into JPEG? Javascript has a number of lovely image processing libraries like Sharp or Jimp, but I promised zero-dependencies. This leads us to one of the most fastinating HTML elements: <canvas>
. Did you catch it in the first code snippet even though it was hidden
, :D? The <canvas>
side-quest involves three parts:
File
to the <canvas>
.File
to the <canvas>
.<canvas>
content as an <img>
.You can't write File
s to a canvas, but you can write [ImageBitmap
]s(https://developer.mozilla.org/en-US/docs/Web/API/ImageBitmap). We'll use the browser's built-in createImageBitmap
function:
$: if (files) {
let pngFile = files[0];
createImageBitmap(pngFile)
.then((bitmap) => {
// Convert to JPEG.
});
}
To complete steps 2-3 we'll finally use our hidden
canvas. Let's give the conversion it's own function:
function convertToJPEG(bitmap) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
ctx.drawImage(bitmap, 0, 0);
return canvas.toDataURL('image/jpeg');
}
After getting the canvas from the DOM and accessing a context, we match the canvas
's size to the bitmap's to avoid accidentally cropping our image. The next line draws the bitmap to the canvas filling out the perfectly sized canvas.
The last line (return canvas.toDataURL('image/jpeg');
) introduces us to the magical toDataURL
method. If you are unfamiliar with Data URLs, they are a way of encoding data into the URL format. The method allows us to specify a MIME type. The browser then converts the canvas
content to the desired MIME type under the hood. What do we do with this data URL now? <img>
tags support using data URLs for the src
attribute. Let's integrate it into our existing code:
<script>
let files;
let jpeg;
...
$: if (files) {
let pngFile = files[0];
createImageBitmap(pngFile)
.then((bitmap) => {
jpeg = convertToJPEG(bitmap);
});
function convertToJPEG(bitmap) {
...
}
}
</script>
<center>
...
<h4>Result:</h4>
{#if jpeg}
<img src={jpeg} alt="PNG Converted to JPEG" />
{/if}
...
</center>
So what about errors? Assuming the browser and Svelte don't act out, the most likely place for errors to occur is within createImageBitmap
. Because it returns a promise we can just use catch
to write errors to error
and Svelte will take care of the rest.
<script>
let error;
let files;
let jpeg;
$: if (files) {
// The "multiple" attribute is required on the input element for multi-upload.
let pngFile = files[0];
createImageBitmap(pngFile)
.then((bitmap) => {
jpeg = convertToJPEG(bitmap);
})
.catch((err) => {
error = err;
});
}
</script>
<center>
...
{#if error}
<h4 style="color: red;">Error encountered!</h4>
<code>{error}</code>
{/if}
</center>
This is another place where we will take advantage of the built-in capabilities of the browser. We're going to use... drum roll. A right-click and "Save As". Although this may not be standard to most tools, we're trying to redefine the user journey and accelerate adoption of modular experience facets. (That's what marketing told me to say, at least.) We will be kind to our users and let them know, though:
{#if jpeg}
<h4><i>Just right-click the image and click "Save As" to download.</i></h4>
<img src={jpeg} alt="PNG Converted to JPEG" />
{/if}
Just like that we've completed a single page PNG to JPEG converter with Svelte:
<script>
let error;
let files;
let jpeg;
$: if (files) {
// The "multiple" attribute is required on the input element for multi-upload.
let pngFile = files[0];
createImageBitmap(pngFile)
.then((bitmap) => {
jpeg = convertToJPEG(bitmap);
})
.catch((err) => {
error = err;
});
}
function convertToJPEG(bitmap) {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = bitmap.width;
canvas.height = bitmap.height;
ctx.drawImage(bitmap, 0, 0);
return canvas.toDataURL('image/jpeg');
}
</script>
<svelte:head>
<title>Image Converter</title>
</svelte:head>
<center>
<canvas hidden id="canvas"></canvas>
<h2>PNG to JPEG Image Converter</h2>
<label for="pngFile">Upload a png file:</label>
<input bind:files type="file" id="pngFile" accept=".png" />
<h4>Result:</h4>
{#if jpeg}
<h4><i>Just right-click the image and click "Save As" to download.</i></h4>
<img src={jpeg} alt="PNG Converted to JPEG" />
{/if}
{#if error}
<h4 style="color: red;">Error encountered!</h4>
<code>{error}</code>
{/if}
</center>
You made it! :champagne: :partying_face:.
What now? Well, because the codebase is so minimal right now, adding features won't make the single file too unwieldy. Here are some ideas:
File
hashing.<marquee>
, cursor-effects, background-color: green;
.<canvas>
methods.