Artigo Técnico

Renderizar Páginas de PDF para JPEG com o PDFium Component em Delphi

A renderização de uma página de PDF para JPEG consiste em duas operações que as pessoas tendem a executar juntas e depois a depurar separadamente. Primeiro, rasteriza-se a página num bitmap de píxeis na resolução escolhida. Em seguida, passa-se esse bitmap para um codificador JPEG e escolhe-se a qualidade. O PDFium Component assegura a primeira metade através do RenderPage; a segunda metade é puramente VCL, com o TJPEGImage da unidade Vcl.Imaging.jpeg. A união entre elas é onde residem as decisões interessantes, porque a resolução que escolhe no lado da renderização e a qualidade que define na codificação afetam-se mutuamente e ao tamanho do ficheiro final, sendo fácil errar nessa proporção

O aspeto a interiorizar antes de qualquer código: uma página PDF não tem píxeis. É descrita em pontos, onde um ponto equivale a 1/72 de polegada, e a página é um desenho vetorial medido nesses pontos. Quando solicita a renderização ao PDFium, está a escolher em quantos píxeis projeta esse desenho, e essa escolha é o DPI. Se errar no cálculo, obterá uma miniatura desfocada quando pretendia uma imagem de alta qualidade, ou alocará um bitmap de 200 megapíxeis para algo destinado a ser uma pré-visualização de 120 píxeis

De DPI para dimensões em píxeis

O RenderPage requer a largura (Width) e a altura (Height) do píxel em números inteiros, não o DPI. Portanto, a primeira tarefa é fazer a conversão. Uma página reporta o seu tamanho em pontos através de PageWidth e PageHeight (ambos do tipo Double), e a conversão é a mesma que qualquer rasterizador utiliza: os píxeis são iguais aos pontos multiplicados pelo DPI de destino e divididos por 72. Uma página em formato Letter americano tem 612 por 792 pontos. A 150 DPI, isso torna-se 1275 por 1650 píxeis; a 72 DPI, permanece 612 por 792 (um píxel por ponto), que é a proporção de identidade que as pessoas frequentemente se esquecem

// Pdf.PageNumber must already point at the page you want.
PixelW := Round(Pdf.PageWidth  * Dpi / 72);
PixelH := Round(Pdf.PageHeight * Dpi / 72);
Bitmap := Pdf.RenderPage(0, 0, PixelW, PixelH, ro0, [], clWhite);
// ... use Bitmap ...
Bitmap.Free;   // the function-form RenderPage hands you ownership

Dois detalhes nessas quatro linhas decidem se o código está correto. O primeiro é que a função RenderPage devolve um TBitmap que você possui. O PDFium alocou-o e terminou a sua tarefa; se não o libertar (Free) em cada iteração, um processamento em lote de centenas de páginas provocará uma fuga de memória de centenas de bitmaps até que a aplicação falhe. O segundo detalhe é o argumento de cor, aqui definido como clWhite. As páginas PDF são normalmente desenhadas assumindo um fundo branco opaco, e uma página com transparência renderizada no fundo incorreto produz margens desfocadas ou halos escuros indesejados. O branco é o padrão adequado para quase todos os documentos; o parâmetro existe para o caso raro em que não o é

Os valores 0, 0 representam os desvios de margem esquerda (Left) e superior (Top) na página, no espaço de coordenadas redimensionado, e deve mantê-los a zero a menos que esteja a recortar a página. O ro0 define a rotação: mantenha a zero e o PDFium respeitará a rotação que a página declarar na sua entrada /Rotate, pelo que uma página criada em formato horizontal (landscape) sairá nesse mesmo formato sem que tenha de fazer nada

Codificar o bitmap como JPEG

Uma vez criado o bitmap, o JPEG is a parte fácil, sendo puramente Delphi. O TJPEGImage.Assign copia o bitmap, o CompressionQuality define a qualidade numa escala de 1 a 100, e o SaveToFile grava o ficheiro. A única regra de ordenação é que a qualidade deve ser definida antes de gravar, uma vez que controla a codificação que o SaveToFile aciona

uses
  Vcl.Graphics, Vcl.Imaging.jpeg, PDFium;

procedure SavePageAsJpeg(Pdf: TPdf; PageNumber, Dpi, Quality: Integer;
  const FileName: string);
var
  Bitmap: TBitmap;
  Jpeg: TJPEGImage;
begin
  Pdf.PageNumber := PageNumber;
  Bitmap := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Dpi / 72),
    Round(Pdf.PageHeight * Dpi / 72),
    ro0, [], clWhite);
  try
    Jpeg := TJPEGImage.Create;
    try
      Jpeg.Assign(Bitmap);
      Jpeg.CompressionQuality := Quality;   // 1..100
      Jpeg.SaveToFile(FileName);
    finally
      Jpeg.Free;
    end;
  finally
    Bitmap.Free;
  end;
end;

Esse bloco try/finally encadeado parece excessivamente complexo para uma função auxiliar de página única, mas é o correto para um processamento em lote. O bloco interno liberta o codificador, o bloco externo liberta o bitmap, e a ativação de qualquer um deles numa exceção continua a libertar os recursos em uso. Se os fundisse num só, uma exceção durante a codificação deixaria o bitmap retido em memória. Num processamento longo, isto é a diferença entre um conversor que conclui a tarefa e um que falha a meio com uma mensagem de falta de memória

Escolher o DPI e a qualidade em conjunto

Os dois controlos não são independentes do objetivo da imagem de saída, e o erro comum é aumentar ambos por excesso de precaução. Uma miniatura para a web renderizada a 300 DPI e gravada com qualidade 95 resultará em várias centenas de kilobytes para apresentar uma imagem de 120 píxeis; o navegador descartará quase toda essa informação ao redimensioná-la para baixo. Faça corresponder a resolução aos píxeis de que a imagem de saída necessita e escolha uma qualidade que sobreviva à compressão com perdas do JPEG sem artefactos visíveis

FinalidadeDPIQualidade JPEG
Miniatura de lista7260-70
Pré-visualização no ecrã96-15080-85
Visualização de detalhe200-30085-95
Original para impressão300-60090-100

A qualidade do JPEG merece uma atenção especial. Não se trata de uma escala linear. O salto de 70 para 85 traz uma melhoria visual real com um aumento de ficheiro moderado; o salto de 95 para 100 quase duplica o tamanho do ficheiro para uma diferença que quase ninguém nota, porque a qualidade 100 continua a não ser livre de perdas, apenas reduz a eliminação de dados. Para páginas ricas em texto, a compressão do JPEG gera um efeito de halo em torno dos contornos dos glifos, pelo que uma qualidade inferior a 80 torna o texto desfocado. Se as páginas forem maioritariamente texto e puder alterar formatos, o PNG renderiza esse texto sem artefactos; o JPEG é a escolha indicada para conteúdo fotográfico e misto onde o seu tamanho é menor

Miniaturas mais rápidas e pequenas

Quando o objetivo é uma miniatura e não uma reprodução fiel, pode indicar ao renderizador para fazer menos esforço. O parâmetro Options recebe um conjunto de opções do tipo TRenderOption, e algumas delas sacrificam a fidelidade pela velocidade, exatamente o que se pretende numa pequena pré-visualização. O reGrayscale fornece tons de cinza, o que acelera a renderização e gera um bitmap menor para codificação. O reNoSmoothImage e o reNoSmoothPath ignoram o anti-aliasing, que seria invisível numa escala de miniatura

function RenderThumbnail(Pdf: TPdf; PageNumber, MaxW, MaxH: Integer): TBitmap;
var
  Scale: Double;
begin
  Pdf.PageNumber := PageNumber;
  // Fit the page inside MaxW x MaxH while preserving aspect ratio.
  Scale := Min(MaxW / Pdf.PageWidth, MaxH / Pdf.PageHeight);
  Result := Pdf.RenderPage(0, 0,
    Round(Pdf.PageWidth  * Scale),
    Round(Pdf.PageHeight * Scale),
    ro0, [reGrayscale, reNoSmoothImage], clWhite);
end;

O caso das miniaturas também ilustra a forma mais limpa de pensar no dimensionamento. Em vez de utilizar o DPI, calcule um fator de escala único que ajuste a página dentro de uma caixa limite e preserve a proporção, que é o que o Min das duas proporções faz. Uma página em modo vertical e outra em modo horizontal acabam por caber na mesma caixa sem distorção, sem ter de calcular qual o DPI correspondente a \"ajustar em 200 por 280\". Uma chamada de atenção com o reGrayscale: este converte o conteúdo de imagem rasterizada para tons de cinzento, mas os preenchimentos vetoriais e o texto mantêm os seus valores de cor no motor de renderização, pelo que uma página que seja maioritariamente arte vetorial pode reter alguma cor. Para um resultado em tons de cinzento completo e fiável, a função GrayscalePdfBitmap presente nas notas de referência é o caminho mais seguro

Processar um documento completo em lote

Juntar tudo para um documento completo consiste num ciclo sobre o PageCount, alterando o PageNumber uma página de cada vez. As páginas são indexadas a partir de 1: a página um é PageNumber := 1, e o ciclo corre até ao PageCount inclusive, e não PageCount - 1. O outro aspeto que o lote deve respeitar é o carregamento silencioso. Definir Active := True nunca gera exceções em ficheiros corrompidos ou com palavra-passe incorreta; apenas mantém a propriedade Active como False. Verifique este estado antes de renderizar qualquer página, caso contrário o RenderPage tentará atuar num documento que nunca foi aberto

procedure ExportAllPages(const PdfPath, OutDir: string; Dpi, Quality: Integer);
var
  Pdf: TPdf;
  I, Digits: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := PdfPath;
    Pdf.Active := True;
    if not Pdf.Active then
      raise Exception.Create('Could not open ' + PdfPath);

    Digits := Length(IntToStr(Pdf.PageCount));   // zero-pad so files sort right
    for I := 1 to Pdf.PageCount do
      SavePageAsJpeg(Pdf, I, Dpi, Quality,
        Format('%s\page_%.*d.jpg', [OutDir, Digits, I]));
  finally
    Pdf.Active := False;
    Pdf.Free;
  end;
end;

O preenchimento com zeros (zero-padding) através da variável Digits é um pequeno detalhe que evita problemas futuros. Se nomear os ficheiros de page_1.jpg a page_10.jpg, qualquer ferramenta que os ordene como strings colocará o page_10 logo após o page_1, alterando a sequência correta das páginas. Adicionar zeros até à largura do maior número de página (de modo que um documento de 300 páginas resulte em page_001.jpg) mantém a ordem lexical e das páginas idênticas em qualquer fase posterior

Para documentos suficientemente grandes em que a conversão demore um tempo considerável, execute-a fora da thread da interface de utilizador ou processe mensagens entre páginas para que a aplicação não fique bloqueada, e dê ao utilizador uma forma de cancelar a operação. Se estiver a renderizar páginas muito grandes e pretender um cancelamento imediato a meio da renderização da própria página, o PDFium Component tem um método de renderização progressiva com um token de cancelamento; este é um mecanismo mais complexo do que o exigido pela maioria das exportações em lote, mas está disponível quando necessário

Uma última combinação útil a conhecer. Rasterizar uma página descarta a sua camada de texto: o JPEG contém apenas píxeis, e as palavras presentes nele já não podem ser selecionadas ou pesquisadas. Quando necessita tanto da imagem como do texto subjacente, renderize para obter a imagem e extrae o texto separadamente, conforme abordado no artigo sobre a extração de texto de documentos PDF com o PDFium Component. As sobrecargas do RenderPage e as opções de renderização apresentadas aqui fazem parte do Componente PDFium Component para Delphi e C++Builder