O HotPDF faz o round trip de valores de list box multiseleção através de FDF e XFDF mantendo o valor do campo como array de ponta a ponta. Desde a versão 2.755.0, o ExportLoadedFormToFDF, o ExportLoadedInterchangeToFDF e o ExportLoadedFormToXFDF escrevem cada opção selecionada como a sua própria string FDF ou elemento <value> XFDF, e os métodos de importação correspondentes verificam 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 ao longo do caminho
A falha que isto corrige é fácil de reproduzir. Pegue num formulário de encomenda com uma list box multiseleção de opções de produto, deixe um utilizador escolher duas, exporte os dados do formulário para um sistema de back-office, e depois importe o ficheiro editado de volta para o PDF. Antes desta mudança a list box voltava vazia ou errada. A razão é que um dos valores de exportação continha uma quebra de linha, e o caminho antigo tinha achatado as seleções numa única string separada por linhas. Tirar várias seleções dessa string nunca foi fiável, e com um valor de exportação que ele próprio contenha uma quebra de linha não pode funcionar de todo
Porque é que juntar valores multiseleção com quebras de linha parte o round trip?
Juntar as seleções numa única string deita fora as fronteiras entre valores, e um valor pode conter o separador, por isso nenhum importador consegue voltar a dividir a string corretamente. A ISO 32000-1 §12.7.4.4 permite que a entrada /V de um campo choice seja uma única string de texto ou um array de strings de texto, e uma list box com a flag MultiSelect (bit 22 de /Ff) usa a forma de array assim que mais de uma opção é escolhida. A mesma secção define /I como um array de índices de opção de base zero por ordem crescente, que os visualizadores usam para distinguir duas opções que por acaso partilhem um valor de exportação. No HotPDF o getter escalar GetFormFieldValue só lê a forma de string, por isso passar um array por ele degradava a exportação para uma string vazia, e a antiga importação XFDF 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 ficheiro não dá maneira de saber qual. A correção foi deixar de usar um escalar a meio do round trip por completo
O que contêm os ficheiros FDF e XFDF exportados?
O HotPDF escreve um valor multiseleção como array tipado em FDF e como um elemento <value> por seleção em XFDF, por isso as fronteiras ficam visíveis em 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 transporta xml:space="preserve" como a ISO 19444-1 exige, o que significa que qualquer espaço em branco dentro de um elemento de texto conta como dados. O HotPDF escreve portanto a tag de abertura, o texto escapado e a tag de fecho de cada <value> numa só peça, mantém a indentação fora do elemento, e codifica CR, LF e TAB como referências de caracteres para que um parser XML que aplique normalização de fins de linha não consiga 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 fronteira de exportação valem a pena conhecer antes de escrever o código chamador. Primeiro, o ExportLoadedFormToFDF constrói o corpo FDF completo em memória antes de criar o ficheiro alvo (corrigido em 2.755.1), por isso um valor que não possa ser exportado, como um array com algo que não strings, levanta exceção sem truncar um ficheiro existente. Segundo, uma seleção vazia numa list box que também ofereça um valor de exportação de string vazia é ambígua em XFDF, porque <value/> pode significar que nada está selecionado ou que a opção vazia está selecionada. O ExportLoadedFormToXFDF levanta exceção nesse caso em vez de adivinhar, e levanta-a antes de o ficheiro alvo ser aberto. O FDF não tem ambiguidade dessas, já que /V [] e /V [()] são distintos. Ambos os exportadores FDF também saltam terminais só de widget sem nome /T, à semelhança do exportador XFDF, porque nenhum importador conseguiria algum dia casar essas entradas 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 multiseleção 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 exportação vazia: o XFDF não distingue
// os dois casos, e o ficheiro .xfdf existente fica intacto
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
Como valida o HotPDF um valor multiseleção na importação?
O HotPDF só aceita um array importado quando o alvo é um campo choice com a flag MultiSelect definida e todos os valores do array correspondem a um valor de exportação no array /Opt do campo. Cada slot de opção pode ser usado uma vez, por isso uma lista com duas opções que partilhem o valor de exportação b aceita [<62> <62>] como duas seleções distintas e rejeita um terceiro b. O /I reconstruído segue a ordem de /Opt e não a ordem dos valores que entram, como a §12.7.4.4 exige índices crescentes. O HotPDF constrói o novo /V e o novo /I como objetos destacados e só os atribui depois de todos os valores passarem a validação, por isso um valor rejeitado nunca deixa para trás meio array ou índices obsoletos. A cópia é escrita no campo que está a ser importado em vez de num array ancestral partilhado, grafias hex vindas do FDF continuam hex através da gravação, e campos cujos cálculos dependem da list box são marcados para recálculo. Se só precisa de definir um único valor, definir um valor de campo de formulário num PDF carregado passa pelo caminho escalar, que por desenho não trata de seleções múltiplas
Algumas outras ferramentas escrevem valores de exportação ASCII simples como hex strings sem marca de ordem de bytes, por exemplo <416272>, e depois exportam XFDF escrevendo esses dígitos hex como texto. Uma comparação literal estrita à volta falha, e a importação aborta. A versão 2.755.1 acrescenta uma repetição: quando um valor não corresponde a opção nenhuma, o HPDFHexSpellingText descodifica o texto como uma carga hex e compara o resultado outra vez. A repetição só se aplica a input que de outro modo teria levantado exceção, por isso nunca muda um valor que já correspondia. A mesma versão também fez os caminhos escalar e array usarem o mesmo descodificador Unicode, que entende PDFDocEncoding, UTF-16 com qualquer marca de ordem de bytes e UTF-8. Antes disso, um valor lógico podia corresponder num caminho e falhar no outro em documentos que misturassem codificações
Porque é que um ficheiro FDF válido ainda pode perder campos durante o parse?
Um scanner FDF que não siga strings hexadecimais pode cortar um dicionário de campo ao meio quando um valor hex acaba mesmo ao lado do terminador do dicionário. Em << /T (region) /V <416273>>> o primeiro > fecha a string hex, mas um scanner ingénuo lê-o juntamente com o próximo > como o fim do dicionário e deixa cair o campo em silêncio. O importador FDF ao nível do ficheiro já seguia se estava dentro de uma string hex, e em 2.755.1 os scanners de array e dicionário por trás do ImportLoadedInterchangeFromFDF fazem o mesmo. Uma segunda questão diz respeito a referências indiretas. Um ficheiro FDF é um pequeno documento de sintaxe PDF com a sua própria numeração de objetos (ISO 32000-1 §12.7.7), por isso um valor como /V [11 0 R] refere o objeto 11 do ficheiro FDF, e não o objeto 11 do PDF que está a preencher. O parser FDF simplificado do HotPDF não resolve referências dentro do ficheiro, por isso rejeita um array desses em vez de ler o que quer que o objeto 11 venha a ser no documento alvo
Importações de ficheiro, stream e XFDF reportam erros de forma diferente
As três vias de importação validam da mesma maneira mas reportam falhas de forma diferente, e vale a pena escolher uma de propósito. O ImportLoadedFormFromFDF salta qualquer campo que falhe a validação e devolve o número de campos que aplicou de facto, por isso uma contagem menor que a esperada é o único sinal de problema. O ImportLoadedInterchangeFromFDF e o ImportLoadedFormFromXFDF levantam exceção ao primeiro campo rejeitado. Cada campo é comprometido por si, por isso os campos processados antes da exceção conservam os seus valores novos. Não trate nenhum destes como uma transação sobre o ficheiro de troca inteiro: se precisa de comportamento tudo-ou-nada, descarte o documento carregado quando ocorre uma exceção em vez de o gravar
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 alvo não multiseleção 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;
Estender os callbacks XFDF sem partir chamadores existentes
O suporte de array na unit XFDF de nível mais baixo vive num registo separado, o THPDFXFDFArrayAccess, e em novas overloads de HPDFXFDFExportFields e HPDFXFDFImportFields, e não em campos extra acrescentados ao fim do registo THPDFXFDFAccess existente. A razão é compatibilidade binária. Código que preenche um THPDFXFDFAccess como variável local frequentemente define só os slots que conhece e nunca limpa o resto, por isso um novo ponteiro de função acrescentado a esse registo conteria lixo da stack, e a biblioteca tomar-lo-ia por um callback a sério. Com um registo separado, os chamadores antigos ficam com o layout antigo e as overloads antigas, e essas overloads passam internamente um registo de array todo a nil. A overload de importação escalar original ainda junta valores repetidos com LF por compatibilidade, e só a overload consciente de arrays os mantém separados. Quando ligar o seu próprio armazém de dados, parta de Default(THPDFXFDFArrayAccess). Devolva True no GetFormFieldValueArray para qualquer campo com valor de lista, incluindo um sem nada selecionado, e False para recuar ao callback escalar
uses HPDFXFDF;
// Ponteiro de função simples, e não «of object»: Context transporta o seu próprio armazém
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); // as suas ligações escalares existentes
ArrayAccess := Default(THPDFXFDFArrayAccess); // todos os slots não usados estão a nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
A troca multiseleção funciona em list boxes que já existam e tenham o bit MultiSelect definido em /Ff. Para saber como os campos choice e os seus bits de flags são criados à partida, veja acrescentar ListBox e outros campos AcroForm a um PDF carregado. Para marcações de comentários que passem pela árvore <annots> do XFDF, veja importação e exportação de anotações XFDF no HotPDF. A referência completa da API e o download de avaliação estão na página do componente PDF HotPDF para Delphi