Creating Image Optimizer With Rio
Published: 2022-12-06 08:30:09

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.
```json
  # 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`.
```yml
# 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](https://dev.to/retterio/how-to-turn-zod-models-into-rio-models-1g77) `uploadInput` and `removeImageInput` to use as inputModel.
```typescript
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.

```typescript
// classes/Image/index.ts

export async function getInstanceId(): Promise<string> {
    return "default"
}
```
## Image.ts
```typescript
const imagePrefix = 'IMAGE_'
const defaultImage = 'defaultImage'
```
### Upload Function
Here `content` is base64 encoded image. Checking if `content` is in base64 format.
```typescript
// 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.
```typescript
// 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.
```typescript
// 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](https://7ef782npg.api.retter.io/7ef782npg/CALL/Image/get/1ae27fe6-731c-4956-84eb-0c71269dabe0_5000x500_high.png) 
```typescript
// 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.
```typescript
// 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.
```typescript
// 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.
```typescript
// 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`.
```typescript
// classes/Image/image.ts

const resizedImage = await getResizedImage(parameters)
```
`getResizedImage` function is below. This function is outside our `get` function.
```typescript
// 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.
```typescript
// 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`.
```typescript
// 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](https://github.com/retterio/image-optimizer) for the project. Thanks.

## References
[Rio Docs](https://docs.retter.io)
[Zod Github](https://github.com/colinhacks/zod)
[Nanoid Github](https://github.com/ai/nanoid)
[Sharp Github](https://github.com/lovell/sharp)