Technický článek

Kontrola anotací PDF v Delphi pomocí komponenty PDFium

Anotace PDF je slovník připojený ke stránce, ne značka na ni namalovaná. ISO 32000-1 §12.5 definuje zhruba dva tucty podtypů, a každý nese /Subtype, obdélník v souřadnicích stránky, sadu příznaků a obvykle appearance stream, který rozhoduje o tom, co prohlížečka skutečně vykreslí. Podtypy neznamenají pro člověka, který dokument recenzuje, všechny totéž. Highlight a tah Ink jsou komentáře; Link je navigace; Popup je malé okénko, které se otevře po kliknutí na lepicí poznámku, uložené jako vlastní objekt, na který ukazuje rodič. Odpovědi jsou plnohodnotné anotace typu Text, které odkazují na komentář, na který reagují, přes položku in-reply-to. Pole anotací na úrovni stránky tedy není recenzentův seznam komentářů. Je to plochý pytel obsahující komentáře, instalatérskou práci, která je propojuje, a několik věcí, které by žádný recenzent komentářem vůbec nenazval. Panel, který zachází s polem jako se seznamem komentářů, se rozejde s každou jinou prohlížečkou, kterou zákazník spustí

Stavba workflow pro recenzi anotací na PDFium Component, komponentě VCL/LCL postavené na PDFiu pro Delphi, C++Builder a Lazarus, znamená soustředit se na místa, kde tato mezera mezi syrovým polem a lidským pohledem způsobuje potíže: počítání, indexování, přebarvování značek, které engine už zamrzl, mazání beze zanechání přízraků a přidávání vlastních značek

Diagram ukazující, jak revizní panel PDFium v Delphi profiltruje surové pole anotací strany z komentářů, popupů, odpovědí a odkazů na kurovaný seznam komentářů, který recenzent vidí
Pole anotací stránky míchá komentáře s popupy, odpověďmi, odkazy i skrytými značkami, takže revizní panel potřebuje pravidlo počítání, dřív než ukáže celkem

Proč se váš počet nikdy neshoduje s panelem komentářů v Acrobatu

Otevřete okomentovanou smlouvu ve své prohlížečce a v Acrobatu vedle sebe a součty se málokdy shodnou. Acrobat ukazuje kurátorský pohled: značky seskupené do vláken odpovědí, popupy sbalené do poznámek, ke kterým patří, odkazy a widgety formulářů vynechané. Syrové pole drží všechno nerozlišené, takže naivní počítání je zároveň v jednom směru vysoké a v druhém nízké

Popupy nafukují celkový součet, protože každá lepicí poznámka přichází se samostatným objektem Popup a počítání obojího zdvojí poznámku. Odpovědi součet sníží, pokud filtrujete jen viditelné značky, protože odpověď je anotace typu Text, u které se nic nevykreslí, dokud někdo vlákno nerozbalí, a jejím vynecháním ztratíte diskuzi. Příznaky Hidden a NoView sundají anotaci z obrazovky, aniž by ji vyndaly z pole, takže počítání slepé k příznakům zahrnuje značky, které uživatel nevidí. Anotace typu Link sedí ve stejném poli jako komentáře a nepatří ani do počtu, ani do seznamu. Rozhodněte pravidlo počítání dřív, než napíšete smyčku, a to rozhodnutí si zapište, protože „proč váš panel ukazuje jiné číslo než Acrobat" je první tiket, který si funkce recenze vyslouží

Vše zaindexujte jednou, pak stránku už nikdy neparsujte znovu

Jedno návrhové pravidlo řídí vše, co následuje: filtrování podle autora, typu nebo stránky nesmí nikdy znovu parsovat objekty stránky. U 300stránkového dokumentu s hustým označkováním by opětovné parsování při každé změně dropdownu proměnilo panel v něco, co se na několik vteřin sekaně zasekává. Komponenta vystavuje AnnotationCount a indexovanou vlastnost Annotation[], obě navázané na aktuálně načtenou stránku, a záznam TPdfAnnotation, který vrací, nese to, co potřebuje seznamový pohled: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Správný tah je jednou při otevření projet každou stránku a udržovat si vlastní plochý index:

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];
      // Ponechat jen podtypy relevantní pro reviewera; zaznamenat stránku a
      // pár index, protože všechny pozdější úpravy se adresují podle něj
      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;

Dvojice, kterou stojí za to podtrhnout, je (PageNo, i). Každá pozdější mutace, ať přebarvení nebo smazání, se adresuje číslem stránky plus indexem anotace, a index je křehký: odebrání anotace přečísluje vše po ní na téže stránce. Naplánujte si tedy po každém smazání přestavbu záznamů dotčené stránky, místo abyste čísla indexů opravovali na místě. Přestavba stojí milisekundu. Zastaralý index naproti tomu smaže komentář špatného recenzenta, což je typ chyby, který podkopává důvěru v celou funkci

Vlákna si zaslouží místo v indexu, i když první verze odpovědi jen počítá, místo aby je zobrazovala. Seskupte položky podle jejich reference na rodiče, dokud máte stránku otevřenou, aby panel později dokázal sbalit vlákno tak, jak to dělá Acrobat. Líné rekonstruování tohoto seskupení během scrollování popírá celý smysl jednorázového indexování, protože znovu otevírá stránky, za jejichž parsování jste už zaplatili. Geometrie chce stejnou disciplínu. Rectangle v každém záznamu je v prostoru stránky, a jeho převod do souřadnic view patří do jednoho sdíleného pomocníka, ne rozprostřený po kódu. Panelům narůstají chyby v souřadnicích, když si výběr, hit-testing a vykreslování každý vymyslí vlastní matematiku zoomu a rotace; protáhněte všechny tři přes jediný převod a zvýraznění, jeho řádek v seznamu a jeho klikací cíl zůstanou připnuté ke stejnému inkoustu

Přebarvení značek a veto ze strany appearance streamu

Změna zvýraznění ze žluté na jantarovou zní jako jednořádková úprava, a někdy jí i je. Háček je v ISO 32000-1 §12.5.5. Když anotace nese appearance stream /AP, konformní prohlížečka vykreslí tento předpřipravený stream a barevnou položku ve slovníku bere jako mrtvá metadata. Acrobat zapisuje appearance streamy prakticky pro vše, co vytvoří, takže většina anotací přicházejících od zákazníků je už v tomto stavu, a barva, kterou jste tak sebejistě nastavili, se na obrazovku nikdy nedostane. Přebarvení je operace typu read-modify-write přes vlastnost Annotation[], a komponenta je ohledně tohoto konfliktu upřímná: když engine odmítne nechat barvu ze slovníku přebít zapečený appearance, zápis vyvolá EPdfError

Diagram cesty přebarvení read-modify-write v komponentě PDFium v Delphi, kde zapečený stream vzhledu vetuje barvu slovníku a vyvolá EPdfError
Když anotace nese předem postavený stream /AP, engine odmítne barvu slovníku a vyhodí EPdfError, takže panel přebarví vlastní překryv, nebo označí řádek jako appearance-locked
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // jantarová
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // Anotace vlastní předrenderovaný stream /AP; samotná
    // barva ve slovníku nemůže změnit to, co prohlížečky vykreslí
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Zachytávejte tuto výjimku pokaždé a berte ji jako informaci, ne jako selhání. Vynechte tuto pojistku a váš panel bude ve vlastním seznamu vesele ukazovat jantarovou, zatímco stránka bude dál malovat žlutou; uživatel to o týdny později nahlásí jako „vaše prohlížečka ignoruje mé úpravy" a vy strávíte odpoledne neúspěšným pokusem to zreprodukovat na souboru, který náhodou nemá žádný appearance stream. Jakmile víte, že je appearance uzamčený, máte dvě poctivé odpovědi: přebarvit vlastní překryvnou vrstvu výběru místo anotace, aby recenzent aspoň viděl zvýraznění, které vybral, nebo označit řádek jako appearance-locked, aby nikdo nečekal, že se změna udrží

Mazání anotací beze zanechání přízraků

DeleteAnnotation odebere objekt ze stromu anotací aktuální stránky, ale mezipaměť rastru stránky nechá být. Vykreslete hned po volání a smazané zvýraznění je pořád na obrazovce, sedící v bitmapě, která už neodpovídá modelu dokumentu za ní. Oprava je zacházet s opětovným vykreslením jako se součástí mazání, ne jako s krokem, na který by volající mohl zapomenout:

Diagram tříkrokového cyklu mazání PDFium v Delphi, který odstraní anotaci, znovu vykreslí stranu s reAnnotations a znovu postaví index strany
Mazání se dotkne jen stromu anotací, takže panel se musí znovu vykreslit pomocí reAnnotations a znovu postavit položky stránek, dřív než budou zobrazení i index znovu poctivá
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // při selhání vyvolá EPdfError
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // RenderPage předává vlastnictví bitmapy volajícímu
end;
RebuildPageEntries(Item.PageNo);  // indexy za Item.Index se posunuly

V tomto bloku se dají snadno pokazit dva detaily. Volba reAnnotations musí být přítomná, jinak nový rastr vyhodí každou zbývající anotaci a stránka bude vypadat, jako byste smazali celou sadu komentářů místo jedné značky. A Bmp.Free není volitelné: přetížení RenderPage ve stylu funkce předává vlastnictví bitmapy volajícímu, takže chybějící free unikne celostránkový rastr při každém jednotlivém mazání, což recenzent procházející dlouhý dokument během pár minut promění ve skutečný tlak na paměť

Přidávání recenzentských značek z vlastního UI

Vytváření anotací prochází přes CreateAnnotation, které vezme vyplněný záznam TPdfAnnotation (podtyp, obdélník, barvu, obsah, autora) a připojí ho k aktuální stránce. Lepicí poznámka, podtyp anText, je snadný případ: nastavíte pozici, obsah a autora a je hotovo. U anotací typu Ink se lidé chytají. Obdélník záznamu jen ohraničuje kresbu; samotné tahy jsou pole bodů, která se musí připojit samostatně přes volání enginu pro tah inkoustem, FPDFAnnot_AddInkStroke nakrmené daty FS_POINTF, zachycenými ze vstupu myši nebo pera tah po tahu. Sestavte anotaci typu Ink jen z obdélníku a ničeho jiného a dostanete prázdné čmáranice, které se vykreslí jako prázdné místo, což vypadá jako chyba enginu a ve skutečnosti je to napůl dokončená anotace

Rovnou ujasněte i politiku autorství. Každá značka, kterou vaše UI vytvoří, by měla nést konzistentní AuthorText, protože filtr recenzentů, který sestavíte příští měsíc, je jen tak dobrý jako jména, která dnes otisknete na komentáře. Prázdné nebo nekonzistentní řetězce autora nelze zpětně opravit bez opětovného otevření každého souboru

Dostání recenze ven z prohlížečky

Data z recenze si vydělají na svou existenci ve chvíli, kdy mohou opustit prohlížečku, jako shrnutí, které vedoucí projektu přečte bez otevření souboru, nebo jako CSV, které nakrmí sledovací tabulku. Exportujte z indexu, který jste už sestavili, nikdy z čerstvého parsování, a zvolte stabilní způsob, jak se ke každé značce zpětně odkazovat. Číslo stránky spárované s obdélníkem anotace přežije round-tripy, které index pole nepřežije, protože další smazání potichu přečísluje indexy a vaše CSV začne ukazovat na špatné komentáře

Řádek, který stojí za uchování, nese stránku, podtyp, autora, časové razítko vytvoření, pokud ho soubor eviduje, text obsahu a sloupec stavu, který vlastníte vy, ne ten, který dodává PDF. Stejný indexovací průchod se hodí i dřív, při vstupní kontrole, když dokument dorazí odjinud než z týmu a vy chcete vědět, co v něm je, dřív, než ho někdo recenzuje. Článek o pracovišti pro vstupní kontrolu PDF prochází tímto tříděním, a navigace polí formuláře pokrývá zrcadlový problém: recenzování dokumentů postavených pro sběr dat, ne komentářů

Jeden případ, který vám pole neukáže

Jeden způsob selhání si zaslouží varovnou vlaječku, protože vypadá jako defekt ve vašem kódu, a není. Zákazník hlásí viditelná zvýraznění po celé stránce, ale váš panel neukazuje nic a AnnotationCount vrátí nulu. Obvyklé vysvětlení je, že značky byly někde po cestě zploštěny (flattened). Zploštění zapeče vzhled anotací do obyčejného obsahu stránky, takže se zvýraznění stanou součástí grafiky stránky a zcela přestanou existovat jako objekty anotací. Pro API anotací už nezbývá nic k enumerování, přebarvení ani smazání. Když vidíte namalované značky s nulovým počtem, přestaňte hledat chybu ve své enumerační smyčce a zeptejte se, jak byl soubor vyroben

Rozhraní pro anotace použité zde, od enumerace a vytváření přes přebarvení a mazání až po možnosti vykreslování, které udržují zobrazení poctivé, je součástí PDFium Component pro Delphi, C++Builder a Lazarus/FPC