Artigo Técnico

Extrair Imagens de PDFs com o PDFium Component em Delphi

O PDF guarda imagens como objetos de primeira classe dentro dos seus content streams. Quando uma página referencia uma fotografia, uma digitalização ou um diagrama, os dados de pixel residem num dicionário XObject ao lado da geometria da página. O PDFium Component expõe isso através de duas propriedades em TPdf: BitmapCount, que devolve quantos bitmaps incorporados existem na página atual, e Bitmap[Index], que descodifica um deles num TBitmap que passa a pertencer ao código chamador e que tem de ser libertado. Esse é todo o modelo de extração. O ciclo tem quatro linhas; o que exige critério é a mecânica à sua volta

Abrir o documento

A primeira coisa a saber sobre TPdf é que Active := True nunca gera uma exceção. Falhas de carregamento, palavras-passe erradas, ficheiros corrompidos: tudo isso é absorvido internamente e o componente simplesmente permanece inativo. É preciso verificar a flag manualmente após a atribuição, ou o código avança para o ciclo de páginas com PageCount a devolver zero, sem se perceber porque é que nada foi extraído

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'report.pdf';
    Pdf.Active := True;
    if not Pdf.Active then
    begin
      Writeln('Failed to open: ', Pdf.FileName);
      Exit;
    end;
    Writeln(Pdf.PageCount, ' pages');
    // prosseguir para a extração
  finally
    Pdf.Free;
  end;
end;

Os ficheiros protegidos por palavra-passe seguem o mesmo padrão: atribui-se Pdf.Password antes de definir Active := True. Se a palavra-passe estiver errada, Active permanece False e não há exceção nenhuma para apanhar. Numa ferramenta de processamento em lote que trata centenas de ficheiros, esse comportamento silencioso é na verdade útil: as falhas acumulam-se numa lista em vez de se desenrolar a pilha de chamadas para cada uma

Percorrer páginas e obter bitmaps

BitmapCount é por página, pelo que se define Pdf.PageNumber antes de a ler. Os números de página começam em 1; o valor predefinido é 0, o que significa que nenhuma página está carregada. A propriedade Bitmap[Index] é indexada a partir de zero e devolve um TBitmap que passa a pertencer ao código chamador. Tem de ser libertado. Negligenciar essa libertação dentro de um ciclo longo sobre um documento grande faz a memória subir rapidamente, porque cada bitmap pode representar vários megabytes de dados de pixel em bruto antes de qualquer compressão

Ciclo de extração de imagens PDF em Delphi com PDFium, verificando o sinalizador silencioso Active, definindo PageNumber por página, lendo BitmapCount, ignorando bitmaps nil e libertando cada TBitmap após gravar
O ciclo lê a contagem só depois de o cursor da página se mover, salta bitmaps nil de XObjects malformados, e liberta cada TBitmap propriedade do chamador antes de ir buscar a seguinte
procedure ExtractAllImages(Pdf: TPdf; const OutputDir: string);
var
  Page, Idx: Integer;
  Bmp: TBitmap;
  OutPath: string;
begin
  for Page := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := Page;
    for Idx := 0 to Pdf.BitmapCount - 1 do
    begin
      Bmp := Pdf.Bitmap[Idx];
      if not Assigned(Bmp) then
        Continue;
      try
        OutPath := Format('%s\p%d_img%d.bmp', [OutputDir, Page, Idx + 1]);
        Bmp.SaveToFile(OutPath);
      finally
        Bmp.Free;
      end;
    end;
  end;
end;

A verificação com Assigned é importante. Um pequeno número de geradores de PDF escreve XObjects de imagem com dimensões de pixel nulas ou dados de outra forma malformados; nesses casos, o componente devolve nil em vez de um bitmap vazio. Tratar um retorno nil como um erro e interromper a extração é o reflexo errado: deve ignorar-se essa entrada, registar a página e o índice caso seja necessária uma trilha de auditoria, e continuar. O resto da página ainda pode produzir imagens válidas

Repare que o ciclo exterior define Pdf.PageNumber em cada iteração. É essa atribuição que carrega a página para o estado interno do componente e torna BitmapCount significativo. Se for omitida, lê-se repetidamente a contagem da mesma página. O padrão parece redundante ao escrevê-lo, mas é assim que a API foi concebida: a página é um cursor, não uma coleção

Escolher um formato de saída

O BMP não tem perdas e está sempre disponível sem unidades adicionais, o que o torna uma opção predefinida sólida quando ainda não se sabe o que a imagem contém. Quando a dimensão do ficheiro é relevante, o formato de pixel do TBitmap devolvido indica qual o codec adequado. Um bitmap de 32 bits transporta um canal alfa; o PNG preserva-o sem perdas. Uma imagem grande de 24 bits com tom contínuo é candidata a JPEG. Imagens mais pequenas ou desenhadas com uma paleta limitada geralmente ficam melhor em BMP do que convertidas em JPEG, que acrescenta artefactos de blocos em definições de qualidade baixas e poupa pouco nas definições altas

procedure SaveBitmap(Bmp: TBitmap; const FileName: string);
var
  Jpg: TJPEGImage;
begin
  case UpperCase(ExtractFileExt(FileName)) of
    '.JPG', '.JPEG':
      begin
        Jpg := TJPEGImage.Create;
        try
          Jpg.Assign(Bmp);
          Jpg.CompressionQuality := 85;
          Jpg.SaveToFile(FileName);
        finally
          Jpg.Free;
        end;
      end;
  else
    Bmp.SaveToFile(FileName);  // BMP: sem perdas, sem unidades extra
  end;
end;

Na prática, a escolha do formato é orientada por Bmp.PixelFormat e pelas dimensões. Se PixelFormat = pf32bit é necessário um formato que transporte alfa; o PNG é a escolha óbvia, embora exija a unidade PNGImage nas versões mais antigas do Delphi. Para imagens de 24 bits com largura superior a cerca de 300 pixels, o JPEG com qualidade 85 dá uma redução de tamanho de três para um relativamente ao BMP, sem perda percetível na maioria do conteúdo fotográfico. Abaixo desse limiar, o BMP tem uma dimensão comparável e evita por completo qualquer decisão sobre qualidade

O que o BitmapCount conta e o que não conta

O PDF distingue entre XObjects de imagem e gráficos vetoriais desenhados com operadores de caminho. Uma página que pareça visualmente complexa pode devolver um BitmapCount de zero se todos os elementos forem vetoriais. As páginas digitalizadas quase sempre devolvem exatamente um: o scanner escreve toda a digitalização como um único XObject de imagem de página inteira, na resolução que tiver sido definida no scanner. As páginas que combinam texto tipografado com fotografias incorporadas devolvem uma entrada por fotografia. Linhas decorativas, fundos sombreados e limites de tabela normalmente nem sequer aparecem na contagem de bitmaps

O que o BitmapCount cobre numa página PDF no PDFium, contando XObjects de imagem, como digitalizações e fotografias, enquanto texto vetorial, linhas de régua, preenchimentos sombreados e imagens inline ficam de fora da contagem
Uma página digitalizada devolve exatamente um XObject de imagem de página inteira, enquanto uma página puramente vetorial pode parecer movimentada e devolver contagem zero

A contagem também não inclui imagens inline, uma construção do PDF raramente usada em que os dados de imagem são incorporados diretamente no content stream da página em vez de num XObject nomeado. Estas ficam fora do que esta API expõe; são suficientemente pouco comuns em documentos reais para que a maioria das ferramentas de extração simplesmente não as trate

Um pormenor a ter em conta: o BitmapCount lido é o da página atual à data da última atribuição a PageNumber. Se o código ramificar ou chamar qualquer função que altere PageNumber entre a contagem e a obtenção, podem ler-se menos imagens do que o espaço reservado, ou indexar além do fim. A leitura da contagem e o ciclo Bitmap[] devem manter-se na mesma página, sem tocar em PageNumber entretanto

Usar o TPdfView numa aplicação de formulário

O componente TPdfView expõe as mesmas propriedades BitmapCount e Bitmap[], mas a página de onde lê é a página atualmente apresentada na vista, não TPdf.PageNumber. Os dois ponteiros de página são independentes; alterar um não desloca o outro. Numa aplicação de formulário VCL com um visualizador ativo, pode chamar-se Pdf.PageNumber := N para conduzir a extração através de TPdf, enquanto o visualizador permanece na página para onde o utilizador tiver deslocado a vista pela última vez. Essa separação é intencional e mantém limpo o estado de apresentação do visualizador enquanto decorre uma extração em segundo plano

Memória e desempenho em trabalhos em lote

Num arquivo de grande dimensão, o orçamento de memória é o principal aspeto a vigiar. Cada chamada a Bitmap[] aloca um novo TBitmap na heap, e numa página digitalizada a 300 DPI isso são facilmente 25 MB de dados de pixel em bruto antes de qualquer codificação. Se as páginas forem processadas num ciclo apertado sem libertação entre iterações, o conjunto de memória em uso cresce linearmente com o número de imagens. A forma correta é sempre: obter um bitmap, fazer o que for necessário, libertá-lo, obter o seguinte. Se for preciso manter referências a vários bitmaps ao mesmo tempo para um passo de comparação, devem contar-se primeiro com BitmapCount e reservar o contentor em conformidade, libertando depois cada um assim que deixar de ser necessário, em vez de adiar a limpeza para o fim do documento. Num documento com 500 páginas digitalizadas, essa distinção pode representar a diferença entre 25 MB e 12 GB de pico de RSS

As propriedades BitmapCount e Bitmap[] apresentadas aqui fazem parte do PDFium Component para Delphi e C++Builder