PDF anotacija je rečnik (dictionary) pridružen stranici, a ne oznaka nacrtana na njoj. ISO 32000-1 §12.5 definiše otprilike dvadesetak podtipova, od kojih svaki sadrži /Subtype, pravougaonik u koordinatama stranice, skup zastavica (flags) i obično tok izgleda (appearance stream) koji određuje šta čitač zapravo iscrtava. Podtipovi ne znače svi istu stvar osobi koja pregleda dokument. Isticanje (Highlight) i potez mastilom (Ink stroke) su komentari; veza (Link) je navigacija; iskačući prozor (Popup) je mali prozor koji se otvara kada kliknete na lepljivu belešku, sačuvan kao sopstveni objekat na koji ukazuje roditeljski objekat. Odgovori su pune tekstualne anotacije (Text annotations) koje referenciraju komentar na koji odgovaraju preko in-reply-to unosa. Dakle, niz anotacija na nivou stranice nije lista komentara za pregledača. To je obična ravna vreća koja sadrži komentare, veze koje ih povezuju i nekoliko stvari koje nijedan pregledač uopšte ne bi nazvao komentarom. Panel koji tretira ovaj niz kao listu komentara razlikovaće se od svakog drugog pregledača koji klijent koristi.
Izgradnja toka rada za pregled anotacija na PDFium komponenti, VCL/LCL komponenti zasnovanoj na PDFium-u za Delphi, C++Builder i Lazarus, znači fokusiranje na tačke gde taj jaz između sirovog niza i ljudskog prikaza izaziva probleme: brojanje, indeksiranje, ponovno bojenje oznaka koje je motor već zamrzao, brisanje bez ostavljanja „duhova“ i dodavanje sopstvenih oznaka.
Zašto se vaš broj nikada ne poklapa sa Acrobat-ovim oknom za komentare
Otvorite ugovor sa oznakama u svom čitaču i u Acrobat-u uporedo i ukupni brojevi će se retko poklapati. Acrobat prikazuje filtrirani prikaz: oznake grupisane u niti odgovora, iskačući prozori sklopljeni u beleške kojima pripadaju, dok su veze i vidžeti obrazaca izostavljeni. Sirovi niz sadrži sve to bez razlike, tako da jednostavno brojanje može biti preveliko na jedan način i premalo na drugi u isto vreme.
Iskačući prozori (Popups) povećavaju ukupan broj, jer svaka lepljiva beleška dolazi sa zasebnim Popup objektom, pa brojanje oba objekta duplira belešku. Odgovori ga smanjuju ako filtrirate vidljive oznake, pošto je odgovor tekstualna anotacija bez iscrtanog sadržaja dok neko ne proširi nit, a njegovo izostavljanje gubi diskusiju. Zastavice Hidden i NoView uklanjaju anotaciju sa ekrana bez uklanjanja iz niza, tako da brojanje koje zanemaruje zastavice uključuje oznake koje korisnik ne može da vidi. Anotacije veza (Link annotations) se nalaze u istom nizu kao i komentari i ne pripadaju ni broju ni listi. Odlučite o pravilu brojanja pre nego što napišete petlju i zapišite tu odluku, jer je pitanje „zašto vaš panel prikazuje drugačiji broj od Acrobat-a“ prva prijava kvara koju funkcija pregleda dobija.
Indeksirajte sve jednom, a zatim nikada više ne parsirajte stranicu
Jedno pravilo dizajna pokreće sve što sledi: filtriranje po autoru, tipu ili stranici nikada ne sme ponovo parsirati objekte stranice. Na dokumentu od 300 stranica sa obimnim oznakama, ponovno parsiranje pri svakoj promeni padajućeg menija pretvara panel u nešto što koči i zastajkuje sekundama. Komponenta izlaže AnnotationCount i indeksirano svojstvo Annotation[], oba u opsegu trenutno učitane stranice, a zapis TPdfAnnotation koji vraćaju nosi ono što je potrebno prikazu liste: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Pravi korak je da prođete kroz svaku stranicu jednom prilikom otvaranja i zadržite sopstveni ravni 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 koji vredi podvući jeste (PageNo, i). Svaka kasnija izmena, bilo promena boje ili brisanje, adresira se brojem stranice i indeksom anotacije, a indeks je osetljiv: uklanjanje anotacije menja brojeve svega što sledi iza nje na toj stranici. Zato planirajte da ponovo izgradite unose pogođene stranice nakon bilo kog brisanja, umesto da prepravljate indeksne brojeve na licu mesta. Ponovna izgradnja traje milisekund. Nasuprot tome, zastareli indeks briše komentar pogrešnog pregledača, što je vrsta greške koja narušava poverenje u celu funkciju.
Grupisanje po nitima (threading) zaslužuje mesto u indeksu čak i ako vaše prvo izdanje samo broji odgovore umesto da ih prikazuje. Grupišite stavke prema njihovoj roditeljskoj referenci dok vam je stranica otvorena, kako bi panel kasnije mogao da sklopi nit na način na koji to radi Acrobat. Naknadno rekonstruisanje tog grupisanja tokom skrolovanja obesmišljava čitavu poentu jednokratnog indeksiranja, jer ponovo otvara stranice čije ste parsiranje već platili. Geometrija zahteva istu disciplinu. Rectangle u svakom zapisu je u prostoru stranice, a pretvaranje u koordinate prikaza pripada jednom zajedničkom pomoćniku (helper), a ne raspoređeno kroz kod. Paneli dobijaju greške u koordinatama kada selekcija, testiranje pogotka (hit-testing) i iscrtavanje svaki za sebe izmišljaju sopstvenu matematiku zumiranja i rotacije; usmerite sva tri kroz jednu konverziju i isticanje, njegov red u listi i njegova meta za klik ostaju vezani za isto mastilo.
Ponovno bojenje oznaka i veto toka izgleda
Promena isticanja sa žute na ćilibar boju zvuči kao jedna linija koda, i ponekad jeste. Ali caka je u ISO 32000-1 §12.5.5. Kada anotacija nosi tok izgleda /AP, kompatibilni čitač iscrtava taj unapred izgrađeni tok i tretira unos boje u rečniku kao neaktivne metapodatke. Acrobat upisuje tokove izgleda za gotovo sve što kreira, tako da je većina anotacija koje stižu od klijenata već u ovom stanju, a boja koju ste tako samouvereno postavili nikada ne stiže do ekrana. Ponovno bojenje je proces čitanja, izmene i upisa (read-modify-write) preko svojstva Annotation[], a komponenta je iskrena u vezi sa ovim konfliktom: kada motor odbije da dozvoli da boja rečnika pregazi ugrađeni izgled, upis podiže izuzetak 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;
Uhvatite taj izuzetak svaki put i tretirajte ga kao informaciju, a ne kao neuspeh. Preskočite zaštitu i vaš panel će veselo prikazivati ćilibar boju u sopstvenoj listi dok stranica nastavlja da se iscrtava žutom bojom; korisnik će to prijaviti nedeljama kasnije sa tekstom „vaš pregledač ignoriše moje izmene“, a vi ćete provesti popodne bezuspešno pokušavajući da to reprodukujete na datoteci koja slučajno nema tok izgleda. Kada saznate da je izgled zaključan, imate dva ispravna odgovora: obojite sopstveni sloj selekcije umesto anotacije, tako da pregledač barem vidi isticanje koje je izabrao, ili označite red kao zaključan za izgled tako da niko ne očekuje da promena uspe.
Brisanje anotacija bez ostavljanja duhova
DeleteAnnotation uklanja objekat iz stabla anotacija trenutne stranice, ali ostavlja keširani raster stranice nepromenjenim. Ako iscrtate stranicu odmah nakon poziva, obrisano isticanje je i dalje na ekranu, nalazeći se u bitmapi koja više ne odgovara modelu dokumenta iza nje. Rešenje je da tretirate ponovno iscrtavanje kao deo brisanja, a ne kao korak koji pozivalac može zaboraviti:
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
Dva detalja u tom bloku je lako pogrešiti. Opcija reAnnotations mora biti prisutna, inače novi raster odbacuje svaku preostalu anotaciju i stranica izgleda kao da ste obrisali ceo skup komentara umesto jedne oznake. Pored toga, Bmp.Free nije opciono: preopterećena funkcija RenderPage predaje vlasništvo nad bitmapom pozivaocu, tako da izostavljanje oslobađanja memorije dovodi do curenja rastera cele stranice pri svakom pojedinačnom brisanju, što će pregledač koji radi na dugačkom dokumentu pretvoriti u ozbiljan pritisak na memoriju u roku od nekoliko minuta.
Dodavanje oznaka pregledača iz sopstvenog korisničkog interfejsa
Kreiranje anotacija ide preko CreateAnnotation, što uzima popunjeni zapis TPdfAnnotation (podtip, pravougaonik, boja, sadržaj, autor) i povezuje ga sa trenutnom stranicom. Lepljiva beleška, podtip anText, jeste lakši slučaj: podesite poziciju, sadržaj i autora i gotovi ste. Anotacije mastilom (Ink annotations) su mesto gde se ljudi sapletu. Pravougaonik zapisa samo ograničava crtež; sami potezi su nizovi tačaka koje se moraju posebno priložiti preko poziva za potez mastilom FPDFAnnot_AddInkStroke sa podacima FS_POINTF, prikupljenim sa miša ili olovke jedan po jedan potez. Izgradite anotaciju mastilom iz pravougaonika i ničeg drugog i dobićete praznu škrabotinu koja se iscrtava kao prazan prostor, što izgleda kao greška u motoru, a zapravo je polovično završena anotacija.
U istom dahu rešite i politiku autorstva. Svaka oznaka koju vaš korisnički interfejs kreira treba da nosi dosledan AuthorText, jer je filter pregledača koji napravite sledećeg meseca dobar samo onoliko koliko su dobra imena kojima danas pečatirate komentare. Prazni ili nedosledni nizovi autora ne mogu se popraviti retroaktivno bez ponovnog otvaranja svake datoteke.
Izvoženje pregleda iz čitača
Podaci o pregledu opravdavaju svoje postojanje kada mogu da napuste čitač, kao rezime koji vođa projekta čita bez otvaranja datoteke ili CSV koji napaja kontrolni list. Izvoz vršite iz indeksa koji ste već izgradili, nikada iz novog parsiranja, i izaberite stabilan način referenciranja svake oznake. Broj stranice uparen sa pravougaonikom anotacije preživljava promene koje indeks niza ne preživljava, jer sledeće brisanje tiho menja indeksne brojeve i vaš CSV počinje da ukazuje na pogrešne komentare.
Red koji vredi sačuvati sadrži stranicu, podtip, autora, vremenski žig kreiranja kada ga datoteka beleži, tekst sadržaja i kolonu statusa koju sami posedujete, a ne onu koju pruža PDF. Isti prolaz indeksiranja je koristan i ranije, tokom prihvata, kada dokument stigne izvan tima i želite da znate šta je u njemu pre nego što ga bilo ko pregleda. Članak o radnom stolu za prihvat PDF-a prolazi kroz tu trijažu, a navigacija kroz polja obrasca pokriva problem u ogledalu: pregledanje dokumenata napravljenih za prikupljanje podataka umesto komentara.
Jedan slučaj koji vam niz neće prikazati
Jedan režim otkazivanja zaslužuje pažnju jer izgleda kao nedostatak u vašem kodu, a zapravo nije. Korisnik prijavljuje vidljiva isticanja po celoj stranici, ali ih vaš panel uopšte ne prikazuje, a AnnotationCount se vraća kao nula. Uobičajeno objašnjenje je da su oznake negde pre toga spljoštene (flattened). Spljoštavanje peče izglede anotacija u običan sadržaj stranice, tako da isticanja postaju deo grafike stranice i potpuno prestaju da postoje kao objekti anotacija. Ne preostaje ništa što bi API za anotacije mogao da nabroji, ponovo oboji ili obriše. Kada vidite iscrtane oznake sa nultim brojem, prestanite da tražite grešku u svojoj petlji za nabrajanje i pitajte kako je datoteka nastala.
Interfejs za anotacije koji se ovde koristi, od nabrajanja i kreiranja do ponovnog bojenja, brisanja i opcija iscrtavanja koje održavaju tačnost prikaza, dolazi uz PDFium Component za Delphi, C++Builder i Lazarus/FPC.