Teknisk artikel

PDF-livscykelhändelser: Katalog /AA mot Sida /AA i Delphi

PDFlibPas, det nativa Delphi- och C++Builder-PDF-biblioteket, ger ett PDF-dokument två separata platser att hänga automatiskt beteende på: dokumentnivå-livscykelhändelser som WillClose, WillSave, DidSave, WillPrint och DidPrint, lagrade i Katalogens /AA-ordbok, och sidnivå-livscykelhändelser — Open och Close — lagrade i varje Sidobjekts egen /AA-ordbok istället. Att förväxla de två behållarna är det enskilt vanligaste sättet en livscykelhändelse tyst gör ingenting

De motiverande fallen är vardagliga. Ett finansteam vill ha en kontoutdragsmall som stämplar en utskriftstidsstämpel och loggar vem som skrev ut den i det ögonblick utskriften faktiskt börjar, inte när filen bara öppnas. Ett formulärtungt arbetsflöde behöver fältvärden pushade till en server automatiskt innan läsarens PDF-klient tillåts stänga fönstret, så en stängd flik aldrig betyder en förlorad redigering. En flersidig rapport vill ha en sidspecifik banderoll som visas bara medan den sidan är på skärmen. PDF erbjuder faktiskt en tredje nivå under dokument och sida för den här typen av beteende — händelser fästa till ett enskilt formulärfälts eller länks egen /A-post, ämnet för en följeartikel om interaktiva formulärhändelser och JavaScript — men den här artikeln stannar på de två nivåerna ovanför den: hela dokumentet, och en enskild sida

Vilka utlösare finns på dokumentkatalogens /AA?

Fem utlösare finns på Katalog-/AA-ordboken, och var och en av dem utlöses för en händelse som påverkar hela dokumentet, inte en enskild sida. ISO 32000-1 §12.6.3 (Trigger Events) listar dokumentnivånycklarna som WC, WS, DS, WP och DP — de bokstavliga tvåbokstavsnamnen skrivna in i /AA-ordboken — för WillClose, WillSave, DidSave, WillPrint respektive DidPrint, och PDFlibPas speglar den uppsättningen exakt i TPDFlibDocumentActionTrigger-enumeringen: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction är den enda ingångspunkten som fäster någon av de fem, och ActionKind-parametern den tar är en av tio PDF_ACTION_BUILDER_*-konstanter delade över varje handlingsbyggaranrop i biblioteket, från en ren URI till ett skript till ett destinationshopp. Vad en GoTo-, fjärrfil-, inbäddad fil-, eller Launch-handling faktiskt gör när den väl utlöses är en annan fråga än var den fästs, och det är ämnet för en följeartikel om GoTo-, fjärr-, inbäddade, och launch-handlingar — den här stannar vid behållarfrågan, Katalog eller Sida, snarare än handlingstypsfrågan

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;

Hur skiljer sig en sidnivåutlösare från en dokumentnivåutlösare?

En sidnivåutlösare utlöses bara för det enskilda Sidobjekt den är fäst till, och PDFlibPas lagrar den i den sidans egen /AA-ordbok snarare än Katalogens. Det finns bara två sidutlösare, Open och Close, motsvarande O- och C-nycklarna ISO 32000-1 definierar för en sidas ytterligare-handlingar-ordbok, och PDFlibPas exponerar dem som patOpen och patClose genom SetPageAction, som fäster till vilken sida som än för närvarande är vald via SelectPage — en detalj som spelar roll första gången man loopar över ett dokument och förväntar sig att ett anrop gäller överallt, eftersom det aldrig gör det. Att fästa endera typen av utlösare höjer också filens minsta PDF-version, och de två behållarna kräver olika golv: PDFlibPas höjer dokumentet till minst PDF 1.4 första gången den skriver en Katalog-/AA-post, och till minst PDF 1.5 första gången den skriver en Sida-/AA-post, oavsett vilken handlingstyp som sitter inuti. Det är ett behållarnivåkrav lagrat ovanpå vad handlingen själv behöver på egen hand, så en ren URI-handling som bara skulle kräva PDF 1.1 ensam drar ändå hela filen upp till PDF 1.5 när den väl är inpackad i en sidöppnings-utlösare

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

Att läsa och ta bort livscykelhändelser

GetDocumentActionInfo och GetPageActionInfo returnerar båda en TPDFlibActionInfo-post, och Kind-fältet kommer tillbaka akNone närhelst den utlösaren inte har något fäst, så kontrollera Kind innan du litar på något annat fält på posten — URI, JavaScript, FileName och resten är bara meningsfulla för den enda handlingstyp Kind faktiskt rapporterar, eftersom samma postform återanvänds över varje handlingstyp byggaren kan producera. RemoveDocumentAction och RemovePageAction rensar var och en en enskild utlösare och rapporterar 1 när de hittade något att ta bort, 0 när utlösaren redan var tom; när den borttagna posten var den sista kvar i /AA-ordboken raderar PDFlibPas den nu tomma /AA:n själv snarare än att lämna en dinglande, meningslös behållare kvar på Katalogen eller sidan

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;

Tillåter PDF/A livscykelhändelser överhuvudtaget?

Nej. PDF/A-konformitet avvisar hela ytterligare-handlingar-behållaren, inte bara handlingstyperna som låter riskabla, eftersom ISO 19005 begränsar PDF:s interaktiva handlingsmodell under antagandet att en arkivfil måste rendreras på samma sätt decennier framåt, utan att bero på en skriptmotor eller en nätverksanslutning som kanske inte finns då. SetLifecycleAction, den delade byggaren bakom både SetDocumentAction och SetPageAction, kontrollerar PDFAMode innan den ens tittar på ActionKind, så en URI-handling som bara öppnar en företagswebbsida eller en Named-handling som bara betyder gå till nästa sida fångas i samma nät som en farlig en — inget en säkerhetsgranskare normalt skulle flagga, blockerat ändå, eftersom begränsningen är strukturell snarare än fall-för-fall. Den praktiska faran är att avvisandet är tyst: SetDocumentAction och SetPageAction returnerar båda 0 utan att kasta ett undantag, så ett anropsställe som aldrig kontrollerar returvärdet levererar ett dokument som tyst saknar utlösaren det förväntades bära

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

En asymmetri är värd att hålla i minnet. RemoveDocumentAction och RemovePageAction kontrollerar aldrig PDFAMode, så att ladda en fil som redan bär icke-konforma livscykelhändelser och strippa bort dem på vägen till en PDF/A-konform sparning fungerar precis som förväntat — bara skrivvägen, att fästa en ny utlösare, är grindad på konformitetsläget

Var passar utskrift-vid-öppning in utan en WillOpen-utlösare?

Katalog-/AA-ordboken har ingen WillOpen-post alls, med avsikt — dokumentnivå-/AA i ISO 32000-1 definierar exakt fem nycklar, WillClose, WillSave, DidSave, WillPrint och DidPrint, och inget i den listan utlöses enbart för att en fil öppnades. Öppningstidskroken finns i en separat Katalogpost, /OpenAction, som PDFlibPas exponerar genom sin egen familj av anrop, SetOpenActionJavaScript, SetOpenActionDestination och SetOpenActionNamedDestination bland dem, ingen av vilka rör /AA-ordboken eller TPDFlibDocumentActionTrigger-enumeringen alls. De två mekanismerna sätts dock samman, och det är vanligtvis vad en utskrift-vid-öppning-mall faktiskt behöver: bygg mallen så dess /OpenAction startar utskriftsjobbet, typiskt en JavaScript-handling som anropar visarens eget utskriftskommando, och själva utskriften är vad som ger WillPrint och DidPrint något att köra mot — en tidsstämpel stämplad in innan sidorna spoolas, en granskningspost skriven när de väl är klara

Hur tillförlitliga är de här utlösarna över PDF-visare?

Inte varje visare kör dem, även utanför PDF/A, så behandla en livscykelhändelse som en begäran snarare än en garanti. Acrobat och de flesta fullständiga skrivbordsläsare exekverar hela uppsättningen troget, men en stor andel av verklig PDF-konsumtion rör aldrig en ytterligare-handlingar-ordbok alls: webbläsarinbäddade visare, de flesta mobilläsare, och nästan varje serversidig rendrerings- eller textextraktionspipeline antingen ignorerar /AA rakt av eller respekterar bara en smal del av den, med WillPrint och DidPrint typiskt klarande sig sämst eftersom huvudlös konvertering inte har någon utskriftsoperation för dem att haka på. Om en WillClose-formulärinlämnings-handling är den enda vägen som fångar formulärdata är det ingen tillförlitlig väg — para den med en explicit inlämningsknapp, och behandla den automatiska utlösaren som en bekvämlighet för de läsare som råkar stödja den

Dokument-, sid-, och fälthändelser är tre nivåer av samma underliggande handlingsordboksmaskineri, och när behållaren väl är klar är resten att välja rätt ActionKind-konstant och kontrollera returkoden. De här livscykelutlösarna, tillsammans med det bredare handlingsbyggar-API:et den här artikeln berör, levereras som en del av standard-PDFlibPas Delphi PDF-biblioteket, med den fullständiga utlösar- och handlingstypsreferensen i produktdokumentationen