Teknisk artikel

Multi-select PDF-felter i FDF- og XFDF-roundtrips (Delphi)

HotPDF round-tripper multi-select list box-værdier gennem FDF og XFDF ved at holde feltværdien som et array hele vejen igennem. Siden version 2.755.0 skriver ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF og ExportLoadedFormToXFDF hver valgte option som sin egen FDF-streng eller sit eget XFDF <value>-element, og de matchende import-metoder tjekker hver værdi mod felt-options og genopbygger /I-valgindekserne, før de ændrer noget. Intet bliver limet sammen til én streng undervejs

Fejlen, dette fikser, er let at reproducere. Tag en ordreformular med en multi-select list box af produktopOptioner, lad en bruger vælge to af dem, eksportér formulardataene til et back-office-system, og importér så den redigerede fil tilbage i PDF'en. Før denne ændring kom list boksen tilbage tom eller forkert. Grunden er, at én af eksportværdierne indeholdt et linjeskift, og den gamle sti havde fladet valgene ud til én enkelt linje-separeret streng. At få flere valg ud af den streng var aldrig pålideligt, og med en eksportværdi, der selv indeholder et linjeskift, kan det slet ikke lade sig gøre

Hvorfor ødelægger sammensætning af multi-select-værdier med linjeskift roundtrippet?

At sætte valgene sammen til én streng smider grænserne mellem værdier væk, og en værdi kan indeholde separatoren, så ingen importer kan splitte strengen korrekt op igen. ISO 32000-1 §12.7.4.4 tillader, at /V-entryen på et choice-felt enten er én enkelt tekststreng eller et array af tekststrenge, og en list box med MultiSelect-flaget (bit 22 i /Ff) bruger array-formen, så snart mere end én option er valgt. Samme sektion definerer /I som et array af 0-baserede option-indekser i stigende rækkefølge, som viewers bruger til at skelne to options, der tilfældigvis deler en eksportværdi. I HotPDF læser den skalære getter GetFormFieldValue kun strengformen, så at køre et array gennem den nedgraderede eksporten til en tom streng, og den gamle XFDF-import satte gentagne <value>-elementer sammen med LF. Forestil dig en option eksporteret som Deep, line feed, Blue: efter sammen sætningen kunne Deep\nBlue\nRed være to valg eller tre, og filen giver ingen måde at vide hvilket. Fixet var at holde op med at bruge en skalar midt i roundtrippet helt

Gammel HotPDF multi-select roundtrip, hvor to valgte list box-options, én med et indlejret linjeskift, flattes ud af den skalære GetFormFieldValue-sti til den enkelte streng Deep, line feed, Blue, line feed, Red, som downstream readers kan parse som enten to valg eller tre
At sætte multi-select-værdier sammen til én streng ødelægger værdigrænserne, og en eksportværdi, der selv indeholder et linjeskift, gør den flattede form tvetydig

Hvad indeholder de eksporterede FDF- og XFDF-filer?

HotPDF skriver en multi-select-værdi som et typet array i FDF og som ét <value>-element pr. valg i XFDF, så grænserne forbliver synlige på disken. I FDF beholder hvert item den stavemåde, det havde i kilde-PDF'en: hexadecimale strenge går ud som hex, og literale strenge escapes af én enkelt helper, der omskriver CR og LF til \r og \n. I XFDF bærer roden xml:space="preserve", som ISO 19444-1 kræver, hvilket betyder, at al whitespace inde i et tekst-element tæller som data. HotPDF skriver derfor start-tagget, den escapede tekst og slut-tagget af hver <value> i ét stykke, holder indentation uden for elementet og encoder CR, LF og TAB som character references, så en XML-parser, der anvender line-ending-normalisering, ikke kan ændre de originale bytes

Eksportformer, HotPDF skriver for en multi-select list box siden 2.755.0: FDF bærer ét typet array pr. felt med /V [(Deep linjeskift Blue) (Red)] og en hex region-værdi, mens XFDF bærer ét value-element pr. valg under xml:space preserve, så whitespace tæller som data
Grænserne forbliver synlige på disken: FDF beholder hvert valg som sit eget array-item, og XFDF skriver hvert i et separat value-element, så ingen importer behøver at gætte
<!-- FDF: ét typet array pr. felt -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: ét <value> pr. valg -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
  <fields>
    <field name="options">
      <value>Deep&#xA;Blue</value>
      <value>Red</value>
    </field>
  </fields>
</xfdf>

To eksport-edge-cases er værd at kende, før du skriver kaldekoden. For det første bygger ExportLoadedFormToFDF den komplette FDF-krop i hukommelsen, før den opretter målfilen (fikset i 2.755.1), så en værdi, der ikke kan eksporteres, såsom et array, der holder noget andet end strenge, rejser uden at afkorte en eksisterende fil. For det andet er et tomt valg på en list box, der også tilbyder en tom-streng-eksportværdi, tvetydigt i XFDF, for <value/> kunne betyde, at intet er valgt, eller at den tomme option er valgt. ExportLoadedFormToXFDF rejser i det tilfælde i stedet for at gætte, og den rejser, før målfilen åbnes. FDF har ingen sådan tvetydighed, da /V [] og /V [()] er forskellige. Begge FDF-eksportører skipper også widget-only-terminaler uden /T-navn, matchende XFDF-eksportøren, for ingen importer kunne nogensinde matche de entries tilbage til et felt

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // Multi-select list boxes skrives som /V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // Tomt valg plus en tom eksport-option: XFDF kan ikke skelne
          // dem, og den eksisterende .xfdf-fil efterlades urørt
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Hvordan validerer HotPDF en multi-select-værdi ved import?

HotPDF accepterer et importeret array kun, når target er et choice-felt med MultiSelect-flaget sat, og hver værdi i arrayet matcher en eksportværdi i feltets /Opt-array. Hver option-slot kan bruges én gang, så en liste med to options, der deler eksportværdien b, accepterer [<62> <62>] som to adskilte valg og afviser en tredje b. Det genopbyggede /I følger /Opt-rækkefølgen og ikke rækkefølgen af de indkommende værdier, da §12.7.4.4 kræver stigende indekser. HotPDF bygger den nye /V og /I som løsrevne objekter og tildeler dem først, efter hver værdi har bestået valideringen, så en afvist værdi aldrig efterlader et halvt array eller forældede indekser. Kopien skrives til feltet, der importeres, snarere end til et delt ancestor-array, hex-stavemåder, der ankommer fra FDF, forbliver hex gennem gemningen, og felter, hvis beregninger afhænger af list boksen, markeres til genberegning. Behøver du kun at sætte én værdi, går at sætte én formularfeltværdi i en loadet PDF gennem den skalære sti, som af design ikke håndterer flere valg

HotPDF-importvalidering af multi-select-værdier: target skal være et choice-felt med MultiSelect sat i /Ff, hver indkommende værdi skal matche en /Opt-eksportværdi med hver slot brugt én gang, /I genopbygges stigende i /Opt-rækkefølge, og løsrevne /V og /I tildeles først, efter alle værdier består
Hver indkommende værdi tjekkes mod felt-options, før noget skrives, så en afvist værdi aldrig efterlader et halvt array eller forældede valgindekser

Nogle andre værktøjer skriver almindelige ASCII-eksportværdier som hex-strenge uden byte order mark, for eksempel <416272>, og eksporterer derefter XFDF ved at skrive de hex-cifre ud som tekst. En streng literal-sammenligning på vejen tilbage fejler, og importen afbrydes. Version 2.755.1 tilføjer én retry: når en værdi ikke matcher nogen option, dekoder HPDFHexSpellingText teksten som en hex-payload og sammenligner resultatet igen. Retry'en gælder kun input, der ellers ville have rejst, så den ændrer aldrig en værdi, der allerede matchede. Samme release fikset også, at den skalære og array-stien bruger samme Unicode-decoder, som forstår PDFDocEncoding, UTF-16 med begge byte order marks og UTF-8. Før det kunne én logisk værdi matche på én sti og fejle på den anden i dokumenter, der blandede encodings

Hvorfor kan en gyldig FDF-fil stadig miste felter under parsing?

En FDF-scanner, der ikke sporer hexadecimale strenge, kan skære en feltdictionary over i halvdele, når en hex-værdi slutter lige ved siden af dictionary-terminatoren. I << /T (region) /V <416273>> lukker den første > hex-strengen, men en naiv scanner læser den sammen med den næste > som enden på dictionaryen og dropper feltet i stilhed. Filniveau-FDF-importeren holdt allerede styr på, om den var inde i en hex-streng, og i 2.755.1 gør array- og dictionary-scannerne bag ImportLoadedInterchangeFromFDF det samme. Et andet problem vedrører indirekte referencer. En FDF-fil er et lille PDF-syntaks-dokument med sin egen objektnummerering (ISO 32000-1 §12.7.7), så en værdi som /V [11 0 R] refererer til objekt 11 i FDF-filen, ikke objekt 11 i den PDF, du udfylder. Den forenklede FDF-parser i HotPDF resolver ikke referencer inde i filen, så den afviser sådan et array i stedet for at læse, hvad objekt 11 nu tilfældigvis er i target-dokumentet

Fil-, stream- og XFDF-importer rapporterer fejl forskelligt

De tre import-ruter validerer på samme måde, men rapporterer fejl forskelligt, og det er værd at vælge én med vilje. ImportLoadedFormFromFDF skipper ethvert felt, der fejler valideringen, og returnerer antallet af felter, den faktisk anvendte, så et antal lavere end forventet er det eneste tegn på et problem. ImportLoadedInterchangeFromFDF og ImportLoadedFormFromXFDF rejser ved det første afviste felt. Hvert felt committes for sig selv, så felter, der er behandlet før exceptionen, beholder deres nye værdier. Behandl ingen af dem som en transaktion over hele exchange-filen: behøver du all-or-nothing-adfærd, så kassér det loadede dokument, når en exception opstår, i stedet for at gemme det

var
  Pdf: THotPDF;
  Source: TMemoryStream;
  Status: AnsiString;
  Info: THPDFFDFInterchangeInfo;
begin
  Pdf := THotPDF.Create(nil);
  Source := TMemoryStream.Create;
  try
    Source.LoadFromFile('order-form-reviewed.fdf');
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    try
      // Kun felter; en værdi uden for /Opt eller et ikke-multi-select-target rejser
      if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
        Pdf.SaveLoadedDocument('order-form-filled.pdf');
    except
      on E: Exception do
        ShowMessage('Import rejected, nothing saved: ' + E.Message);
    end;
  finally
    Source.Free;
    Pdf.Free;
  end;
end;

At udvide XFDF-callbacks uden at bryde eksisterende kaldere

Array-supporten i den lavniveaus XFDF-unit bor i en separat record, THPDFXFDFArrayAccess, og i nye overloads af HPDFXFDFExportFields og HPDFXFDFImportFields, ikke i ekstra felter tilføjet i enden af den eksisterende THPDFXFDFAccess-record. Grunden er binær kompatibilitet. Kode, der fylder THPDFXFDFAccess som en lokal variabel, sætter ofte kun de slots, den kender til, og rydder aldrig resten, så en ny funktionspointer tilføjet til den record ville indeholde stak-skrald, og biblioteket ville tage den for en rigtig callback. Med en separat record beholder gamle kaldere det gamle layout og de gamle overloads, og de overloads giver et all-nil array-record videre internt. Den originale skalære import-overload sætter stadig gentagne værdier sammen med LF af kompatibilitetshensyn, og kun den array-bevidste overload holder dem adskilt. Når du binder din egen datastore, så start fra Default(THPDFXFDFArrayAccess). Returnér True fra GetFormFieldValueArray for ethvert liste-værdi-felt, også ét med intet valgt, og False for at falde tilbage til den skalære callback

uses HPDFXFDF;

// Almindelig funktionspointer, ikke "of object": Context bærer din egen store
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
  out Values: THPDFXFDFValueArray): Boolean;
begin
  Result := TFormStore(Context).IsListField(FieldIndex);
  if Result then
    Values := TFormStore(Context).Selections(FieldIndex);
end;

procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
  Access: THPDFXFDFAccess;
  ArrayAccess: THPDFXFDFArrayAccess;
begin
  Access := MakeStoreAccess(Store);             // dine eksisterende skalære bindings
  ArrayAccess := Default(THPDFXFDFArrayAccess); // hver ubrugt slot er nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

Multi-select-udveksling virker på list boxes, der allerede findes og har MultiSelect-bitten sat i /Ff. For hvordan choice-felter og deres flagbitter oprettes i første omgang, se at tilføje ListBox og andre AcroForm-felter til en loadet PDF. For kommentar-markup, der går gennem XFDFs <annots>-træ, se XFDF-annotation-import og -eksport i HotPDF. Den fulde API-reference og trial-download ligger på HotPDF Delphi PDF-komponent-siden