O HotPDF v2.743.0 achata anotações PDF que não têm um stream de aparência /AP, em vez de ignorá-las silenciosamente. FlattenLoadedAnnotations agora encaminha um widget sem aparência para EnsureLoadedFieldAppearanceStream e cria um Form XObject para marcações sem aparência a partir das propriedades da própria anotação, para que os valores digitados em um formulário /NeedAppearances sobrevivam no conteúdo da página, em vez de desaparecerem no momento do flatten. A falha que forçou essa mudança parece um no-op. Um cliente envia um formulário de inscrição preenchido, impresso em PDF por um browser. Você o carrega no HotPDF, chama FlattenLoadedAnnotations, recebe 0, salva e distribui um documento com caixas vazias onde o candidato digitou um nome e um valor. Nada levantou exceção, nada foi registrado. Os valores estavam no arquivo o tempo todo, guardados na entrada /V de cada campo, e a passagem de flatten passou direto por eles porque nenhum desses widgets carregava um stream de aparência
Por que achatar um formulário impresso pelo browser perde os valores digitados?
Porque um formulário /NeedAppearances guarda o valor sem guardar uma imagem do valor. A ISO 32000-1 12.7.2 permite que um formulário interativo defina /NeedAppearances true no dicionário AcroForm, instruindo o viewer a construir a superfície visual de cada campo no momento da abertura a partir de /V, /DA e /Q. Produtores que geram formulários de forma barata — caminhos de impressão do browser, preenchimentos server-side e alguns front ends de scanner — aceitam essa possibilidade e não gravam /AP. Flattening, conforme definido pelo algoritmo de aparência da ISO 32000-1 12.5.5, é um trabalho de transcrição: pegue o stream de aparência normal da anotação, mapeie seu /BBox para o /Rect, invoque-o a partir do content stream da página com um operador Do e depois apague a anotação. Sem 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 no conjunto: os documentos que mais precisam de flatten são justamente os menos propensos a carregar aparências. A mesma lacuna engolia marcações — um Highlight de uma ferramenta de revisão, um Square de uma revisão visual e uma assinatura Ink — quando o produtor confiava no viewer para desenhá-las
Onde o HotPDF conecta a síntese a FlattenLoadedAnnotations
O ponto de conexão é deliberadamente tardio: depois que a busca da aparência falha, não antes. FlattenLoadedAnnotations ainda pede primeiro o stream de aparência normal a GetLoadedAnnotationAppearanceStream, e uma anotação que já tem um é incorporada exatamente como era na v2.386.0. Somente um resultado nil, em uma anotação com /Rect não degenerado e sem flag hidden, entra no caminho de síntese. Essa ordem importa: um autor de documento que teve o trabalho de gravar um /AP recebe seus próprios bytes de volta, não uma reconstrução do 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);
// pergunte 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ção se separam. Um widget é resolvido de volta ao campo proprietário por meio de GetLoadedFormFieldIndexForAnnotation e entregue a EnsureLoadedFieldAppearanceStream, o gerador de aparência de campos que existe nesta biblioteca PDF Delphi desde a v2.328.0. Reutilizá-lo em vez de escrever um segundo renderer de campos é o ponto principal — ele já cobre fontes Type0, quebra de linha, 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. Todo o restante vai para o sintetizador de marcação. Para o chamador nada muda: a mesma chamada de flatten de uma linha agora retorna uma contagem diferente de zero em documentos que antes retornavam zero
Doc:= THotPDF.Create(nil);
try
Doc.LoadFromFile('needappearances-form.pdf');
// v2.743.0: widgets e marcacoes sem AP sao sintetizados e incorporados
Flattened:= Doc.FlattenLoadedAnnotations; // todas as paginas, 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;
Por 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 em seu próprio espaço /BBox, e as duas origens não são o mesmo ponto. A ISO 32000-1 Table 176 define /QuadPoints para anotações de marcação de texto no user space padrão, e a Table 174 faz o mesmo para os endpoints /L da anotação de linha; /InkList segue a mesma convenção. O HotPDF dá ao Form sintetizado um /BBox de [0 0 W H], cuja origem fica no canto inferior esquerdo de /Rect. Portanto todo ponto retirado de /QuadPoints, /L ou /InkList precisa ser transladado pelo negativo do canto inferior esquerdo de /Rect antes de ser escrito no content stream. Se você errar isso, um highlight em uma linha a 700 pontos no alto da página será desenhado 700 pontos acima da própria caixa, o que na prática significa que ele não será desenhado em lugar algum. A correção é uma subtração por coordenada, e ela compõe com o cm emitido pelo bake depois — essa matriz mapeia o /BBox de volta ao /Rect, então os dois passos se cancelam e produzem a geometria absoluta correta
// endpoints /L estao no user space da pagina (ISO 32000-1 Table 174); a origem
// do BBox do Form fica no canto inferior esquerdo de /Rect, entao 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 a aparência sintetizada de marcação realmente desenha
O sintetizador de marcação lê apenas o dicionário da anotação, o que mantém a saída previsível e honesta sobre o que ele não pode saber. FreeText e Stamp desenham /Contents usando a fonte e a cor extraídas de /DA, alinhadas por /Q, com padding de 2 pt. Square e Circle desenham um contorno re ou uma curva de Bezier com quatro arcos, contornado em /C e preenchido com /IC quando presente, na largura de /BS /W. Line e Ink contornam seus vértices. Highlight preenche cada quad, enquanto Underline, StrikeOut e Squiggly contornam uma linha na base do quad, no ponto médio do quad ou como um zigue-zague de um ponto. Um /CA abaixo de 1 vira um ExtGState com uma entrada ca, referenciada como /GSA gs no início do stream
A codificação do texto é decidida a partir da entrada /DR /Font do AcroForm nomeada por /DA. Se o /Subtype dessa fonte for Type0, o HotPDF grava a string como um literal hexadecimal UTF-16BE com a marca de ordem de bytes FEFF; caso contrário, grava uma string literal escapada, com parênteses e barras invertidas escapados e bytes acima de 126 escritos em octal. O operador Tf de /DA é emitido antes de BT, o que é permitido porque o estado de texto persiste entre os limites do objeto de texto, e evita desmontar a string /DA. Dois limites merecem ser declarados claramente. A largura de linha para wrapping e quadding é estimada com uma heurística de meio-em / em inteiro, não com métricas reais da fonte, então o alinhamento em uma fonte proporcional é próximo, mas não exato. E um subtype sem nada sintetizável — Popup, Link ou um Stamp cujo único conteúdo seja um nome de ícone — produz nil e permanece intocado, exatamente como antes
A troca temporária de /Annots que pune uma limpeza bem-intencionada
FlattenOneWidget, o caminho por widget usado por FlattenLoadedFormFields, é uma armadilha de aliasing que qualquer mudança dentro do loop compartilhado de flatten precisa respeitar. Ele substitui temporariamente o valor de /Annots da página por um array de um elemento para que a passagem genérica de flatten opere sobre um único widget, e depois restaura o ponteiro original de PHPDFDictionaryItem em um bloco finally. A restauração grava de volta em um slot de dicionário que foi capturado 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 loop interno liberar este item
TemporaryAnnots.Free;
end;
Adicione uma limpeza razoável dentro do loop interno compartilhado — um DeleteValue('Annots') assim que o array ficar vazio, para que a página salva não carregue um array vazio vestigial — e essa chamada libera justamente o item de dicionário apontado por DictItem. O finally então grava por um ponteiro pendente e o processo morre com "Invalid pointer operation". Dois testes existentes capturaram isso imediatamente, e essa é a única razão pela qual o caso é uma nota de rodapé, não um ticket de suporte. A regra é geral: antes de adicionar limpeza a um loop compartilhado, verifique nos chamadores os contratos de alias ou troca. Um array /Annots vazio sobrando é um defeito cosmético e não vale trocar uma garantia de tempo de vida do ponteiro por ele
O que não é incorporado e quanto o flatten custa
Anotações ocultas são excluídas de propósito. Uma anotação cujo inteiro /F tem o bit de posição 2 definido está oculta pela ISO 32000-1 12.5.3 e, quando também não tem /AP, existe uma tentação real de sintetizar uma e incorporá-la como as demais. Isso seria um bug com consequências de segurança: incorporar uma nota invisível ao conteúdo da página a torna visível para todos que abrirem o arquivo. O HotPDF deixa essas anotações exatamente onde estão e não as conta no valor de retorno. Seja igualmente claro com seus usuários sobre o preço das que são incorporadas. Flattening é irreversível — a anotação é apagada do array /Annots da página e sua visualização agora é conteúdo de página, portanto não há mais edição do valor do campo, thread de comentários, alternância de estado /AS nem maneira de recuperar os dados estruturados sem o arquivo original. Faça flatten em uma cópia, mantenha o original e use a cópia somente quando o documento deixar de ser um formulário e virar um registro. Se o problema for baseado em XFA, e não uma ausência de aparência, o caminho de flatten de XFA para AcroForm no HotPDF é o ponto de partida, e se você ainda está criando o formulário, as notas sobre conectar ações e validação de campos AcroForm cobrem o lado da escrita
Há uma ressalva de verificação, porque ela pode custar uma tarde. ExtractLoadedPageGlyphs não desce até Form XObjects, e uma aparência incorporada vive dentro de um — o content stream da página contém somente uma sequência q ... cm /FlatAn<n> Do Q. A extração de glifos em uma página achatada, portanto, não reporta nada, e esse é o comportamento correto, não um bake perdido. Verifique no nível dos bytes, procurando o nome de recurso /FlatAn, a invocação Do e /Subtype /Form, ou pelo pipeline de renderização, que expande XObjects
Achatar anotações parece uma transcrição de três linhas até você encontrar os documentos que as pessoas realmente geram. Se você trabalha com formulários preenchidos, marcações de revisão ou saída de arquivamento em Delphi ou C++Builder, vale ler como o componente PDF Delphi HotPDF trata o lado de documento carregado de AcroForms e anotações antes de criar seu próprio gerador de aparência sobre ele