O HotPDF v2.743.0 achata anotações PDF que não têm um stream de aparência /AP, em vez de as ignorar silenciosamente. FlattenLoadedAnnotations encaminha agora um widget sem aparência através de EnsureLoadedFieldAppearanceStream e constrói um Form XObject para markup sem aparência a partir das propriedades da própria anotação, para que os valores introduzidos num formulário /NeedAppearances sobrevivam no conteúdo da página em vez de desaparecerem no momento de achatar. A falha que obrigou a esta alteração parece um no-op. Um cliente envia um formulário preenchido, impresso para PDF a partir de um browser. Carrega-o no HotPDF, chama FlattenLoadedAnnotations, recebe 0, guarda e distribui um documento com caixas vazias onde o candidato escreveu um nome e um montante. Nada levantou uma exceção, nada foi registado. Os valores estiveram no ficheiro o tempo todo, dentro da entrada /V de cada campo, e a passagem de flatten passou diretamente por eles porque nenhum desses widgets trazia um stream de aparência para materializar
Porque é que achatar um formulário impresso pelo browser perde os valores introduzidos?
Porque um formulário /NeedAppearances guarda o valor sem guardar uma imagem desse valor. A ISO 32000-1 12.7.2 permite que um formulário interativo defina /NeedAppearances true no dicionário AcroForm, o que diz ao visualizador para construir a superfície visual de cada campo no momento da abertura a partir de /V, /DA e /Q. Os produtores que geram formulários de forma económica — percursos de impressão de browsers, fillers do lado do servidor e alguns frontends de digitalização — aproveitam essa possibilidade e não escrevem nenhum /AP. Achatar, tal como definido pelo algoritmo de aparências na ISO 32000-1 12.5.5, é um trabalho de transcrição: pegue no stream de aparência normal da anotação, mapeie o seu /BBox para o seu /Rect, invoque-o a partir do content stream da página com um operador Do e depois elimine a anotação. Sem um stream de origem, não há nada para transcrever. A implementação original do HotPDF, desde a v2.386.0, tratava isso como "skip", o que é defensável isoladamente e desastroso em conjunto: os documentos com maior probabilidade de precisar de flatten são os que têm menor probabilidade de trazer aparências. A mesma lacuna engolia markup — um Highlight de uma ferramenta de revisão, um Square de uma passagem de redline ou uma assinatura Ink — sempre que o produtor confiava no visualizador para o desenhar
Onde o HotPDF liga a síntese a FlattenLoadedAnnotations
O ponto de ligação é deliberadamente tardio: depois de a pesquisa de aparência falhar, não antes. FlattenLoadedAnnotations continua a pedir primeiro o stream de aparência normal através de GetLoadedAnnotationAppearanceStream, e uma anotação que já tenha um é materializada exatamente como na v2.386.0. Apenas um resultado nil, numa anotação com um /Rect não degenerado e sem o sinalizador hidden, entra no percurso de síntese. Essa ordem importa: um autor de documentos que teve o cuidado de escrever um /AP recebe os seus próprios bytes de volta, não uma reconstrução feita pelo HotPDF
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
if (NStrm= nil) and (RR> RL) and (RT> RB) and ((FlagsValue and 2)= 0) then
begin
if Subtype= 'Widget' then
begin
FieldIdx:= GetLoadedFormFieldIndexForAnnotation(Indices[PgI], AnI, WidgetIdx);
if FieldIdx>= 0 then
EnsureLoadedFieldAppearanceStream(FieldIdx);
// perguntar novamente: o gerador anexou /AP /N ao widget
NStrm:= GetLoadedAnnotationAppearanceStream(Indices[PgI], AnI, aakNormal);
end
else
NStrm:= SynthesizeMarkupAppearance(AnnotDict, Subtype, RL, RB, RR, RT);
end;
A partir daí, as duas famílias de anotações separam-se. Um widget é resolvido de volta para o campo proprietário através de GetLoadedFormFieldIndexForAnnotation e entregue a EnsureLoadedFieldAppearanceStream, o gerador de aparências de campos que existe nesta biblioteca PDF para Delphi desde a v2.328.0. Reutilizá-lo em vez de escrever um segundo renderer de campos é precisamente o objetivo — já cobre fontes Type0, quebra de linhas, quadding, estados /AS de checkbox e radio e rotação /MK, a mesma maquinaria por trás de adicionar campos AcroForm a um PDF já carregado. Tudo o resto vai para o sintetizador de markup. Para o chamador nada muda: a mesma chamada flatten de uma linha devolve agora uma contagem diferente de zero em documentos que antes devolviam zero
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgets e markup sem AP são sintetizados e depois materializados
Flattened:= Doc.FlattenLoadedAnnotations; // todas as páginas, todos os subtipos
// Flattened:= Doc.FlattenLoadedAnnotations('1-3', 'Highlight');
if Flattened= 0 then
raise Exception.Create('nothing was flattened');
Doc.SaveLoadedDocument('flattened.pdf');
finally
Doc.Free;
end;
Porque é que QuadPoints e InkList ficam no lugar errado?
Porque essas coordenadas estão no user space da página, enquanto o stream de aparência sintetizado desenha no seu próprio espaço /BBox, e as duas origens não são o mesmo ponto. A ISO 32000-1 Tabela 176 define /QuadPoints para anotações de markup de texto no user space predefinido, e a Tabela 174 faz o mesmo para os endpoints /L da anotação de linha; /InkList segue a mesma convenção. O HotPDF dá ao formulário sintetizado um /BBox de [0 0 W H], cuja origem fica no canto inferior esquerdo de /Rect. Assim, cada ponto retirado de /QuadPoints, /L ou /InkList tem de ser transladado pelo negativo do canto inferior esquerdo de /Rect antes de ser escrito no content stream. Faça isto mal e um highlight numa linha 700 pontos acima da página será desenhado 700 pontos acima da sua própria caixa, o que na prática significa que não aparece em lado nenhum. A correção é uma subtração por coordenada, e compõe-se com o cm que o bake emite depois — essa matriz mapeia o /BBox de volta para /Rect, pelo que os dois passos se anulam e deixam a geometria absoluta correta
// Os endpoints /L estão no user space da página (ISO 32000-1 Tabela 174); a origem do
// BBox do formulário está no canto inferior esquerdo de /Rect, por isso desloque por -(RL, RB)
X1:= ArrNum(LA, 0, 0)- RL;
Y1:= ArrNum(LA, 1, 0)- RB;
X2:= ArrNum(LA, 2, 0)- RL;
Y2:= ArrNum(LA, 3, 0)- RB;
StrokeOp:= ColorOp(DArr('C'), true);
if StrokeOp= '' then
StrokeOp:= '0 G';
Result:= _FloatToStrR(BW)+ ' w '#10+ StrokeOp+ #10+
_FloatToStrR(X1)+ ' '+ _FloatToStrR(Y1)+ ' m '+
_FloatToStrR(X2)+ ' '+ _FloatToStrR(Y2)+ ' l S'#10;
O que desenha realmente a aparência de markup sintetizada?
O sintetizador de markup lê apenas o dicionário da anotação, o que mantém a saída previsível e honesta sobre aquilo que não pode saber. FreeText e Stamp desenham /Contents usando a fonte e a cor extraídas de /DA, alinhados por /Q, com 2 pt de padding. Square e Circle desenham um contorno re ou uma curva de Bezier de quatro arcos, traçado em /C e preenchido com /IC quando presente, com a largura de /BS /W. Line e Ink traçam os seus vértices. Highlight preenche cada quad, enquanto Underline, StrikeOut e Squiggly traçam uma linha na base do quad, no ponto médio do quad ou como um ziguezague de um ponto. Um /CA inferior a 1 torna-se um ExtGState com uma entrada ca, referenciada como /GSA gs no início do stream
A codificação de texto é decidida a partir da entrada /DR /Font do AcroForm nomeada por /DA. Se o /Subtype dessa fonte for Type0, o HotPDF escreve a string como um literal hexadecimal UTF-16BE com a marca de ordem de bytes FEFF; caso contrário, escreve uma string literal com escapes, escapando parênteses e barras invertidas e escrevendo em octal os bytes acima de 126. O operador Tf de /DA é emitido antes de BT, o que é legal porque o estado de texto persiste através do limite do objeto de texto, e evita desmontar a string /DA. Convém declarar claramente dois limites. A largura da linha para wrapping e quadding é estimada com uma heurística de meio-em / em completo em vez de métricas reais da fonte, pelo que o alinhamento numa fonte proporcional fica próximo, mas não exato. E um subtipo sem nada sintetizável — Popup, Link ou um Stamp cujo único conteúdo seja um nome de ícone — devolve nil e permanece intocado, exatamente como antes
A troca temporária de /Annots que castiga uma limpeza prestável
FlattenOneWidget, o percurso por widget usado por FlattenLoadedFormFields, é uma armadilha de aliasing que qualquer alteração dentro do ciclo de flatten partilhado tem de respeitar. Substitui temporariamente o valor /Annots da página por um array de um elemento para que a passagem de flatten genérica opere sobre um único widget e depois restaura o ponteiro original PHPDFDictionaryItem num bloco finally. A restauração escreve de volta num slot do dicionário que capturou antes da chamada
DictItem:= PHPDFDictionaryItem(PageObj.Items.Items[AnnotsIndex]);
Item:= DictItem^.Value;
TemporaryAnnots:= THPDFArrayObject.Create(nil);
TemporaryAnnots.AddObject(Target);
DictItem^.Value:= TemporaryAnnots;
try
Result:= FlattenLoadedAnnotations(IntToStr(PageIndex+ 1), 'Widget')= 1;
finally
DictItem^.Value:= Item; // pendente se o ciclo interno libertar este item
TemporaryAnnots.Free;
end;
Acrescente uma limpeza com bom aspeto dentro do ciclo interno partilhado — um DeleteValue('Annots') quando o array fica vazio, para que a página guardada não mantenha um array vazio vestigial — e essa chamada liberta precisamente o item de dicionário para o qual DictItem aponta. O finally escreve então através de um ponteiro pendente e o processo morre com "Invalid pointer operation". Dois testes existentes detetaram-no imediatamente, que é a única razão para isto ser uma nota de rodapé e não um ticket de suporte. A regra generaliza-se: antes de acrescentar uma limpeza a um ciclo partilhado, verifique os callers quanto a contratos de alias ou troca. Um array /Annots vazio que sobra é uma imperfeição cosmética e não vale a pena trocar uma garantia de tempo de vida do ponteiro por ela
O que fica por materializar e quanto custa o flatten?
As anotações hidden são excluídas de propósito. Uma anotação cujo inteiro /F tenha o bit da posição 2 definido está hidden segundo a ISO 32000-1 12.5.3 e, quando também não tem /AP, existe uma tentação real de sintetizar uma aparência e materializá-la como as restantes. Isso seria um bug com consequências de segurança: materializar uma nota invisível no conteúdo da página torna-a visível para todos os que abrirem o ficheiro. O HotPDF deixa essas anotações exatamente onde estão e não as conta no valor de retorno. Seja igualmente claro com os utilizadores sobre o preço das que são materializadas. O flatten é irreversível — a anotação é eliminada do array /Annots da página e o seu visual passa a ser conteúdo da página, pelo que deixa de ser possível editar o valor do campo, manter um thread de comentários, alternar o estado /AS ou recuperar os dados estruturados sem o ficheiro original. Faça flatten de uma cópia, conserve o original e use-o apenas quando o documento deixar de ser um formulário e passar a ser um registo. Se o seu problema for baseado em XFA e não uma questão de aparência ausente, o percurso de flatten de XFA para AcroForm no HotPDF é o ponto de partida certo; e, se ainda estiver a construir o formulário, as notas sobre ligar ações e validação de campos AcroForm cobrem o lado da escrita
Há uma ressalva de verificação, porque de outro modo lhe custará uma tarde. ExtractLoadedPageGlyphs não desce a Form XObjects e uma aparência materializada vive dentro de um — o content stream da página contém apenas uma sequência q ... cm /FlatAn<n> Do Q. A extração de glifos numa página flattenizada não comunica, portanto, nada, e isso é comportamento correto, não um bake perdido. Verifique ao nível dos bytes, procurando o nome de recurso /FlatAn, a invocação Do e /Subtype /Form, ou através do pipeline de renderização, que expande XObjects
O flatten de anotações parece ter três linhas de transcrição até encontrar os documentos que as pessoas realmente geram. Se trabalha com formulários preenchidos, markup de revisão ou saída de arquivo em Delphi ou C++Builder, vale a pena ler como o componente PDF HotPDF para Delphi trata o lado do documento carregado de AcroForms e anotações antes de construir por cima dele um gerador de aparências próprio