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";34export 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);1011 axios12 .post(url, data, {13 headers: {14 "Content-Type": "multipart/form-data",15 },16 })17 .then((response) => successCallback(response.data));18};1920export const getImages = ({ successCallback, imageTag }) => {21 axios22 .get(23 `https://res.cloudinary.com/${cloudName}/image/list/${24 imageTag || defaultUploadTag25 }.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";34const useFilePreview = () => {5 const [previewVisibility, setPreviewVisibility] = useState(false);6 const [previewImage, setPreviewImage] = useState(null);7 const [previewTitle, setPreviewTitle] = useState("");89 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 });1617 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 };2728 const hidePreview = () => {29 setPreviewVisibility(false);30 };3132 const previewContent = (33 <Modal34 visible={previewVisibility}35 title={previewTitle}36 footer={null}37 onCancel={hidePreview}38 >39 <img alt={previewTitle} style={{ width: "100%" }} src={previewImage} />40 </Modal>41 );4243 return [handlePreview, previewContent];44};4546export 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";67const useFileSelection = () => {8 const [selectedFiles, setSelectedFiles] = useState([]);9 const [isUploading, setIsUploading] = useState(false);1011 const navigate = useNavigate();1213 const addFile = (file) => {14 setSelectedFiles((currentSelection) => [...currentSelection, file]);15 };1617 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 };2526 const uploadSelection = () => {27 if (selectedFiles.length === 0) {28 message.error("You need to select at least one image");29 return;30 }3132 setIsUploading(true);33 const uploadResults = [];34 const returnedTags = [];3536 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 };5354 return [addFile, removeFile, isUploading, uploadSelection];55};5657export 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";45const { Dragger } = Upload;67const DragAndDrop = ({ addFile, removeFile }) => {8 const [handlePreview, previewContent] = useFilePreview();910 const beforeUploadHandler = (file) => {11 addFile(file);12 return false;13 };1415 return (16 <>17 <Dragger18 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 upload31 </p>32 </Dragger>33 {previewContent}34 </>35 );36};3738export 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";45const ImageUpload = () => {6 const [addFile, removeFile, isUploading, uploadSelection] =7 useFileSelection();89 return (10 <Card11 style={{ margin: "auto", width: "50%" }}12 actions={[13 <Button type="primary" loading={isUploading} onClick={uploadSelection}>14 Submit15 </Button>,16 ]}17 >18 <DragAndDrop addFile={addFile} removeFile={removeFile} />19 </Card>20 );21};2223export 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";23const tagColours = [4 "magenta",5 "red",6 "volcano",7 "orange",8 "gold",9 "lime",10 "green",11 "cyan",12 "blue",13 "geekblue",14 "purple",15];1617const getTagColour = () =>18 tagColours[Math.floor(Math.random() * tagColours.length)];1920const 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 <Tag28 color={getTagColour()}29 key={index}30 style={{ cursor: "pointer", margin: "5px" }}31 >32 <span33 onClick={() => {34 setActiveTag(tag);35 }}36 >37 {tag}38 </span>39 </Tag>40 ))}41 </div>42 );43};4445export 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";89const 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");1516 useEffect(() => {17 getImages({18 successCallback: setImages,19 imageTag: activeTag === "All" ? null : activeTag,20 });21 }, [activeTag]);2223 useEffect(() => {24 getImages({25 successCallback: setImages,26 });27 }, []);2829 return (30 <>31 <Row>32 <Tags tags={tags} setActiveTag={setActiveTag} />33 </Row>3435 <Row gutter={[0, 16]} align="middle">36 {images.map((image, index) => (37 <Col span={6} key={index}>38 <Image39 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};5354export 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";56const Menu = () => {7 const [currentlySelected, setCurrentlySelected] = useState("upload");89 const handleMenuSelection = (e) => {10 setCurrentlySelected(e.key);11 };1213 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 <AntDMenu27 mode="horizontal"28 onClick={handleMenuSelection}29 selectedKeys={[currentlySelected]}30 items={items}31 />32 );33};3435export 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";34const routes = [5 {6 path: "/",7 element: <ImageUpload />,8 },9 {10 path: "/gallery",11 element: <Gallery />,12 },13];1415export 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";67const App = () => {8 const router = useRoutes(routes);910 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};2122export 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";78const root = ReactDOM.createRoot(document.getElementById("root"));910root.render(11 <React.StrictMode>12 <BrowserRouter>13 <App />14 </BrowserRouter>15 </React.StrictMode>16);1718reportWebVitals();
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: