Transform Animated GIFs Using Cloudinary

Ifeoma Imoh

Animated GIFs consist of multiple images or frames played back in sequence. They were initially designed for graphics but have evolved. They are now commonly used for different use cases on the web, such as video previews, product demonstrations or service instructions, humor, and so on. Animated GIFs are a quick and easy way to present dynamic content on web pages and applications with small sizes compared to other alternatives.

Manually transforming a large number of animated GIFs can be tasking and time-consuming. As a result, Cloudinary extends its image and video transformation prowess to work seamlessly with animated GIFs. In this article, we'll look at ways to transform animated GIFs using Cloudinary. Without further ado, let's get started.

Here is a link to the demo on CodeSandbox.

Project Setup

Create a Next.js app using the following command:

1npx create-next-app cld-gif-transform

Next, add the project dependencies using the following command:

1npm install cloudinary axios

The Node Cloudinary SDK will provide easy-to-use methods to interact with the Cloudinary APIs, while axios will serve as our HTTP client.

Setting up Cloudinary

To use Cloudinary's provisioned services, you must first sign up for a free Cloudinary account if you don’t have one. Displayed on your account’s Management Console (aka Dashboard) are important details: your cloud name, API key, etc.

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:

1CLOUD_NAME = YOUR CLOUD NAME HERE
2API_KEY = YOUR API API KEY
3API_SECRET = YOUR API API SECRET

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.

Upload GIF to Cloudinary

We need to pass the public id of the asset to be transformed as an argument to Cloudinary transformation methods. As a result, we'll need to upload one animated GIF to Cloudinary. See here for how to upload files to Cloudinary via the Media Library.

After that, you should see the newly uploaded animated GIF and its public id in your media library.

We chose this approach of uploading files to Cloudinary to get things up and running. You can also upload files programmatically using Cloudinary's Upload API.

Transforming Animated GIFs

Just like with image transformations, Cloudinary makes it easy to transform the style and dimension of animated GIFs on the fly using the dynamic transformation URL syntax.

The default Cloudinary asset delivery URL has the following structure:

1https://res.cloudinary.com/<cloud_name>/<asset_type>/<delivery_type>/<transformations>/<version>/<public_id>.<extension>

Where cloud_name is the name of any specified Cloudinary account, asset_type depicts the type of asset to deliver, and delivery_type indicates the storage or delivery type. version, public_id, and extension mean the file version, its unique identifier, including the folder structure if applicable, and the file extension of the requested delivery format for the asset. In a single URL component, transformations denote one or more comma-separated transformation parameters.

Although we can manually build transformation URLs in our code to follow the syntax shown above, It is easier to take advantage of Cloudinary’s Node.js SDK to streamline the process. It provides helper methods to simplify building image transformation URLs.

Let's apply some transformations to the GIF uploaded. Create a file called transformGif.js in the pages/api/ folder and add the following to it:

1const cloudinary = require("cloudinary").v2;
2
3cloudinary.config({
4 cloud_name: process.env.CLOUD_NAME,
5 api_key: process.env.API_KEY,
6 api_secret: process.env.API_SECRET,
7 secure: true,
8});
9
10export default async function handler(req, res) {
11 try {
12 const response = await cloudinary.image("giphy_nz4u8x.gif", {
13 transformation: [{ radius: "max" }, { effect: "blur:150" }],
14 });
15 res.status(200).json(response);
16 } catch (error) {
17 console.log(error);
18 res.status(400).json(error);
19 }
20}

In the snippet above, we import and configure an instance of the Cloudinary Node.js SDK using our credentials. Next, we called the image method on the Cloudinary instance, which expects a string containing the public id as an argument. We also added a transformation object as an argument and set a couple of transformation parameters. As seen above, we set a blur effect and a maximum border-radius.

On success, we send a JSON response to the client; otherwise, an error is returned.

Cloudinary provides a lot of other transformation parameters. We’ll look at a few more in the following sections. Click here to learn more about the transformation parameters. To see the difference between the original and the transformed animated GIF, replace the content of your pages/index.js file with the following:

1import { useState } from "react";
2import axios from "axios";
3import MainView from "../components/MainView";
4import styles from "../styles/Home.module.css";
5
6export default function Home() {
7 const [transformedImg, setTransformedImg] = useState("");
8 const [reqStatus, setReqStatus] = useState("");
9
10 const getTransformedImg = async () => {
11 setReqStatus("loading...");
12 try {
13 const response = await axios.get("/api/transformGif");
14 const imgUrl = /'(.+)'/.exec(response.data)[1];
15 setTransformedImg(imgUrl);
16 setReqStatus("Done..");
17 } catch (error) {
18 setReqStatus("An error occurred");
19 console.log(error);
20 }
21 };
22
23 return (
24 <main className={styles.main}>
25 <h1>Transform Gif</h1>
26 <MainView
27 reqStatus={reqStatus}
28 transformedImg={transformedImg}
29 callFunction={getTransformedImg}
30 btnText="Transform GIF"
31 />
32 </main>
33 );
34}

In the code above, we made use of some predefined styles. Copy the styles in this codeSandbox link to your Home.module.css file in the /styles directory.

Next, we created a Home component that defines a transformedImg state to hold the transformed image URL and a reqStatus state to keep track of the request status.

We also defined a getTransformedImg function that makes an Axios request to the /api/transformGif endpoint to get the transformed image. Cloudinary returns a new <img> element with the transformed URL as a response, so we extract just the URL rather than dangerously setting the <img> element to the DOM. The extracted transformation URL and the request status are then set to their corresponding states.

We also imported a component called MainView, which we have yet to create. It gets rendered, and we pass the required props.

Now let's create the MainView component. Create a folder called components at the root level of the application. Create a file called MainView.js in the components folder and add the following to it:

1import styles from "../styles/Home.module.css";
2
3export default function MainView({
4 reqStatus,
5 transformedImg,
6 callFunction,
7 btnText,
8}) {
9 const handleClick = () => {
10 callFunction();
11 };
12 return (
13 <div className={styles.container}>
14 <div className={styles.input}>
15 <div>
16 <img
17 src="https://res.cloudinary.com/ifeomaimoh/image/upload/v1656165531/giphy_nz4u8x.gif"
18 alt="input-gif"
19 />
20 </div>
21 <button onClick={handleClick} disabled={reqStatus === "loading..."}>
22 {btnText}
23 </button>
24 <p>{reqStatus}</p>
25 </div>
26 <div className={styles.output}>
27 {transformedImg && (
28 <img
29 src={transformedImg}
30 alt="transformed-img"
31 className={styles.full_width}
32 />
33 )}
34 </div>
35 </div>
36 );
37}

The component expects the following as props:

  • The request status state.
  • The transformed image.
  • A custom button text.
  • A function that makes the request to the API route.

The MainView component then renders the uploaded GIF, the transformed animated GIF, and a button to the DOM. To keep things simple, we only copied and added the uploaded GIF URL from Cloudinary rather than having to retrieve it programmatically.

When clicked, the button triggers a handleClick function, which calls the request function.

Save the file and start your application on http://localhost:3000/ using the following command:

1npm run dev

Set Delay between Frames

A frame is a single image combined with other images to form an animated GIF. Cloudinary has a delay parameter (dl in URLs) that allows you to set the length of time (in milliseconds) between each frame of an animated GIF.

To set a delay between frames in our animated GIF, create a new file called setGifDelay.js in the pages/api/ folder and add the following to it:

1const cloudinary = require("cloudinary").v2;
2
3cloudinary.config({
4 cloud_name: process.env.CLOUD_NAME,
5 api_key: process.env.API_KEY,
6 api_secret: process.env.API_SECRET,
7 secure: true,
8});
9
10export default async function handler(req, res) {
11 try {
12 const response = await cloudinary.image("giphy_nz4u8x.gif", {
13 delay: "200",
14 });
15 res.status(200).json(response);
16 } catch (error) {
17 console.log(error);
18 res.status(400).json(error);
19 }
20}

The code is identical to what we had in the transformGif.js file, except that we now pass the public id and a delay parameter set to 200 milliseconds.

To see this in the browser, create a new file in the pages/ folder called setDelayPage.js and add the following to it:

1import { useState } from "react";
2import axios from "axios";
3import MainView from "../components/MainView";
4import styles from "../styles/Home.module.css";
5
6export default function Home() {
7 const [transformedImg, setTransformedImg] = useState("");
8 const [reqStatus, setReqStatus] = useState("");
9 const setGifDelay = async () => {
10 setReqStatus("loading...");
11 try {
12 const response = await axios.get("/api/setGifDelay");
13 const imgUrl = /'(.+)'/.exec(response.data)[1];
14 setTransformedImg(imgUrl);
15 setReqStatus("Done..");
16 } catch (error) {
17 setReqStatus("An error occurred");
18 console.log(error);
19 }
20 };
21
22 return (
23 <main className={styles.main}>
24 <h1>Change delay between frames</h1>
25 <MainView
26 reqStatus={reqStatus}
27 transformedImg={transformedImg}
28 callFunction={setGifDelay}
29 btnText="Set delay"
30 />
31 </main>
32 );
33}

The code is also similar to what we have in the index.js file, except that we changed the request function to make a call to the /api/setGifDelay route.

Save the changes and open http://localhost:3000/setDelayPage in your browser.

Get a Single Frame

Since animated GIFs are made up of different images, Cloudinary provides a page parameter (pg in URLs) that can be used to get single frames from an animated GIF.

To get the 10th frame of the GIF, create a new file called getSingleFrame.js in the pages/api/ folder and add the following to it:

1const cloudinary = require("cloudinary").v2;
2
3cloudinary.config({
4 cloud_name: process.env.CLOUD_NAME,
5 api_key: process.env.API_KEY,
6 api_secret: process.env.API_SECRET,
7 secure: true,
8});
9
10export default async function handler(req, res) {
11 try {
12 const response = await cloudinary.image("giphy_nz4u8x.gif", { page: 10 });
13 res.status(200).json(response);
14 } catch (error) {
15 console.log(error);
16 res.status(400).json(error);
17 }
18}

The only difference between this API route and the others is that to get the 10th from our GIF, we set the page parameter to 10.

To display the derived frame in your browser, create a file called singleFramePage.js in the pages/ folder and add the following to it:

1import { useState } from "react";
2import axios from "axios";
3import MainView from "../components/MainView";
4import styles from "../styles/Home.module.css";
5
6export default function Home() {
7 const [transformedImg, setTransformedImg] = useState("");
8 const [reqStatus, setReqStatus] = useState("");
9
10 const getSingleFrame = async () => {
11 setReqStatus("loading...");
12 try {
13 const response = await axios.get("/api/getSingleFrame");
14 const imgUrl = /'(.+)'/.exec(response.data)[1];
15 setTransformedImg(imgUrl);
16 setReqStatus("Done..");
17 } catch (error) {
18 setReqStatus("An error occurred");
19 console.log(error);
20 }
21 };
22
23 return (
24 <main className={styles.main}>
25 <h1>Get single frame</h1>
26 <MainView
27 reqStatus={reqStatus}
28 transformedImg={transformedImg}
29 callFunction={getSingleFrame}
30 btnText="Get frame"
31 />
32 </main>
33 );
34}

Save the changes and open http://localhost:3000/singleFramePage in your browser. To get different frames of the animated GIF, you can also tweak the value of the defined page parameter in the pages/api/getSingleFrame.js file.

Convert Animated GIF to a Video

To automatically convert our GIF to a modern video format like webM or mp4, all we need to do is change the file extension. We would still have the same animated video, but it would result in a trade-off between visual quality and file size.

Create a file called gifToVideo.js in the pages/api folder and add the following to it:

1const cloudinary = require("cloudinary").v2;
2
3cloudinary.config({
4 cloud_name: process.env.CLOUD_NAME,
5 api_key: process.env.API_KEY,
6 api_secret: process.env.API_SECRET,
7 secure: true,
8});
9
10export default async function handler(req, res) {
11 try {
12 const response = await cloudinary.video("giphy_nz4u8x", {
13 resource_type: "image",
14 });
15 res.status(200).send(response);
16 } catch (error) {
17 console.log(error);
18 res.status(400).json(error);
19 }
20}

In the code above, we imported and configured Cloudinary. Then we passed the public id of the uploaded animated GIF together with the resource type as an argument to its video method, which converts the GIF to a video format that works on all modern browsers (such as WebM, mp4, etc.) The response is then sent to the client; otherwise, an error is returned.

To play the resulting in your browser, create a gifToVideoPage.js file in the pages/ folder and add the following to it:

1import { useState } from "react";
2import axios from "axios";
3import styles from "../styles/Home.module.css";
4
5export default function Home() {
6 const [transformedImg, setTransformedImg] = useState("");
7 const [reqStatus, setReqStatus] = useState("");
8
9 const handleClick = () => {
10 convertGif();
11 };
12
13 const convertGif = async () => {
14 setReqStatus("loading...");
15 try {
16 const response = await axios.get("/api/gifToVideo");
17 const mp4Url = /https?[^<>]*?\.mp4/.exec(response.data)[0];
18 setTransformedImg(mp4Url);
19 setReqStatus("Done..");
20 } catch (error) {
21 setReqStatus("An error occurred");
22 console.log(error);
23 }
24 };
25
26 return (
27 <main className={styles.main}>
28 <h1>Transform Gif to video</h1>
29 <div className={styles.container}>
30 <div className={styles.input}>
31 <div>
32 <img
33 src="https://res.cloudinary.com/ifeomaimoh/image/upload/v1656165531/giphy_nz4u8x.gif"
34 alt="input-gif"
35 />
36 </div>
37 <button onClick={handleClick} disabled={reqStatus === "loading..."}>
38 Convert to video
39 </button>
40 <p>{reqStatus}</p>
41 </div>
42 <div className={styles.output}>
43 {transformedImg && (
44 <video controls className={styles.full_width}>
45 <source src={transformedImg} type="video/mp4" />
46 Your browser does not support the video tag.
47 </video>
48 )}
49 </div>
50 </div>
51 </main>
52 );
53}

In the code above, we used the same structure as in the previous pages, except that we no longer need the MainView component since we’re rendering a video in this section.

We defined a convertGif function that gets called when the button gets clicked. The function initiates an axios call to the /api/gifToVideo route to get the Cloudinary response. On conversion, Cloudinary returns a video tag with multiple sources that work in different modern browsers. However, we used a regular expression to extract only the mp4 URL to keep things simple and not have to render anything to the DOM forcefully.

The extracted URL is then saved in the transformedImg state, which is subsequently rendered using the video> tag.

To preview the changes, open http://localhost:3000/gifToVideoPage in your browser and click the button to convert the animated GIF to a video. You can also check the size to see the massive reduction in size.

We've looked at some transformation and optimization methods and created distinct pages in our application to display each. In addition, open your pages/_app.js file and update its content to add links that'll enable easy navigation.

1import Link from "next/link";
2import "../styles/globals.css";
3import styles from "../styles/Home.module.css";
4
5function MyApp({ Component, pageProps }) {
6 return (
7 <>
8 <header className={styles.header}>
9 <ul>
10 <li>
11 <Link href="/">Transform GIF</Link>
12 </li>
13 <li>
14 <Link href="/setDelayPage">Delay between frames</Link>
15 </li>
16 <li>
17 <Link href="/singleFramePage">Single frame</Link>
18 </li>
19 <li>
20 <Link href="/gifToVideoPage">GIF to Video</Link>
21 </li>
22 </ul>
23 </header>
24 <Component {...pageProps} />
25 </>
26 );
27}
28
29export default MyApp;

Find the complete project here on GitHub.

Conclusion

To avoid bloating this post, we'll stop here; however, Cloudinary also allows us to do much more with animated GIFs. For example, we can choose to use lossy compression when delivering the animated GIF files, use Cloudinary's Multi method and Zoompan effect to create animated images from scratch, and so on.

Resources You May Find Helpful

Ifeoma Imoh

Software Developer

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