Tehnički članak

Višekratni pečati za stranice (Page Stamps) putem Form XObjects s PDFiumom

Stavljanje vodenog žiga ili pečata logotipa na svaku stranicu dokumenta izgleda kao posao od pet minuta, sve dok rezultat ne otvorite u nekom alatu za pregled veličine datoteka. Najočitiji bi pristup bio proći redom kroz stranice te na svakoj od njih iznova izgraditi te iste tekstualne ili slikovne objekte. To vizualno sasvim prolazi, no istodobno je neekonomično i rasipno na način koji se samo umnožava. Dijagonalni vodeni žig s tekstom "NACRT" nacrtan izravno na izvješće od stotinu stranica predstavlja točno stotinu kopija istih vektorskih staza i tekstualnih podataka koji se nalaze u tokovima sadržaja, dok spremljena datoteka sa sobom povlači baš svaki od njih

Form XObject je konstrukt koji PDF nudi kako bi se izbjeglo upravo ovo. On umata komad sadržaja za višekratnu upotrebu, bilo čitavu stranicu ili mali predložak, unutar jedinstvenog imenovanog objekta kojeg se zatim može nacrtati na mnogo mjesta i u više navrata. Sadržaj kao takav boravi u datoteci tek jednom. Svaka stranica koja poželi takav pečat sadržava kratku uputu u kojoj stoji: "nacrtaj XObject N ovdje, ovom transformacijom". Vodeni žig od stotinu stranica na taj način datoteci pridodaje tek jedan jedini sadržajni objekt, umjesto njih stotinu, i upravo u tome leži razlika između dokumenta čija veličina raste linearno obzirom na broj stranica i onog gdje to nije slučaj. Vodeni žigovi, pečati s logotipom, predlošci s brojem stranica i slični žigovi - svi oni predstavljaju jedan te isti oblik problema, za koje se Form XObject nudi kao pravo alat rješenje

Zašto jedan pohranjeni objekt nadvladava stotinu ponovnih iscrtavanja

Ušteda je ovdje strukturna, a ne puko kozmetička. PDF stranica renderira se procesuiranjem svog toka sadržaja, zapravo, niza operacija namijenjenih crtanju. Onoga trena kada ponovno iscrtavate pečat na svakoj novoj stranici, vi ujedno pripajate puni slijed operatora za taj pečat i to toku apsolutno svake dotične stranice, pri čemu se bajtovi višestruko udvostručuju i to na onoliko mjesta koliko uopće ima spomenutih stranica. Form XObject prosljeđuje te operatore unutar samog tog jednoga toka pohranjenoga u spomenutom dokumentu. Referenca koju pritom održava takva pojedina stranica poprilično je sitna: ona doslovno prosljeđuje transformacijsku matricu, poziva na izvršavanje onaj XObject i povrati stanje. Broj stranica s tim tako više ne umnožava sam po sebi trošak same te grafike

I navedeno je i te kako presudno kada god taj pečat biva pozamašan. Vektorski žig sa stotinama segmenata putanje ili pak bitmapa logotipa znaju poprilično koštati prilikom pohrane. Jednom spremljen i proslijeđen kroz referencu, navedeni "teški" dio plaća se točno po taj jedan prvi put te je preostali režijski trošak po pojedinoj stranici ograničen na tek pokoji bajt tog poziva. Sami vizualni rezultat postignut na stranici identičan je onom izravnog prebrisavanja, što ujedno i jest sam smisao svega ovoga. Vaš dragocjeni čitatelj tu dotičnu razliku zapravo vjerojatno neće ni naslutiti, no zato će izmjereni obujam dobivene datoteke to itekako znati cijeniti

Zahvaćanje (capturing) stranice unutar XObjecta

PDFium stvara višekratno iskoristivi objekt oslanjajući se pritom na jednu od već postojećih stranica. Sam izvor biva dakle nekakva stranica iz određenog dokumenta koji ste upravo otvorili, neki maleni PDF od točno te jedne takve stranice koja pak osim vaše puke grafike namijenjene tom vodenom žigu ne zadržava više ništa unutra, ili pak može biti neka posebno istaknuta stranica ubačena iznutra neke naoko mnogo gabaritnije datoteke. Metoda CreateXObjectFromPage uhvati odnosno dočepa se tog zatečenog pripadnoga izvornog dotičnoga točnoga sadržaja sa te iste odobrene joj zatražene stranice i pretvara to direktno u višekratno upotrebljivu upravljačku ručku u sklopu odredišnog dokumenta – upravo onoga kojeg pečatirate

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) ...

Potpis iznosi CreateXObjectFromPage(Source: TPdf; SourcePageIndex: Integer): TPdfXObject. Metoda izaziva iznimku ukoliko izvorni dokument nije označen s Active, a kada PDFium nije u stanju izgraditi navedeni objekt tada metoda za povratnu vrijednost naime umjesto iznimke daje nil, i upravo iz tog razloga gornja i spomenuta eksplicitna provjera apsolutno nipošto nije opcionalna. Pokazivač koji se vraća jest pak TPdfXObject koji je vaše vlasništvo, a dva navedena ograničenja životnog vijeka pridružena njemu točno su onaj isti dio cijele ove vježbe na kojemu se ljudi inače znaju redovno poskliznuti, pa stoga istima iz tog istog razloga nižepotpisanom prilažemo i pripadajući posebni zasebni odlomak na samome dnu

Postavljanje pečata na stranicu

Zahvaćeni XObject sam po sebi uopće ne radi ništa. Da bi se mogao ukazati, morat ćete ubaciti njegovu kopiju povrh trenutačne stranice u dokumentu, one na koju pokazujete na temelju njezinog 1-based (bazirano na jedan) PageNumber svojstva, s InsertFormObjectFromXObject. Sam taj poziv povratno vraća onaj pripadni pozadinski objekt od iste dotične stranice, odnosno FPDF_PAGEOBJECT, pa je tada taj izbačeni povratni dotični upravljački dršak naime izravan način s pomoću kojeg onda uspješno definirate i dodijelite poziciju za to dotično smještanje. Bez uobičajene transformacije, isti onakav iznjedreni sporni preostali pečat završit će sletjevši izravno negdje onuda unutar početnog onog ishodišta pripadajućih istih izvornih koordinata onakve pripadne stranice odakle je potekao, što uostalom na kraju rijetko kad ikada i uistinu ispadne upravo na tom dotičnom po izboru mjestu kakvo priliči vama i točno tamo onda negdje kuda točno to redovno vi nasumce zasigurno vjerojatno to prigodno priželjkujete

Budući da InsertFormObjectFromXObject umeće po jednu kopiju sa svakim upućenim pojedinim pozivom te sa svakim pokušajem iznova povratno natrag izbacuje novi sasvim svježi spomenuti objekt zadužen po onoj pojedinoj stranici, isti XObject možete oslikati nekoliko puta uslijed razmještaja po dotičnoj jednoj stranici točno oslonjeni samo uz uporabu raznoraznih tih sasvim drukčijih dotičnih primijenjenih transformacija, i na sve to taj iz početka pohranjeni spomenuti njezini unutar dotične iste prigodne datoteke preostali i nadasve ugrađeni taj onakav nekakav sadržaj svejedno biva nabrojan kao isključivo točno jedan jedini. Kutni logotip i jedva vidljivi vodeni žig koji se proteže po čitavoj stranici mogu poteći iz potpuno istog zahvaćenog objekta

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;

Dva detalja prilikom održavanja ovakvog procesa čine ga u potpunosti sigurnim. Kao prvo, kada ga jednom i umetnete, spomenuti objekt stranice nadalje neprikosnoveno pripada isključivo dotičnoj stranici, a ne nipošto XObjectu. Naknadno oslobađanje XObjecta u tom slučaju onda ni na koji način ne poništava ona smještanja koja ste već obavili. Upravo to jest onaj razlog zbog kojeg uslijed svega navedenoga funkcionira izneseno pravilo temeljeno na 'stvori-smjesti-oslobodi' poretku opisanom u nastavku. Drugo, umetanje te istodobno pozicioniranje mijenja jedino i isključivo samu listu objekata te stranice prisutnih i izgrađenih unutar njene memorije; poziv prema UpdatePage tek zapravo povratno provodi serijalizaciju navedene iste liste, šaljući ih u spomenuti dotični usnuli tok sadržaja na promatranoj pripadnoj stranici, iz čega na koncu posve onda i ne čudi podatak pa time uređena i zatečena stranica iz koje naočigled sasma točno biva izostavljen upravo ovaj istaknuti sam poziv, preostaje prigodno naime sačuvana samo kao da na nju apsolutno na koncu onda onuda nekakav priloženi pečat nikada uopće naposlijetku baš ta ista točno onda zapravo nije bio niti i postavljen

Pravilo o životnom vijeku pokazivača s kojim ljudi obično imaju problema

Dva ograničenja upravljaju pokazivačem na XObject, a ignoriranje bilo kojeg od njih proizvodi grešku koja naizgled nema nikakve veze sa svojim pravim uzrokom. Kao prvo, izvorni dokument mora biti označen pod Active u onom trenutku kada izdajete poziv na CreateXObjectFromPage. Proces zahvaćanja očitava sadržaj na samoj toj izvornoj stranici iz aktivnog te dotičnog izvornog dokumenta, što znači da i sam dokument baš kao i ta pridružena stranica moraju nužno oboje biti otvoreni kao i posve valjani za to vrijeme dok se dotični pokazivač ne izgradi. Kao drugo, a to je upravo ono što najčešće i začudi brojne korisnike, dotični dršak mora u potpunosti obavezno biti oslobođen i to prije nego li se sama izvorna stranica ikada i zatvori, ili točnije puko u praksi znatno prije samog onog trenutka po u kojem naumite onda slobodno sasvim i u potpunosti posve zatvoriti te naprosto i na koncu onako osloboditi taj istaknuti po opisu isti dotični njezini i naime puki njen navedeni izvorni i izvorišni isti izvorni onakav njezin takozvani pripadni prepoznati ovlašteni dotični dokument onaj iz opsega priloženih iz istoga a iz koga je navedeni naime onda sasma i on sam u nizu i zapravo naoko zatečeni iz izloženih spomenuti onda i on na koncu onda posve i sam dotično glatko došao, potekao i naime dotično usput i prigodom posve apsolutno u isto i iz istog navedenoga proizašao

Razlog navedenome je taj što XObject označava referencu unutar strukture koju izvorni dokument još uvijek posjeduje. To nije odvojena, samostalna kopija koju možete naokolo prenositi nakon što izvor nestane. Zatvorite li prvo izvor, pokazivač će ostati uperen u sadržaj koji je uništen, tako da njegovo kasnije oslobađanje ili bilo kakva druga primjena izravno djeluje nad memorijom koja više nije valjana. Simptomi su klasični za slučaj s visećim pokazivačem: prekršaj pristupa pri gašenju aplikacije ili povremeno neobjašnjivo kvarenje memorije obzirom na raspored same dodjele, gdje trag stoga upućuje na kôd čišćenja, a ne na redak koji je zapravo i izazvao problem. Rješenje je preustroj poretka, a nipošto defenzivno programiranje. Izgradite sami taj XObject, zatim ga slobodno umetnite na baš svaku stranicu kojoj treba, potom oslobodite navedeni XObject i tek nakon svega na kraju prigodno usput i zatvorite izvorni dokument. Destruktor klase TPdfXObject u vaše ime oslobađa skriveni pozadinski pokazivač iz PDFiuma, tako da oslobađanje ove omotnice u pravom trenutku ostaje vaša jedina briga te predstavlja u potpunosti vašu cjelokupnu odgovornost

Matrica i što znači njezinih šest brojeva

Smještanje predstavlja 2D afinu transformaciju, potpuno istu onu koju sami PDF redovno primjenjuje baš svuda uokolo ne bi li uspješno preusmjerio i postavio sadržaj (ISO 32000-1, odjeljak 8.3.4). Tu se izrijekom radi o točno šest brojeva zapisanih u slijedu kô a, b, c, d, e, f, a koje zatim onda ujedno PDFium isto izlaže u obliku pripadajućeg dotičnog zapisa vrste FS_MATRIX. Svi oni preslikavaju svaku izlučenu dotičnu odabranu točku po površini onoga njezinoga usputnoga i baš vlastitog prostora kod izlučenoga dotičnoga prepoznatoga spomenutoga objekta unutar izlučenih koordinata usred točno istog od same zatečene dotične i naime usput istaknute iste priložene dotične stranice odnosno njezina onog od prostora te stranice:

// 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)

Ovih šest vrijednosti možete ispuniti i vlastoručno, no njihovo pješačko kombiniranje najčešće je ona točka gdje stvar s rotacijom obično pođe po zlu, budući da ona na kraju ispremiješa sva četiri njezina sastojka, dakle a, b, c, d skupa. Omotnica pod nazivom TPdfMatrix iz FPdfMatrix modula posložit će takve uobičajene postupke umjesto vas i odraditi post-množenje skupa s time u hodu, po čemu se onda Translate, Scale te Rotate posve redovno ulančavaju točno onim navedenim redoslijedom kakvim ih vi i usmjeravate u pozivu. Sam dijagonalni vodeni žig predstavlja rotaciju nakon koje ide translacija za potrebe njezina recentriranja; dok se logotip uz sami kutni obrub namješta pomoću skaliranja iza kojega ide odgovarajuća translacija. U trenu kada se ta matrica izgradi, preslikajte spomenutu sirovu vrijednost, odnosno svojstvo Handle koje pripada po svom tipu uz onu FS_MATRIX, posve u neku ovlaštenu izabranu vašu lokalnu varijablu pa upravo to isto predajte spomenutoj funkciji iz FPDFPageObj_SetMatrix; budući da sam import ovdje tu dotičnu puku navedenu proglašava po uzoru na kakav var parametar, takvo se imovinsko svojstvo s toga nipošto i ne može ni izravno predati istoj, dok je pak njezin navedeni rezultat posve onda onako onuda obično tada puko 0 (nula) pri svakoj pogrešci. Onaj sasvim i po rangu niži FPDFPageObj_Transform, a koji prima te puke šestorke ovlaštene brojeve kô iz opsega priloženoga potpuno izravno u onom formatu kao doubles, dostupan vam je i na raspolaganju ukoliko naprosto mnogo radije u samom pozivu preferirate dostavu samih golih brojeva mjesto izgradnje omotnice kô takve

Pečatiranje svake stranice, u pravom redoslijedu

Potpuni uzorak skuplja navedene komadiće zajedno rabeći onaj poredak koji naprosto nalaže već spomenuto pravilo o životnom vijeku. Otvorite oba dokumenta, usput jednom uhvatite onaj predviđeni pečat, propješačite uzduž svih odredišnih stranica postavljajući njihovo svojstvo 1-based (bazirano na jedan) PageNumber naizmjenično po svome redu pa usput dodavajući, ali istodobno te uz pozicioniranje iz od svake pripadne puke pojedine dotične kopije, neizostavno potvrđujući svaku istu uz UpdatePage, pa onda oslobodite XObject, pa onda spasite putem SaveAs, te dopustite usput naveliko dotičnom izvornome tu iz uloge priloženom takozvanom iz izvornog njezina dokumentu da se slobodno naposljetku zatvori posljednji

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;

Pravi je oblik onih try blokova upravo taj koji naprosto i odrađuje onaj suštinski ključni dio posla. Njegov unutrašnji blok sa finally oslobađa XObject mnogo prije negoli sama kontrola programa uopće naposljetku i dospije do onog vanjskog s finally koji je pak prigodno zadužen tu iz uloge osloboditi puko onaj preostali Stamp, čime se obuzdava redovna pojava da dotični pokazivač stalno biva oslobođen uvijek na koncu isključivo onda i samo onda dok god je i sami usnuli izvor i dalje još preostao znatno živ, pa makar da istodobno i kakva uistinu dotična iznimka uslijed samog postupka naime okine nepredviđeno nadasve izravno tik nasred zatečene petlje. Dobro namjestite ovakvo ugnježđivanje i pravilo životnog vijeka će nadalje povesti brigu o samom sebi

Pečatiranje tek predstavlja jedan maleni kut puno opsežnijeg skupa alata namijenjenog izgradnji te uspješnom uređivanju stranica i njezinog sadržaja. U slučaju ukoliko vaš pečat po svojoj naravi predstavlja sliku umjesto zahvaćene stranice, tada članak o pretvorbi slika u PDF dokumente pomoću PDFiuma obuhvaća način na koji prvenstveno uopće dovesti takvu bitmapu u dokument. A ukoliko poželite da usputna pratnja vidljivom pečatu bude zapravo nekakva datoteka umjesto prosute tinte, članak korištenje PDF privitaka u Delphiju nadasve predočava stranu koja se tiče ugrađenih datoteka. Sve od navedenoga isporučuje se pod imenom PDFium Component komponente za Delphi i C++Builder, bok uz bok s API-jima namijenjenim usput za renderiranje, uređivanje, ali i sami dokument, inače pokrivenima po svim ostalim mjestima ovog usput predočenog bloga