Creating Image Optimizer With Rio

bahadirkurul

Bahadır Kurul

Posted on December 6, 2022

Creating Image Optimizer With Rio

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"
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Index.ts

Authorizer just returns statusCode: 200. And getInstanceId is like below.

// classes/Image/index.ts

export async function getInstanceId(): Promise<string> {
    return "default"
}
Enter fullscreen mode Exit fullscreen mode

Image.ts

const imagePrefix = 'IMAGE_'
const defaultImage = 'defaultImage'
Enter fullscreen mode Exit fullscreen mode

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')
}
Enter fullscreen mode Exit fullscreen mode

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!')
}
Enter fullscreen mode Exit fullscreen mode

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`
        },
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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!")
Enter fullscreen mode Exit fullscreen mode

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,
})
Enter fullscreen mode Exit fullscreen mode

Calling getResizedImage function with parameters.

// classes/Image/image.ts

const resizedImage = await getResizedImage(parameters)
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Finally returning resizedImage in response.

// classes/Image/image.ts

data.response = {
    statusCode: 200,
    body: resizedImage.toString("base64"),
    isBase64Encoded: true,
    headers: {
        "Content-Type": "image/jpg"
    }
}
Enter fullscreen mode Exit fullscreen mode

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.',
    }
}
Enter fullscreen mode Exit fullscreen mode

And thats it. Here is all code for the project. Thanks.

References

Rio Docs
Zod Github
Nanoid Github
Sharp Github

💖 💪 🙅 🚩
bahadirkurul
Bahadır Kurul

Posted on December 6, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related