O PDFium Component cria anotações de marcação de texto — nomeadamente destaque (highlight), sublinhado (underline), rasurado (strikeout) e ondulado (squiggly) — através de TPdf.CreateAnnotation: define-se HasAttachmentPoints := True no registo TPdfAnnotation e preenche-se o seu quadrilátero AttachmentPoints, fazendo com que o componente grave a entrada QuadPoints conforme definido na norma ISO 32000-1 §12.5.6.10. Esta é toda a superfície da API. O motivo de ser deste artigo prende-se com o que ocorre nos bastidores, uma vez que a cadeia de chamadas diretas do PDFium possui um modo de falha que gera o sintoma menos útil de todo o conjunto de ferramentas: FPDFAnnot_SetAttachmentPoints devolve falso em anotações recém-criadas, invariavelmente, sem qualquer código de erro ou indicação. Este constitui o artigo complementar focado na criação face ao nosso artigo sobre leitura e revisão de anotações existentes, que percorre o sentido inverso através das mesmas estruturas
O cenário de depuração é sempre idêntico. Cria uma anotação de destaque, invoca o método de definição dos attachment-points com o índice 0, a função devolve falso, e começa a duvidar das suas coordenadas. Transpõe os pontos, inverte o eixo Y, troca o espaço da página pelo espaço do dispositivo. Nada disso resulta, porque as coordenadas nunca foram o problema. O problema reside na semântica de indexação da API C e, uma vez compreendida, a solução resume-se a duas linhas
O que significam os QuadPoints na norma ISO 32000-1
QuadPoints constitui um array de 8×n números que descrevem n quadrilaterais, sendo exigido pela secção §12.5.6.10 da ISO 32000-1 em todas as anotações de marcação de texto: cada quadrilátero delimita uma palavra ou grupo de palavras contíguas às quais o destaque, sublinhado ou rasurado se aplica. A entrada Rect da anotação continua a existir, mas nos subtipos de marcação apenas limita a região; os quadrilateros (quads) definem o que o renderizador realmente pinta. Opta-se por um quadrilátero em vez de um retângulo porque o texto pode estar rodado ou inclinado, pelo que os quatro cantos são armazenados como quatro pontos independentes: x1 y1 x2 y2 x3 y3 x4 y4
A ordenação desses quatro pontos constitui o ponto em que a especificação e a base instalada divergem. O texto da especificação descreve os pontos traçando o quadrilátero no sentido contrário ao dos ponteiros do relógio, mas o renderizador da própria Adobe sempre os interpretou num padrão em Z: primeiro a aresta superior da esquerda para a direita, depois a aresta inferior da esquerda para a direita. Dado que todos os autores realizavam testes com o Acrobat, praticamente todos os renderizadores, incluindo o PDFium, seguem o padrão em Z, e ficheiros que sigam a formulação literal da especificação aparecem como destaques colapsados ou distorcidos em alguns visualizadores. A estrutura FS_QUADPOINTSF do PDFium codifica exatamente esta convenção: (x1,y1) corresponde ao canto superior esquerdo, (x2,y2) ao superior direito, (x3,y3) ao inferior esquerdo, (x4,y4) ao inferior direito, em coordenadas de página onde o Y cresce para cima. Siga essa ordenação e o problema fica resolvido; os renderizadores toleram muitas falhas, mas um quadrilátero desordenado não é uma delas
Por que razão FPDFAnnot_SetAttachmentPoints devolve falso?
O método FPDFAnnot_SetAttachmentPoints falha numa nova anotação porque o seu contrato visa substituir o quadrilátero num 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 espaço, criando-o se necessário", significa "o quadrilátero 0 existente", e quando a FPDFAnnot_CountAttachmentPoints reporta 0, esse quadrilátero não existe e a chamada devolve falso. A função que cria um espaço é a FPDFAnnot_AppendAttachmentPoints. Cada anotação criada através de FPDFPage_CreateAnnot inicia-se com uma contagem de zero, pelo que o percurso de criação deve invocar a Append primeiro, e apenas as atualizações subsequentes podem chamar a Set
Isto afetou o próprio PDFium Component. Até à versão v1.79.0, a rotina interna partilhada pela CreateAnnotation e pela SetAnnotation definia de forma fixa FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), o que se revelava correto para atualizar uma anotação de marcação existente, mas falhava garantidamente com uma nova, surgindo como uma exceção EPdfException com a mensagem 'Cannot set attachment points'. A correção, disponibilizada na v1.79.1, ramifica a lógica 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');
Criar um destaque com TPdf.CreateAnnotation
Com o componente a gerir o encaminhamento entre Append e Set por si, criar um destaque resume-se ao preenchimento de um registo. O exemplo seguinte cria uma página A4 e insere um destaque amarelo semitransparente sobre uma região de 200×20 pontos; note que o quadrilátero segue o padrão em Z descrito acima, e que a propriedade Rectangle é definida para abranger o quadrilátero, garantindo um comportamento coerente 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;
A alteração de subtipos requer apenas uma linha. Os tipos anUnderline, anStrikeout e anSquiggly utilizam a mesma estrutura de registo, incluindo os quadriláteros, porque a norma ISO 32000-1 trata os quatro como pertencendo à mesma família de anotações, diferindo unicamente na forma como a região do quadrilátero é desenhada. Os subtipos que não consistem em marcações de texto, tais como anSquare, anCircle e anText, posicionam-se apenas a partir da propriedade Rectangle; mantenha a HasAttachmentPoints como False para esses tipos e os mecanismos de quadrilátero nunca serão executados
Por que razão AttachmentPoints[0] compila no Delphi mas falha no FPC?
O tipo TQuadrilateralPoint é declarado como array [1..4] of TPdfPoint, um array baseado em 1, o que costuma induzir em erro quem recorre por hábito a indexações baseadas em zero. Se escrever A.AttachmentPoints[0], o dcc32 do Delphi efetuará a compilação sem qualquer aviso, pois a verificação de limites (range checking) encontra-se desativada por padrão; em tempo de execução, a expressão lê ou escreve silenciosamente na memória localizada logo antes do array, correspondendo a um campo adjacente no registo TPdfAnnotation. O seu destaque ficará com um canto corrompido ou um campo vizinho será adulterado, sem que qualquer erro seja gerado. O Free Pascal detetou precisamente este erro nos códigos de demonstração originais durante a conversão para o Lazarus: o fpc executa verificações de limites em tempo de compilação em índices constantes, rejeitando de imediato a expressão AttachmentPoints[0..3], o que permitiu identificar simultaneamente o desvio de índice e a falha de Set-versus-Append na biblioteca
Daqui resultam dois hábitos essenciais. Indexe o quadrilátero de 1 a 4, respeitando a ordenação dos cantos no código apresentado acima, e compile o seu código de anotações pelo menos uma vez com a verificação de limites ativa — recorrendo a {$R+} no Delphi ou em qualquer build do fpc — antes de confiar nos resultados. Uma compilação bem-sucedida padrão no dcc32 não comprova que os índices se encontrem corretos; apenas demonstra que nada falhou na memória que por acaso ali se situava
Obter coordenadas de quadrilátero a partir de texto real
Retângulos fixos no código servem para fins de demonstração, mas os destaques em ambiente de produção contornam glifos reais, pelo que as coordenadas devem provir da geometria da página de texto do PDFium em vez de aproximações. As rotinas detalhadas no nosso guia de extração de texto com o PDFium Component fornecem caixas delimitadoras (bounding boxes) por carácter no mesmo espaço de coordenadas de página que os quadriláteros utilizam, de forma a que uma ocorrência de pesquisa se converta diretamente nos cantos: à esquerda do primeiro carácter, à direita do último, com o topo e a base definidos pela extensão da linha. Se estiver a gerar o texto e necessitar de saber onde as linhas se situarão antes de existirem, o artigo sobre medição de texto e moldagem de palavras aborda como calcular essas extensões antecipadamente
Um limite realista: o registo TPdfAnnotation comporta um único TQuadrilateralPoint, pelo que uma chamada de CreateAnnotation grava um único quadrilátero. Uma seleção que abranja três linhas necessita de três quadriláteros, um por linha, de acordo com o parágrafo §12.5.6.10, existindo duas formas de o conseguir. O caminho simples consiste em criar uma anotação por linha, o que se processa de forma correta em qualquer visualizador e preserva a API ao nível do componente. O caminho compacto — uma única anotação que suporta três quadriláteros — exige criar a anotação através do componente e depois invocar diretamente a função exportada FPDFAnnot_AppendAttachmentPoints para o segundo e terceiro quadriláteros, tirando partido do facto de a Append criar novos espaços em vez de os substituir. Não tente obter múltiplos quadriláteros através de chamadas repetidas da SetAttachmentPoints; qualquer índice superior à contagem atual devolverá simplesmente falso, pelo mesmo motivo que o índice 0 falhou na anotação inicial
Após a gravação, proceda à verificação num visualizador real em vez de confiar apenas nos códigos de retorno: abra o ficheiro no Acrobat ou em qualquer visualizador baseado em PDFium e confirme se a marcação se alinha com o texto, exibe a opacidade pretendida e sobrevive a um ciclo de gravação e recarregamento. Os tipos de anotação, o tratamento de quadriláteros e o escritor ciente de contagens aqui exemplificados fazem todos parte do pacote padrão PDFium Component para Delphi, C++Builder e Lazarus; a página do produto disponibiliza a referência completa da API de anotações juntamente com o restante conteúdo da biblioteca