Implementando consulta em PDF com Laravel

Recentemente eu recebi a tarefa de implementar uma funcionalidade que permitisse a geração de um PDF a partir de um arquivo de consulta. A ideia era que o usuário pudesse fazer a consulta em mais de 8000 arquivos que pertenciam a um jornal com mais de 100 anos.

Esses arquivos já foram digitalizados e estavam disponíveis no servidor, mas o cliente precisava de uma forma de fazer a consulta de forma mais rápida e eficiente.

Como a aplicação já disponibilizava de um painel de gerenciamento em laravel, com todos esses PDFs já previamente cadastrados. A solução mais viável foi cadastrar as páginas do PDF no banco de dados, assim poderia ser feito a consulta de forma mais rápida com uma simples consulta SQL.

Para isso, utilizei a biblioteca PdfParser para fazer a leitura dos arquivos PDF e salvar as páginas no banco de dados.

Primeiramente eu fiz duas migrations, uma para criar a tabela de PDFs e outra para criar a tabela de páginas.

php artisan make:migration create_pdfs_table
php artisan make:migration create_pages_table

Então, no arquivo de migration de PDFs, eu adicionei os campos necessários para o cadastro do PDF.

Schema::create('pdfs', function (Blueprint $table) {
    $table->id();
    $table->string('name');
    $table->string('path');
    $table->int('number_of_pages');
    $table->timestamps();
});

Schema::create('pages', function (Blueprint $table) {
    $table->id();
    $table->foreignId('pdf_id')->constrained();
    $table->integer('number');
    $table->text('content');
    $table->timestamps();
});

Então, no model de PDF, eu adicionei o relacionamento com as páginas.

class Pdf extends Model
{
    protected $guarded = [];

    public function pages()
    {
        return $this->hasMany(Page::class);
    }
}

class Page extends Model
{
   protected $guarded = [];

   public function pdf()
   {
       return $this->belongsTo(Pdf::class);
   }
}

Para instalar a biblioteca, basta executar o comando abaixo:

composer require smalot/pdfparser

Aqui é um simples formulário para enviar o PDF para o servidor.

<form
  action="{{ route('newspapers.store') }}"
  method="post"
  enctype="multipart/form-data"
>
  @csrf
  <input type="file" name="pdf" />
  <button type="submit">Salvar</button>
</form>

Então, no controller, vou adicionar a função para salvar o pdf. Depois estarei pegando o conteúdo do PDF e a quantidade de páginas, essa quantidade vou salvar no campo number_of_pages. Após isso vou fazer um foreach nas páginas do PDF e salvar no banco de dados.

public function store()
{
    $pdfPath = request()->file("pdf")->store("pdf", "public");

    $parser = new \Smalot\PdfParser\Parser();
    $pdf = $parser->parseContent(file_get_contents($pdfPath));
    $numberOfPages = count($pdf->getPages());

    $pdfCreated = Pdf::create([
        'path' => $pdfPath,
        'number_of_pages' => $numberOfPages,
    ]);

    foreach ($pdf->getPages() as $number => $value) {
        Page::updateOrCreate(
            [
                "newspaper_id" => $pdfCreated->id,
                "page_number" => $number + 1,
            ],
            [
                "newspaper_id" => $pdfCreated->id,
                "page_number" => $number + 1,
                "content" => iconv(
                    "ISO-8859-1",
                    "UTF-8",
                    utf8_decode($value->getText())
                ),
            ]
        );
    }
}

Note que estou usando a função icon para converter o texto para UTF-8, isso é necessário pois os PDFs estavam em ISO-8859-1. Verifique qual a codificação do seu PDF e faça a conversão necessária. E também estou usando o utf8_decode para converter os caracteres especiais.

Essa parte dos caracteres especiais é bastante complicada, é bom dar uma atenção a isso.

Agora, para fazer a consulta, basta fazer uma simples consulta SQL.

public function search()
{
    $pdf = Pdf::whereHas('pages', function ($q) {
        $q->where('content', 'like', '%' . request('search') . '%');
    })->get();

    return response()->json($pages);
}

Essa é uma implementação simples, na aplicação real eu não coloquei a leitura das páginas junto com o upload do arquivo, eu fiz uma fila para processar os PDFs, pois a leitura de um PDF pode estourar o limite de memória facilmente.

Espero que esse artigo tenha te ajudado! Se tiver alguma dúvida pode falar comigo nas minhas redes sociais.

Até a próxima!