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
| Finalidade | DPI | Qualidade JPEG |
|---|---|---|
| Miniatura de lista | 72 | 60-70 |
| Pré-visualização no ecrã | 96-150 | 80-85 |
| Visualização de detalhe | 200-300 | 85-95 |
| Original para impressão | 300-600 | 90-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