Technical Article

Pregled PDF anotacij v Delphi s komponento PDFium

PDF anotacija je slovar, priložen strani, in ne oznaka, narisana na njej. Standard ISO 32000-1 §12.5 opredeljuje približno dva ducata podtipov, od katerih vsak vsebuje /Subtype, pravokotnik v koordinatah strani, nabor zastavic in običajno tok videza (appearance stream), ki določa, kaj pregledovalnik dejansko izriše. Podtipi osebi, ki pregleduje dokument, ne pomenijo vedno iste stvari. Poudarek (Highlight) in poteza s peresom (Ink) sta komentarja; povezava (Link) je navigacija; pojavno okno (Popup) je majhno okno, ki se odpre, ko kliknete samolepilni listek, in je shranjeno kot lasten objekt ter kaže na starša. Odgovori so polne tekstovne anotacije (Text), ki se sklicujejo na komentar, na katerega odgovarjajo, prek vnosa in-reply-to. Zato polje anotacij na ravni strani ni pregledovalčev seznam komentarjev. Je le ploščata zbirka, ki vsebuje komentarje, povezave med njimi in nekaj stvari, ki jih noben pregledovalnik ne bi označil kot komentar. Plošča, ki obravnava to polje kot seznam komentarjev, se ne bo ujemala z nobenim drugim pregledovalnikom, ki ga uporablja stranka.

Gradnja poteka dela za pregledovanje anotacij s komponento PDFium Component (komponento VCL/LCL na osnovi PDFium za Delphi, C++Builder in Lazarus) pomeni osredotočanje na točke, kjer ta vrzel med surovim poljem in človeškim pogledom povzroča težave: štetje, indeksiranje, spreminjanje barve označb, ki jih je pogon že zamrznil, brisanje brez puščanja fantomskih ostankov in dodajanje lastnih označb.

Zakaj se vaše število nikoli ne ujema z Acrobatovo ploščo s komentarji

Odprite označeno pogodbo v svojem pregledovalniku in v programu Acrobat vzporedno in skupne vsote se bodo redko ujemale. Acrobat prikazuje prilagojen pogled: označbe so združene v niti odgovorov, pojavna okna so zložena pod opombe, katerim pripadajo, povezave in gradniki obrazcev pa so izpuščeni. Surovo polje vsebuje vse to brez razlikovanja, zato je preprosto štetje hkrati v nekaterih pogledih preveliko, v drugih pa premajhno.

Pojavna okna (Popups) povečajo skupno število, saj je vsak samolepilni listek opremljen z ločenim objektom Popup, štetje obeh pa podvoji opombo. Odgovori število zmanjšajo, če filtrirate po vidnih označbah, saj je odgovor tekstovna anotacija (Text) brez izrisanih elementov, dokler nekdo ne razširi niti, z njenim izpuščanjem pa se izgubi celotna razprava. Zastavici Hidden in NoView odstranita anotacijo z zaslona, ne da bi jo odstranili iz polja, zato štetje brez preverjanja teh zastavic vključi tudi označbe, ki jih uporabnik ne vidi. Anotacije povezav (Link) se nahajajo v istem polju kot komentarji, vendar ne sodijo ne v štetje ne na seznam. Določite pravilo štetja, preden napišete zanko, in to odločitev zapišite, saj je vprašanje »zakaj vaša plošča prikazuje drugačno število kot Acrobat« prva težava, s katero se boste srečali pri uvajanju funkcije pregledovanja.

Indeksirajte vse enkrat, nato pa nikoli več ne analizirajte strani znova

Eno oblikovalsko pravilo vodi vse, kar sledi: filtriranje po avtorju, vrsti ali strani ne sme nikoli ponovno analizirati objektov strani. Pri dokumentu s 300 stranmi z obsežnimi označbami bi ponovna analiza ob vsaki spremembi spustnega seznama povzročila večsekundno zatikanje plošče. Komponenta ponuja lastnost AnnotationCount in indeksirano lastnost Annotation[], oboje omejeno na trenutno naloženo stran, zapis TPdfAnnotation, ki ga vrneta, pa vsebuje vse, kar potrebuje pogled seznama: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Pravilna poteza je, da ob odprtju enkrat pregledate vsako stran in ustvarite lasten ploščat indeks:

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;

Par, ki ga je vredno poudariti, je (PageNo, i). Vsaka kasnejša sprememba, bodisi sprememba barve ali brisanje, je naslovljena s številko strani in indeksom anotacije. Ta indeks pa je občutljiv: odstranitev anotacije ponovno oštevilči vse naslednje anotacije na tej strani. Zato po vsakem brisanju načrtujte ponovno izgradnjo vnosov za prizadeto stran, namesto da bi indeksne številke popravljali na mestu samem. Ponovna izgradnja traja le milisekundo. Zastarel indeks pa nasprotno izbriše napačen komentar, kar je vrsta hrošča, ki hitro oslabi zaupanje v celotno aplikacijo.

Združevanje v niti si zasluži mesto v indeksu, tudi če vaša prva izdaja le šteje odgovore, namesto da bi jih prikazovala. Združite elemente po njihovi nadrejeni referenci, ko imate stran odprto, da lahko plošča pozneje zloži nit tako kot Acrobat. Lene (lazy) rekonstrukcije tega združevanja med drsenjem izničijo celoten namen enkratnega indeksiranja, saj ponovno odpirajo strani, katerih analizo ste že plačali. Enako disciplino zahteva tudi geometrija. Rectangle v vsakem zapisu predstavlja prostor strani, pretvorba v koordinate pogleda pa sodi v en skupni pomočnik, ne pa razpršeno po celotni kodi. V ploščah se hitro pojavijo napake pri koordinatah, ko izbiranje, testiranje zadetkov (hit-testing) in risanje uporabljajo lastno matematiko povečave in vrtenja; usmerite vse tri operacije skozi eno pretvorbo in poudarek, njegova vrstica v seznamu ter njegov cilj klika bodo ostali poravnani.

Spreminjanje barve označb in zavrnitev toka videza

Spreminjanje barve poudarka iz rumene v oranžno (amber) se sliši preprosto, in včasih tudi je. Težava pa je v standardu ISO 32000-1 §12.5.5. Ko anotacija vsebuje tok videza /AP (appearance stream), skladen pregledovalnik izriše ta vnaprej zgrajeni tok, barvni vnos v slovarju pa obravnava kot neaktivne metapodatke. Acrobat zapiše tokove videza za skoraj vse, kar ustvari, zato je večina anotacij, ki jih prejmete od strank, že v tem stanju in barva, ki jo nastavite, nikoli ne doseže zaslona. Ponovno barvanje poteka po sistemu branje-sprememba-pisanje prek lastnosti Annotation[], komponenta pa je glede konflikta odkrita: ko pogon zavrne preglasitev vdelanega videza s slovarjevo barvo, pisanje sproži izjemo 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;

To izjemo ulovite vsakič in jo obravnavajte kot informacijo, ne kot napako. Če izpustite to zaščito, bo vaša plošča v svojem seznamu prikazala oranžno barvo, medtem ko bo stran še naprej izrisovala rumeno; uporabnik bo čez nekaj tednov prijavil napako »vaš pregledovalnik prezre moje spremembe«, vi pa boste porabili celo popoldne za neuspešno iskanje napake na datoteki, ki po naključju nima toka videza. Ko veste, da je videz zaklenjen, imate na voljo dva pravilna odziva: spremenite barvo lastnega prekrivnega sloja za izbor namesto anotacije, da pregledovalec vsaj vidi izbrani poudarek, ali pa vrstico označite kot zaklenjeno glede videza, da nihče ne pričakuje uveljavitve spremembe.

Brisanje anotacij brez puščanja fantomskih ostankov

Metoda DeleteAnnotation odstrani objekt iz drevesa anotacij trenutne strani, vendar pusti predpomnjeni raster strani nespremenjen. Če rišete takoj po klicu, bo izbrisani poudarek še vedno na zaslonu v bitni sliki, ki se ne ujema več z modelom dokumenta v ozadju. Rešitev je, da ponovni izris obravnavate kot del brisanja in ne kot korak, ki bi ga klicatelj lahko pozabil:

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

Dve podrobnosti v tem bloku je enostavno spregledati. Možnost `reAnnotations` mora biti prisotna, sicer bo nov raster odstranil vse preostale anotacije in stran bo videti, kot da ste izbrisali celoten nabor komentarjev namesto ene same označbe. Tudi `Bmp.Free` ni neobvezen: preobložena funkcija `RenderPage` prenese lastništvo bitne slike na klicatelja, zato opustitev sprostitve povzroči puščanje rastra celotne strani pri vsakem posameznem brisanju, kar bo pregledovalec pri dolgem dokumentu v nekaj minutah spremenil v resno pomanjkanje pomnilnika.

Dodajanje pregledovalčevih označb iz lastnega uporabniškega vmesnika

Ustvarjanje anotacij poteka prek metode CreateAnnotation, ki sprejme izpolnjen zapis TPdfAnnotation (podtip, pravokotnik, barva, vsebina, avtor) in ga pripne trenutni strani. Samolepilni listek, podtip anText, je enostaven primer: nastavite položaj, vsebino ter avtorja in končali ste. Težave pa nastanejo pri anotacijah s peresom (Ink). Pravokotnik v zapisu določa le meje risanja; same poteze so polja točk, ki jih je treba pripeti ločeno prek klica za poteze s peresom `FPDFAnnot_AddInkStroke` s podatki `FS_POINTF`, zajetimi iz vnosa z miško ali peresom, potezo za potezo. Če zgradite anotacijo s peresom samo iz pravokotnika, dobite prazen zapis, ki se izriše kot prazen prostor, kar je videti kot hrošč v pogonu, v resnici pa gre za napol dokončano anotacijo.

Hkrati določite tudi politiko avtorstva. Vsaka označba, ki jo ustvari vaš uporabniški vmesnik, mora vsebovati dosleden AuthorText, saj bo filter pregledovalcev, ki ga boste zgradili naslednji mesec, le toliko dober, kolikor so dobra imena, ki jih danes vpišete v komentarje. Praznih ali nedoslednih nizov avtorjev ni mogoče popraviti za nazaj brez ponovnega odpiranja vsake datoteke.

Izvoz pregleda iz pregledovalnika

Podatki o pregledu postanejo koristni, ko lahko zapustijo pregledovalnik, bodisi kot povzetek, ki ga vodja projekta prebere brez odpiranja datoteke, ali kot datoteka CSV za sledilno tabelo. Izvozite iz indeksa, ki ste ga že zgradili, nikoli pa iz nove analize, in izberite stabilen način sklicevanja na posamezno označbo. Številka strani v paru s pravokotnikom anotacije preživi spremembe, ki jih indeks polja ne, saj naslednji izbris tiho spremeni indekse, vaša datoteka CSV pa začne kazati na napačne komentarje.

Vrstica, ki jo je vredno obdržati, vsebuje stran, podtip, avtorja, časovni žig ustvarjanja (če ga datoteka vsebuje), besedilo vsebine in stolpec stanja, ki ga upravljate sami, namesto tistega, ki ga ponuja PDF. Isti korak indeksiranja je uporaben že prej, med sprejemom dokumenta, ko datoteka prispe izven ekipe in želite izvedeti, kaj vsebuje, preden jo kdo pregleda. Članek o delovnem okolju za sprejem dokumentov PDF opisuje to razvrščanje, članek o navigaciji po poljih obrazcev pa obravnava zrcalni problem: pregledovanje dokumentov, ki so namenjeni zbiranju podatkov in ne komentarjev.

Primer, ki ga polje ne bo prikazalo

Ena izmed napak si zasluži posebno opozorilo, saj je videti kot napaka v vaši kodi, vendar ni. Stranka poroča o vidnih poudarkih po celotni strani, vendar jih vaša plošča ne navaja, lastnost AnnotationCount pa vrne nič. Običajna razlaga je, da so bile označbe predhodno sploščene (flattened). Sploščitev zapeče videz anotacij v običajno vsebino strani, tako da poudarki postanejo del grafike strani in sploh ne obstajajo več kot objekti anotacij. Za API anotacij ne ostane ničesar več za štetje, spreminjanje barve ali brisanje. Ko vidite izrisane označbe ob števcu nič, prenehajte iskati napako v svoji zanki za štetje in preverite, kako je bila datoteka ustvarjena.

Funkcionalnost za delo z anotacijami, uporabljena tukaj (od štetja in ustvarjanja do spreminjanja barv, brisanja ter možnosti izrisa, ki ohranjajo natančen prikaz), je del paketa PDFium Component za Delphi, C++Builder in Lazarus/FPC.