Teknisk artikkel

PDF-livssyklushandlinger: Catalog /AA versus Page /AA i Delphi

PDFlibPas, det native Delphi- og C++Builder-PDF-biblioteket, gir et PDF-dokument to separate steder å henge automatisk atferd på: dokument-nivå-livssyklushandlinger slik som WillClose, WillSave, DidSave, WillPrint og DidPrint, lagret i Catalog-ens /AA-ordbok, og side-nivå-livssyklushandlinger — Open og Close — i stedet lagret i hvert Page-objekts egen /AA-ordbok. Å forveksle de to containerne er den desidert vanligste måten en livssyklushandling stille gjør ingenting på

De motiverende tilfellene er vanlige. Et finansteam ønsker en kontoutskrift-mal som stempler et utskriftstidsstempel og logger hvem som skrev den ut i det øyeblikket utskriften faktisk starter, ikke når filen bare åpnes. En skjema-tung arbeidsflyt trenger feltverdier dyttet til en server automatisk før leserens PDF-klient får lov til å lukke vinduet, slik at en lukket fane aldri betyr en tapt redigering. En flersides rapport ønsker et side-spesifikt banner som bare vises mens den siden er på skjermen. PDF tilbyr faktisk et tredje nivå under dokument og side for denne typen atferd — handlinger festet til et individuelt skjemafelt eller en lenkes egen /A-oppføring, temaet for en følgeartikkel om interaktive skjemahandlinger og JavaScript — men denne artikkelen holder seg til de to nivåene over det: hele dokumentet, og én enkelt side

Hvilke utløsere bor på dokument-Catalog-ens /AA?

Fem utløsere bor på Catalog-/AA-ordboken, og hver eneste av dem utløses for en hendelse som påvirker hele dokumentet, ikke én enkelt side. ISO 32000-1 §12.6.3 (Trigger Events) lister dokument-nivå-nøklene som WC, WS, DS, WP, og DP — de bokstavelige to-bokstavs-navnene skrevet inn i /AA-ordboken — for WillClose, WillSave, DidSave, WillPrint, og DidPrint henholdsvis, og PDFlibPas speiler det settet nøyaktig i TPDFlibDocumentActionTrigger-enumeringen: datWillClose, datWillSave, datDidSave, datWillPrint, datDidPrint. SetDocumentAction er det ene inngangspunktet som fester noen av de fem, og ActionKind-parameteren den tar, er én av ti PDF_ACTION_BUILDER_*-konstanter delt på tvers av hvert handlings-bygger-kall i biblioteket, fra en ren URI til et skript til et destinasjonshopp. Hva en GoTo-, ekstern-fil-, innebygd-fil-, eller Launch-handling faktisk gjør når utløst, er et annet spørsmål enn hvor den festes, og det er temaet for en følgeartikkel om GoTo-, ekstern-, innebygd-, og launch-handlinger — denne holder seg til container-spørsmålet, Catalog eller Page, snarere enn handlingstype-spørsmå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 er en side-nivå-utløser forskjellig fra en dokument-nivå-en?

En side-nivå-utløser utløses bare for den ene Page-objektet den er festet til, og PDFlibPas lagrer den i den sidens egen /AA-ordbok snarere enn Catalog-ens. Det finnes bare to side-utløsere, Open og Close, tilsvarende O- og C-nøklene ISO 32000-1 definerer for en sides tilleggshandlings-ordbok, og PDFlibPas eksponerer dem som patOpen og patClose gjennom SetPageAction, som festes til hvilken som helst side som for øyeblikket er valgt via SelectPage — en detalj som betyr noe første gang man løkker gjennom et dokument og forventer at ett kall gjelder overalt, fordi det aldri gjør det. Å feste hvilken som helst type utløser hever også filens minimums-PDF-versjon, og de to containerne krever forskjellige gulv: PDFlibPas hever dokumentet til minst PDF 1.4 første gang den skriver en Catalog-/AA-oppføring, og til minst PDF 1.5 første gang den skriver en Page-/AA-oppføring, uansett hvilken handlingstype som sitter inni. Det er et container-nivå-krav lagt oppå hva selve handlingen enn trenger på egen hånd, så en ren URI-handling som bare ville krevd PDF 1.1 alene, fortsatt drar hele filen opp til PDF 1.5 når den først er pakket inn i en side-åpne-utløser

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

Å lese og fjerne livssyklushandlinger

GetDocumentActionInfo og GetPageActionInfo returnerer begge en TPDFlibActionInfo-record, og Kind-feltet kommer tilbake akNone hver gang den utløseren ikke har noe festet, så sjekk Kind før man stoler på noe annet felt på recorden — URI, JavaScript, FileName, og resten er bare meningsfulle for den ene handlingstypen Kind faktisk rapporterer, ettersom den samme record-formen gjenbrukes på tvers av hver handlingstype byggeren kan produsere. RemoveDocumentAction og RemovePageAction tømmer hver en enkelt utløser og rapporterer 1 når de fant noe å fjerne, 0 når utløseren allerede var tom; når den fjernede oppføringen var den siste igjen i /AA-ordboken, sletter PDFlibPas den nå-tomme /AA-en selv i stedet for å etterlate en dinglende, meningsløs container på Catalog-en 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;

Tillater PDF/A i det hele tatt livssyklushandlinger?

Nei. PDF/A-konformitet avviser hele tilleggshandlings-containeren, ikke bare handlingstypene som høres risikable ut, fordi ISO 19005 begrenser PDF-ens interaktive-handlings-modell ut fra antakelsen om at en arkivfil må gjengis på samme måte tiår fra nå, uten å avhenge av en skriptmotor eller en nettverkstilkobling som kanskje ikke finnes innen da. SetLifecycleAction, den delte byggeren bak både SetDocumentAction og SetPageAction, sjekker PDFAMode før den i det hele tatt ser på ActionKind, så en URI-handling som bare åpner en firma-nettside eller en Named-handling som bare betyr gå til neste side, fanges i det samme nettet som en farlig en — ingenting en sikkerhetsgjennomganger normalt ville flagget, blokkert uansett, fordi begrensningen er strukturell snarere enn sak-for-sak. Den praktiske faren er at avvisningen er stille: SetDocumentAction og SetPageAction returnerer begge 0 uten å kaste et unntak, så et kallsted som aldri sjekker returverdien, sender ut et dokument som stille mangler utløseren 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 verdt å huske på. RemoveDocumentAction og RemovePageAction sjekker aldri PDFAMode, så å laste inn en fil som allerede bærer ikke-konforme livssyklushandlinger og fjerne dem på vei til en PDF/A-konform lagring, fungerer nøyaktig som forventet — bare skrive-veien, å feste en ny utløser, er sperret av konformitetsmodusen

Hvor passer print-ved-åpning inn uten en WillOpen-utløser?

Catalog-/AA-ordboken har ingen WillOpen-oppføring i det hele tatt, med hensikt — dokument-nivå-/AA i ISO 32000-1 definerer nøyaktig fem nøkler, WillClose, WillSave, DidSave, WillPrint, og DidPrint, og ingenting i den listen utløses rent fordi en fil ble åpnet. Åpne-tids-kroken bor i en separat Catalog-oppføring, /OpenAction, som PDFlibPas eksponerer gjennom sin egen familie av kall, SetOpenActionJavaScript, SetOpenActionDestination, og SetOpenActionNamedDestination blant dem, hvorav ingen rører /AA-ordboken eller TPDFlibDocumentActionTrigger-enumeringen i det hele tatt. De to mekanismene komponerer likevel, og det er vanligvis hva en print-ved-åpning-mal faktisk trenger: bygg malen slik at /OpenAction-en starter utskriftsjobben, typisk en JavaScript-handling som kaller fremviserens egen utskriftskommando, og selve utskriften er det som gir WillPrint og DidPrint noe å kjøre mot — et tidsstempel stemplet inn før sidene spooler, en revisjonsoppføring skrevet når de er ferdige

Hvor pålitelige er disse utløserne på tvers av PDF-fremvisere?

Ikke hver fremviser kjører dem, selv utenfor PDF/A, så behandle en livssyklushandling som en forespørsel snarere enn en garanti. Acrobat og de fleste fulle desktop-lesere utfører hele settet trofast, men en stor andel av virkelig-verden-PDF-konsum rører aldri en tilleggshandlings-ordbok i det hele tatt: nettleser-innebygde fremvisere, de fleste mobile lesere, og nesten hver server-side-gjengivelses- eller tekstuttrekkings-pipeline enten ignorerer /AA rett ut eller respekterer bare en snever del av den, med WillPrint og DidPrint typisk klarende seg dårligst ettersom hodeløs (headless) konvertering ikke har noen utskriftsoperasjon for dem å kroke seg inn i. Hvis en WillClose-submit-form-handling er den eneste veien som fanger skjemadata, er det ikke en pålitelig vei — par den med en eksplisitt send-inn-knapp, og behandle den automatiske utløseren som en bekvemmelighet for de leserne som tilfeldigvis støtter den

Dokument-, side-, og felt-utløsere er tre nivåer av det samme underliggende handlings-ordbok-maskineriet, og når containeren først er klar, er resten å velge riktig ActionKind-konstant og sjekke returkoden. Disse livssyklus-utløserne, sammen med det bredere handlings-bygger-API-et denne artikkelen berører, følger med som en del av den standard PDFlibPas Delphi PDF-biblioteket, med den fulle utløser- og handlingstype-referansen i produktdokumentasjonen