Um valor de export de list box formado por Deep, um line feed e Blue é PDF perfeitamente legal, e uma segunda opção chamada Red também é. Marque as duas, achate a seleção em uma única string separada por LF para um arquivo de intercâmbio, e o outro lado recebe Deep\nBlue\nRed: duas seleções ou três, e nada nos bytes diz qual. Valores multi-select voltam intactos do FDF ou do XFDF apenas quando permanecem arrays o caminho todo, do momento em que saem do PDF até serem gravados de volta nele
Por que uma junção com quebra de linha nunca pode carregar um valor multi-select?
Uma string unida perde as fronteiras entre valores, e como um valor pode conter o separador, nenhum importador consegue reconstruir essas fronteiras. A ISO 32000-1 §12.7.4.4 permite que a entrada /V de um campo choice contenha uma única text string ou um array de text strings; 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 /I, um array de índices de opção com base zero em ordem crescente, que é como um visualizador distingue duas opções que compartilham um valor de export. Nenhuma das duas estruturas passa por uma API tipada como string. GetFormFieldValue no HotPDF retorna um AnsiString e lê a forma string do /V, então qualquer export construído sobre ele só consegue gravar um escalar por campo
O lado do import tem o problema espelhado. O XFDF escreve cada seleção como seu próprio elemento <value>, e um importador que entrega esses valores a um setter escalar precisa colá-los primeiro, normalmente com LF. Com a opção Deep/Blue acima, essa cola é indistinguível dos dados. O HotPDF, portanto, mantém escalares fora do meio do round trip: ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF e ExportLoadedFormToXFDF leem o array direto do campo, e os métodos de import correspondentes constroem um novo array sem nunca passar por uma string unida
O que os arquivos FDF e XFDF exportados contêm
No disco as fronteiras continuam visíveis. No FDF um campo multi-select vira um array tipado, e cada item mantém a grafia que tinha no PDF de origem: strings hexadecimais saem como hex, strings literais saem como literais com CR e LF escapados como \r e \n. No arquivo gravado pelo ExportLoadedFormToFDF, um /RV de rich-text ou um /Ff local não zero do campo vai junto no mesmo dicionário de campo. No XFDF o elemento raiz declara xml:space="preserve", o que a ISO 19444-1 exige e o que transforma cada caractere de espaço em branco dentro de um elemento de texto em dado. Isso é uma armadilha para pretty-printing. O HotPDF escreve a tag de abertura, o texto escapado e a tag de fechamento de cada <value> em uma linha, mantém a indentação fora do elemento, e codifica CR, LF e TAB como referências de caractere para que um parser XML aplicando normalização de finais de linha não consiga reescrever um par CR LF em um LF solitário
<!-- 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 importam quando você escreve o código que chama. O ExportLoadedFormToFDF monta o corpo FDF completo na memória e só então cria o arquivo de destino, então um valor que não pode ser exportado (um array contendo algo que não seja string, por exemplo) levanta enquanto o .fdf anterior no disco continua intacto. O segundo caso é uma lacuna genuína do próprio XFDF: uma seleção vazia em um list box que também oferece um valor de export de string vazia. Um <value/> pode significar "nada selecionado" ou "a opção vazia selecionada", e o formato não tem uma terceira grafia. O ExportLoadedFormToXFDF levanta nessa situação, antes de o arquivo de destino ser aberto. O FDF não tem esse problema, porque /V [] e /V [()] são objetos diferentes. Os três exportadores pulam terminais apenas de widget que não têm nome /T, já que nenhum importador conseguiria casar de volta uma entrada dessas com 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 uma seleção importada é conferida contra /Opt?
O HotPDF aceita um array de entrada apenas quando o alvo é um campo choice com a flag MultiSelect setada e todos os valores casam com um valor de export no array /Opt do campo (em um par de opção de dois elementos, o primeiro elemento). Cada slot de opção pode ser usado uma vez. Uma lista com duas opções que exportam ambas b aceita, portanto, [<62> <62>] como duas seleções distintas e rejeita um terceiro b, porque não sobra slot livre para ele. O /I reconstruído segue a ordem do /Opt independentemente da ordem em que os valores chegaram, o que mantém os índices crescentes como a §12.7.4.4 exige. O novo /V e o novo /I são construídos como objetos destacados e atribuídos apenas depois de todos os valores passarem, então um valor rejeitado não pode deixar meio array ou índices obsoletos no campo
Depois da atribuição o campo ganha um appearance stream novo. Esse passo é fácil de esquecer em um importador feito à mão, e o sintoma confunde: os visualizadores desenham o /AP armazenado, não o /V, então a seleção antiga fica na tela e na impressão mesmo com os dados mudados. O list box também entra no conjunto de campos alterados, o que significa que uma chamada sem parâmetros de RecalculateLoadedFormFieldsIncremental reexecuta exatamente os cálculos que o leem. Grafias hex que chegam do FDF continuam hex no salvamento. Se você só precisa definir um valor escalar, definir um valor de campo de formulário em um PDF carregado cobre esse caminho, que por design não lida com seleções múltiplas
Um padrão de entrada merece tratamento especial. Um PDF pode armazenar um valor de export ASCII simples como string hex sem byte order mark, <416272> para Abr, e um exportador que copia o corpo da string do PDF verbatim produz um XFDF cujo texto do elemento são os dígitos crus 416272 em vez do texto decodificado. Um XFDF que carrega um valor de export codificado em hex como dígitos crus não consegue casar com o /Opt pela grafia: uma comparação estrita não encontra opção chamada 416272 e o import falha. Quando o importador XFDF do HotPDF recebe um arquivo assim, ele tenta o valor sem correspondência mais uma vez, decodificando-o como payload hex. A tentativa é deliberadamente estreita. Ela roda apenas para um valor sem nenhuma correspondência exata; toda correspondência exata é reservada primeiro, então uma grafia resgatada nunca pode tomar um slot de opção que um valor posterior, exatamente grafado, precise; e o item armazenado no /V é a própria string PDF da opção, então /V e /I descrevem a mesma seleção. Essa é a dose certa de tolerância: dispara apenas onde o import levantaria, e um valor que já casou nunca é reinterpretado. O casamento em si passa por um decodificador Unicode único que entende PDFDocEncoding, UTF-16 com qualquer byte order mark e UTF-8, então o mesmo texto lógico casa com a mesma opção mesmo em documentos que misturam codificações
Duas armadilhas de parse de FDF: terminadores hex e números de objeto estranhos
A primeira armadilha é léxica. Em << /T (region) /V <416273>>> o primeiro > fecha a string hex e os dois seguintes fecham o dicionário. Um scanner que só procura pares >> encerra o dicionário um caractere antes e perde o campo. O importador FDF de nível de arquivo do HotPDF acompanha se está dentro de uma string hex ou literal enquanto fatia o array /Fields, e o leitor baseado em stream por trás do ImportLoadedInterchangeFromFDF tokeniza o arquivo inteiro como sintaxe PDF, então ambos veem três colchetes de fechamento onde uma varredura ingênua vê dois
A segunda armadilha é sobre referências. Um arquivo FDF é um pequeno documento de sintaxe PDF com numeração de objeto própria (ISO 32000-1 §12.7.7), então /V [11 0 R] significa o objeto 11 do arquivo FDF, nunca o objeto 11 do PDF sendo preenchido. Resolvê-la contra o documento alvo puxaria silenciosamente o objeto que por acaso carrega aquele número lá. Em builds do Delphi 2009 em diante, o leitor de intercâmbio decodifica o FDF como um grafo de objetos completo, segue a referência dentro desse grafo, valida as strings resultantes como qualquer array inline, e levanta se o FDF não contém o objeto 11. O ImportLoadedFormFromFDF de nível de arquivo lê cada dicionário de campo por conta própria, sem a tabela de objetos do FDF; um array de referências falha na checagem de somente-strings, e esse campo é pulado
Escolhendo uma rota de import: pular, levantar ou reverter
As três rotas de import validam valores multi-select da mesma forma. Elas diferem no que acontece quando um valor falha. O ImportLoadedFormFromFDF pula qualquer campo que falhe e retorna o número de campos que efetivamente aplicou, então uma contagem menor que o número de entradas enviadas é o único sinal de problema. O ImportLoadedFormFromXFDF e o ImportLoadedInterchangeFromFDF levantam no primeiro campo rejeitado, e cada chamada roda como uma transação no documento carregado: quando a exceção sai do método, todo campo (e, na rota de intercâmbio, toda anotação) que a chamada já tinha mudado é revertido. Um import de intercâmbio que retorna False também é revertido, com Status e o record de informações limpos
Para dados que uma pessoa revisou e mandou de volta, use uma das rotas que levantam. Um formulário de pedido aplicado pela metade é pior que um rejeitado, porque nada no PDF salvo mostra quais list boxes vieram do arquivo revisado e quais mantiveram os valores antigos. O comportamento de pular-e-contar do ImportLoadedFormFromFDF serve a um trabalho diferente: pré-preenchimento de melhor esforço a partir de um FDF que pode ter sido gerado para uma revisão um pouco diferente do formulário
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
// Apenas campos. Um valor fora do /Opt, ou vários valores mirando um
// list single-select, levanta e reverte o import inteiro
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
begin
// Reexecutar apenas os cálculos que leem os campos importados
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;
Ligando seu próprio armazém de dados aos callbacks de array do XFDF
A unit HPDFXFDF, que converte entre XFDF e qualquer armazém de campos que você consiga descrever com callbacks, carrega o suporte a array em um record separado, o THPDFXFDFArrayAccess, e em overloads extras de HPDFXFDFExportFields e HPDFXFDFImportFields. Anexar novos slots ao record THPDFXFDFAccess existente teria sido mais curto e errado. Quem chama costuma declarar esse record como variável local e preencher apenas os slots conhecidos, então um novo ponteiro de função no fim guardaria lixo de pilha que a biblioteca confundiria com um callback. Com o record separado, o código existente mantém seu layout e seus overloads, e esses overloads passam internamente um record de array todo nil
Os dois overloads de import se comportam de forma diferente com elementos <value> repetidos. O overload escalar original ainda os une com LF e chama SetFormFieldValue, porque quem já chama depende dessa convenção. O overload consciente de array nunca une: entrega a lista ao SetFormFieldValueArray, volta ao setter escalar apenas para um valor único que seu callback recusar, e levanta quando seu callback recusa uma lista de dois ou mais. No export, parta de Default(THPDFXFDFArrayAccess) para que os slots que você não atribuir (incluindo o SetFormFieldRV opcional) fiquem nil. Retorne True do GetFormFieldValueArray para qualquer campo com valor de lista, incluindo um sem nada selecionado, e False para deixar o callback escalar cuidar do campo. Um array vazio sai como um único <value/> vazio, então um armazém que oferece uma opção de string vazia deve recusar esse caso dentro do callback, da mesma forma que o ExportLoadedFormToXFDF faz
uses HPDFXFDF;
// Ponteiro de função simples, não "of object": o Context carrega o seu armazém.
// TFormStore e seus métodos são código seu, não fazem parte do 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); // suas ligações escalares existentes
ArrayAccess := Default(THPDFXFDFArrayAccess); // todo slot não usado fica nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Tudo acima assume list boxes que já existem e já têm o bit MultiSelect setado no /Ff. Criar campos choice e setar seus bits de flag é coberto em adicionar ListBox e outros campos AcroForm a um PDF carregado, e marcação de comentário que viaja pela parte <annots> do XFDF é coberta em import e export de anotações XFDF no HotPDF. A referência de método de cada chamada mostrada aqui, mais um build de teste para executá-las contra seus próprios formulários, está na página do componente HotPDF Delphi para PDF