Transform Images Stored on Amplify Storage

Ifeoma Imoh

AWS Amplify enables developers of all skill levels to create, deploy, and maintain full-stack applications more effectively. Speaking of services, we will be using the Amplify CLI to create an S3 bucket and upload images from a React app. Next, we'll use Cloudinary's rich media transformation tools to retrieve images stored on the S3 bucket and apply some transformations to them.

Here is a link to the demo on CodeSandbox.

Project Setup

Run the following command in your terminal to set up a React application:

1npx create-react-app my-amplify-project

Let's create a file that'll hold the logic for handling image transformation. Run this command in your terminal to create a file named TransformImage.js:

1cd my-amplify-project
2 cd src
3 mkdir components
4 cd components
5 touch TransformImage.js

Our application will require some dependencies. To keep things simple, we will be installing them when we need them.

Setting up AWS Amplify

To initialize our React application, integrate, create and provision the AWS services required in our app, let's install and configure the Amplify CLI tool. Run the following command in your terminal:

1npm i -g @aws-amplify/cli

The installed tool must be linked to your AWS account; to do so, run the following command in your terminal:

1amplify configure

AWS discourages using your Root-level credentials (i.e., email and password) to manage your account's resources and instead suggests utilizing an IAM user account to specify permissions. You will be sent to your dashboard to fill up your information. We gave the IAM user account AdministratorAccess in the permissions area.

Once this is successful, you will receive an access key ID and a secret key, as shown below:

You will be prompted in the CLI to insert these credentials to complete the configuration step. Finally, select a name that will be bound to the profile of the IAM user you just created — I used “default".

Setting up Cloudinary

Login to your Cloudinary account or create one here if you don't have one already. You can get your credentials in the Account details section on your dashboard.

We only need our public credentials -- our cloud name and API key. Take note of their values; we will use them later to set up the Cloudinary SDK to transform our images.

Uploading an Image to an S3 Bucket

At the moment, our project is just a basic CRA application. To make it amplify compatible, we need to initialize it by running this command:

1amplify init

In the prompt, you will be required to choose the name of your Amplify project. It will also detect the framework you're using (React) and other project-specific information. We are also required to authenticate using an IAM account, so we use the one created earlier. If this is successful, this tool does several things, the most important ones are as follows:

  • It initializes our project in the cloud. You can see the app we just created in the AWS Amplify service section on your dashboard, as seen below:

  • It creates an amplify folder in our working directory containing all the local code representing our backend infrastructure (code representing all the AWS services we need). It also creates an aws-exports.js file in the src folder. This config file holds data that we will use later to configure and integrate the services we include in our backend in our react app. As we add more services to our backend, the contents of the amplify folder and the aws-exports.js file will be updated accordingly to reflect this change.

Create S3 Bucket

Run this command in your terminal:

1amplify add storage

The command above adds a simple storage service to the backend of our application. First, we choose the content service - S3 bucket in the prompts above. Although we are not adding login or signup functionality to our app, creating an S3 bucket requires adding the authentication service (we are using Cognito) to our backend.

Setting up Cognito involves choosing how we want users to signup for our application; in our case, we select Username and Password. We have also specified permissions for different operations (create, read, update, delete). Since our application doesn't need authentication from users to upload files, our upload operations will be unauthenticated.

So far, we've set up Auth and storage on our local application. We need to replicate the changes we have on the cloud. To do that, run this command in your terminal:

1amplify push

Upload logic

Run this command in your terminal to install the amplify dependency:

1npm i -s aws-amplify

This main amplify module will provide us with useful classes and functions. we need to configure and use the services in our back-end and in our React application. Add the following to your App.js file:

1import "./App.css";
2 import { useState } from "react";
3 import Amplify, { Storage } from "aws-amplify";
4 import config from "./aws-exports";
5 import TransformImage from "./components/TransformImage";
6 Amplify.configure(config);
7 function App() {
8 const [loading, setLoading] = useState(false);
9 const [file, setFile] = useState(null);
10 const [key, setKey] = useState("");
11 const handleSelectFile = (e) => setFile(e.target.files[0]);
12 const handleUpload = async () => {
13 try {
14 setLoading(true);
15 const res = await Storage.put(`${file.name}`, file);
16 console.log({ res });
17 setKey(res.key);
18 alert("upload successful");
19 } catch (error) {
20 alert(error.message);
21 } finally {
22 setLoading(false);
23 }
24 };
25 let choosenFileURL;
26 if (file) {
27 choosenFileURL = window.URL.createObjectURL(file);
28 }
29 // console.log({ URL: choosenFileURL, file });
30 return (
31 <div className="App">
32 <label htmlFor="file" className="btn-grey">
33 {" "}
34 select image
35 </label>
36 {file && (
37 <div>
38 <img width={300} alt="sample" src={choosenFileURL} />
39 <center> {file.name}</center>
40 </div>
41 )}
42 <input
43 id="file"
44 type="file"
45 onChange={handleSelectFile}
46 multiple={false}
47 />
48 {file && (
49 <>
50 <button onClick={handleUpload} className="btn-green">
51 {loading ? "uploading..." : "upload to s3 bucket"}
52 </button>
53 </>
54 )}
55 {key && <TransformImage imageKey={key} />}
56 </div>
57 );
58 }
59 export default App;

We start by configuring our application using the configure method of the Amplify class. Next, we define and export our App component, which returns some JSX. It includes an input field that allows you to select an image and store it in a state before it is rendered to the screen. We also have a button that triggers the handleUpload function, which toggles the loading state and attempts to upload the file using the Storage class provided by the Amplify module. If the upload is successful, in the response object, we get an object with a key property whose value is the same as the first parameter we specified in the put call. This key is stored in state and passed as props to the TransformImage component.

Add the following styles to your App.css file:

1* {
2 box-sizing: border-box;
3 }
4 .App {
5 text-align: center;
6 display: grid;
7 gap: 2rem;
8 max-width: 470px;
9 padding: 1.5rem;
10 margin: auto;
11 margin-top: 4rem;
12 }
13 label,
14 button {
15 font-size: 1.2rem;
16 color: #fff;
17 padding: 0.5em 0.9em;
18 cursor: pointer;
19 border: none;
20 }
21 [type="file"] {
22 display: none;
23 }
24 [class*="btn"] {
25 border-radius: 5px;
26 text-transform: capitalize;
27 }
28 .btn-grey {
29 background-color: #f2f2f2;
30 color: #333;
31 border: 1px solid #888;
32 }
33 .btn-green {
34 background-color: #63dd47;
35 }
36 .btn-blue {
37 background-color: #0e80c9;
38 }
39 p {
40 display: block;
41 width: 60%;
42 font-family: monospace;
43 font-size: 1.2rem;
44 background-color: #000;
45 color: #f00;
46 margin: auto;
47 text-overflow: ellipsis;
48 }

To see the running application run the following command in your terminal:

1npm start

Preview Uploaded Images

We can preview images uploaded to our bucket on the cloud console via the following steps:

  1. Search for s3 in the search bar and click on Buckets.
  1. Next, select the bucket's name from the list and click on the public folder.
  1. When you click on the image, you should see a unique URL that points to the image.

If you click the URL of your image now, you will get a 403 message, as seen below:

Make Images on S3 Bucket Publicly Available

By default, all newly created s3 buckets block anonymous access to stored files(objects). This is normal because we don't want unauthorized access to our assets. We need to change that because there are no confidential files in our bucket, and we need to use the URL of our files to fetch and transform our images using Cloudinary's SDK.

  1. Click on the newly created bucket and click on the Permissions tab.

  1. Scroll down to Bucket policy and click the Edit button.

The bucket policy is just JSON written using amazon policy language. For our bucket, this data looks like this:

1{
2 "Version": "2012-10-17",
3 "Statement": [
4 {
5 "Effect": "Allow",
6 "Principal": "*",
7 "Action": "s3:GetObject",
8 "Resource": "arn:aws:s3:::ADD-BUCKET-NAME-HERE/public/*"
9 }
10 ]
11 }

This information provided above states that we want to allow everyone to read ( s3:GetObject) only the public folder contents on our s3 bucket. Copy and paste the bucket policy in the field provided, add your bucket name, and click Save changes as shown below.

Now, if you click the URL of the file in your public folder, you should see the image.

Transform Images Using Cloudinary

We now understand the structure of object URLs that live on an s3 bucket and how to make its contents accessible. We will now see how to transform these images using Cloudinary in our React app.

Let's install our Cloudinary SDK. Run this command in your terminal:

1npm install @cloudinary/url-gen

This dependency will provide us with the tools to build delivery URLs and apply transformations to the images on our bucket.

Add the following to the TransformImage.js file in the components folder.

1import { useState } from "react";
2 import { Cloudinary } from "@cloudinary/url-gen";
3 import {
4 Effect,
5 RoundCorners,
6 Rotate,
7 Resize,
8 Overlay,
9 } from "@cloudinary/url-gen/actions";
10 import { Position, Source } from "@cloudinary/url-gen/qualifiers";
11 import { TextStyle } from "@cloudinary/url-gen/qualifiers/textStyle";
12 import Transformation from "@cloudinary/url-gen/backwards/transformation";
13
14 const myCld = new Cloudinary({
15 cloud: {
16 cloudName: "YOUR-CLOUD-NAME",
17 apiKey: "YOUR-API-KEY",
18 },
19 });
20 const TransformImage = ({ imageKey }) => {
21 const [URL, setURL] = useState("");
22 const getURLofS3Object = (tag) => {
23 const S3BucketName = "YOUR-STORAGE-BUCKET-NAME";
24 return `https://${S3BucketName}.s3.amazonaws.com/${tag}`;
25 };
26 const getImage = async () => {
27 const objectURL = getURLofS3Object(`public/${imageKey}`);
28 let image = myCld.image(objectURL).setDeliveryType("fetch");
29 image
30 .resize(Resize.fill().width(340))
31 .effect(Effect.artisticFilter("frost"))
32 .roundCorners(RoundCorners.byRadius().radius(40))
33 .rotate(Rotate.byAngle(20))
34 .overlay(
35 Overlay.source(
36 Source.text(
37 imageKey.replace(/\.\w+$/, ""),
38 new TextStyle("Cookie", 50).fontWeight(800)
39 )
40 .textColor("#0C0C14")
41 .transformation(
42 new Transformation().effect(Effect.shadow(4).color("#FFCC43"))
43 )
44 ).position(new Position().offsetY("160").offsetX("20"))
45 );
46 setURL(image.toURL());
47 };
48 return (
49 <>
50 <button onClick={getImage} className="btn-blue">
51 transform image
52 </button>
53 {URL && <img src={URL} alt="some_random_img" />}
54 </>
55 );
56 };
57 export default TransformImage;

In the code above, we created an instance of the Cloudinary class and configured it using our Cloudinary credentials. Next, we define and export the TransformImage component, which accepts an imageKey - a string representing a key to a file on an s3bucket. In the component, we defined a variable that will hold the URL of our transformed image - Cloudinary calls this the delivery URL.

Next, we define a method, getURLofS3Object, which expects the key to a file of an S3 bucket as its parameter, and returns a URL to the object. We also defined a getImageURL function which generates a Cloudinary delivery/transformation URL of our s3 image. getImageURL starts by getting the s3 object URL, and then it uses our Cloudinary instance to create an image object. The call to setDeliveryType, set to fetch specifies that the image we want to transform is a remote file.

Next, we build the transformation section of the delivery URL, which follows a series of chained function calls. Here we resized the image to a width of 300px and rotated it by 20 degrees. We also applied the artistic filter with the frost qualifier value and added a text overlay formed from the image's key without its file extension.

This function calls result in one big delivery URL that we can access by calling the toURL function on the image object and storing it in a state variable passed to a traditional HTML image tag to be rendered to the screen.

The image above shows the various components that make up this URL and where the URL of an S3 file will be embedded.

The strategy employed here applies transformations on-the-fly. This means the image is only transformed when we request it. The original untransformed version is cached on our Cloudinary account.

We can select, upload, and transform our images as expected.

You can find the complete project here on GitHub.

Resources you may find helpful:

Ifeoma Imoh

Software Developer

Ifeoma is a software developer and technical content creator in love with all things JavaScript.