Technical Article

Reduzir Páginas PDF para 70% com a Biblioteca PDF losLab

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