.
.
.
At a high level we accept an uploaded image into an in-memory file blob in the browser,
and then post that file as part of a multipart form along with some extra info to a URL
with the shape
{HOSTNAME}/api/image/{ACTION}
.
This URL corresponds to a Go serverless function running on Vercel. That function processes the image in some way and builds a response with the processed image; which we use to replace the in-memory file blob client-side.
When an image is uploaded we collect the image into a file blob, then use URL.createObjectURL()
to display a preview of the working image:
function setPreviewSrc(img: Blob) {
// imgEl refers to a bound <img /> element
imgEl.src = URL.createObjectURL(img);
imgEl.onload = () => {
URL.revokeObjectURL(imgEl.src); // free memory
};
}
After the image is uploaded we show a preview, as well as a form that contains several inputs as described here. A select sets the action to be applied to the image, and also toggles additional inputs depending on the option selected. We use a hidden input to send the content type (collected on upload) along with the form data:
{#if showPreview}
<!-- PREVIEW -->
<form bind:this={form} on:submit|preventDefault={handleSubmit} class="...">
<input hidden name="content_type" bind:value={contentType} />
<div>
<label for="formAction" class="..."> Select an action </label>
<select id="formAction" name="action" bind:value={action} class="...">
{#each Object.entries(actions) as [value, name]}
<option {value}>{name}</option>
{/each}
</select>
</div>
{#if action == "blur"}
<!-- BLUR SLIDER -->
{:else if action == "pixelate"}
<!-- PIXELATE SLIDER -->
{/if}
...
</form>
{/if}
We then use the Javascript Fetch API to POST to the relevant transform API endpoint — our serverless function:
// build form
const fData = new FormData(form);
fData.set("image", image);
const gRes = await fetch(`http://localhost:3000/api/image/${action}`, {
method: "POST",
body: fData,
});
In the Go function we extract the image from the multipart form and then decode it based on the content type:
// this keeps the whole form in memory, so be careful of space
err := r.ParseMultipartForm(32 << 20) // max 32MB
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
file, _, err := r.FormFile("image")
if err != nil {
w.WriteHeader(http.StatusBadRequest)
return
}
defer file.Close()
contentType := r.PostFormValue("content_type")
// util.Decode() is an internal function that contains
// a simple switch statement used to decode the image
// correctly based on contentType
img, err := util.Decode(file, contentType)
if err != nil {
w.WriteHeader(http.StatusUnsupportedMediaType)
return
}
Once we decode the image we perform some processing on it depending on the function being called. We then re-encode the image to a buffer and write it to the response:
processedImg := someProcess(img)
outputBuf := new(bytes.Buffer)
// like util.Decode(), util.Encode() is an internal function
// that contains a simple switch statement used to encode the image
err = util.Encode(outputBuf, processedImg, contentType)
if err != nil {
panic(err)
}
w.Header().Set("Content-Type", "application/octet-stream")
w.Write(outputBuf.Bytes())
Finally, we retrieve the new binary from the response on the client side and set the preview to use the new URL:
if (gRes.status < 400) {
// set image to blob from response
image = await gRes.blob();
}
setPreviewSrc(image);