Artigo Técnico

Combinar Imagens Digitalizadas num Único PDF com o PDFium Component em Delphi

Uma equipa de processamento de sinistros tinha trinta anos de ficheiros em papel a passar por um digitalizador alimentado por folhas. O digitalizador gerava um JPEG por página para uma pasta, com nomes como 0001.jpg, 0002.jpg e assim sucessivamente. O que o arquivo realmente necessitava era de um único PDF por ficheiro de caso, com as páginas ordenadas, para que um revisor pudesse abrir um único documento em vez de clicar em centenas de miniaturas de imagens. Esse último passo, transformar uma pilha numerada de imagens digitalizadas num único PDF ordenado, é o trabalho aqui abordado

O PDFium Component trata disso diretamente. Além da renderização e da extração de texto, o componente pode construir um PDF do zero: criar um documento vazio, adicionar uma página em branco dimensionada como desejar, colocar uma imagem nessa página em coordenadas do espaço do utilizador e, em seguida, gravar. Todo o processo reside no componente TPdf, pelo que um conversor em lote se resume a um ciclo sobre nomes de ficheiros mais algumas chamadas de métodos

Um pipeline em lote Delphi usa as chamadas AddPage, PageNumber, AddImage e SaveAs do PDFium Component para transformar uma pasta de digitalizações numeradas num único PDF ordenado
Cada digitalização torna-se uma página criada por AddPage e apontada por PageNumber antes de AddImage a desenhar; SaveAs escreve o documento concluído uma vez

A estrutura da conversão

Três coisas têm de acontecer para cada imagem digitalizada. Decide o tamanho da página, coloca a imagem dentro da página deixando uma margem e avança para a página seguinte. O PDFium Component fornece um método para cada uma destas ações: o AddPage cria uma página em branco com um determinado tamanho, o AddImage (ou o AddPicture se já possuir um TPicture) desenha o bitmap na página atual, e o PageNumber indica ao componente quais as páginas que as chamadas de desenho seguintes têm como alvo

O único detalhe que baralha as pessoas é o sistema de coordenadas. O espaço do utilizador do PDF coloca a origem no canto inferior esquerdo da página, com o Y a crescer para cima, o oposto das coordenadas do ecrã que os programadores Delphi utilizam por reflexo. O X, Y que passa ao AddImage é o canto inferior esquerdo do retângulo da imagem, e o Width, Height representa o tamanho de colocação em pontos, não o tamanho em píxeis do ficheiro de origem. Se errar nesta lógica, as suas imagens digitalizadas irão parar fora da página ou de pernas para o ar em relação ao esperado

Criar o documento e uma página por imagem digitalizada

Comece com um documento vazio. O CreateDocument aloca um novo PDF e deixa o componente ativo, pelo que não existe um passo de abertura separado. A partir daí, percorre a lista de ficheiros digitalizados e, para cada um deles, adiciona uma página, torna-a ativa e coloca a imagem. As dimensões da página aqui são as do formato A4 em pontos (595 × 842 em modo vertical), 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;   // margem de meia polegada à volta 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 define imediatamente o PageNumber para a mesma. Essa segunda linha é crucial: o AddPage insere a página, mas os métodos de desenho atuam na página que estiver ativa, pelo que definir o PageNumber é o que direciona o AddImage para a página que acabou de criar. Ignore este passo e as suas imagens acumular-se-ão na página que estivesse carregada anteriormente

Uma suposição oculta-se nesse ciclo: a ordenação dos Files. Um digitalizador nomeia as páginas de 0001.jpg a 0100.jpg, mas a enumeração de uma pasta nem sempre as devolve ordenadas. No momento em que encontra o page9.jpg junto ao page10.jpg, uma ordenação simples de strings coloca a página 10 antes da página 9. Ordene a lista explicitamente antes do ciclo e prefira nomes com preenchimento de zeros no momento da digitalização para que a ordem lexical corresponda à ordem das páginas. A sequência de páginas é a primeira coisa que um revisor nota e é o erro mais barato de evitar

Colocar uma imagem digitalizada e manter a sua proporção

Uma digitalização raramente tem a mesma forma que a página. Se a esticar para preencher a folha, distorce o texto; se a colocar com o tamanho de píxel original, ela ultrapassa os limites. A solução é dimensionar pela menor das duas proporções (ajuste de largura ou ajuste de altura) e centrar o espaço restante. Como a origem está no canto inferior esquerdo, centrar significa dividir o espaço restante uniformemente e adicioná-lo tanto ao X como ao Y

Um diagrama de página A4 mostra como o AddImage do PDFium Component coloca uma digitalização dimensionada dentro das margens usando código Delphi e uma origem inferior esquerda
AddImage toma o canto inferior esquerdo do retângulo de colocação, pelo que o ajuste e a centragem 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. através das unidades gráficas da VCL

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

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

    // Centrar: o espaço restante dividido uniformemente. O 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;

Isto carrega o ficheiro uma vez para ler as suas dimensões em píxeis, calcula uma escala uniforme e passa o retângulo de colocação ao AddImage. O AddImage aceita diretamente um caminho de ficheiro e encaminha-o através do mesmo processo de imagem que o AddPicture, pelo que qualquer formato que as unidades gráficas da VCL reconheçam funciona sem tratamento especial. Se já tiver a imagem descodificada num TPicture a partir de um painel de pré-visualização, chame AddPicture(Pic, X, Y, DrawW, DrawH) com o mesmo retângulo e evite uma segunda leitura do ficheiro

Evitar a descodificação em imagens JPEG

Os digitalizadores emitem quase sempre ficheiros JPEG. Carregar um JPEG num TPicture descodifica-o para um bitmap, e depois o PDFium volta a codificá-lo ao gravar (dois processos com perdas de qualidade desnecessários). O método AddJpegImage incorpora os bytes comprimidos originais diretamente na página a partir de um fluxo (stream), o que é mais rápido e visualmente mais limpo para um processamento em lote de grande volume

O AddJpegImage incorpora os bytes JPEG originais numa página do PDFium Component, enquanto o AddImage descodifica e o SaveAs recodifica os píxeis em Delphi
AddJpegImage incorpora os bytes comprimidos da digitalização como estão, evitando as passagens de descodificaçã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 JPEG tal como estão; sem ciclo de descodificação/re-codificação.
    Pdf.AddJpegImage(Stream, X, Y, DrawW, DrawH);
  finally
    Stream.Free;
  end;
end;

Continua a calcular o X, Y, DrawW e DrawH da mesma forma, pois necessita das dimensões em píxeis para redimensionar. Leia-as a partir do ficheiro ou através de uma rápida análise do cabeçalho, e depois entregue o fluxo direto ao AddJpegImage. Para digitalizações em PNG ou TIFF, o caminho do AddImage é o correto; reserve o atalho do JPEG apenas para o formato a que realmente se aplica

Identificar cada página

As imagens digitalizadas em arquivo são mais fáceis de auditar quando cada página contém o nome do seu ficheiro de origem. O AddText desenha uma string numa coordenada do espaço do utilizador, pelo que a legenda fica posicionada logo abaixo da imagem. Lembre-se do eixo Y invertido: para colocar uma legenda abaixo da digitalização, subtrai à margem inferior da imagem em vez de somar

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

Um último ponto sobre a gravação. O SaveAs é uma função que retorna um Boolean, pelo que no código de produção deve verificar o seu resultado em vez de assumir que a gravação foi bem-sucedida; caso contrário, um disco cheio ou um caminho de saída bloqueado falhará silenciosamente. Assim que o ciclo terminar e o ficheiro estiver gravado, terá exatamente o que o arquivo necessitava: um PDF ordenado por ficheiro de caso, com páginas dimensionadas para ajuste, pronto para ser lido em qualquer visualizador

Os mesmos blocos de construção cobrem tarefas semelhantes. Altere a regra de dimensionamento por página e obterá um álbum de fotografias com uma imagem por folha; mantenha o ciclo mas leia a partir de uma origem TIFF multipágina e terá um conversor de arquivo de fax. Se pretender uma visão mais ampla da construção programática de PDFs, consulte o artigo sobre a criação de documentos PDF de raiz com o PDFium Component; para renderizar o resultado de volta ao ecrã mais tarde, consulte a conversão de páginas PDF em imagens JPEG com o PDFium Component

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