O HotPDF faz o round trip de valores multi-select de list box por FDF e XFDF mantendo o valor do campo como array do começo ao fim. Desde a versão 2.755.0, o ExportLoadedFormToFDF, o ExportLoadedInterchangeToFDF e o ExportLoadedFormToXFDF escrevem cada opção selecionada como seu próprio string FDF ou elemento <value> XFDF, e os métodos de import correspondentes conferem cada valor contra as opções do campo e reconstruem os índices de seleção /I antes de mudar qualquer coisa. Nada é colado numa única string pelo caminho
A falha que isso conserta é fácil de reproduzir. Pegue um formulário de pedido com um list box multi-select de opções de produto, deixe um usuário marcar duas delas, exporte os dados do formulário para um sistema de back-office, e então importe o arquivo editado de volta ao PDF. Antes desta mudança o list box voltava vazio ou errado. O motivo é que um dos valores de export continha uma quebra de linha, e o caminho antigo tinha achatado as seleções numa única string separada por linhas. Recuperar múltiplas seleções dessa string nunca foi confiável, e com um valor de export que em si contém uma quebra de linha não funciona de jeito nenhum
Por que juntar valores multi-select com quebras de linha quebra o round trip?
Juntar as seleções numa única string joga fora as fronteiras entre valores, e um valor pode conter o separador, então nenhum import consegue dividir a string de volta corretamente. A ISO 32000-1 §12.7.4.4 permite que a entrada /V de um campo choice seja tanto uma text string única quanto um array de text strings, e um list box com a flag MultiSelect (bit 22 do /Ff) usa a forma de array assim que mais de uma opção é marcada. A mesma seção define o /I como um array de índices de opção zero-based em ordem ascendente, que viewers usam para distinguir duas opções que por acaso compartilham um valor de export. No HotPDF o getter escalar GetFormFieldValue só lê a forma de string, então passar um array por ele degradava o export a uma string vazia, e o import XFDF antigo juntava elementos <value> repetidos com LF. Imagine uma opção exportada como Deep, line feed, Blue: depois de juntar, Deep\nBlue\nRed podia ser duas seleções ou três, e o arquivo não dá como saber qual. A correção foi parar de usar um escalar no meio do round trip de vez
O que os arquivos FDF e XFDF exportados contêm?
O HotPDF escreve um valor multi-select como array tipado em FDF e como um elemento <value> por seleção em XFDF, para as fronteiras ficarem visíveis no disco. Em FDF cada item mantém a grafia que tinha no PDF de origem: strings hexadecimais saem como hex, e strings literais são escapadas por um único helper que transforma CR e LF em \r e \n. Em XFDF a raiz carrega xml:space="preserve" como a ISO 19444-1 exige, o que significa que qualquer whitespace dentro de um elemento de texto conta como dado. O HotPDF portanto escreve a tag de abertura, o texto escapado e a tag de fechamento de cada <value> num pedaço só, mantém a indentação fora do elemento, e codifica CR, LF e TAB como character references para que um parser XML aplicando normalização de fim de linha não possa mudar os bytes originais
<!-- FDF: um array tipado por campo -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: um <value> por seleção -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Dois casos de borda de export valem conhecer antes de escrever o código que chama. Primeiro, o ExportLoadedFormToFDF monta o corpo FDF completo na memória antes de criar o arquivo de destino (consertado na 2.755.1), então um valor que não pode ser exportado, como um array guardando algo que não strings, levanta exceção sem truncar um arquivo existente. Segundo, uma seleção vazia num list box que também oferece um valor de export string vazia é ambígua em XFDF, porque <value/> pode significar tanto que nada está selecionado quanto que a opção vazia está selecionada. O ExportLoadedFormToXFDF levanta exceção nesse caso em vez de adivinhar, e levanta antes de o arquivo de destino ser aberto. O FDF não tem essa ambiguidade, já que /V [] e /V [()] são distintos. Ambos os exporters FDF também pulam terminais só de widget que não têm nome /T, no mesmo espírito do exporter XFDF, porque nenhum import jamais conseguiria casar essas entradas de volta a um campo
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// List boxes multi-select são escritos como /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Seleção vazia mais uma opção de export vazia: o XFDF não distingue
// os dois, e o arquivo .xfdf existente fica intacto
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Como o HotPDF valida um valor multi-select no import?
O HotPDF aceita um array importado só quando o target é um campo choice com a flag MultiSelect setada e todo valor do array casa com um valor de export no array /Opt do campo. Cada slot de opção pode ser usado uma vez, então uma lista com duas opções que compartilham o valor de export b aceita [<62> <62>] como duas seleções distintas e rejeita um terceiro b. O /I reconstruído segue a ordem do /Opt e não a ordem dos valores de entrada, já que a §12.7.4.4 exige índices ascendentes. O HotPDF monta o novo /V e o novo /I como objetos destacados e os atribui só depois de todo valor passar na validação, então um valor rejeitado nunca deixa para trás meio array ou índices obsoletos. A cópia é escrita no campo sendo importado em vez de num array ancestral compartilhado, grafias hex chegando do FDF permanecem hex pelo save, e campos cujos cálculos dependem do list box são marcados para recálculo. Se você só precisa definir um valor único, definir um valor de campo de formulário num PDF carregado passa pelo caminho escalar, que por design não trata seleções múltiplas
Algumas outras ferramentas escrevem valores de export ASCII simples como hex strings sem byte order mark, por exemplo <416272>, e depois exportam XFDF escrevendo esses dígitos hex como texto. Uma comparação literal estrita no caminho de volta falha, e o import aborta. A versão 2.755.1 acrescenta uma retentativa: quando um valor não casa com opção nenhuma, o HPDFHexSpellingText decodifica o texto como payload hex e compara o resultado de novo. A retentativa só vale para input que de outra forma teria levantado exceção, então ela nunca muda um valor que já casou. A mesma release também fez os caminhos escalar e de array usarem o mesmo decoder Unicode, que entende PDFDocEncoding, UTF-16 com qualquer byte order mark e UTF-8. Antes disso, um valor lógico podia casar num caminho e falhar no outro em documentos que misturavam codificações
Por que um arquivo FDF válido ainda pode perder campos durante o parse?
Um scanner FDF que não acompanha strings hexadecimais pode cortar um dicionário de campo ao meio quando um valor hex termina exatamente ao lado do terminador do dicionário. Em << /T (region) /V <416273>>> o primeiro > fecha a string hex, mas um scanner ingênuo o lê junto com o próximo > como o fim do dicionário e derruba o campo em silêncio. O import FDF em nível de arquivo já acompanhava se estava dentro de uma string hex, e na 2.755.1 os scanners de array e de dicionário por trás do ImportLoadedInterchangeFromFDF fazem o mesmo. Uma segunda questão diz respeito a referências indiretas. Um arquivo FDF é um pequeno documento de sintaxe PDF com numeração de objetos própria (ISO 32000-1 §12.7.7), então um valor como /V [11 0 R] refere-se ao objeto 11 do arquivo FDF, não ao objeto 11 do PDF que você está preenchendo. O parser FDF simplificado do HotPDF não resolve referências dentro do arquivo, então ele rejeita um array desses em vez de ler o quer que seja que o objeto 11 seja no documento alvo
Imports de arquivo, stream e XFDF reportam erros de formas diferentes
As três rotas de import validam da mesma forma mas reportam falhas de jeitos diferentes, e vale escolher uma de propósito. O ImportLoadedFormFromFDF pula qualquer campo que falhe na validação e retorna o número de campos que de fato aplicou, então uma contagem menor que a esperada é o único sinal de problema. O ImportLoadedInterchangeFromFDF e o ImportLoadedFormFromXFDF levantam exceção no primeiro campo rejeitado. Cada campo é efetivado por conta própria, então campos processados antes da exceção mantêm os valores novos deles. Não trate nenhum deles como uma transação sobre o arquivo de intercâmbio inteiro: se você precisa de comportamento all-or-nothing, descarte o documento carregado quando uma exceção ocorrer em vez de salvá-lo
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
// Só campos; um valor fora de /Opt ou um target não multi-select levanta exceção
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;
Estendendo os callbacks XFDF sem quebrar quem já chama
O suporte a array na unit XFDF de nível mais baixo mora num record separado, o THPDFXFDFArrayAccess, e em novas overloads de HPDFXFDFExportFields e HPDFXFDFImportFields, não em campos extras acrescentados ao fim do record THPDFXFDFAccess existente. O motivo é compatibilidade binária. Código que preenche um THPDFXFDFAccess como variável local costuma setar só os slots que conhece e nunca limpa o resto, então um novo ponteiro de função acrescentado a esse record conteria lixo de stack, e a biblioteca o tomaria por um callback de verdade. Com um record separado, quem já chama mantém o layout antigo e as overloads antigas, e essas overloads passam internamente um record de array todo-nil. A overload de import escalar original ainda junta valores repetidos com LF por compatibilidade, e só a overload ciente de array os mantém separados. Ao amarrar o seu próprio data store, parta de Default(THPDFXFDFArrayAccess). Retorne True do GetFormFieldValueArray para qualquer campo com valor de lista, incluindo um sem nada selecionado, e False para cair no callback escalar
uses HPDFXFDF;
// Ponteiro de função simples, não "of object": o Context carrega o seu próprio 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); // seus bindings escalares existentes
ArrayAccess := Default(THPDFXFDFArrayAccess); // todo slot não usado é nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
O intercâmbio multi-select funciona em list boxes que já existem e têm o bit MultiSelect setado no /Ff. Para como campos choice e os bits de flag deles são criados em primeiro lugar, veja adicionar ListBox e outros campos AcroForm a um PDF carregado. Para markup de comentários que passa pela árvore <annots> do XFDF, veja import e export de annotations XFDF no HotPDF. A referência completa da API e o download de avaliação estão na página do componente HotPDF PDF para Delphi