Aplicarea unui filigran sau a unui logo pe fiecare pagină a unui document pare o treabă de cinci minute, până când deschideți rezultatul într-un inspector de dimensiuni de fișiere. Abordarea evidentă este parcurgerea paginilor și, pe fiecare, construirea din nou a aceluiași obiect de text sau imagine. Aceasta funcționează din punct de vedere vizual, dar este ineficientă într-un mod care se acumulează. Un filigran diagonal "DRAFT" desenat direct pe un raport de o sută de pagini înseamnă o sută de copii ale acelorași date de cale și text aflate în fluxurile de conținut, iar fișierul salvat o poartă pe fiecare dintre ele.
Un Form XObject este construcția pe care o oferă PDF pentru a evita exact acest lucru. Acesta împachetează o bucată de conținut reutilizabil, o pagină întreagă sau un mic șablon, într-un singur obiect numit care poate fi desenat de mai multe ori în mai multe poziții. Conținutul trăiește în fișier o singură dată. Fiecare pagina care dorește ștampila conține o scurtă instrucțiune care spune "desenează XObject N aici, cu această transformare." Un filigran pe o sută de pagini adaugă atunci un singur obiect de conținut în fișier în loc de o sută, iar aceasta este diferența dintre un document care crește liniar cu numărul de pagini și unul care nu o face. Filigranele, ștampilele de logo, șabloanele de numere de pagină și sigiliile reprezintă aceeași formă de problemă, iar Form XObject este instrumentul potrivit pentru fiecare dintre ele.
De ce un singur obiect stocat este mai bun decât o sută de redesenări
Economia este structurală, nu cosmetică. O pagină PDF se redă prin executarea fluxului său de conținut, o secvență de operatori de desenare. Când redesenați o ștampilă pe fiecare pagină, adăugați întreaga secvență de operatori pentru acea ștampilă la fluxul fiecărei pagini, iar octeții sunt duplicați de atâtea ori câte pagini aveți. Un Form XObject mută acei operatori într-un singur flux stocat o singură dată în document. Referința pe care o păstrează o pagină este mică: trimite o matrice de transformare, apelează XObject și restabilește starea. Numărul de pagini nu mai multiplică costul operei de artă.
Acest lucru contează cel mai mult atunci când ștampila este grea. Un sigiliu vectorial cu sute de segmente de cale, sau o imagine bitmap de logo, este costisitor de stocat. Stocată o singură dată și menționată ca referință, partea grea este plătită o singură dată, iar costul per pagină reprezintă doar câțiva octeți de apelare. Rezultatul vizual pe pagină este identic cu o redesenare directă, ceea ce este și scopul. Cititorul nu poate distinge diferența; dimensiunea fișierului o poate face însă foarte clar.
Capturarea unei pagini într-un XObject
PDFium construiește obiectul reutilizabil dintr-o pagină existentă. Sursa este o pagină dintr-un document deschis, un mic PDF de o pagină care conține doar filigranul dumneavoastră, sau o anumită pagină a unui fișier mai mare. CreateXObjectFromPage captează conținutul acelei pagini sursă într-un handle reutilizabil care aparține documentului destinație, cel pe care îl ștampilați.
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile('Report.pdf');
Stamp.LoadFromFile('Watermark.pdf'); // one page of artwork
// Capture page 0 of the stamp document into a reusable handle that
// is owned by Dest. Source must be active; the index is zero-based.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not build the stamp XObject');
// ... place it, then free it before closing Stamp (see below) ...
Semnătura este CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda returnează nil în caz de eșec în loc să genereze o excepție, așa că verificarea explicită de mai sus nu este opțională. Handle-ul returnat este un TPdfXObject pe care îl dețineți, iar cele două constrângeri privind durata de viață atașate acestuia reprezintă partea din acest exercițiu care pune probleme de obicei, așa că primesc propria lor secțiune mai jos.
Plasarea ștampilei pe o pagină
Un XObject capturat nu face nimic singur. Pentru a-l face să apară, inserați o copie a acestuia pe pagina curentă a documentului cu InsertFormObjectFromXObject. Acel apel returnează obiectul de pagină de bază, un FPDF_PAGEOBJECT, iar handle-ul returnat este modul în care poziționați plasarea. Fără o transformare, ștampila aterizează la originea coordonatelor proprii ale paginii sursă, ceea ce rareori este locul dorit.
Deoarece InsertFormObjectFromXObject inserează o copie per apel și returnează un nou obiect de pagină de fiecare dată, puteți picta același XObject de mai multe ori pe o pagină la transformări diferite, iar conținutul stocat este în continuare numărat o singură dată în fișier. Un logo de colț și un filigran discret pe întreaga pagină pot proveni din același obiect capturat.
var
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
begin
// The current page of Dest receives one copy of the XObject.
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
raise Exception.Create('Insert failed on this page');
// Position it: move 200 units right, 500 up, at 70% scale.
M := TPdfMatrix.Create;
try
M.Scale(0.7, 0.7);
M.Translate(200, 500);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
// Dest.SaveLoadedDocument(...) when every page is done.
end;
Regula duratei de viață a handle-ului care pune probleme
Două constrângeri guvernează handle-ul XObject, iar ignorarea oricăreia produce o eroare care pare fără legătură cu cauza sa. În primul rând, documentul sursă trebuie să fie activ în momentul în care apelați CreateXObjectFromPage. Captura citește conținutul paginii sursă din documentul sursă activ, așa că acel document și pagina sa trebuie să fie deschise și valide atunci când handle-ul este construit. În al doilea rând, și aceasta este cea care îi surprinde pe oameni, handle-ul trebuie eliberat înainte ca pagina sursă să fie închisă și, în practică, înainte de a închide sau elibera documentul sursă din care provine.
Motivul este că XObject este o referință în structura pe care documentul sursă o deține încă. Nu este o copie detașată, de sine stătătoare, pe care o puteți purta după ce sursa a dispărut. Închideți mai întâi sursa și handle-ul rămâne îndreptat către un conținut care a fost distrus, astfel încât eliberarea lui ulterioară, sau orice altă utilizare a sa, operează pe o memorie care nu mai este validă. Simptomul este cel clasic pentru un handle suspendat (dangling handle): o eroare de acces la închidere sau o corupție intermitentă care se deplasează în funcție de ordinea de alocare, cu o stivă care indică spre codul de curățare în loc de linia care a cauzat efectiv problema. Soluția este ordinea, nu codificarea defensivă. Construiți XObject, inserați-l pe fiecare pagină care are nevoie de el, eliberați XObject și abia apoi închideți documentul sursă. Destructorul TPdfXObject eliberează handle-ul PDFium de bază pentru dumneavoastră, așa că eliberarea wrapper-ului la momentul potrivit reprezintă întreaga dumneavoastră responsabilitate.
Matricea și ce înseamnă cele șase numere ale sale
Plasarea este o transformare afină 2D, aceeași pe care PDF o folosește peste tot pentru poziționarea conținutului (ISO 32000-1, secțiunea 8.3.4). Sunt șase numere, scrise a, b, c, d, e, f, iar PDFium le expune sub formă de înregistrare FS_MATRIX. Ele mapează un punct din spațiul propriu al obiectului în spațiul paginii:
// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)
Puteți completa manual cele șase valori, dar compunerea lor manuală este momentul în care rotația eșuează, deoarece rotația amestecă toate cele patru valori a, b, c, d. Wrapper-ul TPdfMatrix compune operațiunile comune pentru dumneavoastră și le multiplică pe parcurs, astfel încât Translate, Scale și Rotate se înlănțuiesc în ordinea în care le apelați. Un filigran diagonal este o rotație urmată de o translație pentru recentrare; un logo de colț este o scalare urmată de o translație. Când matricea este gata, transmiteți valoarea sa brută la FPDFPageObj_SetMatrix(PageObj, M.Handle), unde M.Handle este structura FS_MATRIX de bază. Funcția de nivel inferior FPDFPageObj_Transform, care preia cele șase valori direct ca double, este disponibilă dacă preferați să transmiteți numere în loc să construiți un wrapper.
Ștampilarea fiecărei pagini, în ordinea corectă
Modelul complet pune piesele cap la cap cu ordinea cerută de regula duratei de viață. Deschideți ambele documente, capturați ștampila o singură dată, parcurgeți paginile destinație selectând-o pe fiecare pe rând și inserând plus poziționând o copie, apoi eliberați XObject, apoi salvați și lăsați documentul sursă să se închidă ultimul.
procedure StampEveryPage(const ASource, AStamp, AOutput: string);
var
Dest, Stamp: TPdf;
XObject: TPdfXObject;
PageObj: FPDF_PAGEOBJECT;
M: TPdfMatrix;
i: Integer;
begin
Dest := TPdf.Create;
Stamp := TPdf.Create;
try
Dest.LoadFromFile(ASource);
Stamp.LoadFromFile(AStamp);
// 1. Capture the artwork once. Stamp is active here.
XObject := Dest.CreateXObjectFromPage(Stamp, 0);
if XObject = nil then
raise Exception.Create('Could not capture the stamp page');
try
// 2. Place a copy on every page of Dest.
for i := 0 to Dest.PageCount - 1 do
begin
Dest.CurrentPageIndex := i; // make page i current
PageObj := Dest.InsertFormObjectFromXObject(XObject);
if PageObj = nil then
Continue;
M := TPdfMatrix.Create;
try
M.Rotate(45);
M.Translate(150, 100);
FPDFPageObj_SetMatrix(PageObj, M.Handle);
finally
M.Free;
end;
end;
finally
XObject.Free; // 3. free BEFORE Stamp closes
end;
// 4. Write the result while Dest is still open.
Dest.SaveLoadedDocument(AOutput);
finally
Stamp.Free; // source closes last
Dest.Free;
end;
end;
Structura blocurilor try face munca reală. Blocul interior finally eliberează XObject înainte ca execuția să poată ajunge la blocul exterior finally care eliberează Stamp, astfel încât handle-ul este întotdeauna eliberat în timp ce sursa sa este încă activă, chiar dacă se declanșează o excepție în mijlocul buclei. Realizați corect această imbricare și regula duratei de viață se rezolvă de la sine. (Folosiți orice selector de pagină curentă expune compilarea dumneavoastră; corpul buclei este același în ambele cazuri.)
Ștampilarea este doar un colț al unui set de instrumente mai mare pentru crearea și editarea conținutului paginii. Dacă ștampila dumneavoastră este o imagine în sine, mai degrabă decât o pagină capturată, conversia imaginilor în documente PDF cu PDFium acoperă introducerea acelei imagini bitmap într-un document mai întâi. Și când ceea ce doriți să transportați alături de ștampila vizibilă este un fișier în loc de cerneală pe pagină, lucrul cu atașamente PDF în Delphi arată partea de fișiere încorporate. Totul este livrat cu PDFium Component pentru Delphi și C++Builder, alături de API-urile de redare, editare și documente acoperite în alte părți ale acestui blog.