Tehnički članak

Pečati na stranicama za višekratnu upotrebu putem Form XObjects uz PDFium

Dodavanje vodenog žiga ili logotipa na svaku stranicu izgleda kao petominutni posao dok rezultat ne proverite alatom za analizu veličine datoteke. Očigledan pristup prolazi kroz stranice i na svakoj ponovo gradi iste tekstualne ili slikovne objekte. Vizuelno radi, ali nepotrebno umnožava podatke. Dijagonalni vodeni žig „DRAFT“ nacrtan direktno na sto stranica znači sto kopija istih putanja i tekstualnih podataka u tokovima sadržaja, a sačuvana datoteka nosi svaku od njih

PDF za ovaj problem koristi Form XObject. On obavija sadržaj za višekratnu upotrebu, celu stranicu ili mali šablon, u jedan imenovani objekat koji se može iscrtati mnogo puta na različitim mestima. Sadržaj postoji jednom, a svaka stranica čuva kratku instrukciju da iscrta XObject N sa zadatom transformacijom. Vodeni žig na sto stranica zato dodaje jedan objekat sadržaja umesto stotinu. Vodeni žigovi, logotipi, šabloni brojeva stranica i pečati imaju isti oblik problema, a Form XObject je odgovarajući alat za sve njih

Dijagram koji suprotstavlja crtanje operatora vodenog žiga na svaku PDF stranu sa čuvanjem jednom u Form XObject-u sa PDFium-om
Ponovno crtanje žiga na svakoj stranici udvostručuje njegove bajtove kroz svaki tok sadržaja, dok Form XObject čuva umetninu jednom i pusti svaku stranicu da je referencira

Jedan objekat je bolji od stotinu ponovnih crtanja

Ušteda je strukturna, a ne kozmetička. PDF stranica se renderuje izvršavanjem toka sadržaja, niza operatora za crtanje. Ako pečat ponovo crtate na svakoj stranici, ceo niz operatora dodaje se svakom toku i bajtovi se dupliraju onoliko puta koliko ima stranica. Form XObject premešta operatore u jedan tok sačuvan jednom u dokumentu. Pojedinačna stranica čuva samo malu referencu: postavlja matricu transformacije, poziva XObject i vraća stanje. Broj stranica više ne množi cenu grafike

Razlika je najveća kod složenog pečata. Vektorski znak sa stotinama segmenata putanje ili bitmap logotip skupi su za čuvanje. Kada se sačuvaju jednom i samo referenciraju, težak sadržaj plaća se jednom, dok je trošak po stranici nekoliko bajtova poziva. Vizuelni rezultat je isti kao pri direktnom crtanju; čitač ne vidi razliku, ali veličina datoteke vidi

Snimanje stranice u XObject

PDFium gradi objekat za višekratnu upotrebu iz postojeće stranice. Izvor može biti stranica otvorenog dokumenta, mali PDF od jedne stranice koji sadrži samo grafiku vodenog žiga ili određena stranica većeg dokumenta. CreateXObjectFromPage snima sadržaj izvorne stranice u ručicu za višekratnu upotrebu koja pripada odredišnom dokumentu na koji se pečat postavlja

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

    // Snimite stranicu 0 dokumenta pečata u ručicu za višekratnu upotrebu koju
    // poseduje Dest. Izvor mora biti Active; indeks je zasnovan na nuli.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not build the stamp XObject');
    // ... postavite ga, zatim ga oslobodite pre zatvaranja Stamp (vidi ispod) ...

Potpis je CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda izaziva izuzetak ako izvorni dokument nije Active, a vraća nil bez izuzetka kada PDFium ne može da napravi objekat, zato eksplicitna provera nije opciona. Vraćeni TPdfXObject pripada pozivaocu, a njegova dva pravila životnog veka opisana su u posebnoj sekciji ispod

Postavljanje pečata na stranicu

Snimljeni XObject sam po sebi ništa ne prikazuje. Kopiju umećete na trenutnu stranicu odredišnog dokumenta, izabranu svojstvom PageNumber zasnovanim na 1, pozivom InsertFormObjectFromXObject. Poziv vraća osnovni objekat stranice, FPDF_PAGEOBJECT, preko kojeg određujete položaj. Bez transformacije pečat završava u koordinatnom početku izvorne stranice, što retko odgovara željenom mestu

Pošto InsertFormObjectFromXObject umeće po jednu kopiju i svaki put vraća novi objekat stranice, isti XObject možete iscrtati više puta na jednoj stranici sa različitim transformacijama, dok se sadržaj u datoteci i dalje čuva jednom. Logotip u uglu i bledi vodeni žig preko cele stranice mogu poticati iz istog snimljenog objekta

var
  PageObj: FPDF_PAGEOBJECT;
  M: TPdfMatrix;
  RawM: FS_MATRIX;
begin
  // Trenutna stranica dokumenta Dest dobija jednu kopiju XObject-a.
  PageObj := Dest.InsertFormObjectFromXObject(XObject);
  if PageObj = nil then
    raise Exception.Create('Insert failed on this page');

  // Pozicionirajte ga: pomerite 200 jedinica desno, 500 gore, sa 70% skaliranja.
  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;   // potvrdi izmene ove stranice u njen tok sadržaja
  // if not Dest.SaveAs(...) then ... kada su sve stranice gotove.
end;

Dva detalja čine postupak bezbednim. Posle umetanja objekat stranice pripada stranici, a ne XObject-u; kasnije oslobađanje XObject-a ne poništava već postavljene kopije. Zato radi redosled kreiraj-postavi-oslobodi. Drugo, umetanje i pozicioniranje menjaju samo listu objekata stranice u memoriji. UpdatePage serijalizuje tu listu nazad u tok sadržaja, pa se izmena stranice bez tog poziva čuva kao da pečat nije ni postavljen

Pravilo životnog veka ručice

Dva ograničenja upravljaju XObject ručicom. Prvo, izvorni dokument mora biti aktivan u trenutku poziva CreateXObjectFromPage, jer snimanje čita sadržaj žive izvorne stranice. Drugo, ručica mora biti oslobođena pre zatvaranja izvorne stranice, a u praksi pre zatvaranja ili oslobađanja izvornog dokumenta iz kojeg je nastala

Razlog je to što XObject referencira strukturu koja je i dalje u vlasništvu izvornog dokumenta. Nije samostalna kopija koju možete zadržati nakon zatvaranja izvora. Ako prvo zatvorite izvor, ručica pokazuje na već uklonjen sadržaj, pa njeno kasnije oslobađanje ili korišćenje pristupa nevažećoj memoriji. Simptomi su tipični za viseću ručicu: access violation pri gašenju ili povremeno oštećenje koje zavisi od redosleda alokacije, sa stekom u kodu za čišćenje umesto na mestu uzroka. Ispravka je pravilan redosled: napravite XObject, umetnite ga na potrebne stranice, oslobodite XObject i tek zatim zatvorite izvorni dokument. Destruktor TPdfXObject oslobađa osnovnu PDFium ručicu; vaša odgovornost je da omotač oslobodite u pravom trenutku

Uređeni dijagram životnog veka PDFium pečata strane koji pokazuje hvatanje, postavljanje, oslobađanje TPdfXObject handle-a i zatvaranje dokumenta pečata poslednje
Uhvatite žig jednom, postavite ga na svaku stranicu, oslobodite XObject dok je dokument žiga još otvoren, zatim sačuvajte i zatvorite izvor poslednje

Matrica i značenje njenih šest brojeva

Položaj je 2D afina transformacija koju PDF svuda koristi za sadržaj, prema ISO 32000-1, odeljak 8.3.4. Sastoji se od šest brojeva a, b, c, d, e, f, koje PDFium izlaže zapisom FS_MATRIX. Oni preslikavaju tačku iz prostora objekta u prostor stranice:

// x' = a*x + c*y + e
// y' = b*x + d*y + f
//
// a, d : vodoravno i uspravno skaliranje
// b, c : članovi smicanja / rotacije
// e, f : translacija (gde koordinatni početak sleti na stranici)

Šest vrednosti možete popuniti ručno, ali se pri ručnom sastavljanju najčešće greši u rotaciji jer ona istovremeno menja a, b, c, d. Omotač TPdfMatrix iz jedinice FPdfMatrix sastavlja uobičajene operacije i naknadno ih množi, pa se Translate, Scale i Rotate ulančavaju redosledom poziva. Dijagonalni vodeni žig koristi rotaciju pa translaciju radi ponovnog centriranja; logotip u uglu koristi skaliranje pa translaciju. Kada je matrica spremna, kopirajte njenu sirovu vrednost, svojstvo Handle tipa FS_MATRIX, u lokalnu promenljivu i prosledite je funkciji FPDFPageObj_SetMatrix. Import očekuje var parametar, pa svojstvo ne može direktno da se prosledi, a rezultat 0 označava neuspeh. Niži poziv FPDFPageObj_Transform prima šest Double vrednosti kada ne želite omotač

Pečatiranje svake stranice pravilnim redosledom

Potpun obrazac prati pravilo životnog veka: otvorite oba dokumenta, jednom snimite pečat, prođite kroz odredišne stranice postavljanjem svojstva PageNumber zasnovanog na 1, umetnite i pozicionirajte kopiju, potvrdite svaku stranicu pozivom UpdatePage, zatim oslobodite XObject, sačuvajte pomoću SaveAs i na kraju zatvorite izvorni dokument

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. Snimite grafiku jednom. Stamp je ovde Active.
    XObject := Dest.CreateXObjectFromPage(Stamp, 0);
    if XObject = nil then
      raise Exception.Create('Could not capture the stamp page');
    try
      // 2. Postavite kopiju na svaku stranicu Dest. PageNumber je zasnovan na 1.
      for I := 1 to Dest.PageCount do
      begin
        Dest.PageNumber := I;                // učini stranicu I trenutnom
        PageObj := Dest.InsertFormObjectFromXObject(XObject);
        if PageObj = nil then
          Continue;

        M := TPdfMatrix.Create;
        try
          M.Rotate(45);                      // dijagonalni vodeni žig
          M.Translate(150, 100);             // dovedi u položaj
          RawM := M.Handle;
          FPDFPageObj_SetMatrix(PageObj, RawM);
        finally
          M.Free;
        end;
        Dest.UpdatePage;                     // potvrdi izmene ove stranice
      end;
    finally
      XObject.Free;                          // 3. oslobodi PRE nego što se Stamp zatvori
    end;

    // 4. Upišite rezultat dok je Dest još otvoren.
    if not Dest.SaveAs(AOutput) then
      raise Exception.Create('Could not save ' + AOutput);
  finally
    Stamp.Free;                              // izvor se zatvara poslednji
    Dest.Free;
  end;
end;

Oblik blokova try obezbeđuje pravilan rad. Unutrašnji finally oslobađa XObject pre nego što kontrola stigne do spoljnog finally koji oslobađa Stamp, pa se ručica uvek oslobađa dok je izvor živ, čak i ako izuzetak nastane usred petlje. Sa pravilnim ugnježđavanjem pravilo životnog veka sprovodi se samo

FS_MATRIX anatomija koja pokazuje šest afinih koeficijenata koje PDFium koristi da skalira, rotira i prevede pečati Form XObject na strani
Šest brojeva mapira koordinate žiga u prostor stranice, i TPdfMatrix sastavlja Scale, Rotate i Translate u redu poziva da namesti dijagonalni vodeni žig

Pečatiranje je deo šireg skupa alata za izgradnju i uređivanje sadržaja stranice. Ako je pečat slika, tekst o pretvaranju slika u PDF pomoću PDFium-a pokazuje kako bitmapu prvo uneti u dokument. Ako uz vidljivi pečat želite da prenesete datoteku, tekst o PDF prilozima u Delphiju opisuje embedded-file stranu. Sve je deo PDFium Component za Delphi i C++Builder, zajedno sa API-jima za renderovanje, uređivanje i dokumente