Image tagging is the process of labeling images based on figures within that image – the labels are known as tags. This has the obvious benefit of providing some level of organization to an image library, as it is easier to find images with tags. Additionally, image tag metadata can be used for a variety of purposes ranging from web accessibility to SEO. The release of image tagging software makes it possible to automate the tagging process, thus enjoying all these benefits while saving valuable time.
In this tutorial, we will use Imagga to add tags to images that have been uploaded to our Cloudinary store. Additionally, we will render the images in a gallery, providing a list of tags that the user can click to filter the rendered images. Cloudinary provides an add-on for Imagga’s automatic image tagging capabilities, fully integrated into its image management and transformation pipeline, and that is what we will be using.
For UI components in our application, we will use antd while axios will be used for uploading our images to our Cloudinary store. React router will be used to transition between pages.
Here is a link to the demo CodeSandbox.
Create a new React app using the following command:
npx create-react-app cloudinary-imagga
Next, add the project dependencies using the following command:
npm install antd @ant-design/icons axios react-router-dom cloudinary-react
Code language: CSS (css)
Next, we need to import the antd CSS. To do this, open src/App.css
and edit its content to match the following:
@import "~antd/dist/antd.css";
Code language: CSS (css)
To use Cloudinary’s provisioned services, you need to sign up for a free Cloudinary account if you don’t have one already. Displayed on your account’s Management Console (aka Dashboard) are important details: your cloud name, API key, etc.
For this tutorial, we will be sending images to Cloudinary via unsigned POST requests. To do this, we need our account cloud name and an unsigned upload preset. To create one, log into the Management Console and select Settings > Upload and then scroll to the Upload presets section. Create a new upload preset by clicking Add upload preset at the bottom of the upload preset list. In the displayed form, make sure the Signing Mode is set to Unsigned as shown below.
In the Media Analysis and AI section, ensure that Imagga Auto Tagging is selected. With the selection of Imagga auto tagging, you also have the option to specify a confidence level threshold for generated tags.
Click Save to complete the upload preset definition, then copy the upload preset name as displayed on the Upload Settings page.
Next, let’s create environment variables to hold the details of our Cloudinary account. Create a new file called .env
at the root of your project and add the following to it:
REACT_APP_CLOUD_NAME = "INSERT YOUR CLOUD NAME HERE";
REACT_APP_UPLOAD_PRESET = "INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE";
Code language: JavaScript (javascript)
This will be used as a default when the project is set up on another system. To update your local environment, create a copy of the .env
file using the following command:
cp.env.env.local;
Code language: CSS (css)
By default, this local file resides in the .gitignore folder, mitigating the security risk of inadvertently exposing secret credentials to the public. You can update the .env.local
file with your Cloudinary credentials.
When we sign up for a Cloudinary account, we don’t immediately get access to the add-ons. To access the Imagga Auto Tagging add-on, you need to register. Each add-on gives us a variety of plans and their associated pricing. Fortunately, most of them also have free plans, which is what we will be using for our application.
To register, click on the Add-ons link in the header of the management console on your Cloudinary dashboard.
You should see the Cloudinary Add-ons page with a list of all available add-ons. Select the Imagga Auto Tagging add-on and subscribe to the free plan, which allows 15 artworks monthly, or you can select your preferred plan.
In the src
directory of the project, create a new folder named cloudinary
. This folder will hold all the Cloudinary related helper classes we will need in our components. In the cloudinary
folder, create a new file called cloudinaryConfig.js
. This file will give access to the environment variables and prevent repeated process.env.
calls throughout the project. Add the following to thecloudinaryConfig.js
file:
export const cloudName = process.env.REACT_APP_CLOUD_NAME;
export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
export const defaultUploadTag = "cloudinary_imagga";
Code language: JavaScript (javascript)
Let’s write a helper function that we will use to upload images to Cloudinary and another to delete an image from Cloudinary. In the cloudinary
folder, create a new file named cloudinaryHelper.js
and add the following to it:
import axios from "axios";
import { cloudName, defaultUploadTag, uploadPreset } from "./cloudinaryConfig";
export const uploadImage = ({ file, successCallback }) => {
const url = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
const data = new FormData();
data.append("file", file);
data.append("upload_preset", uploadPreset);
data.append("tags", defaultUploadTag);
axios
.post(url, data, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => successCallback(response.data));
};
export const getImages = ({ successCallback, imageTag }) => {
axios
.get(
`https://res.cloudinary.com/${cloudName}/image/list/${
imageTag || defaultUploadTag
}.json`
)
.then((response) => successCallback(response.data.resources));
};
Code language: JavaScript (javascript)
The uploadImage
function is used to upload an image to our Cloudinary store. In addition to the image file and the upload preset, we append a tag to the image. This will allow us to retrieve all the images without a backend API, as we will see later.
In the getImages
function, we use the Client-side asset lists feature to retrieve the list of images with our set tag. Where a tag is specified in the imageTag
key, a subset of images – classified by Imagga to match that tag; are retrieved from our Cloudinary store. If no tag is provided, then the default tag is used to retrieve all the images tagged by our application during the upload process.
NOTE: To ensure that this feature is available on your Cloudinary account, you must ensure that the Resource list option is enabled. By default, the list delivery type is restricted. To enable it, open the Security settings in your Management console and clear the Resource list item under Restricted image types. You may want to clear this option only temporarily, as needed. Alternatively, you can bypass this (and any) delivery type restriction using a signed URL.
Next, let’s add some hooks to provide the functionality for our image upload. Create a new folder called hooks
in the’ src’ directory. In the src/hooks
folder, create a new file named useFilePreview.js
and add the following to it:
import { Modal } from "antd";
import { useState } from "react";
const useFilePreview = () => {
const [previewVisibility, setPreviewVisibility] = useState(false);
const [previewImage, setPreviewImage] = useState(null);
const [previewTitle, setPreviewTitle] = useState("");
const getBase64Representation = (file) =>
new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = (error) => reject(error);
});
const handlePreview = async (file) => {
if (!file.url && !file.preview) {
file.preview = await getBase64Representation(file.originFileObj);
}
setPreviewImage(file.url || file.preview);
setPreviewVisibility(true);
setPreviewTitle(
file.name || file.url.substring(file.url.lastIndexOf("/") + 1)
);
};
const hidePreview = () => {
setPreviewVisibility(false);
};
const previewContent = (
<Modal
visible={previewVisibility}
title={previewTitle}
footer={null}
onCancel={hidePreview}
>
<img alt={previewTitle} style={{ width: "100%" }} src={previewImage} />
</Modal>
);
return [handlePreview, previewContent];
};
export default useFilePreview;
Code language: JavaScript (javascript)
This hook provides the functionality to allow us to preview selected images before uploading by generating a base64 string representation of the file and rendering it in a modal when required.
Next, in the src/hooks
directory, create a new file called useFileSelection.js
and add the following to it:
import { message } from "antd";
import { uploadImage } from "../cloudinary/cloudinaryHelper";
import { defaultUploadTag } from "../cloudinary/cloudinaryConfig";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
const useFileSelection = () => {
const [selectedFiles, setSelectedFiles] = useState([]);
const [isUploading, setIsUploading] = useState(false);
const navigate = useNavigate();
const addFile = (file) => {
setSelectedFiles((currentSelection) => [...currentSelection, file]);
};
const removeFile = (file) => {
setSelectedFiles((currentSelection) => {
const newSelection = currentSelection.slice();
const fileIndex = currentSelection.indexOf(file);
newSelection.splice(fileIndex, 1);
return newSelection;
});
};
const uploadSelection = () => {
if (selectedFiles.length === 0) {
message.error("You need to select at least one image");
return;
}
setIsUploading(true);
const uploadResults = [];
const returnedTags = [];
selectedFiles.forEach((file) => {
uploadImage({
file,
successCallback: (response) => {
returnedTags.push(...response.tags);
uploadResults.push(response);
if (uploadResults.length === selectedFiles.length) {
setIsUploading(false);
const tags = new Set(returnedTags);
tags.delete(defaultUploadTag);
message.success("Images uploaded successfully");
navigate("/gallery", { state: [...tags] });
}
},
});
});
};
return [addFile, removeFile, isUploading, uploadSelection];
};
export default useFileSelection;
Code language: JavaScript (javascript)
This hook helps us keep track of the files selected we want to upload, as well as add or remove files. It also provides a function that allows us to upload the selected files to Cloudinary using the uploadImage
function declared earlier. In the successCallback
provided, we retrieve the tags provided from the Cloudinary upload response and add them to an array. Once all the images have been uploaded, we generate a Set object, which removes duplicate tags and passes the destructured Set object to the component rendered by the /gallery
route. This is a workaround to access the generated tags temporarily.
NOTE: Cloudinary also provides an Admin API, which among other things, can retrieve all the tags currently used for a specified resource type.
With our hooks in place, let’s build our components.
In the src
directory, create a new folder named components
. In the src/components
folder, create a new file named DragAndDrop.js
and add the following to it:
import { Upload } from "antd";
import { PlusOutlined } from "@ant-design/icons";
import useFilePreview from "../hooks/useFilePreview";
const { Dragger } = Upload;
const DragAndDrop = ({ addFile, removeFile }) => {
const [handlePreview, previewContent] = useFilePreview();
const beforeUploadHandler = (file) => {
addFile(file);
return false;
};
return (
<>
<Dragger
multiple={true}
onRemove={removeFile}
showUploadList={true}
listType="picture-card"
beforeUpload={beforeUploadHandler}
onPreview={handlePreview}
accept="image/*"
>
<p className="ant-upload-drag-icon">
<PlusOutlined />
</p>
<p className="ant-upload-text">
Click this area or drag files to upload
</p>
</Dragger>
{previewContent}
</>
);
};
export default DragAndDrop;
Code language: JavaScript (javascript)
The DragAndDrop
component wraps the AntD Upload component and uses the useFilePreview
hook to add the required functionality for us to preview files before uploading.
Next, in the src/components
directory, create a new file named ImageUpload.js
and add the following to it:
import useFileSelection from "../hooks/useFileSelection";
import { Button, Card } from "antd";
import DragAndDrop from "./DragAndDrop";
const ImageUpload = () => {
const [addFile, removeFile, isUploading, uploadSelection] =
useFileSelection();
return (
<Card
style={{ margin: "auto", width: "50%" }}
actions={[
<Button type="primary" loading={isUploading} onClick={uploadSelection}>
Submit
</Button>,
]}
>
<DragAndDrop addFile={addFile} removeFile={removeFile} />
</Card>
);
};
export default ImageUpload;
Code language: JavaScript (javascript)
This component builds on the DragAndDrop
component and adds a submit button which triggers the upload process. The useFileSelection
hook provides the functionality for adding files, removing files, and uploading the selection.
Next, let’s create a component to render the tags returned by Imagga. In the src/components
folder, create a new file named Tags.js
and add the following to it:
import { Tag } from "antd";
const tagColours = [
"magenta",
"red",
"volcano",
"orange",
"gold",
"lime",
"green",
"cyan",
"blue",
"geekblue",
"purple",
];
const getTagColour = () =>
tagColours[Math.floor(Math.random() * tagColours.length)];
const Tags = ({ tags, setActiveTag }) => {
if (tags.length === 0) {
return <></>;
}
return (
<div style={{ width: "80%", margin: "auto", marginBottom: "5px" }}>
{tags.map((tag, index) => (
<Tag
color={getTagColour()}
key={index}
style={{ cursor: "pointer", margin: "5px" }}
>
<span
onClick={() => {
setActiveTag(tag);
}}
>
{tag}
</span>
</Tag>
))}
</div>
);
};
export default Tags;
Code language: JavaScript (javascript)
We start by declaring an array of colors and a function to return a random color from the array.
The Tags
component takes two props – the tags
array and a setActiveTag
callback which is triggered when a tag is clicked.
Next, we iterate through the tags and render an AntD Tag component with an onClick
handler that calls the setActiveTag
function.
The next component we will build is one to display the uploaded images. In the src/components
folder, create a new file named Gallery.js
and add the following to it:
import { getImages } from "../cloudinary/cloudinaryHelper";
import { useEffect, useState } from "react";
import { Image, Transformation } from "cloudinary-react";
import { cloudName, uploadPreset } from "../cloudinary/cloudinaryConfig";
import { useLocation } from "react-router-dom";
import { Row, Col } from "antd";
import Tags from "./Tags";
const Gallery = () => {
const [images, setImages] = useState([]);
const location = useLocation();
const uploadResultTags = location.state ?? [];
const tags = ["All", ...uploadResultTags];
const [activeTag, setActiveTag] = useState("All");
useEffect(() => {
getImages({
successCallback: setImages,
imageTag: activeTag === "All" ? null : activeTag,
});
}, [activeTag]);
useEffect(() => {
getImages({
successCallback: setImages,
});
}, []);
return (
<>
<Row>
<Tags tags={tags} setActiveTag={setActiveTag} />
</Row>
<Row gutter={[0, 16]} align="middle">
{images.map((image, index) => (
<Col span={6} key={index}>
<Image
publicId={image.public_id}
cloudName={cloudName}
upload_preset={uploadPreset}
secure={true}
alt={image.originalFilename}
>
<Transformation width={300} height={300} crop="scale" />
</Image>
</Col>
))}
</Row>
</>
);
};
export default Gallery;
Code language: JavaScript (javascript)
Upon mounting, this component retrieves all the images uploaded (using the default upload tag) and renders them via the functionality provided by the Cloudinary React SDK. Additionally, it renders the tags passed to it by the useFileSelection
hook and listen for changes to the active tag – in the event of a change, a request is made to pull images corresponding to the selected tag.
To make navigation between the ImageUpload
and Gallery
components easier, let’s add a menu to our application. In the src/components
folder, create a new file named Menu.js
and add the following to it:
import { Menu as AntDMenu } from "antd";
import { useState } from "react";
import { Link } from "react-router-dom";
import { PictureOutlined, UploadOutlined } from "@ant-design/icons";
const Menu = () => {
const [currentlySelected, setCurrentlySelected] = useState("upload");
const handleMenuSelection = (e) => {
setCurrentlySelected(e.key);
};
const items = [
{
label: <Link to="/">Upload Images</Link>,
key: "upload",
icon: <UploadOutlined />,
},
{
label: <Link to="/gallery">Gallery</Link>,
key: "gallery",
icon: <PictureOutlined />,
},
];
return (
<AntDMenu
mode="horizontal"
onClick={handleMenuSelection}
selectedKeys={[currentlySelected]}
items={items}
/>
);
};
export default Menu;
Code language: JavaScript (javascript)
With our components in place, let’s add routing to handle the switching between components based on the selected route. In the src
folder, create a new file named routes.js
and add the following to it:
import Gallery from "./components/Gallery";
import ImageUpload from "./components/ImageUpload";
const routes = [
{
path: "/",
element: <ImageUpload />,
},
{
path: "/gallery",
element: <Gallery />,
},
];
export default routes;
Code language: JavaScript (javascript)
Here we declare two routes and the element to be rendered when the route is hit. Next, update the src/App.js
file to match the following:
import "./App.css";
import { useRoutes } from "react-router-dom";
import routes from "./routes";
import Menu from "./components/Menu";
import { Col, Row } from "antd";
const App = () => {
const router = useRoutes(routes);
return (
<div style={{ margin: "1%" }}>
<Menu />
<div style={{ textAlign: "center" }}>
<Row justify="center" align="middle" style={{ textAlign: "center" }}>
<Col style={{ width: "100%", margin: "2%" }}>{router}</Col>
</Row>
</div>
</div>
);
};
export default App;
Code language: JavaScript (javascript)
Here, we render the menu and the router – which renders the relevant component based on the current route.
Finally, update the src/index.js
to match the following:
import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { BrowserRouter } from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>
);
reportWebVitals();
Code language: JavaScript (javascript)
With everything in place, run your application using the following command:
npm start
By default, the application will be available at http://localhost:3000/. The final result will look like the gif shown below.
Find the complete project here on GitHub.
In this article, we looked at how to take advantage of Imagga to automatically add tags to images uploaded to Cloudinary. Taking advantage of the flexibility of the Cloudinary API, we were able to build an application equipped with the tagging functionality and retrieve images based on tags – all without a backend.
Cloudinary also offers even more robust features which can take our application further by allowing us to retrieve ALL the tags used to upload images, thereby giving our application even more control over the tags instead of being restricted to recently classified tags.
Resources you may find helpful: