Add Tags to Images before Upload with Imagga

Ifeoma Imoh

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.

Project Setup

Create a new React app using the following command:

1npx create-react-app cloudinary-imagga

Next, add the project dependencies using the following command:

1npm install antd @ant-design/icons axios react-router-dom cloudinary-react

Next, we need to import the antd CSS. To do this, open src/App.css and edit its content to match the following:

1@import "~antd/dist/antd.css";

Setting up Cloudinary

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:

1REACT_APP_CLOUD_NAME = "INSERT YOUR CLOUD NAME HERE";
2REACT_APP_UPLOAD_PRESET = "INSERT YOUR UNSIGNED UPLOAD PRESET KEY HERE";

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:

1cp.env.env.local;

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:

1export const cloudName = process.env.REACT_APP_CLOUD_NAME;
2export const uploadPreset = process.env.REACT_APP_UPLOAD_PRESET;
3export const defaultUploadTag = "cloudinary_imagga";

Create Helper Class for API Requests

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:

1import axios from "axios";
2import { cloudName, defaultUploadTag, uploadPreset } from "./cloudinaryConfig";
3
4export const uploadImage = ({ file, successCallback }) => {
5 const url = `https://api.cloudinary.com/v1_1/${cloudName}/image/upload`;
6 const data = new FormData();
7 data.append("file", file);
8 data.append("upload_preset", uploadPreset);
9 data.append("tags", defaultUploadTag);
10
11 axios
12 .post(url, data, {
13 headers: {
14 "Content-Type": "multipart/form-data",
15 },
16 })
17 .then((response) => successCallback(response.data));
18};
19
20export const getImages = ({ successCallback, imageTag }) => {
21 axios
22 .get(
23 `https://res.cloudinary.com/${cloudName}/image/list/${
24 imageTag || defaultUploadTag
25 }.json`
26 )
27 .then((response) => successCallback(response.data.resources));
28};

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.

Adding Hooks

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:

1import { Modal } from "antd";
2import { useState } from "react";
3
4const useFilePreview = () => {
5 const [previewVisibility, setPreviewVisibility] = useState(false);
6 const [previewImage, setPreviewImage] = useState(null);
7 const [previewTitle, setPreviewTitle] = useState("");
8
9 const getBase64Representation = (file) =>
10 new Promise((resolve, reject) => {
11 const reader = new FileReader();
12 reader.readAsDataURL(file);
13 reader.onload = () => resolve(reader.result);
14 reader.onerror = (error) => reject(error);
15 });
16
17 const handlePreview = async (file) => {
18 if (!file.url && !file.preview) {
19 file.preview = await getBase64Representation(file.originFileObj);
20 }
21 setPreviewImage(file.url || file.preview);
22 setPreviewVisibility(true);
23 setPreviewTitle(
24 file.name || file.url.substring(file.url.lastIndexOf("/") + 1)
25 );
26 };
27
28 const hidePreview = () => {
29 setPreviewVisibility(false);
30 };
31
32 const previewContent = (
33 <Modal
34 visible={previewVisibility}
35 title={previewTitle}
36 footer={null}
37 onCancel={hidePreview}
38 >
39 <img alt={previewTitle} style={{ width: "100%" }} src={previewImage} />
40 </Modal>
41 );
42
43 return [handlePreview, previewContent];
44};
45
46export default useFilePreview;

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:

1import { message } from "antd";
2import { uploadImage } from "../cloudinary/cloudinaryHelper";
3import { defaultUploadTag } from "../cloudinary/cloudinaryConfig";
4import { useState } from "react";
5import { useNavigate } from "react-router-dom";
6
7const useFileSelection = () => {
8 const [selectedFiles, setSelectedFiles] = useState([]);
9 const [isUploading, setIsUploading] = useState(false);
10
11 const navigate = useNavigate();
12
13 const addFile = (file) => {
14 setSelectedFiles((currentSelection) => [...currentSelection, file]);
15 };
16
17 const removeFile = (file) => {
18 setSelectedFiles((currentSelection) => {
19 const newSelection = currentSelection.slice();
20 const fileIndex = currentSelection.indexOf(file);
21 newSelection.splice(fileIndex, 1);
22 return newSelection;
23 });
24 };
25
26 const uploadSelection = () => {
27 if (selectedFiles.length === 0) {
28 message.error("You need to select at least one image");
29 return;
30 }
31
32 setIsUploading(true);
33 const uploadResults = [];
34 const returnedTags = [];
35
36 selectedFiles.forEach((file) => {
37 uploadImage({
38 file,
39 successCallback: (response) => {
40 returnedTags.push(...response.tags);
41 uploadResults.push(response);
42 if (uploadResults.length === selectedFiles.length) {
43 setIsUploading(false);
44 const tags = new Set(returnedTags);
45 tags.delete(defaultUploadTag);
46 message.success("Images uploaded successfully");
47 navigate("/gallery", { state: [...tags] });
48 }
49 },
50 });
51 });
52 };
53
54 return [addFile, removeFile, isUploading, uploadSelection];
55};
56
57export default useFileSelection;

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.

Adding 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:

1import { Upload } from "antd";
2import { PlusOutlined } from "@ant-design/icons";
3import useFilePreview from "../hooks/useFilePreview";
4
5const { Dragger } = Upload;
6
7const DragAndDrop = ({ addFile, removeFile }) => {
8 const [handlePreview, previewContent] = useFilePreview();
9
10 const beforeUploadHandler = (file) => {
11 addFile(file);
12 return false;
13 };
14
15 return (
16 <>
17 <Dragger
18 multiple={true}
19 onRemove={removeFile}
20 showUploadList={true}
21 listType="picture-card"
22 beforeUpload={beforeUploadHandler}
23 onPreview={handlePreview}
24 accept="image/*"
25 >
26 <p className="ant-upload-drag-icon">
27 <PlusOutlined />
28 </p>
29 <p className="ant-upload-text">
30 Click this area or drag files to upload
31 </p>
32 </Dragger>
33 {previewContent}
34 </>
35 );
36};
37
38export default DragAndDrop;

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:

1import useFileSelection from "../hooks/useFileSelection";
2import { Button, Card } from "antd";
3import DragAndDrop from "./DragAndDrop";
4
5const ImageUpload = () => {
6 const [addFile, removeFile, isUploading, uploadSelection] =
7 useFileSelection();
8
9 return (
10 <Card
11 style={{ margin: "auto", width: "50%" }}
12 actions={[
13 <Button type="primary" loading={isUploading} onClick={uploadSelection}>
14 Submit
15 </Button>,
16 ]}
17 >
18 <DragAndDrop addFile={addFile} removeFile={removeFile} />
19 </Card>
20 );
21};
22
23export default ImageUpload;

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:

1import { Tag } from "antd";
2
3const tagColours = [
4 "magenta",
5 "red",
6 "volcano",
7 "orange",
8 "gold",
9 "lime",
10 "green",
11 "cyan",
12 "blue",
13 "geekblue",
14 "purple",
15];
16
17const getTagColour = () =>
18 tagColours[Math.floor(Math.random() * tagColours.length)];
19
20const Tags = ({ tags, setActiveTag }) => {
21 if (tags.length === 0) {
22 return <></>;
23 }
24 return (
25 <div style={{ width: "80%", margin: "auto", marginBottom: "5px" }}>
26 {tags.map((tag, index) => (
27 <Tag
28 color={getTagColour()}
29 key={index}
30 style={{ cursor: "pointer", margin: "5px" }}
31 >
32 <span
33 onClick={() => {
34 setActiveTag(tag);
35 }}
36 >
37 {tag}
38 </span>
39 </Tag>
40 ))}
41 </div>
42 );
43};
44
45export default Tags;

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:

1import { getImages } from "../cloudinary/cloudinaryHelper";
2import { useEffect, useState } from "react";
3import { Image, Transformation } from "cloudinary-react";
4import { cloudName, uploadPreset } from "../cloudinary/cloudinaryConfig";
5import { useLocation } from "react-router-dom";
6import { Row, Col } from "antd";
7import Tags from "./Tags";
8
9const Gallery = () => {
10 const [images, setImages] = useState([]);
11 const location = useLocation();
12 const uploadResultTags = location.state ?? [];
13 const tags = ["All", ...uploadResultTags];
14 const [activeTag, setActiveTag] = useState("All");
15
16 useEffect(() => {
17 getImages({
18 successCallback: setImages,
19 imageTag: activeTag === "All" ? null : activeTag,
20 });
21 }, [activeTag]);
22
23 useEffect(() => {
24 getImages({
25 successCallback: setImages,
26 });
27 }, []);
28
29 return (
30 <>
31 <Row>
32 <Tags tags={tags} setActiveTag={setActiveTag} />
33 </Row>
34
35 <Row gutter={[0, 16]} align="middle">
36 {images.map((image, index) => (
37 <Col span={6} key={index}>
38 <Image
39 publicId={image.public_id}
40 cloudName={cloudName}
41 upload_preset={uploadPreset}
42 secure={true}
43 alt={image.originalFilename}
44 >
45 <Transformation width={300} height={300} crop="scale" />
46 </Image>
47 </Col>
48 ))}
49 </Row>
50 </>
51 );
52};
53
54export default Gallery;

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:

1import { Menu as AntDMenu } from "antd";
2import { useState } from "react";
3import { Link } from "react-router-dom";
4import { PictureOutlined, UploadOutlined } from "@ant-design/icons";
5
6const Menu = () => {
7 const [currentlySelected, setCurrentlySelected] = useState("upload");
8
9 const handleMenuSelection = (e) => {
10 setCurrentlySelected(e.key);
11 };
12
13 const items = [
14 {
15 label: <Link to="/">Upload Images</Link>,
16 key: "upload",
17 icon: <UploadOutlined />,
18 },
19 {
20 label: <Link to="/gallery">Gallery</Link>,
21 key: "gallery",
22 icon: <PictureOutlined />,
23 },
24 ];
25 return (
26 <AntDMenu
27 mode="horizontal"
28 onClick={handleMenuSelection}
29 selectedKeys={[currentlySelected]}
30 items={items}
31 />
32 );
33};
34
35export default Menu;

Add Routing

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:

1import Gallery from "./components/Gallery";
2import ImageUpload from "./components/ImageUpload";
3
4const routes = [
5 {
6 path: "/",
7 element: <ImageUpload />,
8 },
9 {
10 path: "/gallery",
11 element: <Gallery />,
12 },
13];
14
15export default routes;

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:

1import "./App.css";
2import { useRoutes } from "react-router-dom";
3import routes from "./routes";
4import Menu from "./components/Menu";
5import { Col, Row } from "antd";
6
7const App = () => {
8 const router = useRoutes(routes);
9
10 return (
11 <div style={{ margin: "1%" }}>
12 <Menu />
13 <div style={{ textAlign: "center" }}>
14 <Row justify="center" align="middle" style={{ textAlign: "center" }}>
15 <Col style={{ width: "100%", margin: "2%" }}>{router}</Col>
16 </Row>
17 </div>
18 </div>
19 );
20};
21
22export default App;

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:

1import React from "react";
2import ReactDOM from "react-dom/client";
3import "./index.css";
4import App from "./App";
5import reportWebVitals from "./reportWebVitals";
6import { BrowserRouter } from "react-router-dom";
7
8const root = ReactDOM.createRoot(document.getElementById("root"));
9
10root.render(
11 <React.StrictMode>
12 <BrowserRouter>
13 <App />
14 </BrowserRouter>
15 </React.StrictMode>
16);
17
18reportWebVitals();

With everything in place, run your application using the following command:

1npm 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.

Conclusion

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:

Ifeoma Imoh

Software Developer

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