Teknisk artikel

PDF-livscyklus-handlinger: Catalog /AA vs. Page /AA i Delphi

PDFlibPas, det native Delphi- og C++Builder-PDF-bibliotek, giver et PDF-dokument to separate steder at hænge automatisk adfærd på: dokument-niveau-livscyklus-handlinger såsom WillClose, WillSave, DidSave, WillPrint og DidPrint, gemt i katalogets /AA-ordbog, og side-niveau-livscyklus-handlinger — Open og Close — gemt i hvert Page-objekts egen /AA-ordbog i stedet. At forveksle de to containere er den enkeltvis mest almindelige måde, en livscyklus-handling i stilhed gør ingenting

De motiverende tilfælde er almindelige. Et finansteam vil have en kontoudtogs-skabelon, der stempler et udskrifts-tidsstempel og logger, hvem der udskrev det, i det øjeblik udskrivning rent faktisk starter, ikke når filen blot åbner. Et formular-tungt workflow har brug for feltværdier skubbet til en server automatisk, før læserens PDF-klient tillades at lukke vinduet, så en lukket fane aldrig betyder en mistet redigering. En flersidet rapport vil have et side-specifikt banner, der kun vises, mens den side er på skærmen. PDF tilbyder faktisk et tredje niveau under dokument og side til denne slags adfærd — handlinger knyttet til et individuelt formularfelt eller links egen /A-post, emnet for en følgeartikel om interaktive formular-handlinger og JavaScript — men denne artikel bliver på de to niveauer ovenover: hele dokumentet, og en enkelt side

Hvilke triggere bor på dokument-katalogets /AA?

Fem triggere bor på Catalog-/AA-ordbogen, og hver af dem udløses for en hændelse, der påvirker hele dokumentet, ikke en enkelt side. ISO 32000-1 §12.6.3 (Trigger Events) lister dokument-niveau-nøglerne som WC, WS, DS, WP og DP — de bogstavelige to-bogstavs-navne skrevet ind i /AA-ordbogen — til henholdsvis WillClose, WillSave, DidSave, WillPrint og DidPrint, og PDFlibPas spejler det sæt nøjagtigt i TPDFlibDocumentActionTrigger-enum'et: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction er det ene indgangspunkt, der knytter en hvilken som helst af de fem, og den ActionKind-parameter, den tager, er én af ti PDF_ACTION_BUILDER_*-konstanter delt på tværs af hvert handlings-bygger-kald i biblioteket, fra en ren URI til et script til et destinations-hop. Hvad en GoTo-, ekstern-fil-, indlejret-fil- eller Launch-handling rent faktisk gør, når den udløses, er et andet spørgsmål end hvor den knyttes, og det er emnet for en følgeartikel om GoTo-, ekstern-, indlejret- og launch-handlinger — denne bliver ved container-spørgsmålet, Catalog eller Page, frem for handlingstype-spørgsmålet

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;

Hvordan adskiller en side-niveau-trigger sig fra en dokument-niveau-en?

En side-niveau-trigger udløses kun for det ene Page-objekt, den er knyttet til, og PDFlibPas gemmer den i den sides egen /AA-ordbog frem for katalogets. Der er kun to side-triggere, Open og Close, svarende til O- og C-nøglerne ISO 32000-1 definerer til en sides yderligere-handlinger-ordbog, og PDFlibPas eksponerer dem som patOpen og patClose gennem SetPageAction, som knytter til hvilken som helst side, der aktuelt er valgt via SelectPage — en detalje der betyder noget, første gang man løkker på tværs af et dokument og forventer, at ét kald gælder overalt, fordi det aldrig gør. At knytte enten slags trigger hæver også filens minimums-PDF-version, og de to containere beder om forskellige gulve: PDFlibPas hæver dokumentet til mindst PDF 1.4, første gang den skriver en Catalog-/AA-post, og til mindst PDF 1.5, første gang den skriver en Page-/AA-post, uanset hvilken handlingstype der sidder indeni. Det er et container-niveau-krav lagdelt oven på, hvad selve handlingen har brug for på egen hånd, så en ren URI-handling, der alene kun ville kræve PDF 1.1, stadig trækker hele filen op til PDF 1.5, når den pakkes ind i en side-åbne-trigger

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

At læse og fjerne livscyklus-handlinger

GetDocumentActionInfo og GetPageActionInfo returnerer begge en TPDFlibActionInfo-record, og Kind-feltet kommer tilbage akNone, hver gang den trigger ikke har noget knyttet, så tjek Kind, før man stoler på noget andet felt på recorden — URI, JavaScript, FileName og resten er kun meningsfulde for den ene handlingstype, Kind rent faktisk rapporterer, da den samme record-form genbruges på tværs af hver handlingstype, byggeren kan producere. RemoveDocumentAction og RemovePageAction rydder hver én enkelt trigger og rapporterer 1, når de fandt noget at fjerne, 0 når triggeren allerede var tom; når den fjernede post var den sidste tilbage i /AA-ordbogen, sletter PDFlibPas den nu-tomme /AA selv frem for at efterlade en dinglende, betydningsløs container på katalogen eller siden

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;

Tillader PDF/A overhovedet livscyklus-handlinger?

Nej. PDF/A-konformitet afviser hele den yderligere-handlinger-container, ikke bare de handlingstyper der lyder risikable, fordi ISO 19005 begrænser PDF's interaktive-handlings-model ud fra antagelsen om, at en arkivfil skal gengives på samme måde årtier fra nu, uden at afhænge af en script-motor eller en netværksforbindelse, der måske ikke findes til den tid. SetLifecycleAction, den delte bygger bag både SetDocumentAction og SetPageAction, tjekker PDFAMode, før den overhovedet kigger på ActionKind, så en URI-handling, der bare åbner en virksomheds-webside, eller en Named-handling, der kun betyder gå til næste side, fanges i det samme net som en farlig en — intet en sikkerheds-reviewer normalt ville flage, blokeret alligevel, fordi begrænsningen er strukturel frem for sag-for-sag. Den praktiske fare er, at afvisningen er tavs: SetDocumentAction og SetPageAction returnerer begge 0 uden at kaste en undtagelse, så et kaldested, der aldrig tjekker returværdien, sender et dokument ud der i stilhed mangler den trigger, det skulle bære

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

Én asymmetri er værd at holde i tankerne. RemoveDocumentAction og RemovePageAction tjekker aldrig PDFAMode, så at indlæse en fil, der allerede bærer ikke-konforme livscyklus-handlinger, og strimle dem ud på vej til en PDF/A-konform gemning, fungerer nøjagtig som forventet — kun skrive-vejen, at knytte en ny trigger, er gated på compliance-tilstanden

Hvor passer print-ved-åbning uden en WillOpen-trigger?

Catalog-/AA-ordbogen har slet ingen WillOpen-post, med vilje — dokument-niveau-/AA i ISO 32000-1 definerer præcis fem nøgler, WillClose, WillSave, DidSave, WillPrint og DidPrint, og intet på den liste udløses rent, fordi en fil blev åbnet. Åbne-tids-krogen bor i en separat Catalog-post, /OpenAction, som PDFlibPas eksponerer gennem sin egen familie af kald, SetOpenActionJavaScript, SetOpenActionDestination og SetOpenActionNamedDestination iblandt dem, ingen af hvilke rører /AA-ordbogen eller TPDFlibDocumentActionTrigger-enum'et overhovedet. De to mekanismer sammensætter dog, og det er sædvanligvis, hvad en print-ved-åbning-skabelon rent faktisk har brug for: byg skabelonen, så dens /OpenAction starter udskriftsjobbet, typisk en JavaScript-handling der kalder fremviserens egen udskriftskommando, og selve udskrivningen er, hvad der giver WillPrint og DidPrint noget at køre mod — et tidsstempel stemplet ind før siderne spooler, en audit-post skrevet, når de er færdige

Hvor pålidelige er disse triggere på tværs af PDF-fremvisere?

Ikke hver fremviser kører dem, selv uden for PDF/A, så behandl en livscyklus-handling som en anmodning frem for en garanti. Acrobat og de fleste fulde desktop-læsere udfører hele sættet trofast, men en stor andel af rigtig-verden-PDF-forbrug rører aldrig en yderligere-handlinger-ordbog overhovedet: browser-indlejrede fremvisere, de fleste mobile læsere, og næsten hver server-side-gengivelses- eller tekst-udtræknings-pipeline enten ignorerer /AA direkte eller respekterer kun en smal skive af den, med WillPrint og DidPrint typisk klarende sig værst, da headless-konvertering ikke har nogen udskriftsoperation for dem at koble til. Hvis en WillClose-indsend-formular-handling er den eneste vej, der indfanger formulardata, er det ikke en pålidelig vej — par den med en eksplicit indsend-knap, og behandl den automatiske trigger som en bekvemmelighed for de læsere, der tilfældigvis understøtter den

Dokument-, side- og felt-triggere er tre niveauer af det samme underliggende handlings-ordbogs-maskineri, og når først containeren er klar, er resten at vælge den rigtige ActionKind-konstant og tjekke returkoden. Disse livscyklus-triggere, sammen med det bredere handlings-bygger-API denne artikel rører ved, leveres som en del af standard-PDFlibPas Delphi-PDF-biblioteket, med den fulde trigger- og handlingstype-reference i produktdokumentationen