Efficiency in performing tasks is often defined by how much automation we can achieve. Automation allows us to avoid repetitive tasks which may consume a lot of time. One of the things we can automate is text recognition in images. Optical Character Recognition(OCR) has been around for quite a while. In this tutorial, we’ll be leveraging the technology via a Javascript library known as Tesseract.js. This library is a wrapper of the Tesseract OCR engine which allows us to use it in a Node/Browser environment. We’ll be building using Next.js and Cloudinary
The final project can be viewed on Codesandbox.
You can find the full source code on my Github repository.
You will need to be familiar with Javascript for this tutorial. Working knowledge of Node.js and React is also recommended. Install Node.js and NPM on your development machine if you haven’t yet. Check out the official documentation on how to do this. Also, ensure you have a text editor or IDE.
Cloudinary is an amazing service that provides developers with a suite of APIs for storage, optimization, and delivery of different types of media including images and videos. You can easily get started with a free developer account. Create a new account at cloudinary and log in. Head over to the dashboard where you’ll find your API keys and credentials. Take note of the Cloud name
, API Key
, and API Secret
we’ll use them later.
First things first, let’s create a new Next.js project. Run the following command in your terminal.
npx create-next-app extract-text-from-images
Code language: JavaScript (javascript)
The create-next-app
CLI utility scaffolds a new project called extract-text-from-images
. You can use any appropriate name for your project. Once the project has been created and the dependencies have been installed, change directory into the new project and open it inside a code editor/IDE.
cd extract-text-from-images
Code language: JavaScript (javascript)
We’ll be using the Cloudinary Node.js SDK. Let’s install that.
npm install cloudinary
We also need a way to parse form data on the backend so that we can upload images. For this let’s install Formidable.
npm install formidable
Finally, let’s install Tesseract.js
npm install tesseract.js
Code language: CSS (css)
One more thing. We’ll be using environment variables to store our API Keys securely. Next.js has built-in support for environment variables so we don’t need to install anything. Read about this here. Create a new file named .env.local
at the root of your project and paste the following inside.
CLOUD_NAME=YOUR_CLOUD_NAME
API_KEY=YOUR_API_KEY
API_SECRET=YOUR_API_SECRET
Remember to replace YOUR_CLOUD_NAME
YOUR_API_KEY
and YOUR_API_SECRET
with the Cloud name
, API Key
, and API Secret
values that we got from the Cloudinary-account-and-credentials section.
Create a new folder called lib
at the root of your project then create a new file named cloudinary.js
inside the lib
folder. Paste the following code inside lib/cloudinary.js
// lib/cloudinary.js
// Import the v2 api and rename it to cloudinary
import { v2 as cloudinary, TransformationOptions } from "cloudinary";
const CLOUDINARY_FOLDER_NAME = "images-with-text/";
// Initialize the sdk with cloud_name, api_key and api_secret
cloudinary.config({
cloud_name: process.env.CLOUD_NAME,
api_key: process.env.API_KEY,
api_secret: process.env.API_SECRET,
});
/**
* Gets a resource from cloudinary using its public id
*
* @param {string} publicId The public id of the image
*/
export const handleGetCloudinaryResource = (publicId) => {
return cloudinary.api.resource(publicId, {
resource_type: "image",
type: "upload",
});
};
/**
* Get cloudinary uploads
* @returns {Promise}
*/
export const handleGetCloudinaryUploads = () => {
return cloudinary.api.resources({
type: "upload",
prefix: CLOUDINARY_FOLDER_NAME,
resource_type: "image",
});
};
/**
* Uploads an image to cloudinary and returns the upload result
*
* @param {{path: string; transformation?:TransformationOptions,publicId?: string }} resource
*/
export const handleCloudinaryUpload = (resource) => {
return cloudinary.uploader.upload(resource.path, {
// Folder to store the image in
folder: CLOUDINARY_FOLDER_NAME,
// Public id of the image.
public_id: resource.publicId,
// Type of resource
resource_type: "auto",
// Transformation to apply to the video
transformation: resource.transformation,
});
};
/**
* Deletes resources from cloudinary. Takes in an array of public ids
* @param {string[]} ids
*/
export const handleCloudinaryDelete = (ids) => {
return cloudinary.api.delete_resources(ids, {
resource_type: "image",
});
};
Code language: JavaScript (javascript)
At the top, we import the cloudinary v2 API and rename it to cloudinary for readability purposes. CLOUDINARY_FOLDER_NAME
is the cloudinary folder where we’ll store all our images. This will allow us to get all of them easily later.
We then initialize the cloudinary SDK by calling the config
method on the API and passing the environment variables that we defined in the previous section.
handleGetCloudinaryResource
gets an uploaded resource using its public ID. It calls the api.resource
method to get the resource. Read about this here
handleGetCloudinaryUploads
gets all the images that have been uploaded to the folder that we defined in CLOUDINARY_FOLDER_NAME
. Read about the api.resources
method here
handleCloudinaryUpload
uploads an image or video to cloudinary. In our case, it’s an image. It calls the uploaded.upload
method on the API and passes the path to the image we want to upload, as well as some options. Notice how we have specified the folder to be the one we defined earlier in CLOUDINARY_FOLDER_NAME
. Read about the upload method and its options here
handleCloudinaryDelete
takes in an array of public IDs and passes them to the api.delete_resources
method which deletes them from cloudinary. Read more about this here. Please note that you can also use the Destroy method to delete a single resource.
We’ll be using Formidable to parse incoming form data. Create a file called parse-form.js
under the lib
folder and paste the following inside.
// lib/parse-form.js
import { IncomingForm } from "formidable";
/**
* Parses the incoming form data.
*
* @param {NextApiRequest} req The incoming request object
*/
export const parseForm = (req) => {
return new Promise((resolve, reject) => {
const form = new IncomingForm({ keepExtensions: true, multiples: true });
form.parse(req, (error, fields, files) => {
if (error) {
return reject(error);
}
return resolve({ fields, files });
});
});
};
Code language: JavaScript (javascript)
There’s not much here. You can have a look at the Formidable docs to learn more about the options used.
Let’s work on the backend. We need to create handlers for the /api/images
and /api/images/:id
routes. If you’re not familiar with Next.js API Routes, I highly recommend you read the documentation. This will help you better grasp how API routes work in Next.js.
Create a new folder called images
under the pages/api
folder. Inside pages/api/images
create a new file named index.js
and paste the following inside.
// pages/api/index.js
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import { NextApiRequest, NextApiResponse } from "next";
import {
handleCloudinaryUpload,
handleGetCloudinaryUploads,
} from "../../../lib/cloudinary";
import { parseForm } from "../../../lib/parse-form";
// Custom config for our API route
export const config = {
api: {
bodyParser: false,
},
};
/**
* The handler function for the API route. Takes in an incoming request and outgoing response.
*
* @param {NextApiRequest} req The incoming request object
* @param {NextApiResponse} res The outgoing response object
*/
export default async function handler(req, res) {
switch (req.method) {
case "GET": {
try {
const result = await handleGetRequest();
return res.status(200).json({ message: "Success", result });
} catch (error) {
return res.status(400).json({ message: "Error", error });
}
}
case "POST": {
try {
const result = await handlePostRequest(req);
return res.status(201).json({ message: "Success", result });
} catch (error) {
return res.status(400).json({ message: "Error", error });
}
}
default: {
return res.status(405).json({ message: "Method not allowed" });
}
}
}
const handleGetRequest = async () => {
const uploads = await handleGetCloudinaryUploads();
return uploads;
};
/**
* Handles the POST request to the API route.
*
* @param {NextApiRequest} req The incoming request object
*/
const handlePostRequest = async (req) => {
// Get the form data using the parseForm function
const data = await parseForm(req);
// Get the how it started image file from the incoming form data
const image = data.files.image;
// Upload the image to Cloudinary
const imageUploadResult = await handleCloudinaryUpload({
path: image.path,
});
return imageUploadResult;
};
Code language: JavaScript (javascript)
At the top, we export a custom config object. This one tells Next.js not to use the default body-parser middleware since we want to handle the form data ourselves.
We then export the handler/controller for our route. Please note that it’s a default export. This takes in the incoming request object and the outgoing response object. we use a switch statement to only handle GET and POST requests.
handleGetRequest
gets all uploads by calling the handleGetCloudinaryUploads
function that we created earlier.
handlePostRequest
gets the incoming form data using the parseForm
function that we defined. It then uploads the image to cloudinary using the handleCloudinaryUpload
function and returns the result.
Next, create a file under pages/api/images
called [id].js
. This will contain the handler for the api/images/:id
route. Paste the following inside.
//pages/api/images
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
import {
handleCloudinaryDelete,
handleGetCloudinaryResource,
} from "../../../lib/cloudinary";
import {
createWorker,
createScheduler,
RecognizeResult,
setLogging,
} from "tesseract.js";
/**
* The handler function for the API route. Takes in an incoming request and outgoing response.
*
* @param {NextApiRequest} req The incoming request object
* @param {NextApiResponse} res The outgoing response object
*/
export default async function handler(req, res) {
const { id } = req.query;
switch (req.method) {
case "GET": {
try {
if (!id) {
throw "id param is required";
}
const result = await handleGetRequest(id);
return res.status(200).json({ message: "Success", result });
} catch (error) {
console.log(error);
return res
.status(error?.error?.http_code ?? 400)
.json({ message: "Error", error });
}
}
case "DELETE": {
try {
const { id } = req.query;
if (!id) {
throw "id param is required";
}
const result = await handleDeleteRequest(id);
return res.status(200).json({ message: "Success", result });
} catch (error) {
return res.status(400).json({ message: "Error", error });
}
}
default: {
return res.status(405).json({ message: "Method not allowed" });
}
}
}
/**
* Handles the GET request to the API route.
*
* @param {string} id The public id of the image to retrieve
* @returns
*/
const handleGetRequest = async (id) => {
// Get the image from cloudinary using the public id
const resource = await handleGetCloudinaryResource(id.replace(":", "/"));
// Set Tesseract.js worker verbose logging to true for debugging
setLogging(true);
// Create a scheduler
const scheduler = createScheduler();
// Create a worker to run the OCR
const worker1 = createWorker({
errorHandler: (error) => {
console.error("Worker 1");
console.error(error);
throw error;
},
});
// Create a second worker to run the OCR
const worker2 = createWorker({
errorHandler: (error) => {
console.error("Worker 2");
console.error(error);
throw error;
},
});
// Load both workers
await Promise.all([worker1.load(), worker2.load()]);
// Load English language data for both workers
await Promise.all([worker1.loadLanguage("eng"), worker2.loadLanguage("eng")]);
// Initialize English language data for both workers
await Promise.all([worker1.initialize("eng"), worker2.initialize("eng")]);
// Add both workers to the scheduler
scheduler.addWorker(worker1);
scheduler.addWorker(worker2);
/**
* Use the scheduler to run the OCR on the image.
* @type {RecognizeResult}
*/
const result = await scheduler.addJob("recognize", resource.secure_url);
const { data } = result;
// Terminate all workers in the scheduler
await scheduler.terminate();
// Return the image along with the OCR text
return { ...resource, text: data.text };
};
/**
* Deletes a resource from cloudinary
* @param {string} id
* @returns
*/
const handleDeleteRequest = async (id) =>
handleCloudinaryDelete([id.replace(":", "/")]);
Code language: JavaScript (javascript)
Most of the handler/controller is similar to what I just explained above.
handleGetRequest
gets a cloudinary resource using its public ID by calling the handleGetCloudinaryResource
function. Note that we replace all colons(:) with a forward slash(/). This is because we switch them up in the frontend as you’ll see later. Next, we turn on verbose logging for Tesseract.js. This is useful in the development environment for debugging. We then create a scheduler that will allow us to run multiple workers concurrently. Following that is two workers with an error handler. We then proceed to load the workers, load our language and then add the workers to our scheduler. We run the scheduler and await the OCR results. It’s important to terminate the scheduler once we’re done to avoid it using up resources. Now I’m sure this was quite overwhelming. The best way to understand what each of those lines does is to read the Tesseract.js docs
handleDeleteRequest
just calls the handleCloudinaryDelete
function and passes public IDs of the resources we want to delete.
And we’re done with the backend.
For the front end, we’ll have three pages. One for uploading images, another for viewing all uploaded images, and another for viewing an image and extracting text.
Create a new folder at the root of your project and name it components
. Create a new file inside this folder and call it Layout.js
. Paste the following inside
// components/Layout.js
import Head from "next/head";
import Link from "next/link";
export default function Layout({ children }) {
return (
<div>
<Head>
<title>Extract Text From Images Using Tesseract.js</title>
<meta
name="description"
content="Extract Text From Images Using Tesseract.js"
/>
<link rel="icon" href="/favicon.ico" />
</Head>
<nav>
<ul>
<li>
<Link href="/">
<a>Home</a>
</Link>
</li>
<li>
<Link href="/images">
<a>Images</a>
</Link>
</li>
</ul>
</nav>
<main>{children}</main>
<style jsx>{`
nav {
min-height: 100px;
background-color: #fafafa;
display: flex;
align-items: center;
}
nav > ul {
display: flex;
justify-content: flex-end;
height: 100%;
width: 100%;
padding-right: 50px;
list-style: none;
}
nav > ul > li {
margin: 0 10px;
}
nav > ul > li > a {
padding: 10px 20px;
background-color: #7811ff;
color: #ffffff;
font-weight: bold;
border-radius: 5px;
}
nav > ul > li > a:hover {
background-color: #5d1bb4;
}
main {
min-height: calc(100vh - 100px);
}
`}</style>
</div>
);
}
Code language: JavaScript (javascript)
This is just a layout component that we can use to wrap our pages instead of defining it for every page.
Paste the following inside pages/index.js
import Layout from "../components/Layout";
import Link from "next/link";
import Image from "next/image";
import { useRouter } from "next/router";
import { useState } from "react";
export default function Home() {
const router = useRouter();
/**
* State to hold the how it started image
* @type {[File,Function]}
*/
const [image, setImage] = useState(null);
/**
* Loading state
* @type {[boolean,Function]}
*/
const [loading, setLoading] = useState(false);
const handleFormSubmit = async (e) => {
e.preventDefault();
setLoading(true);
try {
// Get the form data
const formData = new FormData(e.target);
// Post the form data to the /api/images endpoint
const response = await fetch("/api/images", {
method: "POST",
body: formData,
});
const data = await response.json();
if (!response.ok) {
throw data;
}
// Navigate to the images page
router.push("/images");
} catch (error) {
// TODO: Show error message to the user
console.error(error);
} finally {
setLoading(false);
}
};
return (
<Layout>
<div className="wrapper">
<h1>
Extract Text From Images Using{" "}
<Link href="https://github.com/naptha/tesseract.js">
<a target="_blank">Tesseract.js</a>
</Link>
</h1>
<form onSubmit={handleFormSubmit}>
<div className="form-group">
{image && (
<div>
<Image
src={URL.createObjectURL(image)}
width={200}
height={200}
layout="fixed"
alt="Selected Image"
/>
</div>
)}
<label htmlFor="image">Click To Select Image</label>
<input
type="file"
multiple={false}
hidden
required
disabled={loading}
id="image"
name="image"
accept=".jpg, .jpeg, .png"
onChange={(e) => {
const file = e.target.files[0];
setImage(file);
}}
></input>
</div>
<button type="submit" disabled={!image || loading}>
Upload
</button>
</form>
</div>
<style jsx>{`
div.wrapper {
width: 100%;
height: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
align-items: center;
}
div.wrapper > h1 > a {
color: #7811ff;
}
div.wrapper > h1 > a:hover {
color: #5d1bb4;
text-decoration: underline;
}
div.wrapper > form {
width: 500px;
min-height: 300px;
background-color: #f5f5f5;
border-radius: 5px;
padding: 20px;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
div.wrapper > form > div.form-group {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
div.wrapper > form > div.form-group > label {
font-size: 1.2rem;
font-weight: bold;
background-color: #cfcccc;
padding: 40px 60px;
cursor: pointer;
border-radius: 5px;
margin: 20px 0;
}
div.wrapper > form > div.form-group > label:hover {
color: #5d1bb4;
}
div.wrapper > form > button {
margin-top: 20px;
padding: 10px 20px;
background-color: #7811ff;
color: #ffffff;
font-weight: bold;
border: none;
border-radius: 5px;
}
div.wrapper > form > button:disabled {
background-color: #cfcfcf;
}
div.wrapper > form > button:hover:not([disabled]) {
background-color: #5d1bb4;
}
`}</style>
</Layout>
);
}
Code language: JavaScript (javascript)
Apart from the useState
and useRouter
hooks, the rest is just basic javascript and html. Read about the former from the React docs and the latter from the Next.js docs. The image
state stores the file that is selected from the form input. We also have the loading
state. handleFormSubmit
is called when the form is submitted. It makes a POST request to /api/images
with the form data then navigates to the /images
page which we’re creating next. The rest is just some basic styling for our page.
Create a new folder called images
under pages
. Please note that this is not the same as the one we created earlier under pages/api
. Create a new file called index.js
under pages/images
and paste the following inside.
// pages/images/index.js
import { useCallback, useEffect, useState } from "react";
import Layout from "../../components/Layout";
import Link from "next/link";
import Image from "next/image";
export default function ImagesPage() {
/**
* Looading state
* @type {[boolean, (boolean) => void]}
*/
const [loading, setLoading] = useState(false);
/**
* All uploaded images state
* @type {[object[], (object[]) => void]}
*/
const [images, setImages] = useState([]);
const getImages = useCallback(async () => {
try {
setLoading(true);
const response = await fetch("/api/images", {
method: "GET",
});
const data = await response.json();
if (!response.ok) {
throw new Error(data);
}
setImages(data.result.resources);
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
getImages();
}, [getImages]);
const handleDeleteImage = async (id) => {
try {
setLoading(true);
const response = await fetch(`/api/images/${id}`, {
method: "DELETE",
});
const data = await response.json();
if (!response.ok) {
throw new Error(data);
}
getImages();
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
};
return (
<Layout>
{loading ? (
<div className="loading-wrapper">
<p>Loading...</p>
</div>
) : (
<div className="wrapper">
<p>
Please note that{" "}
<a
href="https://github.com/naptha/tesseract.js/"
target="_blank"
rel="noreferrer"
>
Tesseract.js
</a>{" "}
may have issues recognizing text from certain images. See{" "}
<a
href="https://github.com/naptha/tesseract.js/issues"
target="_blank"
rel="noreferrer"
>
Github Issues
</a>
</p>
{images.length > 0 ? (
<div className="images-wrapper">
{images.map((image, index) => (
<div className="image-wrapper" key={`image-${index}`}>
<Link
href={`/images/${image.public_id.replace(/\//g, ":")}`}
passHref
>
<div className="image-container">
<Image
src={image.secure_url}
alt={image.public_id}
layout="fill"
></Image>
</div>
</Link>
<div className="controls">
<a href={image.secure_url} target="_blank" rel="noreferrer">
{image.secure_url}
</a>
<Link
href={`/images/${image.public_id.replace(/\//g, ":")}`}
passHref
>
<button>Open</button>
</Link>
<button
onClick={(e) => {
e.preventDefault();
handleDeleteImage(image.public_id.replace(/\//g, ":"));
}}
>
{" "}
Delete
</button>
</div>
</div>
))}
</div>
) : (
<div className="no-images">
<p>No Images Yet</p>
<Link href="/">
<a>Upload Image</a>
</Link>
</div>
)}
</div>
)}
<style jsx>{`
div.loading-wrapper {
height: calc(100vh - 100px);
width: 100%;
display: flex;
justify-content: center;
align-items: center;
}
div.wrapper {
height: calc(100vh - 100px);
width: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
align-items: center;
}
div.wrapper > div.images-wrapper {
height: 100%;
width: 100%;
display: flex;
flex-flow: row wrap;
gap: 20px;
padding: 20px;
}
div.wrapper > div.images-wrapper > div.image-wrapper {
flex: 0 0 400px;
height: 300px;
background-color: #fafafa;
border-radius: 5px;
}
div.wrapper
> div.images-wrapper
> div.image-wrapper
> div.image-container {
position: relative;
height: 70%;
background-color: #c29b9b;
}
div.wrapper
> div.images-wrapper
> div.image-wrapper
> div.image-container:hover {
border-bottom: 0 none;
box-shadow: 0 1px 5px rgba(0, 0, 0, 0.46);
cursor: pointer;
}
div.wrapper > div.images-wrapper > div.image-wrapper > div.controls {
padding: 10px;
}
div.wrapper
> div.images-wrapper
> div.image-wrapper
> div.controls
> a {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
div.wrapper
> div.images-wrapper
> div.image-wrapper
> div.controls
> button {
margin: 10px;
}
div.wrapper > div.no-images {
height: 100%;
width: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
div.wrapper > div.no-images > a {
padding: 10px 20px;
background-color: #7811ff;
color: #ffffff;
font-weight: bold;
border-radius: 5px;
}
div.wrapper > div.no-images > a:hover {
background-color: #5d1bb4;
}
`}</style>
</Layout>
);
}
Code language: JavaScript (javascript)
Here we use the useCallback
hook to define a memoized callback function. Read more about this hook here. This memoized function makes a GET request to api/images
and gets all uploaded images then updates the images
state.
We run the getImages
function when the component is first rendered using the useEffect
hook. This hook is useful when working with side effects. Read about it here
handleDeleteImage
takes in a public id and then makes a DELETE request to api/images/:id
. For the HTML, we just show all images with a delete button in a flex container. The rest is some simple css styling. Clicking on an image navigates you to the next page. Note that we replace all forward slashes(/) in the public id with colons(:). This is because if we leave the slashes and pass that as the id parameter to the url it will basically be pointing to a non-existent page. Instead of pointing to images/:id
it will be pointing to images/:id/:somethingelse/
. We’ll replace back the slashes if we need to in the backend.
Let’s create the next page now.Create a file called [id].js
under pages/images
. Again note that this isn’t the same as pages/api/images
. Paste the following inside pages/images/[id].js
// pages/images/[id].js
import { useRouter } from "next/router";
import { useCallback, useEffect, useState } from "react";
import Layout from "../../components/Layout";
import Image from "next/image";
export default function ImagePage() {
// Get the Next.js router
const router = useRouter();
// Get the image public id from the URL
const { id } = router.query;
/**
* Looading state
* @type {[boolean, (boolean) => void]}
*/
const [loading, setLoading] = useState(false);
/**
* Image results
* @type {[object, (object) => void]}
*/
const [image, setImage] = useState(null);
/**
* Error state
* @type {[boolean, (boolean) => void]}
*/
const [error, setError] = useState(false);
const getImage = useCallback(async () => {
setLoading(true);
setError(false);
if (id) {
try {
// Make GET request to the /api/images endpoint to get image
const response = await fetch(`/api/images/${id}`, {
method: "GET",
});
const data = await response.json();
// Check if the response is a failure
if (!response.ok) {
throw data;
}
// Update image state
setImage(data.result);
} catch (error) {
// TODO: Show error message to user
console.error(error);
setError(true);
} finally {
setLoading(false);
}
}
}, [id]);
useEffect(() => {
getImage();
}, [getImage]);
return (
<Layout>
<div className="wrapper">
{loading && (
<div className="loading">
<p>Please be patient as we fetch your image</p>
<p>...</p>
</div>
)}
{error && (
<div className="error">
<h2>Error</h2>
<p>There was an error getting your image</p>
<p>
Please note that{" "}
<a
href="https://github.com/naptha/tesseract.js/"
target="_blank"
rel="noreferrer"
>
Tesseract.js
</a>{" "}
may have issues recognizing text from certain images. See{" "}
<a
href="https://github.com/naptha/tesseract.js/issues"
target="_blank"
rel="noreferrer"
>
Github Issues
</a>
</p>
<button
onClick={() => {
router.reload();
}}
>
Try Again!
</button>
</div>
)}
{!error && image && (
<div className="image-wrapper">
<div className="image-container">
<Image
src={image.secure_url}
alt={image.public_id}
layout="fill"
></Image>
</div>
<div className="text-container">
<h2>Extracted Text</h2>
{image.text ? <p>{image.text}</p> : <p>No text detected</p>}
</div>
</div>
)}
</div>
<style jsx>{`
div.wrapper {
height: calc(100vh - 100px);
width: 100%;
}
div.wrapper > div.loading,
div.wrapper > div.error {
height: 100%;
width: 100%;
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
}
div.wrapper > div.loading > p,
div.wrapper > div.error > p {
font-size: 1.5rem;
font-weight: bold;
}
div.wrapper > div.image-wrapper {
max-width: 600px;
margin: 0 auto;
height: 100%;
background-color: #fafafa;
}
div.wrapper > div.image-wrapper > div.image-container {
position: relative;
height: 50%;
}
div.wrapper > div.image-wrapper > div.text-container {
height: 50%;
overflow-y: auto;
padding: 20px;
white-space: pre-wrap;
}
div.wrapper button {
margin-top: 20px;
padding: 10px 20px;
background-color: #7811ff;
color: #ffffff;
font-weight: bold;
border: none;
border-radius: 5px;
}
div.wrapper button:disabled {
background-color: #cfcfcf;
}
div.wrapper button:hover:not([disabled]) {
background-color: #5d1bb4;
}
`}</style>
</Layout>
);
}
Code language: JavaScript (javascript)
getImage
makes a GET call to api/images/:id
. In the backend, we get the image from cloudinary, run the OCR recognition then return the results along with the extracted text. We did all this in the pages/api/images/[id].js
file. The rest is just some HTML to show the image and the extracted text under the image and some basic styling.
We’re almost done. Add the following styles to styles/globals.css
a {
color: #7811ff;
text-decoration: none;
}
a:hover {
color: #5d1bb4;
text-decoration: underline;
}
button {
padding: 10px 20px;
background-color: #7811ff;
color: #ffffff;
font-weight: bold;
border: none;
border-radius: 5px;
}
button:disabled {
background-color: #cfcfcf;
}
button:hover:not([disabled]) {
background-color: #5d1bb4;
}
img {
object-fit: cover;
}
Code language: CSS (css)
We also need to add one more thing. Since we’re using the Image component from Next.js, we need to conform to a few practices to ensure that our images are optimized and we get the best performance. Read about this here. Create a file called next.config.js
at the root of your project if it doesn’t exist and add the following
module.exports = {
// ... other options
images: {
domains: ["res.cloudinary.com"],
},
};
Code language: JavaScript (javascript)
We just added the res.cloudinary.com
domain to allow the component to load images from cloudinary.
That’s about it. You can now run your project in dev mode
npm run dev
DISCLAIMER: Please note that the Tesseract.js library won’t always recognize text from all images so you might run into some errors with some specific images. There are all sorts of open issues on their Github
You can find the full source code on my Github