Articol tehnic

Ștampile de Pagină Reutilizabile via Form XObjects Cu PDFium

Aplicarea unui filigran sau a unui logo pe fiecare pagină a unui document pare a fi o treabă de cinci minute până când deschideți rezultatul într-un inspector de dimensiune a fișierului. Abordarea evidentă este de a parcurge paginile și, pe fiecare, de a construi din nou aceleași obiecte text sau imagine. Asta funcționează vizual și este o risipă într-un mod care se agravează. Un filigran diagonal „DRAFT” desenat direct pe un raport de o sută de pagini reprezintă o sută de copii ale acelorași date de cale și text care se află în fluxurile de conținut, iar fișierul salvat le poartă pe fiecare dintre ele

Un Form XObject este construcția pe care PDF o oferă pentru a evita exact acest lucru. Încapsulează o bucată de conținut reutilizabil, o pagină întreagă sau un șablon mic, într-un singur obiect numit care poate fi pictat de mai multe ori în mai multe poziții. Conținutul se află în fișier o singură dată. Fiecare pagină care dorește ștampila conține o scurtă instrucțiune care spune „pictează XObject N aici, cu această transformare”. Un filigran pe o sută de pagini adaugă apoi 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. Filigranele, ștampilele logo, șabloanele pentru numărul paginii și sigiliile sunt toate de aceeași formă de problemă, iar Form XObject este instrumentul potrivit pentru fiecare dintre ele

Diagramă contrastând desenarea operatorilor de watermark pe fiecare pagină PDF cu stocarea lor o dată într-un Form XObject cu PDFium
Redesenarea ștampilei pe fiecare pagină duplicatează octeții săi în fiecare stream de conținut, în timp ce un Form XObject stochează lucrarea o dată și lasă fiecare pagină să o referențieze

De ce un obiect stocat învinge 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 pagină, adăugați secvența completă a operatorilor 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ă acești operatori într-un singur flux stocat o singură dată în document. Referința pe care o păstrează o pagină individuală este mică: împinge o matrice de transformare, invocă 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 un bitmap de logo, este costisitor de stocat. Stocat o dată și referențiat, partea grea este plătită o singură dată, iar costul pe pagină este de câțiva octeți de invocare. Rezultatul vizual de pe pagină este identic cu o redesenare directă, ceea ce este esența. Cititorul nu poate face diferența; dimensiunea fișierului o poate face foarte mult

Capturarea unei pagini într-un XObject

PDFium construiește obiectul reutilizabil dintr-o pagină existentă. Sursa este o pagină din documentul pe care îl aveți deschis, un PDF mic de o singură pagină care conține doar ilustrația filigranului sau o anumită pagină a unui fișier mai mare. CreateXObjectFromPage capturează conținutul acelei pagini sursă într-un handle reutilizabil care aparține documentului de destinație, cel pe care îl ștampilați

var
  Dest, Stamp: TPdf;
  XObject: TPdfXObject;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := 'Report.pdf';
    Dest.Active := True;
    Stamp.FileName := 'Watermark.pdf';   // o pagină de grafică artistică
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // Capturați pagina 0 a documentului-ștampilă într-un handle reutilizabil pe care
    // Dest îl deține. Sursa trebuie să fie Active; indexul se numără de la zero.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... plasați-l, apoi eliberați-l înainte de a închide Stamp (vezi mai jos) ...

Semnătura este CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda lansează o excepție dacă documentul sursă nu este Active și returnează nil în loc să lanseze o excepție atunci când PDFium nu poate construi obiectul, deci 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 tot acest exercițiu care îi prinde pe oameni, așa că primesc propria lor secțiune mai jos

Plasarea ștampilei pe o pagină

Un XObject capturat nu face nimic pe cont propriu. Pentru a-l face să apară, introduceți o copie a acestuia pe pagina curentă a documentului, cea selectată de proprietatea PageNumber bazată pe 1, cu InsertFormObjectFromXObject. Acel apel returnează obiectul de pagină subiacent, un FPDF_PAGEOBJECT, iar handle-ul returnat este modul în care poziționați plasarea. Fără o transformare, ștampila ajunge la originea din coordonatele proprii ale paginii sursă, care este rareori locul în care o doriți

Deoarece InsertFormObjectFromXObject inserează o copie per apel și returnează de fiecare dată un obiect de pagină proaspăt, puteți picta același XObject de mai multe ori pe o singură pagină la transformări diferite, iar conținutul stocat este totuși numărat o singură dată în fișier. Un logo de colț și un filigran slab pe toată pagina pot proveni din același obiect capturat

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
begin
  // Pagina curentă a lui Dest primește o copie a XObject-ului.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // Poziționați-l: mutați 200 de unități la dreapta, 500 în sus, la o scară de 70%.
  M := TPdfMatrix.Create;
  try
    M.Scale(0.7, 0.7);
    M.Translate(200, 500);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // comiteți editările acestei pagini în fluxul său de conținut
  // dacă nu Dest.SaveAs(...) atunci ... când fiecare pagină este gata.
end;

Două detalii de menaj fac acest lucru sigur. În primul rând, odată inserat, obiectul pagină aparține paginii, nu XObject-ului. Eliberarea ulterioară a XObject-ului nu invalidează plasările pe care le-ați făcut deja. Aceasta este ceea ce face ca ordonarea creare-plasare-eliberare descrisă mai jos să funcționeze. În al doilea rând, inserarea și poziționarea modifică doar lista de obiecte a paginii în memorie; UpdatePage este cel care serializează acea listă înapoi în fluxul de conținut al paginii, astfel încât o pagină pe care o editați fără a-l apela se salvează ca și cum ștampila nu ar fi fost niciodată plasată

Regula duratei de viață a handle-ului care îi surprinde pe oameni

Două constrângeri guvernează handle-ul XObject, iar ignorarea oricăreia produce un eșec care pare a nu avea 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, deci acel document și pagina sa trebuie să fie deschise și valide atunci când este construit handle-ul. Î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 a provenit

Motivul este că XObject este o referință într-o structură pe care documentul sursă o deține încă. Nu este o copie detașată, independentă, pe care să o puteți transporta după ce sursa a dispărut. Închideți mai întâi sursa și handle-ul rămâne indicând către conținut care a fost desființat, așadar eliberarea lui ulterioară, sau orice altă utilizare a acestuia, operează pe o memorie care nu mai este validă. Simptomul este cel clasic pentru un handle suspendat: o încălcare de acces la oprire sau o corupere intermitentă care se mișcă în funcție de ordinea alocării, cu o stivă care indică spre codul de curățare în loc de linia care a cauzat de fapt problema. Remedierea este ordonarea, nu codificarea defensivă. Construiți XObject, introduceț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 subiacent pentru dvs., deci eliberarea wrapper-ului la momentul potrivit reprezintă întreaga dvs. responsabilitate

Diagramă de ciclu de viață ordonat pentru ștampilele de pagină PDFium, arătând captura, plasarea, eliberarea handle-ului TPdfXObject și închiderea documentului de ștampilă ultima
Capturează ștampila o dată, plasează-o pe fiecare pagină, eliberează XObject-ul cât timp documentul ștampilă este încă deschis, apoi salvează și închide sursa ultima

Matricea și ce înseamnă cele șase numere ale sale

Plasarea este o transformare afină 2D, aceeași pe care PDF-ul 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, și PDFium le expune ca înregistrarea 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 : scară orizontală și verticală
// b, c : termenii de forfecare / rotație
// e, f : translație (unde aterizează originea pe pagină)

Puteți completa acele șase valori manual, dar compunerea lor manuală este locul în care rotația merge prost, deoarece rotația amestecă toate cele patru din a, b, c, d împreună. Wrapper-ul TPdfMatrix, din unitatea FPdfMatrix, compune operațiunile comune pentru dvs. și post-multiplică pe măsură ce avansează, astfel încât Translate, Scale și Rotate se înlănțuie în ordinea în care le apelați. Un filigran diagonal este o rotire urmată de o translatare pentru a-l recentra; un logo de colț este o scalare urmată de o translatare. Când matricea este gata, copiați valoarea ei brută, proprietatea Handle de tip FS_MATRIX, într-o variabilă locală și transmiteți-o la FPDFPageObj_SetMatrix; importul declară matricea ca parametru var, deci o proprietate nu îi poate fi predată direct, iar rezultatul său este 0 în caz de eșec. FPDFPageObj_Transform de nivel inferior, care preia cele șase valori direct ca double, este disponibilă atunci când preferați să transmiteți numere în loc să construiți un wrapper

Ștampilarea fiecărei pagini, în ordinea corectă

Tiparul complet pune piesele laolaltă cu ordinea cerută de regula duratei de viață. Deschideți ambele documente, capturați ștampila o singură dată, parcurgeți paginile de destinație setând pe rând PageNumber bazat pe 1 și inserând plus poziționând o copie, comitând fiecare pagină cu UpdatePage, apoi eliberați XObject-ul, apoi salvați cu SaveAs, ș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;
  RawM: FS_MATRIX;
  I: Integer;
begin
  Dest := TPdf.Create(nil);
  Stamp := TPdf.Create(nil);
  try
    Dest.FileName := ASource;
    Dest.Active := True;
    Stamp.FileName := AStamp;
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 1. Capturați grafica artistică o singură dată. Stamp este Active aici.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. Plasați o copie pe fiecare pagină a lui Dest. PageNumber se numără de la 1.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := I;                // face pagina I curentă
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // filigran diagonal
          M.Translate(150, 100);             // împingeți în poziție
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // comiteți editările acestei pagini
      end;
    finally
      XObject.Free;                          // 3. eliberați ÎNAINTE ca Stamp să se închidă
    end;

    // 4. Scrieți rezultatul cât timp Dest este încă deschis.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // sursa se închide ultima
    Dest.Free;
  end;
end;

Forma blocurilor try face munca reală. Blocul finally interior eliberează XObject-ul înainte ca controlul să poată ajunge vreodată la blocul finally exterior care eliberează Stamp, astfel încât handle-ul este întotdeauna eliberat în timp ce sursa sa este încă în viață, chiar dacă se declanșează o excepție la mijlocul buclei. Obțineți corect acea imbricare și regula duratei de viață se ocupă de la sine

Anatomia FS_MATRIX arătând cei șase coeficienți afini pe care PDFium îi folosește pentru a scala, roti și translata un Form XObject ștampilat pe pagină
Șase numere mapează coordonatele ștampilei în spațiul paginii, iar TPdfMatrix compune Scale, Rotate și Translate în ordinea apelului pentru a așeza un filigran diagonal

Ștampilarea este un colț al unui set de instrumente mai mare pentru construirea și editarea conținutului paginii. Dacă ștampila dvs. este în sine o imagine în loc de o pagină capturată, conversia imaginilor în documente PDF cu PDFium acoperă mai întâi obținerea acelui bitmap într-un document. Și atunci când lucrul pe care doriți să îl purtați alături de ștampila vizibilă este un fișier mai degrabă decât cerneală pe pagină, lucrul cu atașamente PDF în Delphi arată partea de fișiere încorporate. Toate acestea sunt livrate cu Componenta PDFium pentru Delphi și C++Builder, alături de API-urile de redare, editare și document acoperite în altă parte pe acest blog