Műszaki cikk

PDF-élettartam-akciók: Catalog /AA vs Page /AA Delphiben

A PDFlibPas, a natív Delphi és C++Builder PDF-könyvtár, két külön helyet ad egy PDF-dokumentumnak, ahová automatikus viselkedést akaszthat: dokumentum-szintű élettartam-akciókat, mint a WillClose, WillSave, DidSave, WillPrint, és DidPrint, a Catalog /AA szótárában tárolva, és oldal-szintű élettartam-akciókat — Open és Close — minden Page-objektum saját /AA szótárában tárolva ehelyett. A két konténer összekeverése az egyetlen leggyakoribb módja annak, hogy egy élettartam-akció csendben semmit ne tegyen

A motiváló esetek hétköznapiak. Egy pénzügyi csapat egy kimutatás-sablont akar, amely lebélyegzi a nyomtatási időbélyeget, és naplózza, ki nyomtatta, abban a pillanatban, amikor a nyomtatás ténylegesen elkezdődik, nem amikor a fájl csupán megnyílik. Egy form-nehéz munkafolyamatnak automatikusan szerverre kell tolnia a mezőértékeket, mielőtt az olvasó PDF-kliense bezárhatná az ablakot, így egy bezárt fül soha ne jelentsen elvesztett szerkesztést. Egy többoldalas jelentés egy oldal-specifikus bannert akar, amely csak akkor jelenik meg, amíg az az oldal a képernyőn van. A PDF valójában egy harmadik szintet is kínál dokumentum és oldal alatt ehhez a fajta viselkedéshez — akciók, amelyek egy egyéni formmezőhöz vagy linkhez csatolt saját /A bejegyzésükhöz kapcsolódnak, ez a témája az interaktív form-akciókról és JavaScriptről szóló kísérőcikknek — de ez a cikk a fölötte lévő két szintnél marad: a teljes dokumentumnál, és egyetlen oldalnál

Milyen kiváltók élnek a dokumentum Catalog /AA-ján?

Öt kiváltó él a Catalog /AA szótárán, és mindegyikük egy olyan eseményre sül el, amely az egész dokumentumot érinti, nem egyetlen oldalt. Az ISO 32000-1 §12.6.3 (Trigger Events) a dokumentum-szintű kulcsokat WC, WS, DS, WP, és DP-ként sorolja fel — a szó szerinti kétbetűs nevek, amiket az /AA szótárba írnak — a WillClose, WillSave, DidSave, WillPrint, és DidPrint megfelelően, és a PDFlibPas pontosan ezt a halmazt tükrözi a TPDFlibDocumentActionTrigger felsorolásban: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. A SetDocumentAction az egyetlen belépési pont, amely csatolja bármelyiket az ötből, és az ActionKind paraméter, amit vesz, tíz PDF_ACTION_BUILDER_* konstans egyike, amelyeket a könyvtárban minden akció-építő hívás megoszt, egy egyszerű URI-tól egy szkriptig egy célugrásig. Az, hogy egy GoTo, távoli-fájl, beágyazott-fájl, vagy Launch akció ténylegesen mit tesz, miután kiváltódott, más kérdés attól, hova csatolják, és ez a témája a GoTo, távoli, beágyazott, és launch akciókról szóló kísérőcikknek — ez itt a konténer-kérdésnél marad, Catalog vagy Page, nem az akciótípus-kérdésnél

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;

Miben különbözik egy oldal-szintű kiváltó egy dokumentum-szintűtől?

Egy oldal-szintű kiváltó csak azért az egy Page-objektumért sül el, amelyhez csatolva van, és a PDFlibPas azon az oldal saját /AA szótárában tárolja, nem a Catalogéban. Csak két oldal-kiváltó van, Open és Close, megfelelve az O és C kulcsoknak, amiket az ISO 32000-1 definiál egy oldal további-akciók szótárához, és a PDFlibPas ezeket patOpen-ként és patClose-ként teszi elérhetővé a SetPageAction-en keresztül, amely ahhoz az oldalhoz csatolódik, amelyik éppen ki van választva a SelectPage-en keresztül — egy részlet, amely számít, amikor először végigfutsz egy dokumentumon, azt várva, hogy egyetlen hívás mindenhol érvényesüljön, mert soha nem így van. Bármelyik fajta kiváltó csatolása is emeli a fájl minimum PDF-verzióját, és a két konténer eltérő padlót kér: a PDFlibPas legalább PDF 1.4-re emeli a dokumentumot, amikor először ír egy Catalog /AA bejegyzést, és legalább PDF 1.5-re, amikor először ír egy Page /AA bejegyzést, függetlenül attól, melyik akciótípus ül benne. Ez egy konténer-szintű követelmény, amely bármi fölé rétegződik, amit maga az akció önmagában igényel, így egy sima URI-akció, amely önmagában csak PDF 1.1-et igényelne, mégis felhúzza a teljes fájlt PDF 1.5-re, amint egy oldal-nyitási kiváltóba csomagolják

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

Élettartam-akciók olvasása és eltávolítása

A GetDocumentActionInfo és a GetPageActionInfo mindkettő egy TPDFlibActionInfo rekordot ad vissza, és a Kind mező akNone-ként tér vissza, valahányszor ahhoz a kiváltóhoz semmi nincs csatolva, így ellenőrizd a Kind-ot, mielőtt megbíznál bármely más mezőben a rekordon — az URI, a JavaScript, a FileName, és a többi csak azon egy akciótípushoz jelentős, amit a Kind ténylegesen jelent, mivel ugyanaz a rekordalak van újrahasznosítva minden akciótípusnál, amit az építő elő tud állítani. A RemoveDocumentAction és a RemovePageAction mindegyike egyetlen kiváltót töröl, és 1-et jelent, amikor talált valamit eltávolítani, 0-t, amikor a kiváltó már üres volt; amikor az eltávolított bejegyzés volt az utolsó, amely az /AA szótárban maradt, a PDFlibPas törli a most üres /AA-t magát, ahelyett hogy hagyna egy lógó, jelentés nélküli konténert hátra a Catalogon vagy az oldalon

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;

Megengedi-e egyáltalán a PDF/A az élettartam-akciókat?

Nem. A PDF/A-megfelelőség elutasítja a teljes további-akciók konténert, nem csak a kockázatosnak hangzó akciótípusokat, mert az ISO 19005 korlátozza a PDF interaktív-akció modelljét azon feltevés alapján, hogy egy archivális fájlnak évtizedekkel később ugyanúgy kell renderelnie, anélkül hogy egy szkriptmotortól vagy egy hálózati kapcsolattól függene, amely akkorra talán nem is létezik. A SetLifecycleAction, a megosztott építő mind a SetDocumentAction, mind a SetPageAction mögött, ellenőrzi a PDFAMode-ot, mielőtt egyáltalán megnézné az ActionKind-ot, így egy URI-akció, amely csak egy vállalati weboldalt nyit meg, vagy egy Named-akció, amely csak azt jelenti, ugorj a következő oldalra, ugyanabba a hálóba kerül, mint egy veszélyes, semmi, amit egy biztonsági szakértő normál esetben megjelölne, mégis blokkolva, mert a korlátozás strukturális, nem eset-eset alapú. A gyakorlati veszély az, hogy az elutasítás csendes: a SetDocumentAction és a SetPageAction mindkettő 0-t ad vissza kivétel dobása nélkül, így egy híváshely, amely soha nem ellenőrzi a visszatérési értéket, csendben olyan dokumentumot szállít, amelyből hiányzik a kiváltó, amit hordoznia kellett volna

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

Egy aszimmetriát érdemes szem előtt tartani. A RemoveDocumentAction és a RemovePageAction soha nem ellenőrzi a PDFAMode-ot, így egy fájl betöltése, amely már nem-megfelelő élettartam-akciókat hordoz, és azok kiszedése egy PDF/A-megfelelő mentés felé vezető úton pontosan úgy működik, ahogyan elvárnád — csak az írási útvonal, egy új kiváltó csatolása, van a megfelelőségi módhoz kapuzva

Hova illik a nyomtatás-megnyitáskor egy WillOpen kiváltó nélkül?

A Catalog /AA szótárnak egyáltalán nincs WillOpen bejegyzése, szándékosan — a dokumentum-szintű /AA az ISO 32000-1-ben pontosan öt kulcsot definiál, WillClose, WillSave, DidSave, WillPrint, és DidPrint, és semmi abban a listában nem sül el pusztán azért, mert egy fájl megnyílt. A megnyitás-idejű horog egy külön Catalog-bejegyzésben él, a /OpenAction-ban, amit a PDFlibPas a saját hívás-családján keresztül tesz elérhetővé, a SetOpenActionJavaScript, a SetOpenActionDestination, és a SetOpenActionNamedDestination köztük, egyik sem érinti egyáltalán az /AA szótárt vagy a TPDFlibDocumentActionTrigger felsorolást. A két mechanizmus mégis komponálódik, és ez általában az, amire egy nyomtatás-megnyitáskor sablonnak ténylegesen szüksége van: építsd a sablont úgy, hogy /OpenAction-je elindítsa a nyomtatási feladatot, jellemzően egy JavaScript-akció, amely meghívja a megjelenítő saját nyomtatási parancsát, és maga a nyomtatás az, ami ad valamit, ami ellen a WillPrint és a DidPrint futhat — egy időbélyeg lebélyegezve, mielőtt az oldalak sorba kerülnének, egy audit-bejegyzés megírva, amint elkészültek

Mennyire megbízhatóak ezek a kiváltók PDF-megjelenítők között?

Nem minden megjelenítő futtatja őket, még a PDF/A-n kívül is, így kezelj egy élettartam-akciót kérésként, ne garanciaként. Az Acrobat és a legtöbb teljes asztali olvasó hűen végrehajtja a teljes halmazt, de a valós-világbeli PDF-fogyasztás nagy része soha nem érinti egyáltalán a további-akciók szótárat: böngészőbe ágyazott megjelenítők, a legtöbb mobil olvasó, és szinte minden szerver-oldali renderelési vagy szövegkinyerési csővezeték vagy teljesen figyelmen kívül hagyja az /AA-t, vagy csak egy szűk szeletét tartja tiszteletben, a WillPrint és a DidPrint jellemzően a legrosszabbul teljesítve, mivel a fej nélküli konverziónak nincs nyomtatási művelete, amibe beakaszthatnák magukat. Ha egy WillClose submit-form akció az egyetlen útvonal, amely rögzíti a form-adatot, az nem megbízható útvonal — párosítsd egy explicit submit gombbal, és kezeld az automatikus kiváltót kényelmi funkcióként azoknak az olvasóknak, amelyek véletlenül támogatják

A dokumentum-, oldal-, és mező-kiváltók ugyanannak az alapul szolgáló akció-szótár gépezetnek három szintje, és amint a konténer tiszta, a többi a helyes ActionKind konstans kiválasztásáról és a visszatérési kód ellenőrzéséről szól. Ezek az élettartam-kiváltók, a szélesebb akció-építő API-val együtt, amit ez a cikk érint, a szabványos PDFlibPas Delphi PDF-könyvtár részeként érkeznek, a teljes kiváltó- és akciótípus-referenciával a termékdokumentációban