PDF анотацията е речник, прикачен към страницата, а не маркер, нарисуван върху нея. Стандартът ISO 32000-1 §12.5 дефинира около две дузини подтипове, като всеки носи /Subtype, правоъгълник в координати на страницата, набор от флагове и обикновено поток от данни за външния вид (appearance stream), който определя какво всъщност изобразява визуализаторът. Подтиповете не означават едно и също нещо за човека, преглеждащ документа. Маркирането (Highlight) и щрихите с мастило (Ink stroke) са коментари; връзката (Link) е навигация; изскачащият прозорец (Popup) е малкият прозорец, който се отваря при щракване върху лепкава бележка, съхраняван като самостоятелен обект и посочен от родителския елемент. Отговорите са пълни текстови анотации, които препращат към коментара, на който отговарят, чрез запис in-reply-to. Следователно масивът от анотации на ниво страница не е списъкът с коментари на рецензента. Той е плоска структура, съдържаща коментари, връзките между тях и няколко елемента, които никой рецензент не би нарекъл коментар. Панел, който третира масива като списък с коментари, ще показва данни, различни от тези във всеки друг визуализатор, който клиентът използва
Изграждането на работен процес за преглед на анотации с PDFium Component (базираният на PDFium Component/LCL компонент за Delphi, C++Builder и Lazarus) означава концентриране върху точките, където разликата между необработения масив и човешкия изглед създава проблеми: броене, индексиране, преоцветяване на маркери, които енджинът вече е фиксирал, изтриване без оставяне на остатъчни следи (ghosts) и добавяне на ваши собствени маркери
Защо вашият брой никога не съвпада с панела за коментари на Acrobat
Отворете маркиран договор във вашия визуализатор и в Acrobat един до друг и общият брой рядко ще съвпадне. Acrobat показва филтриран изглед: маркировки, групирани в нишки с отговори, изскачащи прозорци, сгънати в бележките, към които принадлежат, без връзки и графични компоненти на формуляри. Необработеният масив съдържа всичко това без разграничение, така че простото преброяване едновременно завишава броя в някои аспекти и го занижава в други
Изскачащите прозорци (Popups) раздуват общия брой, тъй като всяка лепкава бележка се доставя с отделен обект Popup и броенето на двата елемента удвоява бележката. Отговорите намаляват броя, ако филтрирате по видими маркери, тъй като отговорът е текстова анотация без нищо нарисувано, докато някой не разгъне нишката, а пропускането му губи дискусията. Флаговете 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];
// Keep reviewer-relevant subtypes only; record the page and
// index pair because all later edits are addressed by it
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 и изчертаването използват различна математика за мащаб и ротация; насочете и трите през едно преобразуване, за да останат маркировката, редът й в списъка и целта за щракване върху едно и също място
Преоцветяване на маркирането и ограничението на appearance-stream
Промяната на маркиране от жълто в кехлибарено звучи като проста задача от един ред, и понякога наистина е така. Уловката е в стандарта ISO 32000-1 §12.5.5. Когато една анотация носи /AP поток за външен вид (appearance stream), съвместимият визуализатор изчертава този предварително изграден поток и третира записа за цвят в речника като неактивни метаданни. Acrobat записва потоци за външен вид за почти всичко, което създава, така че повечето анотации, идващи от клиенти, вече са в това състояние и цветът, който задавате, никога не се показва на екрана. Преоцветяването е процес на четене-промяна-запис чрез свойството Annotation[], а компонентът сигнализира за този конфликт: когато енджинът откаже да позволи на цвета от речника да замени вградения външен вид, записът предизвиква изключение EPdfError
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF; // amber
A.ColorAlpha := 160;
try
Pdf.Annotation[Item.Index] := A;
except
on EPdfError do
begin
// The annotation owns a pre-rendered /AP stream; the dictionary
// color alone cannot change what viewers paint
Item.AppearanceLocked := True;
StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
end;
end;
Прихващайте това изключение всеки път и го третирайте като информация, а не като грешка. Ако пропуснете тази защита, вашият панел ще показва кехлибарен цвят в своя списък, докато страницата продължава да се изобразява в жълто; седмици по-късно потребителят ще изпрати оплакване, че визуализаторът ви игнорира редакциите му, а вие ще прекарате цял следобед в неуспешни опити да възпроизведете проблема с файл, който случайно няма поток за външен вид. След като разберете, че външният вид е заключен, имате два правилни начина за реакция: преоцветете собствения си слой за селекция вместо анотацията, така че рецензентът поне да вижда избрания от него цвят, или маркирайте реда като заключен (appearance-locked), за да не очаква никой промяната да се запази
Изтриване на анотации без оставяне на остатъчни следи (ghosts)
DeleteAnnotation премахва обекта от дървото на анотациите на текущата страница, но оставя кеширания растер на страницата непроменен. Ако извършите изчертаване веднага след повикването, изтритото маркиране все още ще бъде на екрана, намирайки се в растерно изображение, което вече не съответства на модела на документа зад него. Решението е да третирате повторното изчертаване като част от изтриването, а не като стъпка, която извикващият може да забрави:
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index); // raises EPdfError on failure
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
PaintPageBitmap(Bmp);
finally
Bmp.Free; // RenderPage hands bitmap ownership to the caller
end;
RebuildPageEntries(Item.PageNo); // indices after Item.Index shifted
Два детайла в този блок лесно могат да бъдат сгрешени. Опцията reAnnotations трябва да присъства, в противен случай новият растер ще изгуби всички останали анотации и страницата ще изглежда така, сякаш сте изтрили целия набор от коментари вместо само един маркер. Освен това Bmp.Free е задължително: функцията RenderPage прехвърля собствеността върху растерното изображение на извикващия код, така че липсата на освобождаване изтича растер на цяла страница при всяко изтриване, което при работа с дълъг документ бързо ще доведе до сериозен разход на памет
Добавяне на рецензентски маркери от вашия собствен потребителски интерфейс
Създаването на анотации преминава през CreateAnnotation, която приема попълнен запис TPdfAnnotation (подтип, правоъгълник, цвят, съдържание, автор) и го прикачва към текущата страница. Лепкавата бележка с подтип anText е лесният случай: задавате позицията, съдържанието и автора и сте готови. Анотациите с мастило (Ink) са мястото, където разработчиците срещат трудности. Правоъгълникът на записа само ограничава чертежа; самите щрихи са масиви от точки, които трябва да бъдат прикачени отделно чрез повикване на функцията за щрихи с мастило на енджина FPDFAnnot_AddInkStroke с данни FS_POINTF, заснети от мишката или писалката щрих по щрих. Ако изградите анотация с мастило само от правоъгълник, ще получите празна драсканица, която се рендерира като празно пространство, което изглежда като бъг в енджина, а всъщност е наполовина завършена анотация
Решете политиката за авторство веднага. Всеки маркер, кито вашият интерфейс създава, трябва да носи постоянен AuthorText, тъй като филтърът за рецензенти, който ще изградите следващия месец, е толкова добър, колкото са имената, които поставяте върху коментарите днес. Празни или непоследователни авторски низове не могат да бъдат коригирани със задна дата, без да отваряте отново всеки файл
Експортиране на прегледа извън визуализатора
Данните от прегледа стават полезни, когато могат да напуснат визуализатора под формата на резюме, което ръководителят на проекта чете без да отваря файла, или на CSV файл за проследяване. Експортирайте от индекса, който вече сте изградили, а не от нов анализ, и изберете стабилен начин за препратка към всеки маркер. Номерът на страницата, съчетан с правоъгълника на анотацията, оцелява при промени, при които индексът на масива се губи, тъй като следващото изтриване тихо преномерира индексите и вашият CSV файл започва да сочи към грешните коментари
Редът, който си струва да се запази, съдържа страницата, подтипа, автора, времевото клеймо за създаване (когдато е налично във файла), текста на съдържанието и колона за статус, която вие управлявате, а не предоставена от PDF документа. Същият индексен обход е полезен и по-рано, при приемане на документи, когато документът пристига отвън и искате да разберете какво съдържа, преди някой да го прегледа. Статията за работната среда за приемане на PDF описва това сортиране, а статията за навигацията в полета на формуляри разглежда огледалния проблем: преглед на документи, създадени за събиране на данни, а не за коментари
Един случай, който масивът няма да ви покаже
Един от сценариите за неуспех заслужава да бъде отбелязан, тъй като изглежда като дефект във вашия код, но не е. Потребител съобщава за видими маркирания по цялата страница, но вашият панел не показва нищо, а AnnotationCount връща нула. Обичайното обяснение е, че маркиранията са били сляти (flattened) някъде по веригата. Сливането вгражда външния вид на анотациите в обикновеното съдържание на страницата, така че маркиранията стават част от графиките на страницата и спират да съществуват като обекти на анотации. Не остава нищо, което API за анотации да изброи, преоцвети или изтрие. Когато видите изрисувано маркиране при нулев брой, спрете да търсите грешка във вашия цикъл за изброяване и попитайте как е бил създаден файлът
Приложният интерфейс за анотации, използван тук (от изброяване и създаване до преоцветяване, изтриване и опции за рендиране, които поддържат дисплея верен), се доставя с PDFium Component за Delphi, C++Builder, and Lazarus/FPC