Tehnički članak

Akcije životnog ciklusa PDF-a: Catalog /AA naspram Page /AA u Delphiju

PDFlibPas, izvorna Delphi i C++Builder PDF biblioteka, daje PDF dokumentu dva odvojena mesta za automatsko ponašanje: radnje životnog ciklusa na nivou dokumenta, kao što su WillClose, WillSave, DidSave, WillPrint i DidPrint, čuvaju se u rečniku /AA objekta Catalog, dok se radnje životnog ciklusa na nivou stranice — Open i Close — čuvaju u sopstvenom rečniku /AA svakog objekta Page. Mešanje ta dva kontejnera najčešći je način da radnja životnog ciklusa nečujno ne uradi ništa

Povodi su sasvim uobičajeni. Finansijski tim želi obrazac izveštaja koji upisuje vreme štampanja i beleži ko je štampao čim štampanje zaista počne, a ne kada se datoteka samo otvori. Tok sa mnogo obrazaca mora automatski da pošalje vrednosti polja serveru pre nego što PDF klijent čitaoca sme da zatvori prozor, tako da zatvorena kartica nikada ne znači izgubljenu izmenu. Izveštaj sa više stranica želi baner specifičan za stranicu koji se prikazuje samo dok je ta stranica na ekranu. PDF za ovakvo ponašanje nudi i treći nivo ispod dokumenta i stranice — radnje vezane za sopstveni unos /A pojedinačnog polja obrasca ili veze, što je tema pratećeg članka o interaktivnim radnjama obrazaca i JavaScriptu — ali ovaj članak ostaje na dva viša nivoa: celom dokumentu i jednoj stranici

Koji okidači žive u /AA rečniku dokumenta Catalog

U rečniku /AA objekta Catalog postoji pet okidača i svaki se aktivira za događaj koji utiče na ceo dokument, a ne na jednu stranicu. ISO 32000-1 §12.6.3 (Događaji okidača) navodi ključeve na nivou dokumenta kao WC, WS, DS, WP i DP — doslovne nazive od dva slova upisane u rečnik /AA — redom za WillClose, WillSave, DidSave, WillPrint i DidPrint, a PDFlibPas taj skup potpuno preslikava u enumeraciju TPDFlibDocumentActionTrigger: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction je jedinstvena ulazna tačka koja vezuje bilo koji od pet okidača, a njen parametar ActionKind jedna je od deset konstanti PDF_ACTION_BUILDER_* koje biblioteka deli između svih poziva graditelja radnji, od običnog URI-ja do skripte ili skoka na odredište. Šta radnja GoTo, remote-file, embedded-file ili Launch zaista radi nakon aktiviranja drugo je pitanje od mesta na koje se vezuje i tema je pratećeg članka o GoTo, udaljenim, ugrađenim i launch radnjama — ovaj članak ostaje uz pitanje kontejnera, Catalog ili Page, a ne uz pitanje vrste radnje

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č nivoa stranice razlikuje od okidača nivoa dokumenta

Okidač na nivou stranice aktivira se samo za objekat Page za koji je vezan, a PDFlibPas ga čuva u sopstvenom rečniku /AA te stranice, a ne u rečniku objekta Catalog. Postoje samo dva okidača stranice, Open i Close, koji odgovaraju ključevima O i C koje ISO 32000-1 definiše za rečnik dodatnih radnji stranice, a PDFlibPas ih izlaže kao patOpen i patClose kroz SetPageAction, koji se vezuje za trenutno izabranu stranicu pomoću SelectPage — detalj važan kada prvi put prolazite kroz dokument očekujući da jedan poziv važi svuda, jer se to nikada ne dešava. Vezivanje bilo koje vrste okidača takođe podiže minimalnu PDF verziju datoteke, a dva kontejnera zahtevaju različite nivoe: PDFlibPas podiže dokument najmanje na PDF 1.4 kada prvi put upiše unos Catalog /AA, a najmanje na PDF 1.5 kada prvi put upiše unos Page /AA, nezavisno od vrste radnje u njemu. To je zahtev na nivou kontejnera dodat zahtevima same radnje, pa obična URI radnja kojoj bi samoj bila dovoljna verzija PDF 1.1 ipak podiže celu datoteku na PDF 1.5 kada se umota 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 vraćaju zapis TPDFlibActionInfo, a polje Kind dobija vrednost akNone kada za taj okidač ništa nije vezano, zato proverite Kind pre nego što se oslonite na bilo koje drugo polje zapisa — URI, JavaScript, FileName i ostali podaci imaju smisla samo za onu vrstu radnje koju Kind zaista prijavljuje, pošto se isti oblik zapisa ponovo koristi za svaki tip radnje koji graditelj može da proizvede. RemoveDocumentAction i RemovePageAction pojedinačno uklanjaju 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 poslednji u rečniku /AA, PDFlibPas briše i sada prazan /AA, umesto da na Catalogu ili stranici ostavi viseći, besmisleni kontejner

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;

Da li PDF/A uopšte dozvoljava akcije životnog ciklusa

Ne. PDF/A usaglašenost odbacuje ceo kontejner dodatnih radnji, a ne samo vrste radnji koje zvuče rizično, jer ISO 19005 ograničava model interaktivnih radnji u PDF-u uz pretpostavku da arhivska datoteka mora da se prikazuje isto i decenijama kasnije, bez oslanjanja na skriptni mehanizam ili mrežnu vezu koja tada možda neće postojati. SetLifecycleAction, zajednički graditelj iza metoda SetDocumentAction i SetPageAction, proverava PDFAMode pre nego što uopšte pogleda ActionKind, pa URI radnja koja samo otvara veb stranicu kompanije ili Named radnja čije je jedino značenje prelazak na sledeću stranicu upada u istu mrežu kao i opasna radnja — blokirana je čak i kada je bezbednosni pregled obično ne bi označio, zato što je ograničenje strukturno, a ne pojedinačno. Praktična opasnost je u tome što je odbijanje nečujno: SetDocumentAction i SetPageAction vraćaju 0 bez izuzetka, pa pozivno mesto koje nikada ne proveri povratnu vrednost isporuči dokument kojem nedostaje okidač koji je trebalo da nosi

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');

Vredi imati na umu jednu asimetriju. RemoveDocumentAction i RemovePageAction nikada ne proveravaju PDFAMode, pa učitavanje datoteke koja već sadrži neusaglašene radnje životnog ciklusa i njihovo uklanjanje pre čuvanja usaglašenog PDF/A dokumenta radi upravo kako se očekuje — samo putanja upisa, odnosno vezivanje novog okidača, zavisi od režima usaglašenosti

Gde se uklapa štampanje pri otvaranju bez WillOpen okidača

Rečnik /AA objekta Catalog po nameni uopšte nema unos WillOpen — /AA na nivou dokumenta u standardu ISO 32000-1 definiše tačno pet ključeva, WillClose, WillSave, DidSave, WillPrint i DidPrint, i nijedan od njih se ne aktivira samo zato što je datoteka otvorena. Kuka za trenutak otvaranja nalazi se u posebnom unosu objekta Catalog, /OpenAction, koji PDFlibPas izlaže kroz sopstvenu grupu poziva, među kojima su SetOpenActionJavaScript, SetOpenActionDestination i SetOpenActionNamedDestination, a nijedan od njih uopšte ne dodiruje rečnik /AA niti enumeraciju TPDFlibDocumentActionTrigger. Ipak, ta dva mehanizma mogu da se kombinuju, što je obično upravo potrebno obrascu za štampanje pri otvaranju: napravite obrazac tako da njegov /OpenAction pokrene posao štampanja, najčešće JavaScript radnjom koja poziva sopstvenu komandu pregledača za štampanje, a samo štampanje daje WillPrint i DidPrint događaj nad kojim mogu da rade — vreme se upisuje pre nego što stranice krenu u štampu, a revizijski unos nakon završetka

Koliko su ovi okidači pouzdani u različitim PDF pregledačima

Ne izvršava ih svaki pregledač, čak ni izvan PDF/A režima, zato radnju životnog ciklusa posmatrajte kao zahtev, a ne kao garanciju. Acrobat i većina potpunih desktop čitača verno izvršavaju ceo skup, ali veliki deo stvarne upotrebe PDF-a uopšte ne dodiruje rečnik dodatnih radnji: pregledači ugrađeni u veb, većina mobilnih čitača i gotovo svaki serverski tok za prikazivanje ili izdvajanje teksta ili potpuno ignorišu /AA ili podržavaju samo njegov uzak deo, pri čemu WillPrint i DidPrint obično prolaze najgore jer konverzija bez grafičkog interfejsa nema operaciju štampanja na koju bi se zakačili. Ako je WillClose submit-form radnja jedini put za prikupljanje podataka obrasca, to nije pouzdan put — uparite je sa eksplicitnim dugmetom za slanje i automatski okidač posmatrajte kao pogodnost za čitače koji ga podržavaju

Okidači dokumenta, stranice i polja predstavljaju tri nivoa iste osnovne infrastrukture rečnika radnji, a kada je kontejner jasan, preostaje izbor odgovarajuće konstante ActionKind i provera povratnog koda. Ovi okidači životnog ciklusa, zajedno sa širim API-jem graditelja radnji koji članak dotiče, dolaze u standardnoj PDFlibPas Delphi PDF biblioteci, uz potpunu referencu okidača i vrsta radnji u dokumentaciji proizvoda