Tehnički članak

Životni ciklus PDF dokumenta i stranice u Delphi-ju

PDFlibPas, nativna PDF biblioteka za Delphi i C++Builder, PDF dokumentu daje dva odvojena mjesta za automatsko ponašanje: akcije životnog ciklusa na razini dokumenta, poput WillClose, WillSave, DidSave, WillPrint i DidPrint, pohranjene u rječniku /AA kataloga, te akcije životnog ciklusa na razini stranice — Open i Close — pohranjene u vlastitom rječniku /AA svakog objekta Page. Zamjena ta dva spremnika najčešći je način na koji akcija životnog ciklusa neprimjetno ne učini ništa

Pokretački slučajevi sasvim su uobičajeni. Financijski tim želi predložak izvoda koji upisuje vremensku oznaku ispisa i bilježi tko je ispisao dokument u trenutku kada ispis stvarno počne, a ne kada se datoteka samo otvori. Tijek rada s mnogo obrazaca treba automatski poslati vrijednosti polja na poslužitelj prije nego što PDF klijent čitatelja smije zatvoriti prozor, kako zatvorena kartica nikada ne bi značila izgubljenu izmjenu. Višestranično izvješće želi natpis specifičan za stranicu koji se pojavljuje samo dok je ta stranica na zaslonu. PDF zapravo nudi i treću razinu ispod dokumenta i stranice za takvo ponašanje — akcije pridružene vlastitom unosu /A pojedinačnog polja obrasca ili poveznice, o čemu govori prateći članak o interaktivnim akcijama obrazaca i JavaScriptu — ali ovaj članak ostaje na dvije više razine: cijelom dokumentu i jednoj stranici

Koji se okidači nalaze u rječniku /AA kataloga dokumenta?

U rječniku /AA kataloga nalazi se pet okidača, a svaki se aktivira za događaj koji utječe na cijeli dokument, a ne na pojedinačnu stranicu. ISO 32000-1 §12.6.3 (Trigger Events) navodi ključeve na razini dokumenta kao WC, WS, DS, WP i DP — doslovne nazive od dva slova koji se upisuju u rječnik /AA — za WillClose, WillSave, DidSave, WillPrint i DidPrint tim redom, a PDFlibPas potpuno jednako preslikava taj skup u enumeraciju TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction jedina je ulazna točka koja pridružuje bilo koji od tih pet okidača, a parametar ActionKind koji prima jedna je od deset konstanti PDF_ACTION_BUILDER_* zajedničkih svim pozivima za izgradnju akcija u biblioteci, od običnog URI-ja do skripte ili skoka na odredište. Što GoTo, udaljena datoteka, ugrađena datoteka ili akcija Launch stvarno rade nakon aktiviranja drugo je pitanje od onoga gdje su pridruženi, a time se bavi prateći članak o akcijama GoTo, remote, embedded i launch — ovaj članak ostaje uz pitanje spremnika, kataloga ili stranice, a ne uz pitanje vrste akcije

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.AddStandardFont(4);
    Lib.DrawText(40, 700, 'Quarterly statement');
    Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
      'https://example.com/audit/will-save', '', 0, 0);
    Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
      'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
    Lib.SaveToFile('statement.pdf');
  finally
    Lib.Free;
  end;
end;

Po čemu se okidač na razini stranice razlikuje od onoga na razini dokumenta?

Okidač na razini stranice aktivira se samo za pojedinačni objekt Page kojemu je pridružen, a PDFlibPas ga pohranjuje u vlastiti rječnik /AA te stranice, a ne u katalog. Postoje samo dva okidača stranice, Open i Close, koji odgovaraju ključevima O i C koje ISO 32000-1 definira za rječnik dodatnih akcija stranice, a PDFlibPas ih izlaže kao patOpen i patClose kroz SetPageAction, koji ih pridružuje stranici trenutačno odabranoj pozivom SelectPage — detalj koji je važan kada prvi put prolazite kroz dokument očekujući da će jedan poziv djelovati svugdje, jer se to nikada ne događa. Pridruživanje bilo koje vrste okidača također podiže minimalnu verziju PDF-a datoteke, a dva spremnika traže različite pragove: PDFlibPas podiže dokument najmanje na PDF 1.4 prvi put kada upiše unos /AA u katalog, a najmanje na PDF 1.5 prvi put kada upiše unos /AA na stranicu, neovisno o tome koja se vrsta akcije nalazi unutar njega. To je zahtjev na razini spremnika koji se nadovezuje na sve što sama akcija treba, pa obična URI akcija kojoj bi samoj bio dovoljan PDF 1.1 ipak podiže cijelu datoteku na PDF 1.5 kada se omota u okidač otvaranja stranice

Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
  'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
  'https://example.com/analytics/page-3-closed', '', 0, 0);

Čitanje i uklanjanje akcija životnog ciklusa

GetDocumentActionInfo i GetPageActionInfo oba vraćaju zapis TPDFlibActionInfo, a polje Kind ima vrijednost akNone kada tom okidaču ništa nije pridruženo, stoga provjerite Kind prije nego što se oslonite na bilo koje drugo polje zapisa — URI, JavaScript, FileName i ostala polja imaju smisla samo za onu vrstu akcije koju Kind stvarno prijavljuje, jer se isti oblik zapisa ponovno koristi za svaku vrstu akcije koju graditelj može proizvesti. RemoveDocumentAction i RemovePageAction svaki čiste jedan okidač i vraćaju 1 kada su pronašli nešto za uklanjanje, a 0 kada je okidač već bio prazan; kada je uklonjeni unos bio posljednji preostali u rječniku /AA, PDFlibPas briše i sada prazni /AA, umjesto da iza sebe ostavi viseći, besmisleni spremnik na katalogu ili stranici

var
  Info: TPDFlibActionInfo;
begin
  Info := Lib.GetDocumentActionInfo(datWillSave);
  if Info.Kind = akURI then
    WriteLn('WillSave calls out to: ', string(Info.URI));

  if Lib.RemoveDocumentAction(datWillSave) = 1 then
    Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
      'https://example.com/audit/will-save-v2', '', 0, 0);
end;

Dopušta li PDF/A uopće akcije životnog ciklusa?

Ne. Sukladnost s PDF/A odbacuje cijeli spremnik dodatnih akcija, a ne samo vrste akcija koje zvuče rizično, jer ISO 19005 ograničava model interaktivnih akcija PDF-a uz pretpostavku da se arhivska datoteka mora jednako prikazivati desetljećima poslije, bez ovisnosti o skriptnom mehanizmu ili mrežnoj vezi koja možda tada neće postojati. SetLifecycleAction, zajednički graditelj iza SetDocumentAction i SetPageAction, provjerava PDFAMode prije nego što uopće pogleda ActionKind, pa se URI akcija koja samo otvara web-stranicu tvrtke ili Named akcija koja samo znači prijelaz na sljedeću stranicu nađe u istoj mreži kao i opasna akcija — blokirana je iako je sigurnosni preglednik inače ne bi označio, jer je ograničenje strukturno, a ne pojedinačno. Praktična opasnost jest to što je odbijanje tiho: SetDocumentAction i SetPageAction vraćaju 0 bez iznimke, pa pozivno mjesto koje nikada ne provjeri povratnu vrijednost isporuči dokument kojemu tiho nedostaje okidač koji je trebao sadržavati

Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
     '', '', 0, 0) = 0 then
  // rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
  WriteLn('lifecycle action not attached');

Vrijedi zapamtiti jednu asimetriju. RemoveDocumentAction i RemovePageAction nikada ne provjeravaju PDFAMode, pa učitavanje datoteke koja već sadrži akcije životnog ciklusa nesukladne pravilima i njihovo uklanjanje na putu prema spremanju sukladnom s PDF/A radi upravo kako se očekuje — samo je put zapisivanja, odnosno pridruživanje novog okidača, uvjetovan načinom sukladnosti

Gdje se uklapa ispis pri otvaranju bez okidača WillOpen?

Rječnik /AA kataloga uopće nema unos WillOpen, namjerno — /AA na razini dokumenta u ISO 32000-1 definira točno pet ključeva, WillClose, WillSave, DidSave, WillPrint i DidPrint, a nijedan se s tog popisa ne aktivira samo zato što je datoteka otvorena. Kuka za vrijeme otvaranja nalazi se u zasebnom unosu kataloga, /OpenAction, koji PDFlibPas izlaže kroz vlastitu skupinu poziva, među kojima su SetOpenActionJavaScript, SetOpenActionDestination i SetOpenActionNamedDestination, a nijedan od njih uopće ne dodiruje rječnik /AA ni enumeraciju TPDFlibDocumentActionTrigger. Ta se dva mehanizma ipak mogu kombinirati, što je obično upravo ono što predložak za ispis pri otvaranju treba: izgradite predložak tako da njegov /OpenAction pokrene posao ispisa, obično JavaScript akcijom koja poziva vlastitu naredbu preglednika za ispis, a sam ispis daje akcijama WillPrint i DidPrint nešto nad čim mogu raditi — vremensku oznaku upisanu prije slanja stranica na ispis i revizijski zapis napisan kada je to dovršeno

Koliko su ovi okidači pouzdani u različitim PDF preglednicima?

Ne izvršava ih svaki preglednik, čak ni izvan PDF/A, stoga akciju životnog ciklusa tretirajte kao zahtjev, a ne jamstvo. Acrobat i većina punih stolnih čitača vjerno izvršavaju cijeli skup, ali velik dio stvarne potrošnje PDF-a uopće ne dodiruje rječnik dodatnih akcija: preglednici ugrađeni u web-stranice, većina mobilnih čitača i gotovo svaki poslužiteljski tijek prikazivanja ili izdvajanja teksta ili potpuno zanemaruju /AA ili podržavaju samo njegov uski dio, pri čemu WillPrint i DidPrint obično prolaze najlošije jer konverzija bez grafičkog sučelja nema operaciju ispisa na koju bi ih mogla priključiti. Ako je akcija slanja obrasca pri WillClose jedini put kojim se hvataju podaci obrasca, to nije pouzdan put — uparite je s izričitim gumbom za slanje i automatski okidač tretirajte kao pogodnost za čitače koji ga slučajno podržavaju

Okidači dokumenta, stranice i polja tri su razine istog temeljnog mehanizma rječnika akcija, a kada je spremnik jasan, preostaje odabrati odgovarajuću konstantu ActionKind i provjeriti povratni kod. Ovi okidači životnog ciklusa, zajedno sa širim API-jem za izgradnju akcija koji ovaj članak dotiče, dio su standardne PDFlibPas PDF biblioteke za Delphi, s potpunim opisom okidača i vrsta akcija u dokumentaciji proizvoda