Компонент 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, і файли, що слідують буквальному формулюванню специфікації, відображаються як згорнуті або перекручені виділення в деяких переглядачах. Структура PDFium FS_QUADPOINTSF кодує саме це правило: (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, починається з нульової кількості, тому шлях створення повинен спочатку викликати Append, і лише подальші оновлення можуть викликати Set
Це зачепило і сам компонент PDFium. До версії v1.79.0 внутрішня процедура, спільна для CreateAnnotation та SetAnnotation, жорстко кодувала виклик FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), що було правильно для оновлення існуючої розмічальної анотації та гарантувало збій для нової, повертаючи виняток EPdfException із повідомленням 'Cannot set attachment points'. Виправлення, випущене у версії v1.79.1, розгалужує виконання залежно від кількості точок
// 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');
Той самий шаблон застосовується, якщо ви викликаєте експортовані функції C безпосередньо, що компонент дозволяє робити, оскільки всі точки входу FPDFAnnot_* доступні в PDFium.pas. Щоразу, коли ви тримаєте дескриптор FPDF_ANNOTATION і хочете записати чотирикутники, спочатку запитайте FPDFAnnot_CountAttachmentPoints та направляйте виклики відповідно. Якщо ви шукаєте відповідь на питання «чому FPDFAnnot_SetAttachmentPoints повертає false», це розгалуження count-then-append майже напевно є вашим рішенням
Створення виділення за допомогою TPdf.CreateAnnotation
Оскільки компонент бере на себе розгалуження між Append та Set, створення виділення зводиться до заповнення запису. Приклад нижче створює сторінку A4 та накладає напівпрозоре жовте виділення на область розміром 200×20 точок; зверніть увагу, що чотирикутник слідує порядку Z, описаному вище, і що Rectangle встановлено так, щоб він оточував чотирикутник, що забезпечує коректну поведінку переглядачів, які перевіряють попадання (hit-test) за 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;
Зміна підтипів коштує одного рядка. anUnderline, anStrikeout та anSquiggly приймають однаковий вигляд запису, включаючи чотирикутники, оскільки стандарт ISO 32000-1 розглядає всі чотири як одну родину анотацій, що відрізняються лише тим, як декорується область чотирикутника. Підтипи, які не є текстовою розміткою, такі як anSquare, anCircle та anText, позиціонуються виключно за Rectangle; залиште для них HasAttachmentPoints у значенні False, і механізм чотирикутників ніколи не запуститься
Чому AttachmentPoints[0] компілюється в Delphi, але зазнає невдачі у FPC?
Тип TQuadrilateralPoint оголошений як array [1..4] of TPdfPoint, тобто як 1-індексований масив, і це збиває з пантелику тих, чиї пальці за звичкою набирають індексацію з нуля. Напишіть A.AttachmentPoints[0], і компілятор dcc32 в Delphi скомпілює це без скарг, оскільки перевірка діапазону за замовчуванням вимкнена; під час виконання вираз тихо зчитує або записує пам'ять безпосередньо перед масивом, що в записі TPdfAnnotation є сусіднім полем. Ваше виділення отримує один сміттєвий кут, або сусіднє поле пошкоджується, і нічого не викликає помилки. Free Pascal виявив саме цю помилку в наших демонстраційних джерелах під час портування на Lazarus: fpc виконує перевірку діапазону на етапі компіляції для константних індексів і відхилив AttachmentPoints[0..3] повністю, завдяки чому помилка зсуву на одиніцю та помилка бібліотеки Set-versus-Append були виявлені разом
Звідси випливають дві звички. Індексуйте чотирикутник від 1 до 4, відповідаючи порядку кутів у наведеному вище коді, і збирайте свій код анотацій принаймні один раз із увімкненою перевіркою діапазону (або {$R+} в Delphi, або будь-яка збірка fpc) перед тим, як довіряти йому. Успішна збірка dcc32 за замовчуванням не є свідченням того, що індекси правильні; це лише свідчення того, що нічого не впало на тій пам'яті, яка там опинилася
Отримання координат чотирикутника з реального тексту
Жорстко закодовані прямокутники підходять для демо, але реальні виділення відстежують реальні гліфи, і координати повинні надходити з геометрії сторінки тексту PDFium, а не з припущень. Процедури, описані в нашому посібнику з вилучення тексту за допомогою компонента PDFium, дають вам обмежувальні рамки символів у тому самому координатному просторі сторінки, який використовують чотирикутники, тому збіг пошуку безпосередньо конвертується в кутові точки: ліворуч від першого символу, праворуч від останнього, верх і низ від меж рядка. Якщо ви самі генеруєте текст і вам потрібно знати, куди впадуть рядки ще до їх створення, стаття про вимірювання тексту та перенесення слів описує обчислення цих меж заздалегідь
Один чесний ліміт: запис TPdfAnnotation містить один TQuadrilateralPoint, тому один виклик CreateAnnotation записує один чотирикутник. Виділення, що охоплює три рядки, потребує трьох чотирикутників (по одному на рядок відповідно до §12.5.6.10), і у вас є два шляхи для цього. Простий шлях — одна анотація на рядок, що скрізь відображається правильно та зберігає API рівня компонента. Компактний шлях — одна анотація, що містить три чотирикутники, означає створення анотації через компонент, а потім самостійний виклик експортованої функції FPDFAnnot_AppendAttachmentPoints для другого та третього чотирикутників, що працює саме тому, що Append створює слоти, а не замінює їх. Не намагайтеся досягти кількох чотирикутників через повторні виклики SetAttachmentPoints; кожен індекс за межами поточного значення просто поверне false з тієї ж причини, що й індекс 0 на новій анотації
Після запису перевірте результат у реальному переглядачі, а не довіряйте кодам повернення: відкрийте файл в Acrobat або будь-якому переглядачі на базі PDFium і переконайтеся, що розмітка лягає на текст, зчитується з потрібною непрозорістю та виживає після циклу збереження та повторного завантаження. Типи анотацій, обробка чотирикутників та чутливий до кількості записувач, показані тут, є частиною стандартного компонента PDFium Component для Delphi, C++Builder та Lazarus; сторінка продукту містить повний довідник API анотацій разом із іншою частиною бібліотеки