Artigo Técnico

Renderizar Páginas de PDF para Imagens JPEG no Delphi com o Componente PDFium

Renderizar uma página PDF para um JPEG envolve duas operações que as pessoas tendem a executar juntas e depois depurar separadamente. Primeiro, você rasteriza a página em um bitmap de pixels em uma resolução de sua escolha. Em seguida, você passa esse bitmap para um codificador JPEG e escolhe uma qualidade. O Componente PDFium é responsável pela primeira metade através do RenderPage; a segunda metade é VCL puro, TJPEGImage da Vcl.Imaging.jpeg. A junção entre elas é onde residem as decisões interessantes, porque a resolução que você escolhe no lado da renderização e a qualidade que você escolhe no lado da codificação se equilibram entre si e com o tamanho do arquivo de maneiras fáceis de errar

O que você deve internalizar antes de qualquer código: uma página PDF não possui pixels. Ela é descrita em pontos, onde um ponto equivale a 1/72 de polegada, e a página é um desenho vetorial medido nesses pontos. Quando você pede ao PDFium para renderizar, você está escolhendo sobre quantos pixels projetar esse desenho, e essa escolha é o DPI. Se errar na aritmética, você renderizará uma miniatura borrada quando queria uma matriz de impressão, ou alocará um bitmap de 200 megapixels para algo destinado a ser uma visualização de 120 pixels

De DPI para dimensões de pixels

O RenderPage requer Width e Height inteiros em pixels, não um DPI. Então, o primeiro trabalho é converter. Uma página informa seu tamanho em pontos através de PageWidth e PageHeight (ambos Double), e a conversão é a mesma que todo rasterizador usa: pixels equivalem a pontos vezes o DPI alvo dividido por 72. Uma página US Letter tem 612 por 792 pontos. A 150 DPI isso se torna 1275 por 1650 pixels; a 72 DPI ela permanece com 612 por 792, um pixel por ponto, o que é o caso que as pessoas esquecem que é apenas a identidade

// 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 nestas quatro linhas decidem se o código está correto. O primeiro é que o formato de função de RenderPage retorna um TBitmap que você possui. O PDFium o alocou e o deixou; se você não fizer o Free em cada iteração, um lote de algumas centenas de páginas vazará algumas centenas de bitmaps e o processo inchará até que algo falhe. O segundo é o argumento Color, clWhite aqui. As páginas PDF geralmente são desenhadas assumindo um substrato branco opaco, e uma página com transparência renderizada em uma cor de fundo incorreta produz bordas turvas ou halos escuros dispersos. O branco é o padrão correto para quase todos os documentos; o parâmetro existe para os raros casos em que não é

Os 0, 0 são os deslocamentos de Left e Top na página, no espaço de coordenadas dimensionado, e você os deixa em zero, a menos que esteja cortando. O ro0 é a rotação: deixe-o em zero e o PDFium respeitará qualquer rotação que a página já declare em sua entrada /Rotate, de modo que uma página criada em paisagem sai em paisagem sem você precisar fazer nada

Codificando o bitmap como JPEG

Assim que o bitmap existe, o JPEG é a parte fácil e é Delphi puro. O TJPEGImage.Assign copia o bitmap para dentro dele, CompressionQuality define a qualidade em uma escala de 1 a 100, e SaveToFile grava o arquivo. A única regra de ordenação é que a qualidade deve ser definida antes de você salvar, porque ela governa 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;

Aquele try/finally aninhado parece exigente para um utilitário de uma única página, mas é perfeitamente adequado para processamento em lote. O bloco interno libera o codificador, o bloco externo libera o bitmap, e se qualquer um deles for acionado em uma exceção ainda assim libera o que possui. Se você juntar os dois em um só, uma exceção durante a codificação poderá deixar o bitmap órfão. A longo prazo, essa é a diferença entre um conversor que termina e um que morre na página 300 com um arquivo corrompido e uma caixa de diálogo de falta de memória

Escolhendo DPI e qualidade juntos

Os dois controles não são independentes do objetivo da saída, e o erro comum é aumentar ambos por precaução. Uma miniatura para web renderizada a 300 DPI e salva na qualidade 95 tem centenas de kilobytes fingindo ser uma imagem de 120 pixels; o navegador descarta quase tudo no redimensionamento. Combine a resolução com os pixels que a saída realmente necessita e depois escolha uma qualidade que sobreviva à compressão com perdas do JPEG sem artefatos visíveis

SaídaDPIQualidade JPEG
Miniatura de lista7260-70
Visualização em tela96-15080-85
Visualização de altos detalhes200-30085-95
Matriz de impressão300-60090-100

A qualidade JPEG merece uma palavra de cautela por si só. Não é um controle linear. O salto de 70 para 85 proporciona uma melhora visual real para um crescimento modesto do arquivo; o salto de 95 para 100 aproximadamente dobra o arquivo por uma diferença que quase ninguém pode ver, porque a qualidade 100 ainda não é sem perdas, ela apenas para de descartar muito. Para páginas carregadas de texto, a compressão baseada em blocos do JPEG mancha as bordas nítidas dos glifos em um ruído fraco, e é por isso que uma qualidade abaixo de cerca de 80 cria um texto com aspecto de digitalizado naquilo que deveria ser uma saída nítida. Se as páginas forem principalmente texto e você puder alterar os formatos, o PNG renderiza esse texto sem o ruído; o JPEG ganha seu espaço em conteúdo fotográfico e misto onde sua compressão é genuinamente menor

Miniaturas mais rápidas e menores

Quando o destino for uma miniatura em vez de uma reprodução fiel, você pode dizer ao renderizador para fazer menos trabalho. O parâmetro Options usa um conjunto de flags TRenderOption e algumas delas trocam a fidelidade pela velocidade exatamente do jeito que uma pequena pré-visualização precisa. reGrayscale remove a cor, o que renderiza mais rápido e produz um bitmap menor para codificar. reNoSmoothImage e reNoSmoothPath ignoram o anti-aliasing que de qualquer forma é invisível na escala da 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 da miniatura também mostra a forma mais limpa de pensar no dimensionamento. Em vez de passar por DPI, calcule um único fator de escala que caiba a página dentro de uma caixa delimitadora e preserve a proporção da imagem, que é o que o Min das duas proporções faz. Uma página em retrato e uma página em paisagem terminam dentro da mesma caixa sem distorção, e você nunca tem que pensar em que DPI corresponde a "caber em 200 por 280". Uma advertência sobre o reGrayscale: ele converte conteúdo de imagem rasterizada para cinza, mas preenchimentos vetoriais e textos mantêm seus valores de cor no motor, então uma página que for principalmente arte vetorial pode voltar menos monocromática do que o nome da flag sugere. Para um resultado verdadeiramente totalmente em escala de cinza, converter o bitmap renderizado usando GrayscalePdfBitmap é o caminho confiável

Processando todo um documento em lote

Reunir tudo para um documento completo é um loop sobre o PageCount, com o PageNumber avançando uma página de cada vez. As páginas são baseadas em 1: a página um é PageNumber := 1, e o loop vai até PageCount de forma inclusiva, e não PageCount - 1. A outra coisa que o lote deve respeitar é o contrato de carregamento silencioso. Definir Active := True nunca gera exceção em um arquivo danificado ou com senha incorreta; ele simplesmente deixa o Active como False. Verifique isso antes de renderizar uma única página, senão o primeiro RenderPage trabalhará sobre um 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 através de Digits é um detalhe que salva uma tarde mais tarde. Se nomear os arquivos de page_1.jpg a page_10.jpg, qualquer ferramenta que os ordenar como strings colocará page_10 logo depois de page_1, embaralhando a ordem. Preencher de acordo com a largura do maior número de página, de forma que um documento de 300 páginas crie page_001.jpg, mantém a ordem léxica e a ordem das páginas idênticas em todos os pontos subsequentes

Para documentos suficientemente grandes de modo que a conversão leve um tempo perceptível, execute-o fora da thread da UI ou bombeie as mensagens entre as páginas para que o aplicativo permaneça responsivo, e ofereça ao usuário uma forma de parar. Se você estiver renderizando páginas muito grandes e quiser um cancelamento que afete o meio da página e não apenas entre as páginas, o Componente PDFium possui um caminho de renderização progressiva com um token de cancelamento; esse é um mecanismo mais pesado que a maioria das exportações em lote necessita, mas ele está lá quando uma única página a 600 DPI for lenta o suficiente para travar

Uma última combinação que vale a pena conhecer. Rasterizar uma página descarta sua camada de texto: o JPEG é feito de pixels, e as palavras nele não são mais selecionáveis ou pesquisáveis. Quando você precisa da imagem e do texto subjacente, renderize para obter a imagem e extraia o texto separadamente, processo abordado no artigo complementar sobre a extração de texto de documentos PDF com o Componente PDFium. As sobrecargas e opções de renderização do RenderPage exibidas aqui fazem parte do Componente PDFium para Delphi e C++Builder