Creating Image Optimizer With Rio
Bahadır Kurul
Posted on December 6, 2022
In this project we will upload an image and get image with different sizes.
We will use nanoid
to create ids for images. Sharp
to resize images and zod
for models.
# classes/Image/package.json
"dependencies": {
"@retter/rdk": "1.1.15",
"nanoid": "3.3.4",
"sharp": "0.30.6",
"zod": "3.19.1"
},
"devDependencies": {
"@types/node": "17.0.45",
"@types/sharp": "0.31.0",
"ts-node": "10.9.1"
}
As you can see we will have 3 functions upload
, get
and remove
.
# classes/Image/template.yml
init: index.init
getInstanceId: index.getInstanceId
methods:
- method: get
type: STATIC
handler: image.get
- method: upload
type: STATIC
inputModel: UploadInput
handler: image.upload
- method: remove
type: STATIC
inputModel: RemoveInput
handler: image.remove
Models
Export uploadInput
and removeImageInput
to use as inputModel.
import { z } from 'zod'
const imageId = z.string().regex(/^[\dA-Za-z-]{5,50}$/)
export const uploadInput = z.object({
content: z.string()
})
export const resizedImageParameters = z.object({
id: z.string(),
width: z
.preprocess((value) => Number.parseInt(value as string, 10), z.number())
.optional()
.default(128),
height: z
.preprocess((value) => Number.parseInt(value as string, 10), z.number())
.optional()
.default(128),
quality: z.enum(['low', 'default', 'medium', 'high']).optional().default('default'),
fit: z.enum(['contain', 'cover', 'fill', 'inside', 'outside']).optional().default('inside'),
content: z.any(),
})
export const removeImageInput = z.object({
imageId: z.string()
})
export const parsedPath = z.object({
imageId: z.string(),
width: z.string().optional(),
height: z.string().optional(),
quality: z.string().optional(),
format: z.string().optional(),
fit: z.string().optional(),
})
export type UploadInput = z.infer<typeof uploadInput>
export type ResizedImageParameters = z.infer<typeof resizedImageParameters>
export type RemoveImageInput = z.infer<typeof removeImageInput>
export type ParsedPath = z.infer<typeof parsedPath>
Index.ts
Authorizer just returns statusCode: 200
. And getInstanceId
is like below.
// classes/Image/index.ts
export async function getInstanceId(): Promise<string> {
return "default"
}
Image.ts
const imagePrefix = 'IMAGE_'
const defaultImage = 'defaultImage'
Upload Function
Here content
is base64 encoded image. Checking if content
is in base64 format.
// classes/Image/image.ts
const { content } = data.request.body as UploadInput
const projectId = data.context.projectId
// 10 Character ID
const nanoid = customAlphabet('0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-', 10)
const imageId = nanoid();
if (Buffer.from(content, 'base64').toString('base64') !== content) {
throw new Error('Content is not base-64 format')
}
After that uploading image with generated id and checking if upload succeeded.
// classes/Image/image.ts
const setFileResponse = await rdk.setFile({
filename: imagePrefix + imageId,
body: content,
})
if (!setFileResponse?.success) {
throw new Error('Image could not be uploaded!')
}
Lastly our functions response is like below.
// classes/Image/image.ts
data.response = {
statusCode: 200,
body: {
message: 'Image has been uploaded',
id: imageId,
url: `https://${projectId}.api.retter.io/${projectId}/CALL/Image/get/${imageId}_1024x1024_default_inside.png`
},
}
Get Function
This function will be called by an url like this. Example
// classes/Image/image.ts
const path = data.context.pathParameters.path
if (!path) {
throw new Error('Path does not exist')
}
const newPath = parsePath(path)
parsePath function is below. This function splits url path with _
, .
, x
seperators. Example imageId_500x500_high_cover.png
. This way we are giving parameters of function with url.
// classes/Image/image.ts
const parsePath = (path: string): ParsedPath => {
// Get path
const fullPath = path.split('/')[1]
// Get before "." and split it every "_"
const ids = fullPath.split('.')[0].split('_')
if (ids.length < 2 || ids.length > 4 || !fullPath.includes('.')) {
throw new Error('Invalid image path')
}
const quality = ids.length > 2 ? ids[2] : undefined
const fit = ids.length > 3 ? ids[3] : undefined
const parameterObject = {
imageId: ids[0],
width: ids[1].split('x')[0],
height: ids[1].split('x')[1],
quality,
format: fullPath.split('.')[1],
fit,
}
return parameterObject
}
After getting parameters on newPath
we are getting our file like below.
// classes/Image/image.ts
if (!file?.success) {
file = await rdk.getFile({
filename: imagePrefix + defaultImage,
})
}
if (!file?.success) throw new Error("Something went wrong while getting file!")
Now we will use sharp
to resize our image with given parameters. Uploaded string contains data header like this data:image/png;base64,
. We have to remove this or sharp
throws an error.
// classes/Image/image.ts
const fileData: string = file.data
const uri = fileData.split(';base64,').pop()
const image = Buffer.from(uri, 'base64')
const parameters: ResizedImageParameters = resizedImageParameters.parse({
content: image,
id: newPath.imageId,
width: newPath.width,
height: newPath.height,
quality: newPath.quality,
fit: newPath.fit,
})
Calling getResizedImage
function with parameters
.
// classes/Image/image.ts
const resizedImage = await getResizedImage(parameters)
getResizedImage
function is below. This function is outside our get
function.
// classes/Image/image.ts
const getResizedImage = async ({
height, width, quality, content, fit
} : ResizedImageParameters): Promise<Buffer> => {
const image = await sharp(content)
.resize(width, height, {
fit,
})
.toFormat('png', { quality: qualities[quality] })
.toBuffer()
return image
}
Finally returning resizedImage
in response.
// classes/Image/image.ts
data.response = {
statusCode: 200,
body: resizedImage.toString("base64"),
isBase64Encoded: true,
headers: {
"Content-Type": "image/jpg"
}
}
Remove Function
Removing image with given imageId
.
// classes/Image/image.ts
const { imageId } = data.request.body as RemoveImageInput
if (!imageId) throw new Error('Invalid remove request')
const deleteFileResponse = await rdk.deleteFile({
filename: imagePrefix + imageId,
})
if (!deleteFileResponse?.success) {
throw new Error('Image file could not removed')
}
data.response = {
statusCode: 200,
body: {
message: 'Image has been removed.',
}
}
And thats it. Here is all code for the project. Thanks.
References
Posted on December 6, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.