O PDFium Component cria anotações de marcação de texto, ou seja, realce, sublinhado, tachado e ondulado, por meio de TPdf.CreateAnnotation: você define HasAttachmentPoints := True no registro TPdfAnnotation e preenche o quadrilátero AttachmentPoints dele, e o componente escreve a entrada QuadPoints definida na ISO 32000-1 §12.5.6.10. Essa é a superfície de API inteira. O motivo de este artigo existir é o que acontece por baixo dela, porque a cadeia de chamadas bruta do PDFium tem um modo de falha que produz o sintoma menos útil de todo o kit: FPDFAnnot_SetAttachmentPoints devolve false em uma anotação recém-criada, sempre, sem código de erro e sem pista. Este é o companheiro, do lado da criação, do nosso artigo sobre ler e revisar anotações existentes, que percorre a direção oposta pelas mesmas estruturas
A cena de depuração é sempre a mesma. Você cria uma anotação de realce, chama o setter de pontos de anexação com o índice 0, a função devolve false e você começa a duvidar das suas coordenadas. Você transpõe os pontos, inverte o eixo Y, troca espaço de página por espaço de dispositivo. Nada disso ajuda, porque as coordenadas nunca foram o problema. O problema é a semântica de índice da API C e, uma vez que você a enxerga, a correção são duas linhas
O que os QuadPoints significam na ISO 32000-1
QuadPoints é um array de 8×n números que descreve n quadriláteros, e a ISO 32000-1 §12.5.6.10 o exige em toda anotação de marcação de texto: cada quadrilátero marca uma palavra ou grupo de palavras contíguas às quais o realce, o sublinhado ou o tachado se aplicam. A entrada Rect da anotação continua existindo, mas para os subtipos de marcação ela apenas delimita a região; são os quadriláteros que o renderizador de fato pinta. Um quadrilátero em vez de um retângulo porque o texto pode estar rotacionado ou inclinado, então os quatro cantos são guardados como quatro pontos independentes: x1 y1 x2 y2 x3 y3 x4 y4
A ordem desses quatro pontos é onde a especificação e a base instalada se separam. O texto da norma descreve os pontos como traçando o quadrilátero no sentido anti-horário, mas o próprio renderizador da Adobe sempre os interpretou em um padrão em Z: primeiro a aresta de cima, da esquerda para a direita, depois a aresta de baixo, da esquerda para a direita. Como todo autor testou contra o Acrobat, praticamente todo renderizador, o PDFium incluído, segue o padrão em Z, e arquivos que seguem a redação literal da norma renderizam como realces achatados ou torcidos em alguns visualizadores. A estrutura FS_QUADPOINTSF do PDFium codifica exatamente essa convenção: (x1,y1) é o canto superior esquerdo, (x2,y2) o superior direito, (x3,y3) o inferior esquerdo e (x4,y4) o inferior direito, em coordenadas de página em que Y cresce para cima. Siga essa ordem e pronto; os renderizadores são tolerantes com muitas coisas, mas um quadrilátero embaralhado não é uma delas
Por que FPDFAnnot_SetAttachmentPoints devolve false?
FPDFAnnot_SetAttachmentPoints falha em uma anotação nova porque o contrato dele é substituir o quadrilátero em um dado índice, e uma anotação recém-criada tem zero quadriláteros a substituir. A assinatura recebe um handle de anotação, um quad_index e os pontos; o índice 0 não significa "a primeira posição, criando-a se preciso", significa "o quadrilátero existente de número 0" e, quando FPDFAnnot_CountAttachmentPoints reporta 0, esse quadrilátero não existe e a chamada devolve false. A função que cria uma posição é FPDFAnnot_AppendAttachmentPoints. Toda anotação criada por FPDFPage_CreateAnnot começa com contagem zero, então o caminho de criação precisa chamar Append primeiro, e só as atualizações seguintes podem chamar Set
Isso mordeu o próprio PDFium Component. Até a v1.79.0 a rotina interna compartilhada por CreateAnnotation e SetAnnotation fixava no código FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), o que estava correto para atualizar uma anotação de marcação existente e com falha garantida para uma nova, aparecendo como uma EPdfException com a mensagem 'Cannot set attachment points'. A correção, entregue na v1.79.1, ramifica pela contagem
// Dentro do escritor de anotações do componente (v1.79.1+):
// uma anotação nova ainda não tem posições de quadrilátero, então Append cria
// a primeira; Set só substitui uma posição que já existe
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
'Cannot set attachment points')
else
Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
'Cannot set attachment points');
O mesmo padrão vale se você chamar diretamente as funções C exportadas, o que o componente permite, já que todos os pontos de entrada FPDFAnnot_* são expostos em PDFium.pas. Sempre que você tiver um handle FPDF_ANNOTATION e quiser escrever quadriláteros, pergunte antes a FPDFAnnot_CountAttachmentPoints e roteie de acordo. Se você está procurando por "FPDFAnnot_SetAttachmentPoints returns false", essa ramificação de contar e então acrescentar é quase certamente a sua resposta
Criando um realce com TPdf.CreateAnnotation
Com o componente fazendo o roteamento entre Append e Set por você, criar um realce se reduz a preencher um registro. O exemplo abaixo cria uma página A4 e coloca um realce amarelo semitransparente sobre uma região de 200×20 pontos; repare que o quadrilátero segue a ordem Z descrita acima e que Rectangle é definido para envolver o quadrilátero, o que mantém sensato o comportamento de visualizadores que fazem teste de acerto contra o Rect
var
Pdf: TPdf;
A: TPdfAnnotation;
begin
Pdf := TPdf.Create(nil);
try
Pdf.CreateDocument;
Pdf.AddPage(0, 595, 842);
FillChar(A, SizeOf(A), 0);
A.Subtype := anHighlight;
A.HasColor := True;
A.Color := clYellow;
A.ColorAlpha := $80; // 50% de opacidade
A.HasAttachmentPoints := True;
A.AttachmentPoints[1].X := 50; A.AttachmentPoints[1].Y := 700; // superior esquerdo
A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // superior direito
A.AttachmentPoints[3].X := 50; A.AttachmentPoints[3].Y := 680; // inferior esquerdo
A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // inferior direito
A.Rectangle.Left := 50; A.Rectangle.Top := 700;
A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
A.ContentsText := 'Highlighted region';
Pdf.CreateAnnotation(A);
Pdf.SaveAs('highlighted.pdf');
finally
Pdf.Free;
end;
end;
Trocar de subtipo custa uma linha. anUnderline, anStrikeout e anSquiggly aceitam o mesmo formato de registro, quadriláteros e tudo, porque a ISO 32000-1 trata os quatro como a mesma família de anotação, distinguida apenas por como a região do quadrilátero é decorada. Subtipos que não são marcação de texto, como anSquare, anCircle e anText, se posicionam apenas pelo Rectangle; deixe HasAttachmentPoints em False para eles, e a maquinaria de quadriláteros nunca roda
Por que AttachmentPoints[0] compila no Delphi mas falha no FPC?
TQuadrilateralPoint é declarado como array [1..4] of TPdfPoint, um array com base 1, e isso derruba quem tem os dedos viciados em indexação a partir de zero. Escreva A.AttachmentPoints[0] e o dcc32 do Delphi compila sem reclamar, porque a verificação de faixa vem desligada por padrão; em tempo de execução a expressão lê ou escreve em silêncio a memória logo antes do array, que em um registro TPdfAnnotation é um campo vizinho. O seu realce ganha um canto com lixo, ou um campo vizinho é corrompido, e nada levanta exceção. O Free Pascal pegou exatamente esse bug nos nossos próprios fontes de demonstração durante a portabilidade para Lazarus: o fpc faz verificação de faixa em tempo de compilação para índices constantes e rejeitou AttachmentPoints[0..3] de saída, e foi assim que o erro de um a menos e o bug de Set em vez de Append na biblioteca vieram à tona juntos
Duas práticas decorrem disso. Indexe o quadrilátero de 1 a 4, casando com a ordem dos cantos do código acima, e compile o seu código de anotações ao menos uma vez com a verificação de faixa ligada, seja com {$R+} no Delphi, seja com qualquer compilação do fpc, antes de confiar nele. Uma compilação padrão do dcc32 passando não é evidência de que os índices estão certos; é apenas evidência de que nada quebrou na memória que por acaso estava ali
Obtendo coordenadas de quadrilátero a partir de texto real
Retângulos fixados no código servem para uma demonstração, mas realces de produção traçam glifos reais, e as coordenadas devem vir da geometria da página de texto do PDFium, e não de chute. As rotinas tratadas no nosso guia de extração de texto com o PDFium Component entregam caixas delimitadoras por caractere no mesmo espaço de coordenadas de página que os quadriláteros usam, então um acerto de busca se converte diretamente em pontos de canto: a esquerda do primeiro caractere, a direita do último, o topo e a base a partir dos limites da linha. Se você mesmo está gerando o texto e precisa saber onde as linhas vão cair antes de elas existirem, o artigo sobre medição de texto e quebra de linha cobre como calcular esses limites de antemão
Um limite honesto: o registro TPdfAnnotation carrega um único TQuadrilateralPoint, então uma chamada de CreateAnnotation escreve um quadrilátero. Uma seleção que atravessa três linhas precisa de três quadriláteros, um por linha, conforme a §12.5.6.10, e você tem dois caminhos para chegar lá. O simples é uma anotação por linha, que renderiza corretamente em todo lugar e mantém a API de nível de componente. O compacto, uma anotação carregando três quadriláteros, significa criar a anotação pelo componente e depois chamar você mesmo o FPDFAnnot_AppendAttachmentPoints exportado para o segundo e o terceiro quadriláteros, o que funciona justamente porque Append cria posições em vez de substituí-las. Não tente chegar a vários quadriláteros por chamadas repetidas de SetAttachmentPoints; todo índice além da contagem atual simplesmente devolve false, pelo mesmo motivo que o índice 0 devolveu na anotação nova
Depois de escrever, verifique em um visualizador de verdade em vez de confiar nos códigos de retorno: abra o arquivo no Acrobat ou em qualquer visualizador baseado em PDFium e confirme que a marcação cai sobre o texto, aparece na opacidade pretendida e sobrevive a um ciclo de salvar e recarregar. Os tipos de anotação, o tratamento de quadriláteros e o escritor ciente da contagem mostrados aqui fazem parte do PDFium Component padrão para Delphi, C++Builder e Lazarus; a página do produto traz a referência completa da API de anotações junto do resto da biblioteca