Serverless App - Extração de Textos com Exibição de Layouts com Textract

gugamainchein

Gustavo Mainchein

Posted on October 7, 2024

Serverless App - Extração de Textos com Exibição de Layouts com Textract

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:

Image description

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
Enter fullscreen mode Exit fullscreen mode
  • 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"
  }
}
Enter fullscreen mode Exit fullscreen mode
  • 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 });
Enter fullscreen mode Exit fullscreen mode

} 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;
Enter fullscreen mode Exit fullscreen mode
  • 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.
Enter fullscreen mode Exit fullscreen mode
💖 💪 🙅 🚩
gugamainchein
Gustavo Mainchein

Posted on October 7, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related