Artigo Técnico

Combine Imagens Digitalizadas em um PDF com o Componente PDFium

Uma equipe de processamento de sinistros tinha trinta anos de arquivos em papel passando por um scanner com alimentação de folhas. O scanner cuspiu um JPEG por página em uma pasta, com os nomes 0001.jpg, 0002.jpg e assim por diante. O que o arquivo realmente precisava era de um PDF por arquivo de caso, com as páginas em ordem, para que um revisor pudesse abrir um único documento em vez de clicar em cem miniaturas de imagens. Essa última etapa, transformando uma pilha numerada de digitalizações em um único PDF ordenado, é o trabalho aqui

O Componente PDFium lida com isso diretamente. Além da renderização e da extração de texto, o componente pode criar um PDF do zero: criar um documento vazio, adicionar uma página em branco com o tamanho que você desejar, soltar uma imagem nessa página nas coordenadas do espaço do usuário e depois salvar. Todo o pipeline reside no componente TPdf, portanto, um conversor em lote é um loop sobre nomes de arquivo além de um punhado de chamadas

Um pipeline em lote Delphi usa as chamadas AddPage, PageNumber, AddImage e SaveAs do PDFium Component para transformar uma pasta de digitalizações numeradas em um único PDF ordenado
Cada digitalização se torna uma página criada por AddPage e direcionada por PageNumber antes de AddImage desenhá-la; SaveAs grava o documento pronto uma vez

O formato da conversão

Três coisas precisam acontecer para cada digitalização. Você decide o tamanho da página, você coloca a imagem dentro da página deixando uma margem, e você avança para a próxima página. O Componente PDFium lhe dá um método para cada: AddPage cria uma página em branco com um determinado tamanho, AddImage (ou AddPicture se você já tiver um TPicture) desenha o bitmap na página atual, e PageNumber diz ao componente qual página as chamadas de desenho subsequentes têm como alvo

A única coisa que confunde as pessoas é o sistema de coordenadas. O espaço do usuário do PDF coloca a origem no canto inferior esquerdo da página, com o Y aumentando para cima, o oposto das coordenadas de tela que os desenvolvedores Delphi buscam por reflexo. O X, Y que você passa para AddImage é o canto inferior esquerdo do retângulo da imagem, e Width, Height são o tamanho da colocação em pontos, não o tamanho em pixels do arquivo de origem. Faça o inverso e suas digitalizações cairão fora da página ou de cabeça para baixo em relação a onde você esperava que elas estivessem

Criando o documento e uma página por digitalização

Comece com um documento vazio. CreateDocument aloca um PDF novo e deixa o componente ativo, portanto, não há uma etapa de abertura separada. A partir daí você percorre a lista de arquivos digitalizados e, para cada um, você adiciona uma página, a torna atual e coloca a imagem. As dimensões da página aqui são A4 em pontos (595 × 842 retrato), o tamanho de folha padrão para correspondência arquivada

procedure TArchiveForm.ScansToPdf(const Files: TStrings; const OutputPath: string);
const
  PageW = 595.0;   // largura A4 em pontos
  PageH = 842.0;   // altura A4 em pontos
  Margin = 36.0;   // borda de meia polegada ao redor de cada digitalização
var
  I: Integer;
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;                       // novo, vazio, já ativo
    for I := 0 to Files.Count - 1 do
    begin
      Pdf.AddPage(I + 1, PageW, PageH);       // índice de página baseado em 1
      Pdf.PageNumber := I + 1;                // torna a nova página atual
      PlaceScan(Pdf, Files[I], PageW, PageH, Margin);
    end;
    Pdf.SaveAs(OutputPath);
  finally
    Pdf.Free;
  end;
end;

Cada iteração cria uma página e imediatamente define o PageNumber para ela. Essa segunda linha importa: AddPage insere a página, mas os métodos de desenho atuam na página que estiver atual, então definir o PageNumber é o que direciona AddImage para a página que você acabou de criar. Pule isso e suas imagens vão se empilhar em qualquer página que por acaso estivesse carregada antes

Uma suposição se esconde naquele loop: a ordem de Files. Um scanner nomeia páginas de 0001.jpg a 0100.jpg, mas uma enumeração de diretório nem sempre as retorna classificadas e, no momento em que você encontra a page9.jpg perto da page10.jpg, uma classificação de string simples coloca a página 10 antes da página 9. Classifique a lista explicitamente antes do loop e prefira nomes preenchidos com zeros na hora da digitalização, para que a ordem lexical corresponda à ordem das páginas. A sequência das páginas é a primeira coisa que um revisor percebe e é o erro mais barato de se evitar

Colocando uma digitalização e mantendo a sua proporção

Raramente uma digitalização tem o mesmo formato da página. Se você a esticar para preencher a folha, você distorce o texto; se você a colocar no tamanho total em pixels, ela transborda. A solução é escalar pela menor das duas proporções, adequar à largura ou adequar à altura e centralizar o que sobrar. Como a origem fica no canto inferior esquerdo, a centralização significa dividir o espaço restante uniformemente e adicioná-lo tanto ao X quanto ao Y

Um diagrama de página A4 mostra como o AddImage do PDFium Component posiciona uma digitalização escalada dentro das margens usando código Delphi e origem no canto inferior esquerdo
AddImage recebe o canto inferior esquerdo do retângulo de posicionamento, então ajustar e centralizar são calculados em pontos de página a partir da origem
procedure TArchiveForm.PlaceScan(Pdf: TPdf; const FileName: string;
  PageW, PageH, Margin: Double);
var
  Pic: TPicture;
  AvailW, AvailH, Scale, DrawW, DrawH, X, Y: Double;
begin
  Pic := TPicture.Create;
  try
    Pic.LoadFromFile(FileName);              // BMP, JPG, PNG etc. pelas unidades gráficas da VCL

    AvailW := PageW - 2 * Margin;
    AvailH := PageH - 2 * Margin;

    // Ajusta dentro das margens sem distorcer a digitalização.
    Scale := Min(AvailW / Pic.Width, AvailH / Pic.Height);
    DrawW := Pic.Width * Scale;
    DrawH := Pic.Height * Scale;

    // Centraliza: espaço restante dividido igualmente. Y medido a partir do fundo da página.
    X := (PageW - DrawW) / 2;
    Y := (PageH - DrawH) / 2;

    Pdf.AddImage(FileName, X, Y, DrawW, DrawH);
  finally
    Pic.Free;
  end;
end;

Isso carrega o arquivo uma vez para ler as suas dimensões em pixels, calcula uma escala uniforme única e passa o retângulo de posicionamento para AddImage. O AddImage aceita um caminho de arquivo diretamente e o encaminha por meio do mesmo pipeline de imagem do AddPicture, portanto, qualquer formato que as unidades gráficas VCL reconheçam funciona sem casos especiais. Se você já tem a imagem decodificada em um TPicture vinda do painel de visualização, chame o AddPicture(Pic, X, Y, DrawW, DrawH) contendo o idêntico arranjo sem efetuar a leiturada e carregar de novo

Pulando a decodificação para digitalizações em JPEG

Scanners quase sempre geram arquivos JPEG. Carregar um JPEG em um TPicture o decodifica para bitmap e, na hora de salvar, o PDFium recodifica essa imagem — dois ciclos com perda de qualidade que você não precisa enfrentar. O AddJpegImage incorpora os bytes comprimidos originais diretamente na página a partir de um stream, o que é ao mesmo tempo mais rápido e visualmente mais limpo para um lote de grande volume

AddJpegImage incorpora os bytes JPEG originais em uma página do PDFium Component, enquanto AddImage decodifica e SaveAs recodifica os pixels em Delphi
AddJpegImage incorpora os bytes comprimidos do scanner como estão, evitando as passadas de decodificação e recodificação que AddImage e SaveAs realizam
var
  Stream: TFileStream;
begin
  // ... depois de AddPage + PageNumber para a página atual ...
  Stream := TFileStream.Create(FileName, fmOpenRead);
  try
    // Incorpora os bytes do JPEG como estão; sem ciclo de decodificação/recodificação.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Você continua calculando X, Y, DrawW e DrawH da mesma forma, já que ainda precisa das dimensões em pixels para definir a escala. Leia esses valores do arquivo ou faça uma leitura rápida do cabeçalho e, em seguida, passe o stream bruto para o AddJpegImage. Para digitalizações em PNG ou TIFF, o caminho correto é o AddImage; reserve o atalho do JPEG apenas para o formato ao qual ele realmente se aplica

Rotulando cada página

Arquivos digitalizados ficam mais fáceis de auditar quando cada página traz o nome do arquivo de origem. O AddText desenha uma string em uma coordenada do espaço do usuário, de modo que a legenda fique logo abaixo da imagem. Lembre-se do eixo Y invertido: para colocar um rótulo abaixo da digitalização, você subtrai a partir da borda inferior da imagem, em vez de somar a ela:

// Legenda abaixo da digitalização: Y diminui em direção ao fundo da página.
Pdf.AddText('File: ' + ExtractFileName(FileName), 'Helvetica', 9,
  X, Y - 14, clGray);

Um último detalhe sobre o salvamento. O SaveAs é uma função que retorna um Boolean, então, em código de produção, verifique o resultado em vez de simplesmente supor que a gravação deu certo; um disco cheio ou um caminho de saída bloqueado falha silenciosamente se você não checar. Assim que o loop terminar e o arquivo for gravado, você terá exatamente o que o arquivo precisava: um PDF ordenado por processo, com as páginas escaladas para caber, pronto para ser lido em qualquer visualizador

Os mesmos blocos de construção cobrem tarefas relacionadas. Troque a regra de dimensionamento por página e você tem um álbum de fotos com uma imagem por folha; mantenha o loop, mas leia a partir de uma origem TIFF multipágina, e você tem um conversor de arquivo de fax. Se quiser uma visão mais ampla sobre a criação de PDFs de forma programática, veja criando documentos PDF do zero com o Componente PDFium; para renderizar o resultado de volta na tela mais tarde, veja convertendo páginas de PDF para imagens JPEG com o Componente PDFium

O Componente PDFium da loslab.com reúne as APIs de criação de documentos, renderização e texto usadas ao longo desta série