Technical Article

Impunerea N-up și reordonarea paginilor cu PDFium

Îmbinarea și divizarea (merge și split) sunt cele două operațiuni de pagină la care apelează toată lumea mai întâi și acoperă mult teren. Ele nu acoperă totul. Există o familie separată de activități care rearanjează paginile, mai degrabă decât să mute fișiere întregi: așezarea a patru diapozitive pe o singură foaie pentru un pliant, tragerea unei pagini din spatele unui document în față sau extragerea paginilor 3, 7 și 12 într-un scurt extras fără a atinge restul. PDFium expune trei metode exact pentru aceasta, iar fiecare se comportă diferit de îmbinarea și divizarea pe care le cunoașteți deja. Acest articol prezintă ce fac acestea, unde se află punctele de ieșire și un detaliu de deținere (ownership) care a provocat un blocaj în practică.

Cele trei sunt ImportNPagesToOne pentru impunerea N-up, MovePages pentru reordonarea locală (în loc) și ImportPagesByIndex pentru extragerea unui subset. Îmbinarea stivuiește documentele cap la cap și lasă numărul de pagini egal cu suma intrărilor. Divizarea scrie mai multe fișiere de ieșire dintr-o singură intrare. Cele trei operațiuni de aici se situează la mijloc: una dintre ele modifică numărul de pagini sursă care împart o foaie, una dintre ele modifică ordinea în cadrul unui singur document, iar una dintre ele copiază o mână aleasă de pagini într-un alt document. Știind care este care, vă scutește de a forța un dans de îmbinare și ștergere acolo unde ar fi suficient un singur apel.

Ce face de fapt impunerea N-up

Impunerea (imposition) este termenul prepress pentru aranjarea mai multor pagini sursă pe o singură foaie mai mare, astfel încât rezultatul tipărit și pliat să fie citit în ordinea corectă. Versiunea zilnică este pliantul 2-up, broșura 4-up sau foaia de contact care încape o duzină de miniaturi pe o pagină. PDFium gestionează geometria printr-un singur apel:

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

NumX și NumY descriu grila. O valoare de 2, 1 plasează două pagini sursă una lângă alta; 2, 2 împachetează patru într-un aspect de cadran; 4, 3 construiește o foaie de contact cu douăsprezece elemente. PDFium citește paginile sursă în ordine, o scalează pe fiecare în jos pentru a se potrivi în celula sa și umple grila de la stânga la dreapta, de sus în jos, începând o nouă foaie de ieșire ori de câte ori grila curentă este plină. Paginile sursă nu sunt modificate. Ceea cu primiți înapoi este un document nou ale cărui pagini sunt compozite.

Dimensiunea de ieșire este în puncte, nu în pixeli

OutputWidth și OutputHeight sunt unități de utilizator PDF, iar o unitate de utilizator PDF este un punct (point), adică a șaptezeci și doua parte dintr-un inch. Unitatea declară dimensiunea fizică a foii de ieșire și nu are nimic de-a face cu pixelii ecranului sau cu DPI-ul de redare. Acesta este cel mai frecvent loc în care se greșește o impunere, deoarece un dezvoltator obișnuit cu hărți de biți (bitmaps) caută un număr de pixeli și ajunge cu o foaie de dimensiunea unui timbru poștal sau a unui panou publicitar.

Numerele care merită memorate sunt cele două dimensiuni de pagină pe care le veți folosi cel mai des. US Letter are 612 pe 792 de puncte, deoarece 8,5 inchi înmulțit cu 72 este 612 și 11 inchi înmulțit cu 72 este 792. A4 are aproximativ 595 pe 842 de puncte, din dimensiunile sale de 210 pe 297 milimetri. Antetul legăturii indică clar regula că o unitate este a șaptezeci și doua parte dintr-un inch, iar unitatea conține o constantă PointsPerInch egală cu 72, în cazul în care preferați să calculați o dimensiune din inchi în cod, în loc să scrieți valoarea literală.

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

Handle-ul returnat trebuie eliberat de dumneavoastră

Citiți din nou semnătura. ImportNPagesToOne returnează un TPdf, nu un Boolean. Acea valoare de returnare este un descriptor de document complet nou, alocat separat de sursă, iar apelantul îl deține. Obiectul sursă TPdf pe care ați apelat metoda este neatins și încă își deține propriul descriptor; compozitul este un al doilea obiect, independent. Dacă lăsați obiectul TPdf returnat să iasă din domeniul de aplicare (scope) fără a-l elibera, veți pierde un document PDFium întreg.

Greșeala mai periculoasă se desfășoară invers. Dedesubt, metoda cere PDFium un nou FPDF_DOCUMENT prin FPDF_ImportNPagesToOne, apoi înfășoară acel descriptor brut în interiorul obiectului TPdf returnat, astfel încât durata de viață a învelișului (wrapper) guvernează viața descriptorului. Din acel moment, există exact un singur proprietar al descriptorului și exact un singur loc în care ar trebui închis: când apelați Free pe obiectul returnat. O cale de eroare neglijentă care eliberează învelișul și, de asemenea, apelează FPDF_CloseDocument pe descriptorul brut pe care l-a capturat închide de două ori același document PDFium. Aceasta este o eliberare dublă (double-free) și este bugul specific care a afectat un apelant aici o dată. Regula care o previne este scurtă. Închideți documentul pe o singură cale, prin eliberarea obiectului TPdf pe care vi l-a predat metoda, și nu treceți niciodată dincolo de înveliș pentru a închide descriptorul pe care acesta l-a adoptat deja.

Două corolarii decurg din aceasta. În primul rând, metoda returnează nil când PDFium respinge argumentele, cum ar fi o valoare zero pe oricare dintre axele grilei sau o eroare de alocare, așa că o verificare nil își are locul înainte de a atinge rezultatul. În al doilea rând, inițializați variabila de ieșire la nil înainte de blocul try și eliberați-o în finally, așa cum face exemplul de mai sus, astfel încât o eroare la jumătatea procesului să nu vă lase să eliberați o referință nedefinită sau să omiteți eliberarea complet.

Reordonarea paginilor fără rescrierea lor

Impunerea construiește un document nou. Reordonarea modifică un singur document local. MovePages ridică un set de pagini din pozițiile lor curente și le plasează la o destinație, deplasând tot restul în jurul blocului mutat, astfel încât numărul de pagini rămâne același:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Indicii sunt bazați pe zero. PageIndices listează paginile de mutat, în ordinea în care ar trebui să ajungă, iar DestPageIndex este indexul pe care prima pagină mutată ajunge după ce mutarea se stabilizează. Deoarece PDFium relocă paginile în loc să le copieze și să le recomprime conținutul, operațiunea este ieftină și fără pierderi: obiectele paginii își păstrează fluxurile, resursele și fidelitatea. Acesta este apelul din spatele unui panou de pagină de tip drag-to-reorder, unde un utilizator trage o miniatură într-un slot nou, iar dvs. confirmați noua ordine cu o singură mutare. Returnează False când un index este în afara limitelor, așa că validați rezultatul în loc să presupuneți că rearanjarea a funcționat.

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Extragerea unui subset după index

A treia operațiune copiază un set explicit de pagini dintr-un document în altul. ImportPagesByIndex preia documentul sursă și o matrice de indici bazată pe zero și inserează acele pagini în țintă la o poziție aleasă:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

O apelați pe documentul țintă și transmiteți sursa ca prim argument. PageIndices numește paginile sursă de preluat, în ordinea în care le doriți; InsertAt este slotul bazat pe zero din țintă în care merge prima pagină importată, astfel încât 0 le plasează înaintea primei pagini existente și numărul curent de pagini al țintei se adaugă. O matrice goală importă fiecare pagină, ceea ce face ca apelul să fie o copie completă atunci când aveți nevoie de una. Returnează False dacă vreun index este în afara limitelor în sursă.

Aici contează contrastul cu divizarea (split). Divizarea scrie fișiere separate, o singură operațiune producând mai multe ieșiri pe disc. ImportPagesByIndex face opusul acestei activități: adună un set ales de pagini într-un singur document țintă în memorie, pe care apoi îl salvați o singură dată. Când sarcina este „dă-mi paginile 3, 7 și 12 ca un singur PDF scurt”, aceasta este calea directă și îmbracă funcția FPDF_ImportPagesByIndex dedesubt.

var
  Source, Excerpt: TPdf;
try
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Asamblarea curată a procesului

Forma de la un capăt la altul este aceeași pentru toate cele trei: deschideți sursa setând FileName și trecând Active la True, efectuați operațiunea, salvați cu SaveAs și eliberați ceea ce dețineți. Singura ramură care necesită atenție este care dintre apeluri alocă un document nou. MovePages modifică documentul pe care îl dețineți deja, așa că există un singur obiect de eliberat. ImportPagesByIndex scrie într-o țintă pe care ați creat-o singur, așa că eliberați sursa și ținta pe care ați deschis-o. ImportNPagesToOne este elementul atipic, deoarece noul document este valoarea de returnare a metodei și nu ceva ce ați construit dvs., iar uitarea faptului că este un descriptor separat, deținut de apelant, este modul în care apar atât pierderea de memorie (leak), cât și eliberarea dublă (double-free). Inițializați rezultatul la nil, verificați-l după apel și eliberați-l pe o singură cale.

Dacă activitatea pe care o aveți de desfășurat este combinarea fișierelor întregi în loc de reordonarea paginilor, consultați îmbinarea mai multor fișiere PDF într-un singur document. Dacă este invers, adică spargerea unui document în mai multe fișiere, consultați divizarea documentelor PDF în mai multe fișiere. Metodele de impunere și reordonare descrise aici sunt livrate ca parte a PDFium Component pentru Delphi și C++Builder, alături de API-urile de încărcare, redare și editare acoperite în alte părți ale acestui blog.