Teknisk artikel

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

PDF Library for Delphi, 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 PDF Library for Delphi 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

PDF Library for Delphi-diagram over ét PDF-dokument med to additional-action-containere: Catalog /AA med de fem dokumentlivscyklustriggere WC, WS, DS, WP og DP, og hver Page /AA med kun Open- og Close-sidetriggerne
Catalog /AA bærer de fem dokumentlivscyklus-udløsere, mens hver Page /AA kun kender Open- og Close-parret
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 PDF Library for Delphi 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 PDF Library for Delphi 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: PDF Library for Delphi 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 PDF Library for Delphi 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

PDF Library for Delphi: Flowdiagram over PDF/A-complianceporten: SetDocumentAction og SetPageAction går gennem SetLifecycleAction, som tjekker PDFAMode først, skriver /AA-posten og returnerer 1 uden PDF/A-tilstand, men returnerer lydløst 0 under PDF/A-tilstand, mens fjernelseskaldene går helt uden om porten
Under PDF/A nægter den delte builder hver livscyklusaction med en lydløs nul-returnering, mens fjernelse af udløsere forbliver tilladt
Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
     '', '', 0, 0) = 0 then
  // afvist: PDF/A-1b forbyder Catalog /AA, selv en simpel 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 PDF Library for Delphi 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

PDF Library for Delphi: Kompositionsdiagram, der viser, at Catalog /AA ikke har nogen WillOpen-nøgle: viewerens åbningshændelse starter et /OpenAction JavaScript-printjob, som får WillPrint til at fyre, før siderne spoles, for at stemple et tidsstempel, og DidPrint til at fyre bagefter for at skrive auditposten
Print-ved-åbning sammensætter Catalog /OpenAction-udskriftsjobbet med WillPrint-stempling før spoolen og DidPrint-logning efter

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-PDF Library for Delphi Delphi-PDF-biblioteket, med den fulde trigger- og handlingstype-reference i produktdokumentationen