Анотація PDF — це словник, прикріплений до сторінки, а не позначка, намальована на ній. ISO 32000-1 §12.5 визначає приблизно два десятки підтипів, і кожен несе /Subtype, прямокутник у координатах сторінки, набір прапорців і зазвичай потік вигляду, який визначає, що саме малює переглядач. Не всі підтипи означають одне й те саме для людини, яка перевіряє документ. Highlight та Ink-штрих — це коментарі; Link — це навігація; Popup — це те маленьке вікно, що відкривається при натисканні на липку нотатку, зберігається як окремий об'єкт, і на нього вказує батьківський елемент. Replies — це повноцінні Text-анотації, які посилаються на коментар, якому відповідають, через запис in-reply-to. Тож масив анотацій рівня сторінки — це не список коментарів рецензента. Це плаский набір, що містить коментарі, з'єднувальну «сантехніку» між ними, і кілька речей, які жоден рецензент узагалі не назвав би коментарем. Панель, яка трактує масив як список коментарів, розходитиметься з кожним іншим переглядачем, який запускає клієнт
Побудова робочого процесу перевірки анотацій на PDFium Component, VCL/LCL-компоненті на основі PDFium для Delphi, C++Builder і Lazarus, означає зосередження на тих точках, де розрив між сирим масивом і людським поглядом спричиняє проблеми: підрахунок, індексування, перефарбування позначок, які рушій уже заморозив, видалення без привидів, що залишаються, і додавання власних позначок
Чому ваш підрахунок ніколи не збігається з панеллю коментарів Acrobat
Відкрийте позначений правками контракт у своєму переглядачі та в Acrobat поруч, і підсумки рідко збігаються. Acrobat показує впорядкований вигляд: розмітку згруповано в гілки відповідей, спливні вікна згорнуто в нотатки, яким вони належать, а посилання та віджети форм відкинуто. Сирий масив містить усе це без розрізнення, тож наївний підрахунок одночасно десь завищений, а десь занижений
Спливні вікна роздувають підсумок, тому що кожна липка нотатка постачається з окремим об'єктом Popup, і підрахунок обох подвоює нотатку. Відповіді занижують підсумок, якщо ви фільтруєте за видимими позначками, оскільки відповідь — це Text-анотація, яка нічого не малює, поки хтось не розгорне гілку, і її відкидання губить обговорення. Прапорці Hidden і NoView прибирають анотацію з екрана, не прибираючи її з масиву, тож підрахунок, сліпий до прапорців, включає позначки, яких користувач не бачить. Анотації Link сидять у тому самому масиві, що й коментарі, і не належать ані до підрахунку, ані до списку. Визначте правило підрахунку до того, як напишете цикл, і запишіть це рішення, бо «чому ваша панель показує інше число, ніж Acrobat» — це перший тікет, який заробляє собі функція перевірки
Проіндексуйте все одразу, а потім більше ніколи не перепарсюйте сторінку
Одне правило проєктування визначає все, що йде далі: фільтрування за автором, типом чи сторінкою ніколи не повинно повторно парсити об'єкти сторінки. На 300-сторінковому документі з насиченою розміткою повторний парсинг при кожній зміні випадного списку перетворює панель на щось, що на секунди зависає. Компонент надає AnnotationCount та індексовану властивість Annotation[], обидві прив'язані до поточно завантаженої сторінки, а запис TPdfAnnotation, який вони повертають, несе те, що потрібно для списку: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Правильний хід — пройти кожну сторінку один раз при відкритті й тримати власний плаский індекс:
procedure TReviewPanel.BuildIndex;
var
PageNo, i: Integer;
A: TPdfAnnotation;
begin
FItems.Clear;
for PageNo := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := PageNo;
for i := 0 to Pdf.AnnotationCount - 1 do
begin
A := Pdf.Annotation[i];
// Залишаємо лише підтипи, важливі для рецензента; фіксуємо сторінку та
// пару індексів, бо всі подальші правки адресуються через неї
if A.Subtype in [anText, anHighlight, anInk] then
FItems.Add(TReviewItem.Create(PageNo, i,
A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
end;
end;
end;
Пара, яку варто підкреслити, — це (PageNo, i). Кожна подальша мутація, будь то перефарбування чи видалення, адресується за номером сторінки плюс індексом анотації, а індекс крихкий: видалення анотації перенумеровує все, що йде за нею на цій сторінці. Тож плануйте перебудову записів ураженої сторінки після кожного видалення, а не латання номерів індексів на місці. Перебудова коштує мілісекунду. Застарілий же індекс видаляє коментар не того рецензента, а це той різновид бага, який підриває довіру до всієї функції
Ланцюжки обговорень заслуговують на місце в індексі, навіть якщо ваш перший реліз лише рахує відповіді, а не показує їх. Групуйте елементи за посиланням на батьківський елемент, поки сторінка відкрита, щоб панель могла пізніше згорнути гілку так, як це робить Acrobat. Ліниве відновлення цього групування під час прокрутки зводить нанівець увесь сенс одноразового індексування, бо це знову відкриває сторінки, які ви вже заплатили за парсинг. Геометрія вимагає тієї самої дисципліни. Rectangle у кожному записі — це простір сторінки, і перетворення його на координати перегляду має жити в одному спільному хелпері, а не бути розкиданим по коду. У панелях заводяться баги координат, коли вибір, hit-testing і малювання кожен вигадує власну математику масштабу й повороту; проведіть усі три через одне єдине перетворення — і виділення, його рядок у списку та ціль кліка залишаться прив'язаними до одного й того самого штриха
Перефарбування розмітки і вето потоку вигляду
Зміна кольору виділення з жовтого на бурштиновий звучить як однорядкова правка, і іноді так і є. Заковика — в ISO 32000-1 §12.5.5. Коли анотація несе потік вигляду /AP, відповідний переглядач малює саме цей заздалегідь побудований потік і трактує запис кольору в словнику як мертві метадані. Acrobat записує потоки вигляду практично для всього, що створює, тож більшість анотацій, які надходять від клієнтів, уже перебувають у цьому стані, і колір, який ви так упевнено встановили, ніколи не досягає екрана. Перефарбування — це read-modify-write через властивість Annotation[], і компонент чесний щодо цього конфлікту: коли рушій відмовляється дозволити кольору зі словника перекрити вже запечений вигляд, запис підіймає EPdfError
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF; // бурштиновий
A.ColorAlpha := 160;
try
Pdf.Annotation[Item.Index] := A;
except
on EPdfError do
begin
// Анотація володіє заздалегідь відрендереним потоком /AP; сам лише
// колір у словнику не може змінити те, що малюють переглядачі
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Ловіть цей виняток щоразу і трактуйте його як інформацію, а не як збій. Пропустіть цю перевірку — і ваша панель бадьоро показуватиме бурштиновий колір у власному списку, поки сторінка й далі малює жовтий; користувач заведе тікет тижнями пізніше як «ваш переглядач ігнорує мої правки», і ви витратите пообіддя, намагаючись відтворити це на файлі, у якому просто немає потоку вигляду. Щойно ви дізналися, що вигляд заблоковано, у вас є дві чесні відповіді: перефарбувати власний оверлей виділення замість самої анотації, щоб рецензент принаймні бачив виділення, яке обрав, або позначити рядок як заблокований за виглядом, щоб ніхто не очікував, що зміна закріпиться
Видалення анотацій без привидів, що залишаються
DeleteAnnotation прибирає об'єкт із дерева анотацій поточної сторінки, але залишає кешований растр сторінки недоторканим. Намалюйте одразу після виклику — і видалене виділення досі на екрані, сидячи в бітмапі, який більше не відповідає моделі документа за ним. Виправлення — трактувати перерендер як частину видалення, а не як крок, про який виклик може забути:
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // підіймає EPdfError у разі помилки
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage передає володіння бітмапом виклику
end;
RebuildPageEntries(Item.PageNo); // індекси після Item.Index зсунуті
Два деталі в цьому блоці легко наплутати. Опція reAnnotations має бути присутньою, інакше новий растр скидає всі інші анотації, і сторінка виглядає так, ніби ви стерли весь набір коментарів замість однієї позначки. А Bmp.Free — не опційний: перевантаження RenderPage у стилі функції передає володіння бітмапом виклику, тож пропущене звільнення пам'яті витікає повносторінковим растром на кожному-єдиному видаленні, а це рецензент, що працює над довгим документом, перетворить на реальний тиск на пам'ять протягом кількох хвилин
Додавання позначок рецензента з власного UI
Створення анотацій відбувається через CreateAnnotation, який бере заповнений запис TPdfAnnotation (підтип, прямокутник, колір, вміст, автор) і прикріплює його до поточної сторінки. Липка нотатка, підтип anText, — це простий випадок: встановіть позицію, вміст і автора — і готово. А ось на анотаціях Ink люди й потрапляють у пастку. Прямокутник запису лише обмежує область малювання; самі штрихи — це масиви точок, які потрібно прикріпити окремо через виклик рушія для чорнильних штрихів, FPDFAnnot_AddInkStroke, якому передають дані FS_POINTF, захоплені з вводу миші чи пера один штрих за раз. Побудуйте Ink-анотацію лише з прямокутника й нічого більше — і отримаєте порожню каракулю, яка рендериться як порожнє місце, що виглядає як баг у рушії, а насправді є недобудованою анотацією
Визначіться з політикою авторства в тому самому подиху. Кожна позначка, яку створює ваш UI, повинна нести узгоджений AuthorText, бо фільтр рецензентів, який ви побудуєте наступного місяця, буде настільки хорошим, наскільки хороші імена, які ви ставите на коментарях сьогодні. Порожні або неузгоджені рядки автора неможливо виправити заднім числом без повторного відкриття кожного файлу
Виведення результатів перевірки з переглядача
Дані перевірки виправдовують себе, щойно можуть покинути переглядач — як зведення, яке керівник проєкту читає без відкриття файлу, або як CSV, що живить трекінгову таблицю. Експортуйте з уже побудованого індексу, ніколи зі свіжого парсингу, і оберіть стабільний спосіб посилатися назад на кожну позначку. Номер сторінки в парі з прямокутником анотації переживає round-trip-и, яких не переживає індекс масиву, бо наступне видалення тихо перенумеровує індекси, і ваш CSV починає вказувати на не ті коментарі
Рядок, який варто зберігати, несе сторінку, підтип, автора, часову мітку створення, якщо файл її фіксує, текст вмісту та стовпець статусу, яким володієте ви, а не той, що постачає сам PDF. Той самий прохід з індексації корисний і раніше, під час прийому, коли документ надходить ззовні команди, і ви хочете знати, що в ньому міститься, перш ніж хтось його перевірить. Стаття про робочий стіл прийому PDF детально розглядає це сортування, а навігація полями форм охоплює дзеркальну проблему: перевірку документів, побудованих для збору даних, а не коментарів
Один випадок, якого масив вам не покаже
Один режим збою заслуговує на окрему позначку, бо він виглядає як дефект у вашому коді, хоч насправді ним не є. Клієнт повідомляє про видимі виділення по всій сторінці, але ваша панель нічого не показує, а AnnotationCount повертає нуль. Звичне пояснення — позначки десь вище за потоком були сплощені. Сплощення запікає вигляд анотацій у звичайний вміст сторінки, тож виділення стають частиною графіки сторінки й повністю перестають існувати як об'єкти анотацій. Для API анотацій просто не залишається нічого, що можна було б перелічити, перефарбувати чи видалити. Коли бачите намальовану розмітку з нульовим підрахунком, перестаньте шукати баг у своєму циклі перелічення й запитайте, як був створений файл
Поверхня роботи з анотаціями, використана тут, — від перелічення й створення до перефарбування, видалення та опцій рендеру, які тримають відображення чесним, — постачається разом із PDFium Component для Delphi, C++Builder і Lazarus/FPC