Articolo tecnico

Ridimensionamento delle pagine PDF al 70% con la 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

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 a una pagina; è più simile a uno snapshot del contenuto. Rimane valido finché non si chiama DrawCapturedPage o lo si rilascia esplicitamente. DrawCapturedPage richiede quell'handle oltre a un rettangolo di destinazione definito come offset sinistro, offset inferiore, larghezza e altezza, tutti espressi in punti. La libreria ridimensiona il contenuto catturato per adattarlo esattamente a tale rettangolo, preservando le proporzioni solo se il rettangolo corrisponde alle proporzioni originali. Per un ridimensionamento uniforme, il rettangolo deve avere le dimensioni originali moltiplicate 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

Una cosa da notare: SetOrigin(1) posiziona l'origine delle coordinate nell'angolo in alto a sinistra anziché in basso a sinistra. I valori del bordo passati a DrawCapturedPage sono misurati a partire dall'origine impostata, quindi se si cambia la modalità di origine tra il caricamento e il disegno, il centraggio risulterà errato

Esempio in C#

Il seguente codice elabora ogni pagina di Pages.pdf attraverso il ciclo di cattura e ridisegno, e scrive il risultato in newpages.pdf. PDFL è l'oggetto wrapper ActiveX/COM aggiunto al progetto da 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");
}

Esempio in Delphi

La versione Delphi utilizza direttamente TPDFlib anziché lo strato COM, ma la sequenza di chiamate è identica. Una differenza pratica è la protezione del file di output: l'uso combinato di FileExists e DeleteFile anziché File.Delete, poiché SaveToFile fallirà se la destinazione è bloccata da un'esecuzione precedente ancora aperta in un visualizzatore

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;

Cosa controlla effettivamente il fattore di scala

Il valore 0.70 in questo caso indica che il contenuto visualizzato occupa il 70% di ciascuna dimensione della pagina, e non che il file finale sarà pari al 70% delle sue dimensioni in byte originali. La dimensione del file dopo questa operazione dipende dalla complessità del contenuto originale; una pagina contenente immagini di grandi dimensioni non si ridurrà proporzionalmente perché i dati dei pixel vengono ridisegnati alla stessa risoluzione in un'area più piccola. Se l'obiettivo è la compressione a livello di byte, l'approccio corretto consiste nell'usare LinearizeFile o salvare nuovamente il file con compressione del flusso (stream compression), anziché applicare un ridimensionamento geometrico

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 riguarda la discrepanza nel conteggio delle pagine. Poiché CapturePage rimuove le pagine dal documento durante l'elaborazione, il numero di pagine letto da PageCount() prima del ciclo rappresenta il limite corretto su cui iterare. Chiamare PageCount() all'interno del ciclo restituirebbe un numero decrescente ad ogni passaggio, determinando un'uscita anticipata che lascerebbe non elaborate le ultime pagine. La variabile del ciclo negli esempi serve solo come contatore delle iterazioni rimanenti; non viene mai utilizzata per selezionare una pagina, poiché la pagina da selezionare è sempre la 1 per la ragione spiegata in precedenza

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