O PDFium Component cria anotações de marcação de texto (como destaque, sublinhado, tachado e ondulado) por meio do método TPdf.CreateAnnotation: você define HasAttachmentPoints := True no registro TPdfAnnotation e preenche seu quadrilátero AttachmentPoints, e o componente grava a entrada QuadPoints definida na ISO 32000-1 §12.5.6.10. Essa é toda a superfície da API. O motivo de este artigo existir é o que acontece por baixo dela, pois a cadeia de chamadas da API C bruta do PDFium possui um modo de falha que produz o sintoma menos útil do kit de ferramentas: FPDFAnnot_SetAttachmentPoints retorna falso em uma anotação recém-criada, sempre, sem código de erro ou qualquer dica. Este é o artigo complementar do lado da criação para o nosso artigo sobre leitura e revisão de anotações existentes, que percorre a direção oposta através das mesmas estruturas
O cenário de depuração é sempre o mesmo. Você cria uma anotação de destaque, chama o definidor de pontos de fixação com o índice 0, a função retorna falso e você começa a questionar suas coordenadas. Você transpõe os pontos, inverte o eixo Y, troca o espaço da página pelo espaço do dispositivo. Nada disso ajuda, porque as coordenadas nunca foram o problema. O problema reside nas semânticas de índice da API C e, assim que você as compreende, a correção leva apenas duas linhas
O que significam os QuadPoints na ISO 32000-1
O QuadPoints é uma matriz de 8xN números que descrevem N quadriláteros, e a ISO 32000-1 §12.5.6.10 exige isso em cada anotação de marcação de texto: cada quadrilátero marca uma palavra ou grupo de palavras contíguas às quais o destaque, sublinhado ou tachado se aplica. A entrada Rect da anotação ainda existe, mas para subtipos de marcação ela apenas limita a região; os quadriláteros são o que o renderizador realmente pinta. Adota-se um quadrilátero em vez de um retângulo porque o texto pode ser rotacionado ou distorcido, de modo que os quatro cantos sejam armazenados como quatro pontos independentes: x1 y1 x2 y2 x3 y3 x4 y4
A ordem dos desses quatro pontos é onde a especificação e as implementações instaladas se separam. O texto da especificação 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 Z: primeiro a borda superior da esquerda para a direita, depois a borda inferior da esquerda para a direita. Como todos os autores testavam contra o Acrobat, efetivamente todos os renderizadores, incluindo o PDFium, seguem o padrão Z, e arquivos que seguem a redação literal da especificação renderizam como destaques colapsados ou torcidos em alguns visualizadores. A estrutura FS_QUADPOINTSF do PDFium codifica exatamente essa convenção: (x1,y1) é o canto superior esquerdo, (x2,y2) superior direito, (x3,y3) inferior esquerdo e (x4,y4) inferior direito, em coordenadas de página onde Y cresce para cima. Siga essa ordem e pronto; os renderizadores são tolerantes com muitas coisas, mas um quadrilátero desordenado não é uma delas
Por que o FPDFAnnot_SetAttachmentPoints retorna falso?
O FPDFAnnot_SetAttachmentPoints falha em uma nova anotação porque seu contrato é o de substituir o quadrilátero em um determinado índice, e uma anotação recém-criada possui zero quadriláteros para substituir. A assinatura recebe um identificador (handle) de anotação, um quad_index e os pontos; o índice 0 não significa "o primeiro slot, criando-o se necessário", significa "o quadrilátero existente de número 0" e, quando o FPDFAnnot_CountAttachmentPoints reporta 0, não existe tal quadrilátero e a chamada retorna falso. A função que cria un slot é a FPDFAnnot_AppendAttachmentPoints. Cada anotação criada por meio de FPDFPage_CreateAnnot começa com uma contagem de zero, de modo que o caminho de criação precise chamar o Append primeiro, e apenas as atualizações subsequentes possam chamar o Set
Por que o FPDFAnnot_SetAttachmentPoints retorna falso?
Isso afetou o próprio PDFium Component. Até a v1.79.0, a rotina interna compartilhada por CreateAnnotation e SetAnnotation trazia codificado de forma fixa (hardcoded) o comando FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), o que estava correto para atualizar uma anotação de marcação existente, mas era falha garantida para uma nova, manifestando-se como uma exceção EPdfException com a mensagem 'Cannot set attachment points'. A correção, lançada na v1.79.1, faz uma ramificação com base na contagem
// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
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 se aplica se você chamar as funções C exportadas diretamente, o que o componente permite fazer, já que todos os pontos de entrada FPDFAnnot_* são expostos em PDFium.pas. Sempre que você possuir um identificador FPDF_ANNOTATION e quiser gravar quadriláteros, consulte o FPDFAnnot_CountAttachmentPoints primeiro e direcione o fluxo de acordo. Se você está pesquisando por "FPDFAnnot_SetAttachmentPoints retorna falso", essa ramificação de contagem seguida de anexação (count-then-append) é quase certamente a sua resposta
Criando um destaque com o TPdf.CreateAnnotation
Com o componente realizando o direcionamento entre Append e Set para você, a criação de um destaque reduz-se ao preenchimento de um registro. O exemplo abaixo cria uma página A4 e insere um destaque amarelo semitransparente sobre uma região de 200x20 pontos; note que o quadrilátero segue a ordem Z descrita acima e que o Rectangle é definido para envolver o quadrilátero, o que mantém um comportamento sensato nos visualizadores que realizam testes de colisão (hit-test) 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% opacity
A.HasAttachmentPoints := True;
A.AttachmentPoints[1].X := 50; A.AttachmentPoints[1].Y := 700; // top-left
A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
A.AttachmentPoints[3].X := 50; A.AttachmentPoints[3].Y := 680; // bottom-left
A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
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;
Alterar sublipos custa apenas uma linha. Os tipos anUnderline, anStrikeout e anSquiggly adotam o mesmo formato de registro, quadriláteros e tudo mais, porque a ISO 32000-1 trata os quatro como a mesma família de anotações, diferindo apenas em como a região do quadrilátero é decorada. Subtipos que não são de marcação de texto, como anSquare, anCircle e anText, posicionam-se unicamente a partir do Rectangle; deixe HasAttachmentPoints como False para esses tipos, e o mecanismo de quadriláteros nunca será executado
Por que o AttachmentPoints[0] compila no Delphi, mas falha no FPC?
O TQuadrilateralPoint é declarado como array [1..4] of TPdfPoint, um array baseado em 1, e isso confunde qualquer pessoa cujos dedos usem por padrão a indexação baseada em zero. Se você escrever A.AttachmentPoints[0], o dcc32 do Delphi compilará sem reclamar, porque a verificação de intervalo (range checking) vem desativada por padrão; em tempo de execução, a expressão lê ou grava silenciosamente a memória logo antes do array, que em um registro TPdfAnnotation corresponde a um campo adjacente. Seu destaque ganha um canto inválido ou um campo vizinho é corrompido, e nada acusa o erro. O Free Pascal pegou exatamente esse bug em nossas próprias fontes de demonstração durante a portabilidade para o Lazarus: o FPC realiza verificação de intervalo em tempo de compilação em índices constantes e rejeitou o AttachmentPoints[0..3] sumariamente, que foi como o erro de deslocamento de um (off-by-one) e o bug de Set-versus-Append da biblioteca foram desenterrados juntos
Dois hábitos seguem isso: indexar o quadrilátero de 1 a 4, correspondendo à ordem de cantos descrita no código acima, e compilar seu código de anotações pelo menos uma vez com a verificação de intervalo ativada (seja {$R+} no Delphi ou qualquer compilação FPC), antes de confiar nele. O fato de uma compilação padrão dcc32 passar não prova que os índices estão certos; prova apenas que nada falhou ao acessar a memória que calhou de estar ali
Obtendo coordenadas de quadriláteros a partir de texto real
Retângulos codificados de forma fixa (hardcoded) servem bem para demonstrações, mas os destaques de produção rastreiam glifos reais, e as coordenadas devem vir da geometria da página de texto do PDFium, em vez de adivinhações. As rotinas abordadas em nosso guia de extração de texto com o PDFium Component fornecem caixas delimitadoras (bounding boxes) por caractere no mesmo espaço de coordenadas de página que os quadriláteros usam, de modo que um resultado de pesquisa seja convertido diretamente em pontos de cantos: esquerda do primeiro caractere, direita do último, topo e base a partir das dimensões da linha. Se você estiver gerando o próprio texto e precisar saber onde as linhas cairão antes que existam, o artigo de medição de texto e quebra automática de linha cobre o cálculo dessas extensões antecipadamente
Um limite honesto: o registro TPdfAnnotation carrega um único TQuadrilateralPoint, portanto uma chamada de CreateAnnotation grava um quadrilátero. Uma seleção que abrange três linhas necessita de três quadriláteros, um por linha, conforme a norma §12.5.6.10, e você tem duas maneiras de conseguir isso. A maneira simples é criar uma anotação por linha, o que é renderizado corretamente em todos os lugares e preserva a API no nível do componente. A maneira compacta — uma anotação contendo três quadriláteros — envolve criar a anotação através do componente e depois chamar você mesmo a função exportada FPDFAnnot_AppendAttachmentPoints para o segundo e terceiro quadriláteros, o que funciona precisamente porque o Append cria slots em vez de substituí-los. Não tente obter múltiplos quadriláteros por meio de chamadas consecutivas de SetAttachmentPoints; qualquer índice além da contagem atual apenas retornará falso, pelo mesmo motivo que o índice 0 retornou na anotação nova
Após gravar, verifique em um visualizador real em vez de confiar nos códigos de retorno: abra o arquivo no Acrobat ou em qualquer visualizador baseado no PDFium e confirme se a marcação se posiciona sobre o texto, apresenta-se com a opacidade pretendida e sobrevive a uma viagem de ida e volta de salvamento e recarregamento. Os tipos de anotação, o tratamento de quadriláteros e o gravador com percepção de contagem exibidos aqui fazem todos 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 com o restante da biblioteca