Technical Article

Преглед на анотации в PDF с Delphi и компонента PDFium

PDF анотацията е речник, прикачен към страницата, а не маркер, нарисуван върху нея. Стандартът ISO 32000-1 §12.5 дефинира около две дузини подтипове, като всеки носи /Subtype, правоъгълник в координати на страницата, набор от флагове и обикновено поток от данни за външния вид (appearance stream), който определя какво всъщност изобразява визуализаторът. Подтиповете не означават едно и също нещо за човека, преглеждащ документа. Маркирането (Highlight) и щрихите с мастило (Ink stroke) са коментари; връзката (Link) е навигация; изскачащият прозорец (Popup) е малкият прозорец, който се отваря при щракване върху лепкава бележка, съхраняван като самостоятелен обект и посочен от родителския елемент. Отговорите са пълни текстови анотации, които препращат към коментара, на който отговарят, чрез запис in-reply-to. Следователно масивът от анотации на ниво страница не е списъкът с коментари на рецензента. Той е плоска структура, съдържаща коментари, връзките между тях и няколко елемента, които никой рецензент не би нарекъл коментар. Панел, който третира масива като списък с коментари, ще показва данни, различни от тези във всеки друг визуализатор, който клиентът използва.

Изграждането на работен процес за преглед на анотации с PDFium Component (базираният на PDFium VCL/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). Всяка следваща промяна, независимо дали е преоцветяване или изтриване, се адресира чрез номера на страницата плюс индекса на анотацията, а индексът е крехък: премахването на анотация преномерира всичко след нея на тази страница. Затова планирайте да възстановите записите на засегнатата страница след всяко изтриване, вместо да коригирате индексните номера на място. Възстановяването отнема милисекунда. За разлика от това, остарелият индекс изтрива коментара на грешния рецензент, което е вид бъг, който срива доверието в цялата функция.

Йерархичното структуриране на нишките (threading) заслужава място в индекса, дори ако първата ви версия само брои отговорите, вместо да ги показва. Групирайте елементите по тяхната родителска препратка, докато страницата е отворена, така че панелът по-късно да може да сгъва нишката по начина, по който го прави Acrobat. Възстановяването на това групиране при превъртане обезсмисля индексирането, тъй като отваря отново страници, които вече са били анализирани.

Геометрията изисква същата дисциплина. Свойството Rectangle във всеки запис е в координатна система на страницата (page-space) и преобразуването му в координати на изгледа трябва да се извършва в един споделен помощен метод, а не да се разпръсква из целия код. Панелите проявяват бъгове с координатите, когато селекцията, тестът за попадение (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.