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