Компонент PDFium создает текстовые аннотации разметки (выделение, подчеркивание, зачеркивание и волнистая линия) с помощью метода TPdf.CreateAnnotation. Вы устанавливаете значение HasAttachmentPoints := True в записи TPdfAnnotation и заполняете четырехугольник AttachmentPoints, после чего компонент записывает запись QuadPoints, определенную в ISO 32000-1 §12.5.6.10. Это весь доступный API. Причина написания этой статьи кроется в том, что происходит на более низком уровне, так как сырая цепочка вызовов PDFium имеет неприятную особенность: функция FPDFAnnot_SetAttachmentPoints возвращает false для только что созданной аннотации каждый раз без каких-либо кодов ошибок или подсказок. Эта статья дополняет наше руководство по чтению и проверке существующих аннотаций, которое описывает те же структуры в обратном направлении
Сценарий отладки всегда один и тот же. Вы создаете аннотацию выделения, вызываете функцию установки точек привязки с индексом 0, функция возвращает false, и вы начинаете сомневаться в правильности своих координат. Вы меняете местами точки, переворачиваете ось Y, переводите координаты страницы в координаты устройства. Ничего из этого не помогает, потому что дело никогда не было в координатах. Проблема заключается в логике работы с индексами в C API, и как только вы её поймете, решение займет всего две строки
Что означают QuadPoints в ISO 32000-1
Параметр QuadPoints представляет собой массив из 8×n чисел, описывающих n четырехугольников, и стандарт ISO 32000-1 §12.5.6.10 требует его наличия для каждой текстовой разметки: каждый четырехугольник выделяет слово или группу смежных слов, к которым применяется выделение, подчеркивание или зачеркивание. Запись Rect аннотации по-прежнему существует, но для текстовой разметки она лишь ограничивает область; рендерер же рисует именно четырехугольники (квадранты). Четырехугольник используется вместо прямоугольника, так как текст может быть повернут или наклонен, поэтому четыре угла хранятся как четыре независимые точки: x1 y1 x2 y2 x3 y3 x4 y4
Порядок этих четырех точек — место, где расходятся спецификация и реальная практика. Текст спецификации описывает обход точек четырехугольника против часовой стрелки, но собственный рендерер Adobe всегда интерпретировал их в виде Z-образной структуры: сначала верхний край слева направо, затем нижний край слева направо. Поскольку все разработчики тестировали файлы в Acrobat, практически все рендереры, включая PDFium, следуют Z-образному шаблону. Файлы, созданные по буквальному описанию спецификации, в некоторых просмотрщиках отображаются как сжатые или перекрученные области. Структура FS_QUADPOINTSF в PDFium кодирует именно это соглашение: (x1,y1) — верхний левый угол, (x2,y2) — верхний правый, (x3,y3) — нижний левый, (x4,y4) — нижний правый в координатах страницы, где Y растет вверх. Соблюдайте этот порядок, чтобы избежать проблем: рендереры прощают многое, но не перепутанные вершины квадранта
Почему FPDFAnnot_SetAttachmentPoints возвращает false?
Функция FPDFAnnot_SetAttachmentPoints возвращает ошибку для новой аннотации, так как её задача — заменить четырехугольник по указанному индексу, а только что созданная аннотация не содержит четырехугольников для замены. Сигнатура принимает дескриптор аннотации, параметр quad_index и точки. Индекс 0 означает не «создать первую запись при необходимости», а «заменить существующий квадрант под номером 0». Когда функция FPDFAnnot_CountAttachmentPoints возвращает 0, такого квадранта нет, и вызов возвращает false. Для создания слота служит функция FPDFAnnot_AppendAttachmentPoints. Любая аннотация, созданная через FPDFPage_CreateAnnot, изначально имеет счетчик 0, поэтому при создании необходимо сначала вызывать Append, а метод Set использовать только для последующих обновлений
Эта проблема затрагивала и сам компонент PDFium. До версии v1.79.0 внутренняя процедура, общая для CreateAnnotation and SetAnnotation, жестко использовала вызов FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...). Это работало для обновления существующей аннотации разметки, но всегда давало сбой на новых, генерируя исключение EPdfException с текстом «Cannot set attachment points». Решение, выпущенное в версии v1.79.1, выполняет ветвление в зависимости от количества точек
// Внутри писателя аннотаций компонента (v1.79.1+):
// новая аннотация еще не имеет слотов под квадранты, поэтому Append создает
// первый из них; Set лишь заменяет уже существующий слот
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');
Тот же шаблон применим при прямом вызове экспортируемых C-функций, что компонент позволяет делать, поскольку все точки входа FPDFAnnot_* объявлены в PDFium.pas. Каждый раз, когда у вас есть дескриптор FPDF_ANNOTATION and вы хотите записать четырехугольники, сначала проверьте количество с помощью FPDFAnnot_CountAttachmentPoints и выберите нужный путь. Если вы ищете ответ на вопрос «почему FPDFAnnot_SetAttachmentPoints возвращает false», эта проверка со счетчиком и Append — почти наверняка то, что вам нужно
Создание выделения с помощью TPdf.CreateAnnotation
С помощью компонента, автоматически выбирающего метод Append или Set, создание выделения сводится к заполнению записи. Пример ниже создает страницу формата A4 и накладывает полупрозрачное желтое выделение на область размером 200х20 точек. Обратите внимание, что вершины квадранта следуют Z-образному порядку, а Rectangle охватывает всю область квадранта, обеспечивая корректную работу интерактивных функций в просмотрщиках
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% непрозрачности
A.HasAttachmentPoints := True;
A.AttachmentPoints[1].X := 50; A.AttachmentPoints[1].Y := 700; // верхний левый
A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // верхний правый
A.AttachmentPoints[3].X := 50; A.AttachmentPoints[3].Y := 680; // нижний левый
A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // нижний правый
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;
Смена подтипа разметки требует изменения всего одной строки. Подтипы anUnderline, anStrikeout и anSquiggly используют ту же структуру записи, включая квадранты, так как стандарт ISO 32000-1 относит их к одному семейству аннотаций, различающихся только стилем оформления области. Подтипы, не относящиеся к текстовой разметке (например, anSquare, anCircle и anText), позиционируются только по Rectangle. Для них оставьте HasAttachmentPoints в значении False, и механизм обработки квадрантов не будет запускаться
Почему AttachmentPoints[0] компилируется в Delphi, но дает сбой в FPC?
Тип TQuadrilateralPoint объявлен как array [1..4] of TPdfPoint — массив с базой 1, что часто приводит к ошибкам у тех, кто привык к индексации с 0. Если написать A.AttachmentPoints[0], компилятор Delphi dcc32 скомпилирует этот код без предупреждений, так как проверка диапазонов по умолчанию выключена. Во время выполнения выражение считает или запишет данные в область памяти непосредственно перед массивом, что в записи TPdfAnnotation совпадает с соседним полем. Ваше выделение получит одну некорректную координату или повредит соседнее поле без генерации ошибок. Free Pascal перехватил эту ошибку в наших демо-исходниках при переносе на Lazarus: fpc выполняет проверку диапазонов константных индексов на этапе компиляции и сразу отклонил AttachmentPoints[0..3], что помогло выявить и эту ошибку индекса, и проблему Set/Append в библиотеке
Отсюда следуют две привычки: индексируйте вершины квадранта от 1 до 4, соблюдая порядок из примера выше, и хотя бы раз компилируйте код с включенной проверкой диапазонов (директива {$R+} в Delphi или любая сборка в fpc), прежде чем использовать его. Успешная компиляция dcc32 по умолчанию не доказывает корректность индексов — это лишь говорит о том, что программа не упала при обращении к случайному участку памяти
Получение координат квадранта из реального текста
Фиксированные прямоугольники хороши для демонстрации, но в реальных задачах разметка должна накладываться на конкретные символы, а координаты — браться из геометрии текста PDFium. Функции, описанные в нашем руководстве по извлечению текста с помощью компонента PDFium, возвращают координаты ограничивающих рамок для каждого символа в той же системе координат, которую используют квадранты. Таким образом, найденное совпадение напрямую преобразуется в угловые точки: левая граница первого символа, правая граница последнего, верх и низ по высоте строки. Если вы генерируете текст самостоятельно и хотите рассчитать положение строк заранее, обратитесь к статье о измерении текста и переносе слов
Одно важное ограничение: запись TPdfAnnotation содержит одну структуру TQuadrilateralPoint, поэтому один вызов CreateAnnotation записывает один четырехугольник. Выделение, охватывающее три строки, требует трех квадрантов (по одному на строку согласно §12.5.6.10), и реализовать это можно двумя путями. Простой способ — одна аннотация на каждую строку. Это корректно отображается везде и использует стандартный API компонента. Компактный способ — одна аннотация с тремя квадрантами. Для этого аннотация создается через компонент, а для второго и третьего квадрантов вручную вызывается экспортируемая функция FPDFAnnot_AppendAttachmentPoints, которая добавляет новые слоты вместо замены существующих. Не пытайтесь добавить несколько квадрантов через повторные вызовы SetAttachmentPoints: любой индекс, превышающий текущее количество, вернет false по той же причине, что и индекс 0 для новой аннотации
После записи обязательно проверьте результат в реальном просмотрщике, не полагаясь только на коды возврата. Откройте файл в Acrobat или любом просмотрщике на базе PDFium и убедитесь, что разметка точно ложится на текст, имеет нужную прозрачность и сохраняется после цикла записи и повторного открытия. Поддерживаемые типы аннотаций, обработка квадрантов и учитывающий количество писатель, описанные в этой статье, входят в стандартный комплект поставки PDFium Component для Delphi, C++Builder и Lazarus. Страница продукта содержит полный справочник по API аннотаций и возможностям библиотеки