Articol tehnic

Scalarea paginilor PDF la 70% cu biblioteca PDF losLab

Dimensiunile unei pagini PDF sunt fixate în momentul în care pagina este creată, așa că nu puteți pur și simplu să rescalați conținutul pe loc, așa cum ați redimensiona o imagine. Modelul de bibliotecă ce face micșorarea practicabilă este capture-and-redraw: scoateți conținutul fiecărei pagini din document într-un handle, creați o pagină nouă goală la dimensiunea media originală, apoi desenați conținutul capturat înapoi într-un dreptunghi de încadrare redus. Spațiul alb rămas devine marginea. La o scală de 70% pe o pagină A4, de exemplu, 15% din lățime cade pe fiecare parte, plus aceeași fracțiune sus și jos, exact ce produce aritmetica de margine de mai jos

PDF: conductă de captură-și-re-desenare: fiecare pagină PDF este capturată într-un handle și desenată înapoi pe o pagină goală proaspătă, păstrată la mărimea originală de mediu
Capture-and-redraw ridică conținutul fiecărei pagini într-un handle și îl redesenează la dimensiune redusă pe o pagină nouă de dimensiuni originale

Cum funcționează CapturePage

CapturePage primește un număr de pagină, promovează conținutul acelei pagini într-un obiect de captură din memorie și scoate pagina din arborele de pagini al documentului. Eliminarea este intenționată și este motivul pentru care bucla selectează mereu pagina 1, indiferent de indicele iterației: odată ce pagina 1 este capturată și ștearsă, ce era pagina 2 devine noua pagină 1, și tot așa. Dacă incrementați selectorul de pagină odată cu contorul buclei, veți sări peste fiecare a doua pagină și veți rămâne cu jumătate din rezultatul așteptat

Handle-ul de captură returnat de CapturePage nu este o referință la pagină; seamănă mai degrabă cu un instantaneu al conținutului. Rămâne valid până când apelați DrawCapturedPage sau îl eliberați explicit. DrawCapturedPage primește acel handle plus un dreptunghi destinație dat ca offset stânga, offset jos, lățime și înălțime, toate în puncte. Biblioteca scalează conținutul capturat ca să încapă exact în acel dreptunghi și păstrează raportul de aspect doar dacă dreptunghiul vostru se nimerește să aibă proporțiile originale. Pentru o scalare uniformă vreți ca dreptunghiul să fie dimensiunea originală înmulțită cu factorul de scală, centrat pe pagină

Aritmetica de centrare

Cu un factor de scală de 70%, cei 30% rămași din fiecare dimensiune se împart egal între cele două părți. Deci decalajul orizontal este pageWidth * (1.0 - 0.70) / 2, adică 15% din lățime, iar decalajul vertical urmează aceeași formulă folosind înălțimea paginii. Dreptunghiul destinație pentru DrawCapturedPage pornește atunci din (horizBorder, vertBorder) și se întinde pe pageWidth - 2 * horizBorder cu pageHeight - 2 * vertBorder. Aritmetica asta nu ține de bibliotecă; este pur și simplu geometria încadrării simetrice a unui dreptunghi mai mic într-unul mai mare

PDF: la scală de 70 la sută, dreptunghiul destinație stă centrat pe pagina originală, cu o margine de 15 la sută calculată pentru fiecare parte
Scalarea la 70% lasă o margine de 15 la sută per parte, iar DrawCapturedPage primește acel dreptunghi centrat în puncte

Un lucru merită notat: SetOrigin(1) pune originea sistemului de coordonate în colțul din stânga sus, nu în stânga jos. Valorile de margine pe care le transmiteți lui DrawCapturedPage se măsoară față de originea pe care ați setat-o, deci dacă schimbați modul de origine între încărcare și desenare, centrarea va ieși strâmb

Exemplu în C#

Codul de mai jos trece fiecare pagină din Pages.pdf prin ciclul capture-and-redraw și scrie rezultatul în newpages.pdf. PDFL este obiectul wrapper ActiveX/COM adăugat în proiect din 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++)
    {
        // Selectați întotdeauna pagina 1: CapturePage elimină pagina, deci pagina 2
        // devine pagina 1 la următoarea iterație.
        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");
}

Exemplu în Delphi

Versiunea Delphi folosește TPDFlib direct, nu prin stratul COM, dar secvența de apeluri este identică. O diferență practică este garda pe fișierul de ieșire: FileExists plus DeleteFile în loc de File.Delete, pentru că SaveToFile va eșua dacă destinația este blocată de o rulare anterioară rămasă deschisă într-un vizualizator

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;

Ce controlează de fapt factorul de scală

Valoarea 0.70 de aici înseamnă că, la randare, conținutul ocupă 70% din fiecare dimensiune a paginii, nu că fișierul ajunge la 70% din dimensiunea originală în octeți. Dimensiunea fișierului după această operație depinde de complexitatea conținutului original; o pagină cu imagini mari nu se va micșora proporțional, pentru că datele de pixeli sunt redesenate la aceeași rezoluție într-o zonă mai mică. Dacă țintiți compresie la nivel de octeți, abordarea corectă este LinearizeFile sau resalvarea cu compresie de flux, nu scalarea geometrică

Nici cifra de 70% nu este o limită dură. Funcționează orice valoare între 0.0 și 1.0, iar valorile peste 1.0 măresc conținutul dincolo de marginea paginii originale, ceea ce duce la tăiere la marginea media box, dacă nu creșteți și dimensiunile paginii. Documentele cu pagini de dimensiuni mixte sunt tratate natural, pentru că PageWidth și PageHeight sunt interogate per pagină înainte de calculul marginii, deci un document în care paginile impare sunt A4 și cele pare A3 va produce un rezultat corect centrat pe fiecare dimensiune de pagină, fără niciun caz special

Unde pot apărea probleme

În practică apar două moduri de eșec. Primul este un fișier de ieșire lăsat deschis într-un vizualizator PDF de la o rulare anterioară: SaveToFile va eșua sau va scrie zero octeți, în funcție de platformă, iar noua ieșire nu ajunge niciodată pe disc. Garda de ștergere a fișierului de la începutul funcției rezolvă asta pe timpul dezvoltării, dar într-un pipeline de producție e mai sigur să scrieți într-o cale temporară și să redenumiți la succes

Al doilea este nepotrivirea numărului de pagini. Pentru că CapturePage scoate paginile din document pe măsură ce le procesează, valoarea citită din PageCount() înainte de buclă este limita corectă de iterare. Un apel PageCount() în interiorul buclei ar returna un număr descrescător la fiecare trecere și ar ieși prea devreme, lăsând ultimele pagini neprocesate. Variabila de buclă din exemple servește doar ca un contor de iterații rămase; nu este folosită niciodată pentru a selecta o pagină, pentru că pagina de selectat este mereu 1, din motivul explicat mai sus

PDF: diagramă de buclă arătând de ce fiecare trecere trebuie să apeleze SelectPage(1), întrucât paginile capturate sunt eliminate din arbore, iar paginile rămase se deplasează înainte
CapturePage înlătură fiecare pagină procesată, astfel încât coada se redenumește singură, ceea ce face pagina 1 singura valoare corectă de selector

Apelurile de manipulare a paginilor arătate aici, printre care CapturePage, DrawCapturedPage și SetPageDimensions, fac parte din losLab PDF Library pentru Delphi, C#, VB.NET și C++