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
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
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