Automatically Watermark an Image Gallery on Upload

Tosin Moronfolu

Introduction

A watermark is a logo, text, or pattern intentionally put onto another image to prevent images from being copied or used without permission.

In this article, we will learn how to add watermarks to our images using Cloudinary in our Next.js apps.

Sandbox

The completed project is on CodeSandbox. Fork it and run the code.

GitHub repository

https://github.com/folucode/nextjs_watermark_images

Prerequisite

To follow through with this article, we need to have:

  • An understanding of JavaScript and Next.js
  • A Cloudinary account — sign up here.

Project Setup

Node and its package manager, npm, are required to initialize a new project.

To install Node.js, we go to the Node.js website and follow the instructions. We verify Node.js’ installation using the terminal command below:

1node -v
2v16.10.0 //node version installed

The result shows the version of Node.js we installed on our computer.

We'll now create our Next.js app using create-next-app, which automatically sets up a boilerplate Next.js app. To create a new project, run:

1npx create-next-app@latest <app-name>
2# or
3yarn create next-app <app-name>

After the installation is complete, change the directory to the app we just created:

1cd <app-name>

Now we run npm run dev or yarn dev to start the development server on http://localhost:3000.

Installing Cloudinary

Cloudinary provides a rich media management experience enabling users to upload, store, manage, manipulate, and deliver images and videos for websites and applications.

The @cloudinary/url-gen package provides transformation and optimization functionalities and we’ll use it to transform our project's image. Install the package with the code below:

1npm i @cloudinary/url-gen

With installations completed, we’ll start the next application using the command below:

1npm run dev

Once run, the command spins up a local development server which we can access on http://localhost:3000.

Setting Up Configurations

To begin, we would do some basic setup in our app to help build our gallery. First, we replace the contents in the Home.module.css file in the styles folder with the content below. These are styles we would be using throughout our app.

1.container {
2 padding: 0 2rem;
3 }
4 .main {
5 min-height: 100vh;
6 padding: 4rem 0;
7 flex: 1;
8 display: flex;
9 flex-direction: column;
10 justify-content: center;
11 align-items: center;
12 }
13 .title a {
14 color: #0070f3;
15 text-decoration: none;
16 }
17 .title a:hover,
18 .title a:focus,
19 .title a:active {
20 text-decoration: underline;
21 }
22 .title {
23 margin: 0;
24 line-height: 1.15;
25 font-size: 4rem;
26 }
27 .title,
28 .description {
29 text-align: center;
30 }
31 .description {
32 margin: 4rem 0;
33 line-height: 1.5;
34 font-size: 1.5rem;
35 }
36 .grid {
37 display: flex;
38 align-items: center;
39 justify-content: center;
40 flex-wrap: wrap;
41 max-width: 1000px;
42 margin-top: 30px;
43 }
44 .card {
45 margin: 0.5rem;
46 padding: 0.5rem;
47 text-align: left;
48 color: inherit;
49 text-decoration: none;
50 border: 1px solid #676767;
51 border-radius: 10px;
52 transition: color 0.15s ease, border-color 0.15s ease;
53 max-width: 300px;
54 }
55 .card:hover,
56 .card:focus,
57 .card:active {
58 color: #0070f3;
59 border-color: #0070f3;
60 }
61 .file::-webkit-file-upload-button {
62 visibility: hidden;
63 }
64 .file::before {
65 content: 'Select some files';
66 display: inline-block;
67 background: linear-gradient(to bottom, #f9f9f9, #e3e3e3);
68 border: 1px solid #999;
69 border-radius: 3px;
70 padding: 5px 8px;
71 outline: none;
72 white-space: nowrap;
73 -webkit-user-select: none;
74 cursor: pointer;
75 text-shadow: 1px 1px #fff;
76 font-weight: 700;
77 font-size: 10pt;
78 }
79 .file:hover::before {
80 border-color: black;
81 }
82 .file:active::before {
83 background: -webkit-linear-gradient(top, #e3e3e3, #f9f9f9);
84 }

After that, in the next.config.js file, we need to add Cloudinary as a domain to access remote images using the next/image component.

1/** @type {import('next').NextConfig} */
2 const nextConfig = {
3 ...
4 images: {
5 domains: ['res.cloudinary.com'],
6 },
7 };
8 module.exports = nextConfig;

Building the Gallery

Now in the index.js file in the pages folder, replace the contents of the file with the code below:

1import { Cloudinary } from '@cloudinary/url-gen';
2 import Head from 'next/head';
3 import styles from '../styles/Home.module.css';
4
5 export default function Home() {
6 const cld = new Cloudinary({
7 cloud: {
8 cloudName: 'chukwutosin',
9 },
10 });
11
12 return (
13 <div className={styles.container}>
14 <Head>
15 <title>Image Gallery</title>
16 <meta name='description' content="Tosin's image gallery" />
17 </Head>
18 <main className={styles.main}>
19 <h1 className={styles.title}>Welcome to Tosin&apos;s image gallery</h1>
20 <div>
21 <input
22 className={styles.file}
23 type='file'
24 name='uploadImages'
25 multiple
26 />
27 </div>
28 </main>
29 </div>
30 );
31 }

We first import the Cloudinary component, Next.js Head component, and our CSS styles in this file.

Then we create a Cloudinary instance with our cloudName, obtained from our Cloudinary dashboard. We make a basic HTML setup with an input field to upload multiple images to our gallery.

Upload Images to Cloudinary

Before we can upload images to Cloudinary from our Next.js app, Cloudinary requires us to have an upload preset. The upload_preset allows us to define a set of asset upload choices centrally rather than providing them in each upload call.

We can find our upload preset in the Upload tab of our Cloudinary settings page by clicking on the gear icon in the top right corner of the dashboard page.

By scrolling down to the bottom of the page to the upload presets section, we'll see our upload preset, or there will be an option to create one if we don't have any.

Now that we have gotten our upload preset, we will create a function to handle image upload and transformation.

Handling Image Upload and Transformation

First, we will create a function called handleOnSubmit that would take in an event. We then take all the images from the event target and append our upload preset on each as we upload to Cloudinary.

Cloudinary sends back a public_id to access the image after uploading each image. We then perform the watermark transformation. Our watermark text is "This is my image.". When the image transformation is complete, the function returns the URL of the transformed image. Then we save the URL using useState.

1import { Cloudinary, Transformation } from '@cloudinary/url-gen';
2 import { opacity } from '@cloudinary/url-gen/actions/adjust';
3 import { source } from '@cloudinary/url-gen/actions/overlay';
4 import { Position } from '@cloudinary/url-gen/qualifiers';
5 import { compass } from '@cloudinary/url-gen/qualifiers/gravity';
6 import { text } from '@cloudinary/url-gen/qualifiers/source';
7 import { TextStyle } from '@cloudinary/url-gen/qualifiers/textStyle';
8 import Head from 'next/head';
9 import Image from 'next/image';
10 import { useState } from 'react';
11 import styles from '../styles/Home.module.css';
12
13 export default function Home() {
14 const [imageSrc, setImageSrc] = useState([]);
15
16 const cld = new Cloudinary({
17 cloud: {
18 cloudName: 'chukwutosin',
19 },
20 });
21
22 async function handleOnSubmit(event) {
23 event.preventDefault();
24
25 const images = event.target.files;
26 let imgArray = [];
27
28 if (images) {
29 for (const image of images) {
30 const body = new FormData();
31 body.append('upload_preset', <upload_preset>);
32 body.append('file', image);
33 const response = await fetch(
34 'https://api.cloudinary.com/v1_1/<cloud_name>/image/upload',
35 {
36 method: 'POST',
37 body,
38 }
39 ).then((r) => r.json());
40
41 const myImage = cld.image(response.public_id);
42
43 myImage
44 .overlay(
45 source(
46 text('This is my picture', new TextStyle('arial', 200))
47 .textColor('white')
48 .transformation(new Transformation().adjust(opacity(60)))
49 ).position(new Position().gravity(compass('center')).offsetY(20))
50 )
51 .format('png');
52 const myUrl = myImage.toURL();
53 imgArray.push(myUrl);
54 }
55 setImageSrc(imgArray);
56 }
57 }
58 ...
59 }
60
61```js
62
63## Displaying the Images
64
65Now that we have the transformed images stored in our state variable, let’s display them.
66
67First, let's add an `onChange` handler to the input field. This will automatically call `handleOnSubmit` when we select our images and upload them immediately.
68
69After that, we check if the `imageSrc` variable has a value to display the images or not. We use the Next.js `Image` component to display the images.
70
71Update the `return` statement in the `index.js` file with the code below.
72
73```js
74 return (
75 <div className={styles.container}>
76 <Head>
77 <title>Image Gallery</title>
78 <meta name='description' content="Tosin's image gallery" />
79 </Head>
80 <main className={styles.main}>
81 <h1 className={styles.title}>Welcome to Tosin&apos;s image gallery</h1>
82 <div>
83 <input
84 className={styles.file}
85 type='file'
86 name='uploadImages'
87 multiple
88 onChange={handleOnSubmit}
89 />
90 </div>
91
92 <div className={styles.grid}>
93 {imageSrc.length > 0
94 ? imageSrc.map((img, i) => {
95 return (
96 <div key={i} className={styles.card}>
97 <Image src={img} alt={img} width={300} height={300} />
98 </div>
99 );
100 })
101 : ''}
102 </div>
103 </main>
104 </div>
105 );

Running the App

Now that we have finished setup, we'll start up our app, and we should see our app like so:

We'll click the button 'Select some file' to upload our images, and then our gallery will pop up as so

Note: if nothing shows up immediately after the images are selected, wait a bit. The delay is because of the image upload and transformation. Depending on the image size, it might take a few seconds. In the real world, we should put a loader to show the user that their operation is processing.

Conclusion

This article taught us how to add watermark transformations to our images using Cloudinary.

Resources

Tosin Moronfolu

Software Engineer

I'm a software engineer with hands-on experience in building robust and complex applications improving performance and scalability. I love everything tech and science, especially physics and maths. I also love to read books, mainly non-fiction, and I love music; I play the guitar.