As dimensões de uma página PDF são fixadas no momento em que a página é criada, pelo que não é possível simplesmente redimensionar o conteúdo no lugar da mesma forma que se redimensionaria uma imagem. O modelo da biblioteca que torna a redução prática é capturar-e-redesenhar: retirar o conteúdo de cada página do documento para um identificador, criar uma nova página em branco com o tamanho de papel original e depois desenhar o conteúdo capturado a uma caixa delimitadora reduzida. O espaço em branco circundante torna-se a margem. A uma escala de 70% numa página A4, por exemplo, 15% da largura fica de cada lado e a mesma fração no topo e na base, que é exatamente o que a aritmética de bordas abaixo produz.
Como funciona o CapturePage
O CapturePage recebe um número de página, eleva o conteúdo dessa página para um objeto de captura em memória e remove a página da árvore de páginas do documento. Essa remoção é intencional e é o motivo pelo qual o ciclo seleciona sempre a página 1 independentemente do índice de iteração: assim que a página 1 é capturada e eliminada, o que era a página 2 passa a ser a nova página 1, e assim por diante. Se incrementar o seletor de página juntamente com o contador do ciclo, saltará uma página em cada duas e o resultado terá metade das páginas esperadas.
O identificador de captura devolvido pelo CapturePage não é uma referência de página; é mais como um instantâneo do conteúdo. Permanece válido até chamar DrawCapturedPage ou libertá-lo explicitamente. O DrawCapturedPage recebe esse identificador e um retângulo de destino definido como deslocamento à esquerda, deslocamento na base, largura e altura, todos em pontos. A biblioteca escala o conteúdo capturado para caber exatamente nesse retângulo, preservando a proporção apenas se o retângulo corresponder às proporções originais. Para uma escala uniforme, o retângulo deve ser o tamanho original multiplicado pelo fator de escala, centrado na página.
A matemática de centração
Com um fator de escala de 70%, os 30% restantes de cada dimensão são divididos igualmente entre os dois lados. Assim, o recuo horizontal é pageWidth * (1.0 - 0.70) / 2, que corresponde a 15% da largura, e o recuo vertical segue a mesma fórmula usando a altura da página. O retângulo de destino do DrawCapturedPage começa então em (horizBorder, vertBorder) e abrange pageWidth - 2 * horizBorder por pageHeight - 2 * vertBorder. Esta aritmética não é específica da biblioteca; é simplesmente a geometria de encaixar um retângulo menor simetricamente dentro de um maior.
Vale a pena notar: o SetOrigin(1) coloca a origem das coordenadas no canto superior esquerdo em vez do canto inferior esquerdo. Os valores de borda que passa ao DrawCapturedPage são medidos a partir da origem que definiu, pelo que se mudar os modos de origem entre o carregamento e o desenho, a centração ficará desalinhada.
Exemplo em C#
O código seguinte processa todas as páginas de Pages.pdf através do ciclo capturar-e-redesenhar e escreve o resultado em newpages.pdf. PDFL é o objeto wrapper ActiveX/COM adicionado ao projeto a partir de PDFlibDLL64.dll.
private void ScalePages_Click(object sender, EventArgs e)
{
File.Delete("newpages.pdf");
double pageWidth, pageHeight, horizBorder, vertBorder;
double scaleFactor = 0.70;
int capturedPageId, ret;
PDFL.LoadFromFile("Pages.pdf", "");
PDFL.SetOrigin(1);
int numPages = PDFL.PageCount();
for (int i = 1; i <= numPages; i++)
{
// Always select page 1: CapturePage removes the page, so page 2
// becomes page 1 on the next iteration.
PDFL.SelectPage(1);
pageWidth = PDFL.PageWidth();
pageHeight = PDFL.PageHeight();
horizBorder = pageWidth * (1.0 - scaleFactor) / 2;
vertBorder = pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId = PDFL.CapturePage(1);
PDFL.NewPage();
PDFL.SetPageDimensions(pageWidth, pageHeight);
ret = PDFL.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
}
PDFL.SaveToFile("newpages.pdf");
}
Exemplo em Delphi
A versão Delphi usa o TPDFlib diretamente em vez da camada COM, mas a sequência de chamadas é idêntica. Uma diferença prática é a proteção do ficheiro de saída: FileExists mais DeleteFile em vez de File.Delete, porque o SaveToFile falhará se o destino estiver bloqueado por uma execução anterior ainda aberta num visualizador.
procedure TForm1.ScalePagesClick(Sender: TObject);
var
PDFLib: TPDFlib;
pageWidth, pageHeight, horizBorder, vertBorder: Double;
scaleFactor: Double;
capturedPageId, ret, numPages, i: Integer;
begin
if FileExists('newpages.pdf') then
DeleteFile('newpages.pdf');
scaleFactor := 0.70;
PDFLib := TPDFlib.Create;
try
PDFLib.LoadFromFile('Pages.pdf', '');
PDFLib.SetOrigin(1);
numPages := PDFLib.PageCount();
for i := 1 to numPages do
begin
PDFLib.SelectPage(1);
pageWidth := PDFLib.PageWidth();
pageHeight := PDFLib.PageHeight();
horizBorder := pageWidth * (1.0 - scaleFactor) / 2;
vertBorder := pageHeight * (1.0 - scaleFactor) / 2;
capturedPageId := PDFLib.CapturePage(1);
PDFLib.NewPage();
PDFLib.SetPageDimensions(pageWidth, pageHeight);
ret := PDFLib.DrawCapturedPage(
capturedPageId,
horizBorder, vertBorder,
pageWidth - 2 * horizBorder,
pageHeight - 2 * vertBorder);
end;
PDFLib.SaveToFile('newpages.pdf');
finally
PDFLib.Free;
end;
end;
O que o fator de escala controla efetivamente
O valor 0,70 aqui significa que o conteúdo renderizado ocupa 70% de cada dimensão da página, não que o ficheiro tem 70% do seu tamanho original em bytes. O tamanho do ficheiro após esta operação depende da complexidade do conteúdo original; uma página com imagens de grande dimensão não encolherá proporcionalmente porque os dados de píxeis são redesenhados com a mesma resolução numa área menor. Se o objetivo é a compressão ao nível dos bytes, a abordagem correta é o LinearizeFile ou reguardar com compressão de fluxos, não a escala geométrica.
O valor de 70% também não é um limite fixo. Qualquer valor entre 0,0 e 1,0 funciona, e valores acima de 1,0 ampliam o conteúdo para além do limite da página original, o que é recortado na borda da caixa de média a menos que aumente também as dimensões da página. Documentos com tamanhos mistos são tratados de forma natural porque o PageWidth e o PageHeight são consultados por página antes do cálculo das bordas, pelo que um documento onde as páginas ímpares são A4 e as páginas pares são A3 produzirá saída corretamente centrada em cada tamanho de página sem qualquer tratamento especial.
Onde as coisas podem correr mal
Na prática surgem dois modos de falha. O primeiro é um ficheiro de saída deixado aberto num visualizador de PDF de uma execução anterior: o SaveToFile falhará ou escreverá zero bytes dependendo da plataforma, e a nova saída nunca chega a ser escrita. A proteção de eliminação do ficheiro no início da função resolve isso para o desenvolvimento, mas num pipeline de produção é mais seguro escrever para um caminho temporário e renomear em caso de sucesso.
O segundo é a incompatibilidade na contagem de páginas. Como o CapturePage remove páginas do documento à medida que as processa, o valor lido de PageCount() antes do ciclo é o limite correto para iterar. Chamar PageCount() dentro do ciclo devolveria um número decrescente em cada iteração e terminaria mais cedo, deixando as últimas páginas por processar. A variável de ciclo nos exemplos serve apenas como contador de iterações restantes; nunca é usada para selecionar uma página, porque a página a selecionar é sempre a 1 pela razão explicada anteriormente.
As chamadas de manipulação de páginas aqui apresentadas, incluindo CapturePage, DrawCapturedPage e SetPageDimensions, fazem parte da Biblioteca PDF losLab para Delphi, C#, VB.NET e C++.