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

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';   // one page of artwork
    Stamp.Active := True;
    if not (Dest.Active and Stamp.Active) then
      raise Exception.Create('Could not open the input documents');

    // 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 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
  // 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);
    RawM := M.Handle;
    if FPDFPageObj_SetMatrix(PageObj, RawM) = 0 then
      raise Exception.Create('Cannot assign the stamp matrix');
  finally
    M.Free;
  end;
  Dest.UpdatePage;   // commit this page's edits to its content stream
  // if not Dest.SaveAs(...) then ... when every page is done.
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

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 : horizontal and vertical scale
// b, c : the shear / rotation terms
// e, f : translation (where the origin lands on the page)

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. 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. PageNumber is 1-based.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := I;                // make page I current
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // diagonal watermark
          M.Translate(150, 100);             // nudge into position
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // commit this page's edits
      end;
    finally
      XObject.Free;                          // 3. free BEFORE Stamp closes
    end;

    // 4. Write the result while Dest is still open.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // source closes last
    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

Ș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