Een exportwaarde voor een listbox die bestaat uit Deep, een regelinvoer en Blue is volkomen legale PDF, en een tweede optie genaamd Red is dat ook. Kiest u beide, plattet u de selectie af tot één met LF gescheiden string voor een uitwisselingsbestand, dan ontvangt de andere kant Deep\nBlue\nRed: twee selecties of drie, en niets in de bytes zegt welke. Multi-select-waarden komen alleen ongeschonden terug uit FDF of XFDF als ze de hele weg arrays blijven, vanaf het moment dat ze de PDF verlaten tot ze er weer in worden teruggeschreven
Waarom kan een samenvoeging met regeleinde nooit een multi-select-waarde dragen?
Een samengevoegde string verliest de grenzen tussen waarden, en omdat een waarde de scheider zelf mag bevatten, kan geen enkele importer die grenzen terugzetten. ISO 32000-1 §12.7.4.4 laat de /V-entry van een keuzeveld ofwel één tekststring ofwel een array van tekststrings bevatten; een listbox met de MultiSelect-vlag (bit 22 van /Ff) gebruikt de arrayvorm zodra er meer dan één optie is gekozen. Dezelfde sectie definieert /I, een array van nulgebaseerde optie-indices in oplopende volgorde, waarmee een viewer twee opties uit elkaar houdt die dezelfde exportwaarde delen. Geen van beide structuren past door een string-getypeerde API. GetFormFieldValue in HotPDF geeft een AnsiString terug en leest de stringvorm van /V, dus elke export die erop voortbouwt kan per veld alleen maar één scalair wegschrijven
De importkant heeft het spiegelbeeldprobleem. XFDF schrijft elke selectie als zijn eigen <value>-element, en een importer die die aan een scalaire setter aanbiedt moet ze eerst aan elkaar lijmen, meestal met LF. Bij de optie Deep/Blue hierboven is die lijm niet te onderscheiden van de data. HotPDF houdt scalairen daarom buiten het midden van de round trip: ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF en ExportLoadedFormToXFDF lezen de array rechtstreeks uit het veld, en de bijbehorende importmethoden bouwen een nieuwe array zonder ooit door een samengevoegde string te gaan
Wat de geëxporteerde FDF- en XFDF-bestanden bevatten
Op schijf blijven de grenzen zichtbaar. In FDF wordt een multi-select-veld een getypeerde array, en elk item houdt de spelling die het in de bron-PDF had: hexadecimale strings gaan eruit als hex, letterlijke strings gaan eruit als literals met CR en LF ge-escaped als \r en \n. In het bestand dat ExportLoadedFormToFDF schrijft, rijdt een rich-text /RV of een niet-nul lokale /Ff van het veld mee in dezelfde velddictionary. In XFDF declareert het rootelement xml:space="preserve", wat ISO 19444-1 vereist en wat elk witruimeteken binnen een tekstelement tot data maakt. Dat is een valkuil voor pretty-printing. HotPDF schrijft de starttag, de ge-escapete tekst en de eindtag van elk <value>-element op één regel, houdt inspringing buiten het element en codeert CR, LF en TAB als character references zodat een XML-parser die regeinde-normalisatie toepast een CR LF-paar niet naar een kale LF kan herschrijven
<!-- FDF: één getypeerde array per veld -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: één <value> per selectie -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Twee exportrandgevallen tellen mee wanneer u de aanroepende code schrijft. ExportLoadedFormToFDF bouwt de volledige FDF-body in het geheugen op en maakt het doelbestand pas daarna, dus een waarde die niet geëxporteerd kan worden (een array die iets anders dan strings bevat bijvoorbeeld) gooit een exception terwijl de vorige .fdf op schijf nog intact is. Het tweede geval is een echt gat in XFDF zelf: een lege selectie op een listbox die ook een exportwaarde van de lege string aanbiedt. <value/> kan "niets geselecteerd" betekenen of "de lege optie geselecteerd", en het formaat heeft geen derde spelling. ExportLoadedFormToXFDF gooit in die situatie een exception, nog voordat het doelbestand wordt geopend. FDF heeft dat probleem niet, want /V [] en /V [()] zijn verschillende objecten. Alle drie de exporters slaan widget-only terminals zonder /T-naam over, want geen enkele importer kan zo'n entry ooit terug aan een veld koppelen
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// Multi-select-listboxen worden weggeschreven als /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Lege selectie plus een lege exportoptie: XFDF kan ze niet
// uit elkaar houden, en het bestaande .xfdf-bestand blijft onaangetast
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Hoe wordt een geïmporteerde selectie tegen /Opt getoetst?
HotPDF accepteert een binnenkomende array alleen als het doel een keuzeveld is met de MultiSelect-vlag aan en elke waarde overeenkomt met een exportwaarde in de /Opt-array van het veld (bij een optiepaar van twee elementen, het eerste element). Elke optieslot is één keer te gebruiken. Een lijst met twee opties die allebei b exporteren accepteert daarom [<62> <62>] als twee afzonderlijke selecties en wijst een derde b af, want er is geen ongebruikt slot meer voor hem. De herbouwde /I volgt de /Opt-volgorde, los van de volgorde waarin de waarden binnenkwamen, wat de indices oplopend houdt zoals §12.7.4.4 vereist. De nieuwe /V en /I worden als losse objecten gebouwd en pas toegewezen nadat elke waarde is goedgekeurd, dus een geweigerde waarde kan geen halve array of achterhaalde indices op het veld achterlaten
Na de toewijzing krijgt het veld een verse appearance stream. Die stap is makkelijk te vergeten in een zelfgebouwde importer, en het symptoom is verwarrend: viewers tekenen de opgeslagen /AP, niet /V, dus de oude selectie blijft op het scherm en in de afdruk staan ook al zijn de data veranderd. De listbox komt ook in de set van veranderde velden, wat betekent dat een aanroep van RecalculateLoadedFormFieldsIncremental zonder parameters precies de berekeningen die hem lezen opnieuw uitvoert. Hex-spellingen die uit FDF komen blijven bij het opslaan hex. Als u alleen één scalaire waarde hoeft te zetten, behandelt het zetten van één formulierveldwaarde in een geladen PDF dat pad, dat op basis van het ontwerp geen meervoudige selecties behandelt
Eén invoerpatroon verdient bijzondere behandeling. Een PDF kan een gewone ASCII-exportwaarde als hex-string zonder bytevolgordemarkering opslaan, <416272> voor Abr, en een exporter die de PDF-stringbody letterlijk kopieert produceert XFDF waarvan de elementtekst de kale cijfers 416272 is in plaats van de gedecodeerde tekst. XFDF die een hex-gecodeerde exportwaarde als kale cijfers meedraagt kan niet op spelling matchen met /Opt: een strikte vergelijking vindt geen optie genaamd 416272 en de import faalt. Ontvangt de XFDF-importeur in HotPDF zo'n bestand, dan probeert hij de niet-gematchte waarde één keer opnieuw door hem als hex-payload te decoderen. De herkansing is bewust smal. Hij draait alleen voor een waarde die helemaal geen exacte match heeft; elke exacte match wordt eerst gereserveerd, dus een geredde spelling kan nooit een optieslot innemen dat een latere, exact gespelde waarde nodig heeft; en het opgeslagen /V-item is de eigen PDF-string van de optie, dus /V en /I beschrijven dezelfde selectie. Dat is precies de juiste hoeveelheid soepelheid: hij springt alleen aan waar de import anders een exception had gegooid, en een waarde die al matchte wordt nooit opnieuw geïnterpreteerd. Het matchen zelf loopt via één Unicode-decoder die PDFDocEncoding, UTF-16 met willekeurige bytevolgordemarkering en UTF-8 begrijpt, dus dezelfde logische tekst matcht dezelfde optie, ook in documenten die coderingen mengen
Twee FDF-parsevallen: hex-afsluiters en vreemde objectnummers
Het eerste geval is lexicaal. In << /T (region) /V <416273>>> sluit de eerste > de hex-string en sluiten de volgende twee de dictionary. Een scanner die alleen op >>-paren zoekt beëindigt de dictionary één teken te vroeg en verliest het veld. De FDF-importeur op bestandsniveau van HotPDF houdt bij of hij binnen een hex-string of een literal zit terwijl hij de /Fields-array snijdt, en de streamgebaseerde lezer achter ImportLoadedInterchangeFromFDF tokenizeert het hele bestand als PDF-syntaxis, dus allebei zien drie afsluitende haken waar een naïeve scan er twee ziet
Het tweede geval gaat over verwijzingen. Een FDF-bestand is een klein document in PDF-syntaxis met een eigen objectnummering (ISO 32000-1 §12.7.7), dus /V [11 0 R] betekent object 11 van het FDF-bestand, nooit object 11 van de PDF die wordt ingevuld. Het tegen het doeldocument oplossen zou stilletjes elk object binnentrekken dat daar toevallig dat nummer draagt. In Delphi 2009-builds en later decodeert de interchange-lezer de FDF als een complete objectgraaf, volgt de verwijzing binnen die graaf, valideert de resulterende strings zoals elke inline array en gooit een exception als de FDF geen object 11 bevat. De importeur op bestandsniveau, ImportLoadedFormFromFDF, leest elke velddictionary op zichzelf, zonder de objecttabel van de FDF; een array van verwijzingen zakt voor zijn alleen-strings-controle en dat veld wordt overgeslagen
Een importroute kiezen: overslaan, exception, of terugdraaien
Alle drie de importroutes valideren multi-select-waarden op dezelfde manier. Ze verschillen in wat er gebeurt als een waarde faalt. ImportLoadedFormFromFDF slaat elk veld dat faalt over en geeft het aantal velden terug dat hij wél heeft toegepast, dus een telling lager dan het aantal entries dat u stuurde is het enige teken van narigheid. ImportLoadedFormFromXFDF en ImportLoadedInterchangeFromFDF gooien bij het eerste geweigerde veld een exception, en elke aanroep loopt als één transactie op het geladen document: wanneer de exception de methode verlaat, wordt elk veld (en bij de interchange-route elke annotatie) dat de aanroep al had veranderd teruggedraaid. Een interchange-import die False teruggeeft wordt ook teruggedraaid, met lege Status en info-record
Voor data die een mens heeft beoordeeld en teruggestuurd, gebruikt u een van de routes met exception. Een half toegepast orderformulier is erger dan een geweigerde, want niets in de opgeslagen PDF toont welke listboxen uit het beoordeelde bestand komen en welke hun oude waarden hielden. Het overslaan-en-tellen gedrag van ImportLoadedFormFromFDF past bij een andere klus: best-effort voorinvullen vanuit een FDF die misschien voor een iets andere revisie van het formulier is gegenereerd
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
// Alleen velden. Een waarde buiten /Opt, of meerdere waarden gericht op
// een single-select-lijst, gooit een exception en draait de hele import terug
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
begin
// Voer alleen de berekeningen die de geïmporteerde velden lezen opnieuw uit
Pdf.RecalculateLoadedFormFieldsIncremental;
Pdf.SaveLoadedDocument('order-form-filled.pdf');
end;
except
on E: Exception do
ShowMessage('Import rejected, document unchanged: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Uw eigen gegevensopslag koppelen aan de XFDF-array-callbacks
De unit HPDFXFDF, die converteert tussen XFDF en elke veldopslag die u met callbacks kunt beschrijven, draagt de arrayondersteuning in een apart record, THPDFXFDFArrayAccess, en in extra overloads van HPDFXFDFExportFields en HPDFXFDFImportFields. Nieuwe slots achteraan aan het bestaande record THPDFXFDFAccess toevoegen was korter en fout geweest. Aanroepers declareren dat record vaak als lokale variabele en vullen alleen de slots in die ze kennen, dus een nieuwe functiewijzer achteraan zou stapelafval bevatten dat de bibliotheek voor een callback aan zou zien. Met het aparte record behoudt bestaande code zijn layout en zijn overloads, en die overloads geven intern een all-nil arrayrecord mee
De twee importoverloads gedragen zich anders bij herhaalde <value>-elementen. De originele scalaire overload voegt ze nog steeds samen met LF en roept SetFormFieldValue aan, want bestaande aanroepers vertrouwen op die conventie. De arraybewuste overload voegt nooit samen: hij geeft de lijst aan SetFormFieldValueArray, valt alleen terug op de scalaire setter voor een enkele waarde die uw callback weigert, en gooit een exception wanneer uw callback een lijst van twee of meer weigert. Begin bij export met Default(THPDFXFDFArrayAccess) zodat de slots die u niet toekent (inclusief de optionele SetFormFieldRV) nil blijven. Geef True terug uit GetFormFieldValueArray voor elk veld met een lijstwaarde, ook één waarbij niets is geselecteerd, en False om het veld door de scalaire callback te laten behandelen. Een lege array gaat eruit als één leeg <value/>, dus een opslag die een optie van de lege string aanbiedt moet dat geval in de callback weigeren, op dezelfde manier als ExportLoadedFormToXFDF doet
uses HPDFXFDF;
// Kale functiewijzer, geen "of object": Context draagt uw eigen opslag.
// TFormStore en zijn methoden zijn uw code, geen deel van HotPDF
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); // uw bestaande scalaire koppelingen
ArrayAccess := Default(THPDFXFDFArrayAccess); // elk ongebruikt slot is nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Alles hierboven veronderstelt listboxen die al bestaan en al de MultiSelect-bit in /Ff aan hebben staan. Keuzevelden maken en hun vlagbits zetten staat beschreven in ListBox- en andere AcroForm-velden toevoegen aan een geladen PDF, en commentaarmarkering die via het <annots>-deel van XFDF reist staat beschreven in XFDF-annotatie-import en -export in HotPDF. De methodereferentie voor elke hier getoonde aanroep, plus een proefbuild om ze tegen uw eigen formulieren te draaien, staat op de HotPDF Delphi PDF-componentpagina