Entendendo o Textract:
O Amazon Textract é um serviço avançado de Machine Learning (ML) da AWS projetado para extrair automaticamente textos impressos ou manuscritos, além de identificar elementos de layout e dados estruturados a partir de documentos digitalizados. Ele é capaz de processar diversos tipos de documentos, como formulários, relatórios e recibos, facilitando a automação de tarefas que exigem a extração e organização de informações. A tecnologia é particularmente útil em cenários onde grandes volumes de documentos precisam ser analisados, permitindo uma leitura precisa e eficiente dos conteúdos, sejam eles simples ou complexos.
A base desse serviço é a tecnologia de reconhecimento óptico de caracteres (OCR), que utiliza algoritmos sofisticados de correspondência de padrões para analisar imagens de texto. O OCR realiza uma comparação detalhada, caractere por caractere, entre o conteúdo visualizado e um banco de dados interno, decodificando a imagem para gerar um texto digital legível. No entanto, o OCR convencional pode ser limitado quando se trata de interpretar variações complexas de escrita, especialmente manuscrita. Para superar esses desafios, o Amazon Textract adota o reconhecimento inteligente de caracteres (ICR), uma evolução do OCR. O ICR utiliza técnicas avançadas de machine learning que treinam o sistema para reconhecer caracteres da mesma forma que um humano faria, aprimorando a precisão na leitura de diferentes estilos de escrita, mesmo em formatos menos padronizados.
"Antes de prosseguirmos, é fundamental esclarecer o objetivo desta publicação. Vamos apresentar um exemplo prático de como desenvolver tanto o back-end quanto o front-end para integrar o Amazon Textract, com o foco específico em destacar informações importantes (highlights) em documentos PDF. Isso será feito utilizando o recurso de Layout do serviço, que permite identificar e manipular a estrutura visual dos documentos, como tabelas, parágrafos e outras áreas de interesse. Vale ressaltar que, neste conteúdo, não exploraremos outras funcionalidades do Textract, concentrando-se exclusivamente na extração de layout e destaques em PDFs.”
Como funciona a integração com o Textract:
A AWS possui diversos portais que contém documentações completas sobre o processo de integração com cada serviço, de acordo com sua linguagem. No nosso caso, iremos utilizar o Node.js, que é um software de código aberto, multiplataforma, baseado no interpretador V8 do Google e que permite a execução de códigos JavaScript fora de um navegador web.
No caso do Javascript, a AWS possui um hub grande de integrações, onde você pode realizar a integração com os serviços por meio de módulos. Pensando na integração com o Textract, você pode seguir a documentação e executar os seguintes comandos de instalação:
Nesta publicação, iremos utilizar o método "AnalyzeDocumentCommand” da API do Textract, cuja documentação deixo ao lado: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/client/textract/command/AnalyzeDocumentCommand/
Aplicação Back-End:
Para estruturarmos a aplicação back-end, iremos contar com a utilização do Serverless Framework como biblioteca e framework de projeto, pois essa solução irá nos apoiar nas configurações dos recursos da Infraestrutura e publicação das funções Lambda e API Gateway.
- Começando pelo arquivo serverless.yml, temos:
# Nome da organização da conta to Serverless Framework
org: publicacao
# Nome da aplicação presente na organização
app: documents-analyze
# Nome do serviço pertencente à aplicação
service: back-end
provider:
# Nome do provider de infraestrutura
name: aws
# Linguagem e versão aceita pelo Lambda
runtime: nodejs20.x
# Timeout default das funções Lambda
timeout: 30
# Estrutura da role de IAM para permissionamento das funções Lambda
iamRoleStatements:
- Resource: "*"
Effect: Allow
Action:
- s3:*
- textract:*
plugins:
# Plugin (módulo NPM) para apoiar na execução em ambiente de desenvolvimento
- serverless-offline
# Estruturação das funções Lambda
functions:
# Nome único da função Lambda
extractText:
# Caminho de pastas que a função se encontra
handler: src/extractText.handler
# Eventos que serão o gatilho para triggar a função, no caso aqui é o API Gateway
events:
- httpApi:
path: /{documentName}
method: get
- Seguindo para configuração do arquivo package.json (arquivo padrão para execução de aplicações Node.js):
{
"name": "back-end",
"version": "1.0.0",
"main": "src/extractText.mjs",
"license": "ISC",
"type": "module",
"dependencies": {
"@aws-sdk/client-s3": "^3.658.1",
"@aws-sdk/client-textract": "^3.658.1",
"@aws-sdk/s3-request-presigner": "^3.658.1"
},
"devDependencies": {
"serverless-offline": "^14.3.2"
}
}
- Agora, no arquivo src/extractText.mjs, temos:
```import import {
TextractClient,
AnalyzeDocumentCommand,
} from "@aws-sdk/client-textract";
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// Infrastructure Layer
const region = "us-east-1";
const textractClient = new TextractClient({ region });
const s3Client = new S3Client({ region });
const BUCKET_NAME = "";
const DOCUMENTS_FOLDER = "";
const SIGNED_URL_EXPIRATION = 3600;
// Service Layer: Handles Textract document analysis
const analyzeDocument = async (bucketName, documentPath) => {
const command = new AnalyzeDocumentCommand({
Document: {
S3Object: {
Bucket: bucketName,
Name: documentPath,
},
},
FeatureTypes: ["LAYOUT"],
});
const response = await textractClient.send(command);
return {
blocks: response.Blocks,
pages: response.DocumentMetadata?.Pages,
};
};
// Service Layer: Generates signed URL for S3 object
const generateSignedUrl = async (bucketName, documentPath) => {
const command = new GetObjectCommand({
Bucket: bucketName,
Key: documentPath,
});
return getSignedUrl(s3Client, command, { expiresIn: SIGNED_URL_EXPIRATION });
};
// Domain Layer: Main handler function
const handler = async (event) => {
try {
const { documentName } = event.pathParameters;
const documentParsedName = decodeURIComponent(documentName) + ".pdf";
const documentPath = ${DOCUMENTS_FOLDER}/${documentParsedName}
;
const documentData = await analyzeDocument(BUCKET_NAME, documentPath);
const signedUrl = await generateSignedUrl(BUCKET_NAME, documentPath);
return createSuccessResponse({ documentData, signedUrl });
} catch (error) {
console.error("Error processing document:", error);
return createErrorResponse("Failed to process document.");
}
};
// Helper functions: Response formatting
const createSuccessResponse = (data) => ({
statusCode: 200,
body: JSON.stringify(data),
});
const createErrorResponse = (message) => ({
statusCode: 500,
body: JSON.stringify({ error: message }),
});
export { handler };
Com isso, no back-end está devidamente estruturado e para você executá-lo localmente, siga os comandos abaixo:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/al1kr2fth75x5i1jbsfa.png)
A partir de então, você estará apto a fazer requisições na sua rota local a partir de qualquer navegador ou plataforma de API, como [Postman](https://www.postman.com/) ou [Apidog](https://apidog.com/).
**Aplicação Front-End:**
Para estruturação da aplicação Front-End, utilizamos o Tailwindcss + Vite + React TS, onde você pode encontrar o tutorial de inicialização do projeto na seguinte documentação: https://tailwindcss.com/docs/guides/vite
Após o passo-a-passo acima executado, seu projeto precisará de algumas dependências para exibir os PDFs em tela, assim como os highlights nos textos. Pensando nisso, utilizaremos a biblioteca react-pdf para podermos fazer esse processo no Front-End.
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gf7afqr4xwg7i6dda6ii.png)
Lembre-se de instalar a biblioteca como super usuário, pois ela utiliza configurações de sistema para realizar a exibição do documento.
Com isso feito, você precisará apenas criar um arquivo de componente e alterar o App.tsx, conforme orientações abaixo:
- Começando pela criação do src/components/TextDetection.tsx, que será o responsável pela exibição do PDF e marcação das caixas de posição das extrações:
```import import React, { useEffect, useRef, useState } from "react";
import { Props } from "../@types/blocks";
import { pdfjs } from "react-pdf";
import "react-pdf/dist/esm/Page/AnnotationLayer.css";
import "react-pdf/dist/esm/Page/TextLayer.css";
// Configuração do worker do PDF.js
pdfjs.GlobalWorkerOptions.workerSrc = new URL(
"pdfjs-dist/build/pdf.worker.min.mjs",
import.meta.url
).toString();
// Função para detectar cliques em caixas delimitadoras
const handleBoxClick = (
e: MouseEvent,
block: any,
width: number,
height: number,
setModalText: (text: string) => void
) => {
const canvas = e.target as HTMLCanvasElement;
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
const box = block.Geometry.BoundingBox;
const left = width * box.Left;
const top = height * box.Top;
const boxWidth = width * box.Width;
const boxHeight = height * box.Height;
if (x >= left && x <= left + boxWidth && y >= top && y <= top + boxHeight) {
setModalText(block.Text);
}
};
// Função para desenhar as caixas delimitadoras
const drawBoundingBoxes = (
ctx: CanvasRenderingContext2D,
width: number,
height: number,
canvas: HTMLCanvasElement,
response: Props["response"],
setModalText: (text: string) => void
) => {
response.blocks.forEach((block) => {
if (block.BlockType === "LINE") {
const box = block.Geometry.BoundingBox;
const left = width * box.Left;
const top = height * box.Top;
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
ctx.strokeRect(left, top, width * box.Width, height * box.Height);
canvas.addEventListener("click", (e) =>
handleBoxClick(e, block, width, height, setModalText)
);
}
});
};
// Serviço para carregar o PDF e desenhar caixas delimitadoras
const loadPdfAndDraw = async (
documentUrl: string,
canvasRefs: React.MutableRefObject<HTMLCanvasElement[]>,
response: Props["response"],
setModalText: (text: string) => void
) => {
try {
const pdf = await pdfjs.getDocument(documentUrl).promise;
for (let pageNumber = 1; pageNumber <= pdf.numPages; pageNumber++) {
const page = await pdf.getPage(pageNumber);
const viewport = page.getViewport({ scale: 1 });
const canvas = canvasRefs.current[pageNumber - 1];
if (!canvas) continue;
const ctx = canvas.getContext("2d");
if (!ctx) continue;
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({ canvasContext: ctx, viewport }).promise;
drawBoundingBoxes(
ctx,
viewport.width,
viewport.height,
canvas,
response,
setModalText
);
}
} catch (error) {
console.error("Erro ao carregar o PDF:", error);
}
};
// Componente principal
const TextDetectionCanvas: React.FC<Props> = ({
response,
documentUrl,
qtdPages,
}) => {
const canvasRefs = useRef<HTMLCanvasElement[]>([]);
const [modalText, setModalText] = useState<string | null>(null);
const closeModal = () => setModalText(null);
useEffect(() => {
if (documentUrl && response) {
loadPdfAndDraw(documentUrl, canvasRefs, response, setModalText);
}
}, [documentUrl, response]);
return (
<div>
{/* Renderizar os canvas */}
{Array.from({ length: qtdPages }).map((_, index) => (
<canvas key={index} ref={(el) => (canvasRefs.current[index] = el!)} />
))}
{/* Modal de texto */}
{modalText && <TextModal modalText={modalText} closeModal={closeModal} />}
</div>
);
};
// Componente para o modal de texto
const TextModal: React.FC<{ modalText: string; closeModal: () => void }> = ({
modalText,
closeModal,
}) => (
<div
style={{
position: "fixed",
top: 0,
left: 0,
width: "100vw",
height: "100vh",
backgroundColor: "rgba(0, 0, 0, 0.5)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 1000,
}}
>
<div
style={{
backgroundColor: "white",
padding: "20px",
borderRadius: "8px",
boxShadow: "0 2px 10px rgba(0, 0, 0, 0.3)",
}}
>
<div className="font-bold text-2xl flex gap-10">
<h1>Veja os Detalhes</h1>
<button onClick={closeModal}>X</button>
</div>
<p className="mt-5">Texto: {modalText}</p>
</div>
</div>
);
export default TextDetectionCanvas;
- Agora finalizando com a alteração do App.tsx, que será o responsável por fazer a integração com o Back-End a partir de uma lista de documentos:
```import import React, { useEffect, useState } from "react";
import TextDetectionCanvas from "./components/TextDetection";
import { Response } from "./@types/blocks";
// Constants
const DOCUMENTS = [
"LISTA DE DOCUMENTOS"
];
// Service Layer: Handles document fetching
const fetchDocumentData = async (
documentName: string,
setDocumentData: (data: Response) => void,
setDocumentUrl: (url: string) => void,
setPageNumber: (page: number) => void
) => {
try {
const response = await fetch(http://localhost:3000/${documentName}
);
const data = await response.json();
setDocumentData(data.documentData);
setPageNumber(data.documentData.pages);
setDocumentUrl(data.signedUrl);
} catch (error) {
console.error("Failed to fetch document data", error);
}
};
// Domain Layer: Main Application Component
const App: React.FC = () => {
const [documentData, setDocumentData] = useState();
const [documentUrl, setDocumentUrl] = useState("");
const [pageNumber, setPageNumber] = useState(1);
const [selectedDocument, setSelectedDocument] = useState(
DOCUMENTS[0]
);
// Fetch document data whenever the selected document changes
useEffect(() => {
setPageNumber(0);
setDocumentUrl("");
fetchDocumentData(
selectedDocument,
setDocumentData,
setDocumentUrl,
setPageNumber
);
}, [selectedDocument]);
return (
documents={DOCUMENTS}
selectedDocument={selectedDocument}
onDocumentSelect={setSelectedDocument}
/>
{documentUrl === "" ? (
Carregando...
) : (
documentData={documentData}
documentUrl={documentUrl}
pageNumber={pageNumber}
/>
)}
);
};
// UI Layer: Document Selector Component
const DocumentSelector: React.FC<{
documents: string[];
selectedDocument: string;
onDocumentSelect: (doc: string) => void;
}> = ({ documents, selectedDocument, onDocumentSelect }) => (
value={selectedDocument}
onChange={(e) => onDocumentSelect(e.target.value)}
>
{documents.map((document, index) => (
{document}
))}
);
// UI Layer: Document Viewer Component
const DocumentViewer: React.FC<{
documentData: Response | undefined;
documentUrl: string;
pageNumber: number;
}> = ({ documentData, documentUrl, pageNumber }) => (
response={documentData as Response}
documentUrl={documentUrl}
qtdPages={pageNumber}
/>
);
export default App;
Com as configurações realizadas, basta iniciar seu projeto por meio dos comandos abaixo:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2rjgbynl3gs0trjzr0zq.png)
**Resultado Final:**
Após a execução de todos os passos acima, você irá ter uma aplicação funcional que estará analisando os documentos, extraindo os textos e informando as posições onde encontram-se cada marcação daquele texto no PDF.
Exemplo do resultado final:
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/c4efvwo0ageyidi2s1c3.png)
![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/7a5k09uto60g27igemee.png)
Pensando até mesmo em evoluções futuras, como trata-se de uma solução de OCR e sabemos que erros podem ocorrer, você pode contar com a utilização de um LLM e uma base de conhecimento para poder indexar esses textos, corrigi-los e gerar respostas inteligentes a partir de determinado assunto.
Com isso, notamos que a aplicação *serverless* desenvolvida para extração de textos e exibição de layouts com Amazon Textract demonstrou uma solução eficaz para automatizar a análise de documentos em larga escala. Utilizando OCR avançado e ICR, a ferramenta não apenas extrai informações textuais, mas também identifica e organiza estruturas complexas, como tabelas e parágrafos, diretamente em PDFs. Com a integração entre o *back-end* (Node.js e Serverless Framework) e o *front-end* (React, Vite e TailwindCSS), a aplicação permite uma visualização intuitiva das marcações e dos textos extraídos.
Top comments (0)