Techninis straipsnis

PDF gyvavimo ciklo veiksmai: Catalog /AA ir Page /AA Delphi aplinkoje

PDFlibPas, gimtoji Delphi ir C++Builder PDF biblioteka, PDF dokumente suteikia dvi atskiras vietas automatiniam elgesiui prijungti: dokumento lygio gyvavimo ciklo veiksmus, tokius kaip WillClose, WillSave, DidSave, WillPrint ir DidPrint, saugomus Catalog /AA žodyne, ir puslapio lygio gyvavimo ciklo veiksmus – Open ir Close – saugomus kiekvieno Page objekto nuosavame /AA žodyne. Supainiojus šiuos du konteinerius, gyvavimo ciklo veiksmas dažniausiai tyliai nieko neatlieka

Praktiniai atvejai yra įprasti. Finansų komanda nori ataskaitos šablono, kuris įrašo spausdinimo laiko žymą ir užregistruoja, kas ją išspausdino, vos prasidėjus tikram spausdinimui, o ne vien atidarius failą. Formomis paremtai darbo eigai reikia automatiškai nusiųsti laukų reikšmes į serverį prieš PDF skaitytuvui leidžiant uždaryti langą, kad užvertas skirtukas niekada nereikštų prarasto taisymo. Kelių puslapių ataskaitoje gali reikėti konkrečiam puslapiui skirto skydelio, rodomo tik tada, kai tas puslapis yra ekrane. PDF tokio elgesio atveju iš tikrųjų siūlo trečią lygį žemiau dokumento ir puslapio – veiksmus, prijungtus prie atskiro formos lauko arba nuorodos nuosavo /A įrašo, aptariamus gretimame straipsnyje apie interaktyvius formų veiksmus ir JavaScript – tačiau šiame straipsnyje nagrinėjami du aukštesni lygiai: visas dokumentas ir vienas puslapis

Kokie sužadinimo įvykiai gyvena dokumento Catalog /AA?

Catalog /AA žodyne yra penki sužadinimo įvykiai, ir kiekvienas jų įvyksta dėl įvykio, veikiančio visą dokumentą, o ne vieną puslapį. ISO 32000-1 §12.6.3 (Trigger Events) dokumento lygio raktus nurodo kaip WC, WS, DS, WP ir DP – pažodinius dviejų raidžių pavadinimus, įrašomus į /AA žodyną – atitinkamai WillClose, WillSave, DidSave, WillPrint ir DidPrint, o PDFlibPas tiksliai atkartoja šį rinkinį TPDFlibDocumentActionTrigger išvardijime: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction yra vienintelis įėjimo taškas, prijungiantis bet kurį iš penkių veiksmų, o jo parametras ActionKind yra vienas iš dešimties PDF_ACTION_BUILDER_* konstantų, bendrų visiems bibliotekos veiksmų kūrimo iškvietimams – nuo paprasto URI iki scenarijaus ar perėjimo į paskirties vietą. Ką sužadintas GoTo, nuotolinio failo, įterpto failo ar Launch veiksmas iš tikrųjų daro, yra kitas klausimas nei tai, kur jis prijungiamas, ir jis nagrinėjamas gretimame straipsnyje apie GoTo, nuotolinius, įterptus ir paleidimo veiksmus – čia liekame ties konteinerio klausimu, Catalog ar Page, o ne veiksmo tipo klausimu

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;

Kuo puslapio lygio sužadinimo įvykis skiriasi nuo dokumento lygio?

Puslapio lygio sužadinimo įvykis įvyksta tik vienam Page objektui, prie kurio jis prijungtas, o PDFlibPas jį saugo paties puslapio /AA žodyne, o ne Catalog žodyne. Yra tik du puslapio sužadinimo įvykiai – Open ir Close, atitinkantys O ir C raktus, kuriuos ISO 32000-1 apibrėžia puslapio papildomų veiksmų žodynui, o PDFlibPas juos pateikia kaip patOpen ir patClose per SetPageAction, kuri prijungia veiksmą prie tuo metu pasirinkto puslapio per SelectPage – tai svarbi detalė, kai pirmą kartą cikle pereinate per dokumentą tikėdamiesi, kad vienas iškvietimas bus pritaikytas visur, nes taip niekada neįvyksta. Prijungus bet kurio tipo sužadinimo įvykį taip pat pakeliama minimali failo PDF versija, o šiems dviem konteineriams reikia skirtingų lygių: pirmą kartą įrašydamas Catalog /AA įrašą PDFlibPas pakelia dokumentą bent iki PDF 1.4, o pirmą kartą įrašydamas Page /AA įrašą – bent iki PDF 1.5, nesvarbu, koks veiksmo tipas yra viduje. Tai konteinerio lygio reikalavimas, papildantis tai, ko pačiam veiksmui reikia, todėl paprastas URI veiksmas, veikdamas vienas, reikalaujantis tik PDF 1.1, vis tiek pakelia visą failą iki PDF 1.5, kai įdedamas į puslapio atidarymo sužadinimo įvykį

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

Gyvavimo ciklo veiksmų skaitymas ir šalinimas

GetDocumentActionInfo ir GetPageActionInfo abu grąžina TPDFlibActionInfo įrašą, o laukas Kind grąžinamas kaip akNone, kai tam sužadinimo įvykiui niekas neprijungta, todėl prieš pasitikėdami bet kuriuo kitu įrašo lauku patikrinkite KindURI, JavaScript, FileName ir kiti laukai yra prasmingi tik tam vieninteliam veiksmo tipui, kurį nurodo Kind, nes ta pati įrašo forma pakartotinai naudojama kiekvienam kūrėjo galimam veiksmo tipui. RemoveDocumentAction ir RemovePageAction išvalo po vieną sužadinimo įvykį ir grąžina 1, kai randa ką pašalinti, arba 0, kai sužadinimo įvykis jau tuščias; jei pašalintas įrašas buvo paskutinis /AA žodyne, PDFlibPas ištrina ir dabar tuščią patį /AA, nepalikdamas beprasmiško kabančio konteinerio Catalog arba puslapyje

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;

Ar PDF/A apskritai leidžia gyvavimo ciklo veiksmus?

Ne. PDF/A atitiktis atmeta visą papildomų veiksmų konteinerį, o ne tik rizikingai skambančius veiksmų tipus, nes ISO 19005 riboja PDF interaktyvių veiksmų modelį remdamasis prielaida, kad archyvinis failas po kelių dešimtmečių turi būti atvaizduojamas taip pat, nepriklausydamas nuo scenarijų variklio ar tinklo ryšio, kurio tuo metu gali nebūti. SetLifecycleAction, bendras kūrėjas, naudojamas už SetDocumentAction ir SetPageAction, patikrina PDFAMode dar prieš nagrinėdamas ActionKind, todėl URI veiksmas, tik atveriantis įmonės tinklalapį, arba Named veiksmas, reiškiantis tik perėjimą į kitą puslapį, patenka į tą patį tinklą kaip ir pavojingas veiksmas – užblokuojamas net tas, kurio saugumo tikrintojas paprastai nepažymėtų, nes apribojimas yra struktūrinis, o ne taikomas kiekvienu atveju. Praktinis pavojus tas, kad atmetimas yra tylus: SetDocumentAction ir SetPageAction abi grąžina 0, nesukeldamos išimties, todėl iškvietimo vieta, kuri niekada netikrina grąžinamos reikšmės, gali išsiųsti dokumentą be turėjusio būti jame sužadinimo įvykio

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

Verta prisiminti vieną asimetriją. RemoveDocumentAction ir RemovePageAction niekada netikrina PDFAMode, todėl įkėlus failą, kuriame jau yra neatitinkančių reikalavimų gyvavimo ciklo veiksmų, ir pakeliui į PDF/A atitinkantį išsaugojimą juos pašalinus, viskas veikia tiksliai taip, kaip tikimasi – tik rašymo kelias, naujo sužadinimo įvykio prijungimas, priklauso nuo atitikties režimo

Kaip veikia spausdinimas atidarant failą, jei nėra WillOpen sužadinimo įvykio?

Catalog /AA žodyne tyčia nėra WillOpen įrašo – dokumento lygio /AA ISO 32000-1 apibrėžia tiksliai penkis raktus: WillClose, WillSave, DidSave, WillPrint ir DidPrint, ir nė vienas jų nesuveikia vien dėl to, kad failas buvo atidarytas. Atidarymo momento kabliukas yra atskirame Catalog įraše /OpenAction, kurį PDFlibPas pateikia per atskirą iškvietimų šeimą, tarp jų SetOpenActionJavaScript, SetOpenActionDestination ir SetOpenActionNamedDestination; nė vienas iš jų neliečia /AA žodyno ar TPDFlibDocumentActionTrigger išvardijimo. Vis dėlto abu mechanizmai dera, ir dažniausiai būtent to reikia šablonui, kuris spausdina atidarymo metu: sukurkite šabloną taip, kad jo /OpenAction pradėtų spausdinimo užduotį, paprastai JavaScript veiksmu, iškviečiančiu paties skaitytuvo spausdinimo komandą, o būtent spausdinimas suteiks WillPrint ir DidPrint įvykiams ką vykdyti – laiko žymą įrašyti prieš puslapiams patenkant į eilę ir audito įrašą sukurti jiems baigus spausdinti

Kiek patikimi šie sužadinimo įvykiai skirtinguose PDF skaitytuvuose?

Ne kiekvienas skaitytuvas juos vykdo, net ir ne PDF/A režimu, todėl gyvavimo ciklo veiksmą laikykite prašymu, o ne garantija. Acrobat ir dauguma visaverčių darbalaukio skaitytuvų patikimai vykdo visą rinkinį, tačiau didelė dalis tikro PDF naudojimo apskritai nepaliečia papildomų veiksmų žodyno: naršyklėse įterpti skaitytuvai, dauguma mobiliųjų skaitytuvų ir beveik visi serverio atvaizdavimo ar teksto išgavimo procesai visiškai ignoruoja /AA arba palaiko tik siaurą jo dalį, o WillPrint ir DidPrint paprastai veikia prasčiausiai, nes bekūnei konversijai nėra spausdinimo operacijos, prie kurios būtų galima juos prijungti. Jei WillClose formos pateikimo veiksmas yra vienintelis kelias surinkti formos duomenis, tai nėra patikimas kelias – susiekite jį su aiškiu pateikimo mygtuku ir automatinį sužadinimo įvykį laikykite patogumu skaitytuvams, kurie jį palaiko

Dokumento, puslapio ir lauko sužadinimo įvykiai yra trys tos pačios pagrindinės veiksmų žodyno sistemos lygiai, o išsiaiškinus konteinerį belieka pasirinkti tinkamą ActionKind konstantą ir patikrinti grąžinamą kodą. Šie gyvavimo ciklo sužadinimo įvykiai ir platesnė šiame straipsnyje paliesta veiksmų kūrimo API pateikiami kartu su standartine PDFlibPas Delphi PDF biblioteka, o išsami sužadinimo įvykių ir veiksmų tipų nuoroda pateikiama produkto dokumentacijoje