Техническа статия

Multi-select PDF полета в FDF и XFDF round trip (Delphi)

HotPDF прекарва стойностите на multi-select list box през FDF и XFDF като пази стойността на полето като масив от край до край. От версия 2.755.0 насам ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF и ExportLoadedFormToXFDF записват всяка избрана опция като собствен FDF низ или XFDF елемент <value>, а съответните import методи проверяват всяка стойност срещу опциите на полето и преизграждат селекционните индекси /I, преди да променят нещо. Нищо не се залепва в един низ по пътя

Провалът, който това оправя, лесно се възпроизвежда. Вземете поръчков формуляр с multi-select list box с продуктови опции, оставете потребител да избере две от тях, експортирайте данните на формуляра за back-office система, после импортирайте редактирания файл обратно в PDF. Преди тази промяна list box-ът се връщаше празен или развален. Причината е, че една от export стойностите е съдържала line break, а старият път беше изравнил селекциите в един низ, разделен с редове. Да извадиш няколко селекции обратно от този низ никога не е било надеждно, а с export стойност, съдържаща сама по себе си line break, изобщо не може да работи

Защо съединяването на multi-select стойности с line break-ове чупи round trip-а?

Съединяването на селекциите в един низ изхвърля границите между стойностите, а стойност може да съдържа разделителя, така че никакъв importer не може да разцепи низа обратно коректно. ISO 32000-1 §12.7.4.4 позволява записът /V на choice поле да е или единичен текстов низ, или масив от текстови низове, а list box с флага MultiSelect (бит 22 на /Ff) ползва масивовата форма, щом е избрана повече от една опция. Същата секция дефинира /I като масив от индекси на опции от нулата, във възходящ ред, който viewer-ите ползват, за да разграничат две опции, случайно споделящи export стойност. В HotPDF скаларният getter GetFormFieldValue чете само низовата форма, така че преминаването на масив през него деградираше експорта до празен низ, а старият XFDF import съединяваше повторените елементи <value> с LF. Представете си опция, експортирана като Deep, line feed, Blue: след съединяване Deep\nBlue\nRed може да е две селекции или три, а файлът не дава начин да узнаете кое. Поправката беше изцяло да спре да се ползва скалар по средата на round trip-а

Старият multi-select round trip в HotPDF, при който две избрани list box опции, едната със вграден line break, биват изравнени от скаларния път на GetFormFieldValue в единичния низ Deep, line feed, Blue, line feed, Red, който четеците надолу могат да парснат или като две селекции, или като три
Съединяването на multi-select стойности в един низ унищожава границите между стойностите, а export стойност, съдържаща сама по себе си line break, прави изравнената форма двусмислена

Какво съдържат експортираните FDF и XFDF файлове?

HotPDF записва multi-select стойност като типизиран масив във FDF и като по един елемент <value> на селекция в XFDF, така че границите остават видими на диска. Във FDF всеки елемент пази изписването, което е имал в изходния PDF: hexadecimal низове излизат като hex, а литералните низове се екранират от един-единствен helper, който обръща CR и LF на \r и \n. В XFDF коренът носи xml:space="preserve", както изисква ISO 19444-1, което значи, че всяко whitespace вътре в текстов елемент се брои за данни. HotPDF затова записва началния таг, екранирания текст и крайния таг на всеки <value> в едно цяло, пази отстъпите извън елемента и кодира CR, LF и TAB като character reference-и, така че XML parser, прилагащ line-ending нормализация, не може да смени оригиналните байтове

Export форми, които HotPDF записва за multi-select list box от 2.755.0 насам: FDF носи по един типизиран масив на поле с /V [(Deep line break Blue) (Red)] и hex region стойност, а XFDF носи по един value елемент на селекция под xml:space preserve, така че whitespace се брои за данни
Границите остават видими на диска: FDF пази всяка селекция като собствен елемент на масива, а XFDF записва всяка в отделен value елемент, така че никакъв importer не трябва да гадае
<!-- FDF: по един типизиран масив на поле -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>

<!-- XFDF: по един <value> на селекция -->
<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>

Два export крайни случая си струва да знаете, преди да пишете извикващия код. Първо, ExportLoadedFormToFDF строи пълното FDF тяло в паметта, преди да създаде целевия файл (оправено в 2.755.1), така че стойност, която не може да се експортира — например масив, държащ нещо освен низове — вдига грешка, без да съкрати съществуващ файл. Второ, празна селекция на list box, който предлага и export стойност празен низ, е двусмислена в XFDF, защото <value/> може да значи и че нищо не е избрано, и че празната опция е избрана. ExportLoadedFormToXFDF в този случай вдига грешка, вместо да гадае, и я вдига, преди целевият файл да е отворен. FDF няма такава двусмисленост, понеже /V [] и /V [()] са различни. И двата FDF exporter-а също прескачат widget-only крайни полета без /T име, съгласувайки се с XFDF exporter-а, защото никакъв importer би могъл да съпостави тези записи обратно с поле

var
  Pdf: THotPDF;
  Written: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
    begin
      // Multi-select list box-ите се записват като /V [(...) (...)]
      Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
      try
        Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
      except
        on E: Exception do
          // Празна селекция плюс празна export опция: XFDF не може да ги
          // разграничи, а съществуващият .xfdf файл остава недокоснат
          ShowMessage('XFDF export refused: ' + E.Message);
      end;
    end;
  finally
    Pdf.Free;
  end;
end;

Как HotPDF валидира multi-select стойност при импорт?

HotPDF приема импортиран масив само когато целта е choice поле с вдигнат флаг MultiSelect и всяка стойност в масива съвпада с export стойност в /Opt масива на полето. Всеки слот на опция може да се ползва веднъж, така че списък с две опции, споделящи export стойността b, приема [<62> <62>] като две различни селекции и отхвърля трето b. Преизграденото /I следва реда на /Opt, а не реда на входящите стойности, понеже §12.7.4.4 изисква възходящи индекси. HotPDF строи новите /V и /I като откъснати обекти и ги задава едва след като всяка стойност е минала валидацията, така че отхвърлена стойност никога не оставя половин масив или остарели индекси след себе си. Копието се записва в полето, което се импортира, а не в споделен ancestor масив, hex изписвания, пристигащи от FDF, остават hex през записа, а полетата, чиито изчисления зависят от list box-а, се маркират за преизчисление. Ако ви трябва само да зададете една стойност, задаването на една стойност на form поле в зареден PDF минава през скаларния път, който по дизайн не се справя с няколко селекции

Import валидация на HotPDF за multi-select стойности: целта трябва да е choice поле с вдигнат MultiSelect в /Ff, всяка входяща стойност трябва да съвпада с /Opt export стойност, като всеки слот се ползва веднъж, /I се преизгражда възходящо в реда на /Opt, а откъснати /V и /I се задават едва след като всички стойности минат
Всяка входяща стойност се проверява срещу опциите на полето, преди каквото и да е записано, така че отхвърлена стойност никога не оставя половин масив или остарели селекционни индекси след себе си

Някои други инструменти записват обикновени ASCII export стойности като hex низове без byte order mark, например <416272>, и после експортират XFDF, като изпишат тези hex цифри като текст. Строго литерално сравнение на връщане се проваля и импортът се прекъсва. Версия 2.755.1 добавя едно повторение: когато стойност не съвпада с нито една опция, HPDFHexSpellingText декодира текста като hex payload и сравнява резултата пак. Повторението се прилага само за вход, който иначе би вдигнал грешка, така че никога не сменя стойност, която вече е съвпаднала. Същият release направи скаларния и масивовия път да ползват един и същ Unicode decoder, който разбира PDFDocEncoding, UTF-16 с който и да е byte order mark и UTF-8. Преди това една логическа стойност можеше да съвпадне на единия път и да провали на другия в документи, смесващи кодировки

Защо валиден FDF файл все още може да загуби полета при парсване?

FDF скенер, който не следи hexadecimal низове, може да разреже поле-речник наполовина, когато hex стойност свършва точно до терминатора на речника. В << /T (region) /V <416273>>> първият > затваря hex низа, но наивен скенер чете него заедно със следващия > като край на речника и тихо изпуска полето. FDF importer-ът на файлово ниво вече следеше дали е вътре в hex низ, а в 2.755.1 скенерите за масиви и речници зад ImportLoadedInterchangeFromFDF правят същото. Вторият въпрос засяга indirect референции. FDF файл е малък документ в PDF синтаксис със собствена номерация на обектите (ISO 32000-1 §12.7.7), така че стойност като /V [11 0 R] сочи обект 11 на FDF файла, не обект 11 на PDF-а, който попълвате. Опростеният FDF parser в HotPDF не разрешава референции вътре във файла, така че отхвърля такъв масив, вместо да прочете какъвто и да е обект 11 в целевия документ

Импортите от файл, stream и XFDF докладват грешките различно

Трите import маршрута валидират еднакво, но докладват провалите различно, и си струва да изберете един нарочно. ImportLoadedFormFromFDF прескача всяко поле, провалило валидацията, и връща броя на приложените полета, така че брой, по-малък от очаквания, е единственият знак за проблем. ImportLoadedInterchangeFromFDF и ImportLoadedFormFromXFDF вдигат грешка при първото отхвърлено поле. Всяко поле се ангажира само за себе си, така че полетата, обработени преди изключението, пазят новите си стойности. Не третирайте нито едно от тези като транзакция върху целия exchange файл: ако ви трябва all-or-nothing поведение, изхвърлете заредения документ при изключение, вместо да го записвате

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
      // Само полета; стойност извън /Opt или не-multi-select цел вдига грешка
      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;

Разширяване на XFDF callback-ите, без да се чупят съществуващите извикващи

Поддръжката на масиви в low-level XFDF unit-а живее в отделен запис, THPDFXFDFArrayAccess, и в нови overload-и на HPDFXFDFExportFields и HPDFXFDFImportFields, не в допълнителни полета, добавени в края на съществуващия запис THPDFXFDFAccess. Причината е бинарна съвместимост. Код, който попълва THPDFXFDFAccess като локална променлива, често задава само слотовете, които познава, и никога не изчиства останалите, така че нов указател към функция, добавен към този запис, щеше да съдържа боклук от стека, а библиотеката щеше да го приеме за истински callback. С отделен запис старите извикващи пазят старото разположение и старите overload-и, а тези overload-и подават вътрешно запис-масив с всички слотове nil. Оригиналният скаларен import overload и нататък съединява повторени стойности с LF за съвместимост, а само масиво-съзнаващият overload ги пази разделени. Когато вързвате собственото си хранилище с данни, тръгнете от Default(THPDFXFDFArrayAccess). Върнете True от GetFormFieldValueArray за всяко поле със стойност списък, включително такова без нищо избрано, и False, за да се върне скаларният callback

uses HPDFXFDF;

// Обикновен указател към функция, не „of object": Context носи вашето хранилище
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);             // вашите съществуващи скаларни връзки
  ArrayAccess := Default(THPDFXFDFArrayAccess); // всеки неизползван слот е nil
  ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
  HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;

Multi-select размяната работи върху list box-ове, които вече съществуват и имат вдигнат бит MultiSelect в /Ff. За това как choice полетата и техните флагови битове се създават изобщо, вижте добавянето на ListBox и други AcroForm полета към зареден PDF. За коментарен markup, минаващ през дървото <annots> на XFDF, вижте импорта и експорта на XFDF анотации в HotPDF. Пълната API референция и trial изтеглянето са на страницата на HotPDF Delphi PDF компонента