diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index ebf63fa..cad18b2 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -4,17 +4,16 @@
-
-
-
-
-
-
+
+
+
+
+
-
-
-
-
+
+
+
+
@@ -416,15 +415,7 @@
-
-
-
-
- 1740936527951
-
-
-
- 1740936527951
+
@@ -810,7 +801,15 @@
1743691471368
-
+
+
+ 1743705749057
+
+
+
+ 1743705749057
+
+
@@ -857,7 +856,6 @@
-
@@ -882,7 +880,8 @@
-
+
+
diff --git a/package-lock.json b/package-lock.json
index b2074f9..3e2f349 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -40,6 +40,7 @@
"react-helmet": "^6.1.0",
"react-image-crop": "^11.0.7",
"react-router-dom": "^6.23.1",
+ "tesseract.js": "^6.0.0",
"type-fest": "^4.35.0",
"use-deep-compare-effect": "^1.8.1",
"yup": "^1.4.0"
@@ -6297,6 +6298,12 @@
"node": ">=0.10.0"
}
},
+ "node_modules/idb-keyval": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.1.tgz",
+ "integrity": "sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==",
+ "license": "Apache-2.0"
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -6817,6 +6824,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-url": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+ "license": "MIT"
+ },
"node_modules/is-weakmap": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
@@ -8102,6 +8115,15 @@
"protobufjs": "^7.2.4"
}
},
+ "node_modules/opencollective-postinstall": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
+ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
+ "license": "MIT",
+ "bin": {
+ "opencollective-postinstall": "index.js"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -10091,6 +10113,36 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/tesseract.js": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-6.0.0.tgz",
+ "integrity": "sha512-tqYCod1HwJzkeZw1l6XWx+ly2hhisGcBtak9MArhYwDAxL0NgeVhLJcUjqPxZMQtpgtVUzWcpZPryi+hnaQGVw==",
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bmp-js": "^0.1.0",
+ "idb-keyval": "^6.2.0",
+ "is-url": "^1.2.4",
+ "node-fetch": "^2.6.9",
+ "opencollective-postinstall": "^2.0.3",
+ "regenerator-runtime": "^0.13.3",
+ "tesseract.js-core": "^6.0.0",
+ "wasm-feature-detect": "^1.2.11",
+ "zlibjs": "^0.3.1"
+ }
+ },
+ "node_modules/tesseract.js-core": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-6.0.0.tgz",
+ "integrity": "sha512-1Qncm/9oKM7xgrQXZXNB+NRh19qiXGhxlrR8EwFbK5SaUbPZnS5OMtP/ghtqfd23hsr1ZvZbZjeuAGcMxd/ooA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/tesseract.js/node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT"
+ },
"node_modules/text-extensions": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz",
@@ -10690,6 +10742,12 @@
"node": ">=12.0.0"
}
},
+ "node_modules/wasm-feature-detect": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.8.0.tgz",
+ "integrity": "sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==",
+ "license": "Apache-2.0"
+ },
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
@@ -11103,6 +11161,15 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zlibjs": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
+ "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/zod": {
"version": "3.24.2",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.24.2.tgz",
diff --git a/package.json b/package.json
index 68dfa2d..c0c605b 100644
--- a/package.json
+++ b/package.json
@@ -57,6 +57,7 @@
"react-helmet": "^6.1.0",
"react-image-crop": "^11.0.7",
"react-router-dom": "^6.23.1",
+ "tesseract.js": "^6.0.0",
"type-fest": "^4.35.0",
"use-deep-compare-effect": "^1.8.1",
"yup": "^1.4.0"
diff --git a/src/components/result/ToolTextResult.tsx b/src/components/result/ToolTextResult.tsx
index cefa482..ca2b588 100644
--- a/src/components/result/ToolTextResult.tsx
+++ b/src/components/result/ToolTextResult.tsx
@@ -1,21 +1,24 @@
-import { Box, TextField } from '@mui/material';
+import { Box, CircularProgress, TextField, Typography } from '@mui/material';
import React, { useContext } from 'react';
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
import InputHeader from '../InputHeader';
import ResultFooter from './ResultFooter';
import { replaceSpecialCharacters } from '@utils/string';
import mime from 'mime';
+import { globalInputHeight } from '../../config/uiConfig';
export default function ToolTextResult({
title = 'Result',
value,
extension = 'txt',
- keepSpecialCharacters
+ keepSpecialCharacters,
+ loading
}: {
title?: string;
value: string;
extension?: string;
keepSpecialCharacters?: boolean;
+ loading?: boolean;
}) {
const { showSnackBar } = useContext(CustomSnackBarContext);
const handleCopy = () => {
@@ -46,18 +49,37 @@ export default function ToolTextResult({
return (
-
+
+
+ Loading... This may take a moment.
+
+
+ ) : (
+
+ fullWidth
+ multiline
+ sx={{
+ '&.MuiTextField-root': {
+ backgroundColor: 'background.paper'
+ }
+ }}
+ rows={10}
+ inputProps={{ 'data-testid': 'text-result' }}
+ />
+ )}
);
diff --git a/src/pages/tools/image/generic/image-to-text/index.tsx b/src/pages/tools/image/generic/image-to-text/index.tsx
new file mode 100644
index 0000000..a56917e
--- /dev/null
+++ b/src/pages/tools/image/generic/image-to-text/index.tsx
@@ -0,0 +1,108 @@
+import { Box } from '@mui/material';
+import React, { useContext, useState } from 'react';
+import * as Yup from 'yup';
+import ToolImageInput from '@components/input/ToolImageInput';
+import ToolTextResult from '@components/result/ToolTextResult';
+import { GetGroupsType } from '@components/options/ToolOptions';
+import ToolContent from '@components/ToolContent';
+import { ToolComponentProps } from '@tools/defineTool';
+import SelectWithDesc from '@components/options/SelectWithDesc';
+import CheckboxWithDesc from '@components/options/CheckboxWithDesc';
+import CircularProgress from '@mui/material/CircularProgress';
+import { extractTextFromImage, getAvailableLanguages } from './service';
+import { InitialValuesType } from './types';
+import { CustomSnackBarContext } from '../../../../../contexts/CustomSnackBarContext';
+
+const initialValues: InitialValuesType = {
+ language: 'eng',
+ detectParagraphs: true
+};
+
+const validationSchema = Yup.object({
+ language: Yup.string().required('Language is required')
+});
+
+export default function ImageToText({ title }: ToolComponentProps) {
+ const [input, setInput] = useState(null);
+ const [result, setResult] = useState('');
+ const [isProcessing, setIsProcessing] = useState(false);
+ const { showSnackBar } = useContext(CustomSnackBarContext);
+ const compute = async (optionsValues: InitialValuesType, input: any) => {
+ if (!input) return;
+
+ setIsProcessing(true);
+
+ try {
+ const extractedText = await extractTextFromImage(input, optionsValues);
+ setResult(extractedText);
+ } catch (err: any) {
+ showSnackBar(
+ err.message || 'An error occurred while processing the image',
+ 'error'
+ );
+ setResult('');
+ } finally {
+ setIsProcessing(false);
+ }
+ };
+
+ const getGroups: GetGroupsType = ({
+ values,
+ updateField
+ }) => [
+ {
+ title: 'OCR Options',
+ component: (
+
+ updateField('language', val)}
+ description={
+ 'Select the primary language in the image for better accuracy'
+ }
+ options={getAvailableLanguages()}
+ />
+ updateField('detectParagraphs', value)}
+ description={
+ 'Attempt to preserve paragraph structure in the extracted text'
+ }
+ title={'Detect Paragraphs'}
+ />
+
+ )
+ }
+ ];
+
+ return (
+
+ }
+ resultComponent={
+
+ }
+ toolInfo={{
+ title: 'Image to Text (OCR)',
+ description:
+ 'This tool extracts text from images using Optical Character Recognition (OCR). Upload an image containing text, select the primary language, and get the extracted text. For best results, use clear images with good contrast.'
+ }}
+ />
+ );
+}
diff --git a/src/pages/tools/image/generic/image-to-text/meta.ts b/src/pages/tools/image/generic/image-to-text/meta.ts
new file mode 100644
index 0000000..d35c89d
--- /dev/null
+++ b/src/pages/tools/image/generic/image-to-text/meta.ts
@@ -0,0 +1,22 @@
+import { defineTool } from '@tools/defineTool';
+import { lazy } from 'react';
+
+export const tool = defineTool('image-generic', {
+ name: 'Image to Text (OCR)',
+ path: 'image-to-text',
+ icon: 'mdi:text-recognition', // Iconify icon as a string
+ description:
+ 'Extract text from images (JPG, PNG) using optical character recognition (OCR).',
+ shortDescription: 'Extract text from images using OCR.',
+ keywords: [
+ 'ocr',
+ 'optical character recognition',
+ 'image to text',
+ 'extract text',
+ 'scan',
+ 'tesseract',
+ 'jpg',
+ 'png'
+ ],
+ component: lazy(() => import('./index'))
+});
diff --git a/src/pages/tools/image/generic/image-to-text/service.ts b/src/pages/tools/image/generic/image-to-text/service.ts
new file mode 100644
index 0000000..79d3333
--- /dev/null
+++ b/src/pages/tools/image/generic/image-to-text/service.ts
@@ -0,0 +1,56 @@
+import { createWorker } from 'tesseract.js';
+import { InitialValuesType } from './types';
+
+export const extractTextFromImage = async (
+ file: File,
+ options: InitialValuesType
+): Promise => {
+ try {
+ const { language, detectParagraphs } = options;
+
+ // Create a Tesseract worker
+ const worker = await createWorker(language);
+
+ // Convert file to URL
+ const imageUrl = URL.createObjectURL(file);
+
+ // Recognize text
+ const { data } = await worker.recognize(imageUrl);
+
+ // Clean up
+ URL.revokeObjectURL(imageUrl);
+ await worker.terminate();
+
+ // Process the result based on options
+ if (detectParagraphs) {
+ // Return text with paragraph structure preserved
+ return data.text;
+ } else {
+ // Return plain text with basic formatting
+ return data.text;
+ }
+ } catch (error) {
+ console.error('Error extracting text from image:', error);
+ throw new Error(
+ 'Failed to extract text from image. Please try again with a clearer image.'
+ );
+ }
+};
+
+// Helper function to get available languages
+export const getAvailableLanguages = (): { value: string; label: string }[] => {
+ return [
+ { value: 'eng', label: 'English' },
+ { value: 'fra', label: 'French' },
+ { value: 'deu', label: 'German' },
+ { value: 'spa', label: 'Spanish' },
+ { value: 'ita', label: 'Italian' },
+ { value: 'por', label: 'Portuguese' },
+ { value: 'rus', label: 'Russian' },
+ { value: 'jpn', label: 'Japanese' },
+ { value: 'chi_sim', label: 'Chinese (Simplified)' },
+ { value: 'chi_tra', label: 'Chinese (Traditional)' },
+ { value: 'kor', label: 'Korean' },
+ { value: 'ara', label: 'Arabic' }
+ ];
+};
diff --git a/src/pages/tools/image/generic/image-to-text/types.ts b/src/pages/tools/image/generic/image-to-text/types.ts
new file mode 100644
index 0000000..d9db33d
--- /dev/null
+++ b/src/pages/tools/image/generic/image-to-text/types.ts
@@ -0,0 +1,4 @@
+export type InitialValuesType = {
+ language: string;
+ detectParagraphs: boolean;
+};
diff --git a/src/pages/tools/image/generic/index.ts b/src/pages/tools/image/generic/index.ts
index a3bbb47..fc18106 100644
--- a/src/pages/tools/image/generic/index.ts
+++ b/src/pages/tools/image/generic/index.ts
@@ -5,6 +5,7 @@ import { tool as removeBackground } from './remove-background/meta';
import { tool as cropImage } from './crop/meta';
import { tool as changeOpacity } from './change-opacity/meta';
import { tool as createTransparent } from './create-transparent/meta';
+import { tool as imageToText } from './image-to-text/meta';
export const imageGenericTools = [
resizeImage,
@@ -13,5 +14,6 @@ export const imageGenericTools = [
cropImage,
changeOpacity,
changeColors,
- createTransparent
+ createTransparent,
+ imageToText
];