Articolo tecnico

Ridimensionare le pagine PDF al 70% con losLab PDF Library

Le dimensioni delle pagine PDF sono fisse al momento della creazione della pagina, quindi non è possibile ridimensionare semplicemente il contenuto sul posto nello stesso modo in cui si ridimensiona un'immagine. Il modello della libreria che rende pratica la riduzione è il meccanismo di cattura e ridisegno (capture-and-redraw): si estrae il contenuto di ciascuna pagina dal documento in un handle, si crea una nuova pagina vuota con le dimensioni multimediali originali e poi si ridisegna il contenuto catturato all'interno di un riquadro di delimitazione (bounding box) ridotto. Lo spazio bianco circostante diventa il margine. Con una scala al 70% su una pagina A4, ad esempio, il 15% della larghezza ricade su ciascun lato e la stessa frazione in alto e in basso, che è esattamente ciò che produce l'aritmetica dei bordi descritta di seguito

PDF: pipeline cattura-e-ridisegna: ogni pagina PDF viene catturata in un handle e ridisegnata su una pagina bianca fresca mantenuta alla dimensione media originale
Cattura-e-ridisegno solleva il contenuto di ogni pagina in un handle e lo ridisegna a dimensione ridotta su una pagina nuova delle dimensioni originali

Come funziona CapturePage

CapturePage accetta il numero di una pagina, promuove il contenuto di quella pagina in un oggetto di cattura in memoria e rimuove la pagina dall'albero delle pagine del documento. Questa rimozione è intenzionale ed è il motivo per cui il ciclo seleziona sempre la pagina 1, indipendentemente dall'indice di iterazione: una volta che la pagina 1 viene catturata ed eliminata, quella che era la pagina 2 diventa la nuova pagina 1, e così via. Se si incrementa il selettore di pagina insieme al contatore del ciclo, si salterà una pagina sì e una no, ottenendo la metà dell'output previsto

L'handle di cattura restituito da CapturePage non è un riferimento alla pagina; è più simile a uno snapshot del contenuto. Resta valido dopo il disegno, quindi la stessa cattura può essere piazzata più di una volta, finché non la liberi con ReleaseCapturedPage o il documento non va via. DrawCapturedPage prende quell'handle più un rettangolo di destinazione dato come bordo sinistro, bordo superiore, larghezza e altezza, tutto in punti per impostazione predefinita. La libreria scala il contenuto catturato per entrare esattamente in quel rettangolo, preservando le proporzioni solo se il tuo rettangolo per caso corrisponde alle proporzioni originali. Per una scala uniforme vuoi il rettangolo della dimensione originale moltiplicata per il fattore di scala, centrato sulla pagina

I calcoli per il centraggio

Con un fattore di scala del 70%, il restante 30% di ogni dimensione viene diviso equamente tra i due lati. Quindi il rientro orizzontale é pageWidth * (1.0 - 0.70) / 2, che corrisponde al 15% della larghezza, e il rientro verticale segue la stessa formula utilizzando l'altezza della pagina. Il rettangolo di destinazione per DrawCapturedPage inizia quindi a (horizBorder, vertBorder) ed ha un'estensione di pageWidth - 2 * horizBorder per pageHeight - 2 * vertBorder. Questa aritmetica non è specifica della libreria; si tratta semplicemente della geometria necessaria per inserire simmetricamente un rettangolo più piccolo all'interno di uno più grande

PDF: alla scala 70% il rettangolo di destinazione siede centrato sulla pagina originale con un bordo del 15 percento calcolato per ogni lato
Scalare al 70% lascia un bordo del 15 percento per lato, e DrawCapturedPage riceve quel rettangolo centrato in punti

Una cosa da notare: SetOrigin(1) mette l'origine delle coordinate in alto a sinistra anziché in basso a sinistra. Il secondo valore che passi a DrawCapturedPage è il bordo superiore del rettangolo, misurato da quale origine hai impostato. Ecco perché gli esempi chiamano SetOrigin(1) prima: con l'origine predefinita in basso a sinistra lo stesso vertBorder metterebbe il bordo superiore vicino al fondo della pagina e spingerebbe gran parte del contenuto fuori

Esempio in C#

Il codice seguente elabora ogni pagina di Pages.pdf attraverso il ciclo cattura-e-ridisegno e scrive il risultato su newpages.pdf. PDFL è una istanza della classe di import PDFlibrary che fa parte dell'edizione DLL, creata con new PDFlibrary("PDFlibDLL64.dll"); l'edizione ActiveX espone gli stessi metodi attraverso PDFlibAX.PDFlibrary

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++)
    {
        // Seleziona sempre la pagina 1: CapturePage rimuove la pagina, così la pagina 2
        // diventa pagina 1 alla iterazione successiva.
        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);
        // 0 = niente catturato, per esempio una sorgente di una pagina: l'unica pagina non può essere rimossa
        if (capturedPageId == 0)
            throw new InvalidOperationException("CapturePage failed on source page " + i);

        PDFL.NewPage();
        PDFL.SetPageDimensions(pageWidth, pageHeight);

        ret = PDFL.DrawCapturedPage(
            capturedPageId,
            horizBorder, vertBorder,
            pageWidth  - 2 * horizBorder,
            pageHeight - 2 * vertBorder);
        if (ret != 1)
            throw new InvalidOperationException("DrawCapturedPage failed on source page " + i);
    }

    PDFL.SaveToFile("newpages.pdf");
}

Esempio in Delphi

La versione Delphi usa TPDFlib direttamente anziché attraverso la classe di import della DLL, ma la sequenza di chiamate è identica. Una differenza pratica è la guardia sul file di output: FileExists più DeleteFile invece di File.Delete, perché SaveToFile fallisce se la destinazione è bloccata da una esecuzione precedente ancora aperta in un viewer

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);
      // 0 = niente catturato, per esempio una sorgente di una pagina: l'unica pagina non può essere rimossa
      if capturedPageId = 0 then
        raise Exception.CreateFmt('CapturePage failed on source page %d', [i]);

      PDFLib.NewPage();
      PDFLib.SetPageDimensions(pageWidth, pageHeight);

      ret := PDFLib.DrawCapturedPage(
        capturedPageId,
        horizBorder, vertBorder,
        pageWidth  - 2 * horizBorder,
        pageHeight - 2 * vertBorder);
      if ret <> 1 then
        raise Exception.CreateFmt('DrawCapturedPage failed on source page %d', [i]);
    end;

    PDFLib.SaveToFile('newpages.pdf');
  finally
    PDFLib.Free;
  end;
end;

Cosa controlla effettivamente il fattore di scala

Il valore 0.70 qui significa che il contenuto renderizzato occupa il 70% di ogni dimensione della pagina, non che il file è il 70% della sua dimensione in byte originale. La dimensione del file dopo questa operazione dipende dalla complessità del contenuto originale; una pagina con immagini grandi non si ridurrà proporzionalmente perché i dati pixel vengono ridisegnati alla stessa risoluzione in un'area più piccola. Se l'obiettivo è un file più piccolo, guarda a CompressContent per gli stream di contenuto pagina usciti non compressi e alla ricompressione delle immagini (OptimizeImages nell'edizione Delphi), non alla scala geometrica. LinearizeFile è un altro strumento: riordina il file per il caricamento una pagina alla volta (Fast Web View), aiuta i viewer ad aprire file grandi in rete ma non li riduce

La cifra del 70% non è inoltre un limite rigido. Qualsiasi valore compreso tra 0.0 e 1.0 funziona, e valori superiori a 1.0 ingrandiscono il contenuto oltre il bordo originale della pagina, il quale viene ritagliato in corrispondenza del bordo del media box, a meno che non si aumentino anche le dimensioni della pagina. I documenti con dimensioni di pagina miste vengono gestiti in modo naturale, poiché PageWidth e PageHeight vengono rilevati per ogni pagina prima del calcolo del bordo. Pertanto, un documento in cui le pagine dispari sono in formato A4 e quelle pari sono A3 produrrà un output correttamente centrato per ciascuna dimensione di pagina, senza necessità di casi speciali

Dove le cose possono andare storte

Nella pratica si riscontrano due modalità di errore principali. La prima riguarda un file di output che è rimasto aperto in un visualizzatore PDF da un'esecuzione precedente: in questo caso, SaveToFile fallirà o scriverà zero byte a seconda della piattaforma, impedendo la corretta creazione del nuovo output. La protezione tramite eliminazione del file all'inizio della funzione gestisce questa situazione in fase di sviluppo, ma in una pipeline di produzione è più sicuro scrivere su un percorso temporaneo e rinominare il file solo in caso di successo

La seconda è la discrepanza nel conteggio delle pagine. Siccome CapturePage rimuove le pagine dal documento mentre le elabora, il conteggio che leggi da PageCount() prima del ciclo è il limite corretto contro cui iterare. Chiamare PageCount() dentro il ciclo non aiuta neanche lì: ogni passata rimuove una pagina e ne aggiunge una, quindi il conteggio resta piatto e non dice nulla su quante pagine sorgente restano. La variabile del ciclo negli esempi serve solo da contatore di iterazioni rimanenti; non viene mai usata per selezionare una pagina, perché la pagina da selezionare è sempre la 1 per il motivo spiegato prima

PDF: diagramma del ciclo che mostra perché ogni passata deve chiamare SelectPage(1), dato che le pagine catturate vengono rimosse dall'albero e le pagine rimanenti slittano in avanti
CapturePage rimuove ogni pagina elaborata così la coda si rinomina da sé, il che fa della pagina 1 l'unico valore di selezione corretto

Le chiamate di manipolazione delle pagine mostrate qui, tra cui CapturePage, DrawCapturedPage e SetPageDimensions, fanno parte della losLab PDF Library per Delphi, C#, VB.NET e C++