Technical Article

Revízia anotácií PDF v Delphi pomocou PDFium Component

Anotácia PDF je slovník pripojený k stránke, nie značka nakreslená na nej. Norma ISO 32000-1 §12.5 definuje približne dva tucty podtypov, pričom každý z nich obsahuje kľúč /Subtype, obdĺžnik v súradniciach stránky, sadu príznakov a zvyčajne aj stream vzhľadu (appearance stream), ktorý určuje, čo prehliadač v skutočnosti vykreslí. Tieto podtypy neznamenajú pre osobu recenzujúcu dokument to isté. Zvýraznenie (Highlight) a ťah perom (Ink) sú komentáre; odkaz (Link) slúži na navigáciu; vyskakovacie okno (Popup) je malé okno, ktoré sa otvorí po kliknutí na lístok s poznámkou, uložené ako samostatný objekt, na ktorý ukazuje rodičovský objekt. Odpovede sú plnohodnotné textové anotácie (Text), ktoré odkazujú na komentár, na ktorý odpovedajú, prostredníctvom položky in-reply-to. Pole anotácií na úrovni stránky teda nie je zoznamom komentárov pre recenzenta. Je to plochá štruktúra obsahujúca komentáre, technické prepojenia medzi nimi a niekoľko prvkov, ktoré by žiadny recenzent za komentár nepovažoval. Panel, ktorý s týmto polom zaobchádza ako so zoznamom komentárov, sa nebude zhodovať so žiadnym iným prehliadačom, ktorý zákazník používa.

Vybudovanie procesu revízie anotácií v PDFium Component (komponente VCL/LCL založenom na knižnici PDFium pre Delphi, C++Builder a Lazarus) znamená zamerať sa na miesta, kde tento rozdiel medzi nespracovaným polom a ľudským pohľadom spôsobuje problémy: počítanie, indexovanie, prefarbovanie značiek, ktoré už jadro zafixovalo, mazanie bez zanechania duchov a pridávanie vlastných značiek.

Prečo sa váš počet nikdy nezhoduje s panelom komentárov v Acrobate

Ak otvoríte pripomienkovanú zmluvu vo vašom prehliadači a v programe Acrobat vedľa seba, celkové počty sa zhodujú len málokedy. Acrobat zobrazuje upravené zobrazenie: označenia zoskupené do vlákien odpovedí, vyskakovacie okná (popups) zlúčené do poznámok, ku ktorým patria, a vynechané odkazy a prvky formulárov. Nespracované pole obsahuje všetko bez rozlíšenia, takže jednoduché sčítanie je v niektorých ohľadoch nadhodnotené a v iných podhodnotené.

Vyskakovacie okná (Popups) zvyšujú celkový počet, pretože každý lístok s poznámkou sa dodáva so samostatným objektom Popup a počítanie oboch zdvojnásobuje poznámku. Odpovede naopak počet znižujú, ak filtrujete viditeľné značky, keďže odpoveď je textová anotácia (Text), ktorá sa nevykreslí, kým niekto nerozbalí vlákno, a jej vynechaním stratíte diskusiu.

Príznaky Hidden a NoView odstránia anotáciu z obrazovky bez toho, aby ju odstránili z pola, takže počítanie ignorujúce príznaky zahŕňa aj značky, ktoré používateľ nevidí. Anotácie odkazov (Link) sa nachádzajú v rovnakom poli ako komentáre a nepatria ani do počtu, ani do zoznamu. Rozhodnite o pravidle počítania ešte pred napísaním cyklu a toto rozhodnutie si zaznamenajte, pretože otázka „prečo váš panel zobrazuje iné číslo ako Acrobat“ je prvou požiadavkou na podporu, ktorú funkcia revízie prinesie.

Indexujte všetko raz, potom už stránku znova neanalyzujte

Jedno návrhové pravidlo riadi všetko, čo nasleduje: filtrovanie podľa autora, typu alebo stránky nesmie nikdy znova analyzovať objekty stránky. V 300-stranovom dokumente s hustým pripomienkovaním by opätovná analýza pri každej zmene rozbaľovacieho zoznamu spôsobila sekundové sekanie panelu. Komponent sprístupňuje vlastnosť AnnotationCount a indexovanú vlastnosť Annotation[], obe obmedzené na aktuálne načítanú stránku, a záznam TPdfAnnotation, ktorý vracajú, obsahuje to, čo zoznam potrebuje: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Správnym krokom je prejsť každú stránku raz pri otvorení a vytvoriť 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];
      // 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;

Dvojica, ktorú stojí za to podčiarknuť, je (PageNo, i). Každá neskoršia zmena, či už zmena farby alebo vymazanie, je adresovaná číslom stránky a indexom anotácie. Tento index je však nestabilný: odstránenie anotácie prečísluje všetko, čo po nej na danej stránke nasleduje. Preto plánujte prebudovanie položiek postihnutej stránky po každom vymazaní namiesto opravy indexových čísel na mieste. Opätovné zostavenie trvá milisekundu. Zastaraný index naopak vymaže komentár iného recenzenta, čo je presne ten typ chyby, ktorý narúša dôveru v celú funkciu.

Vlákna (threading) si zaslúžia miesto v indexe, aj keď vaša prvá verzia bude odpovede iba počítať a nebude ich zobrazovať. Zoskupte položky podľa referencie na rodiča, kým máte stránku otvorenú, aby panel mohol neskôr zbaliť vlákno rovnakým spôsobom ako Acrobat. Rekonštrukcia tohto zoskupenia dynamicky počas posúvania popiera celý zmysel jednorazového indexovania, pretože znova otvára stránky, ktorých analýzu ste už raz zaplatili. Geometria si vyžaduje rovnakú disciplínu. Rectangle v každom zázname je v súradniciach stránky a jeho prevod na súradnice zobrazenia patrí do jedného zdieľaného pomocníka, nie roztrúseného po celom kóde. Panely trpia chybami súradníc, keď výber, testovanie kliknutí (hit-testing) a vykresľovanie používajú vlastnú matematiku približovania a otáčania; nasmerujte všetky tri operácie cez jeden prevod a zvýraznenie, jeho riadok v zozname a jeho cieľ kliknutia zostanú priradené k rovnakému miestu.

Zmena farby označenia a veto streamu vzhľadu

Zmena zvýraznenia zo žltej na jantárovú znie jednoducho a niekedy to tak aj je. Problémom je však ISO 32000-1 §12.5.5. Ak anotácia obsahuje stream vzhľadu /AP, kompatibilný prehliadač vykreslí tento predpripravený stream a položku farby v slovníku považuje za neaktívne metadáta. Acrobat vytvára streamy vzhľadu pre takmer všetko, čo vytvorí, takže väčšina anotácií od zákazníkov je už v tomto stave a farba, ktorú tak sebaisto nastavíte, sa na obrazovke nikdy nezobrazí. Zmena farby je proces čítania-úpravy-zápisu cez vlastnosť Annotation[] a komponent k tomuto konfliktu pristupuje úprimne: keď jadro odmietne prepísať farbu zo slovníka zapečeným vzhľadom, zápis vyvolá výnimku 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;

Túto výnimku zachyťte zakaždým a považujte ju za informáciu, nie za zlyhanie. Ak toto ošetrenie vynecháte, váš panel bude v zozname veselo zobrazovať jantárovú farbu, zatiaľ čo stránka sa bude naďalej vykresľovať žlto; používateľ to o niekoľko týždňov nahlási ako chybu „váš prehliadač ignoruje moje úpravy“ a vy strávite popoludnie neúspešnými pokusmi o reprodukciu na súbore, ktorý zhodou okolností nemá žiadny stream vzhľadu. Keď už viete, že vzhľad je uzamknutý, máte dve možnosti: zmeniť farbu vlastného prekrytia výberu namiesto samotnej anotácie, aby recenzent aspoň videl vybrané zvýraznenie, alebo označiť riadok ako uzamknutý pre zmenu vzhľadu, aby nikto neočakával, že sa zmena prejaví.

Mazanie anotácií bez zanechania duchov

Metóda DeleteAnnotation odstráni objekt zo stromu anotácií aktuálnej stránky, ale ponechá vyrovnávaciu pamäť rastra stránky nezmenenú. Ak vykreslíte stránku ihneď po volaní, odstránené zvýraznenie bude stále na obrazovke v bitovej mape, ktorá už nezodpovedá modelu dokumentu pod ňou. Riešením je považovať opätovné vykreslenie za súčasť mazania, nie za krok, na ktorý by volajúci mohol zabudnúť:

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 detaily v tomto bloku sa dajú ľahko pokaziť. Možnosť reAnnotations musí byť prítomná, inak nový raster zahodí všetky zostávajúce anotácie a stránka bude vyzerať, akoby ste vymazali celú sadu komentárov namiesto jednej značky. A uvoľnenie Bmp.Free nie je voliteľné: preťaženie metódy RenderPage vracia vlastníctvo bitovej mapy volajúcemu, takže chýbajúce uvoľnenie spôsobí únik pamäte rastra celej stránky pri každom vymazaní. Recenzent pracujúci s dlhým dokumentom by tak za pár minút spôsobil vážny nedostatok pamäte.

Pridávanie recenzentských značiek z vlastného používateľského rozhrania

Vytváranie anotácií prebieha prostredníctvom metódy CreateAnnotation, ktorá prijíma vyplnený záznam TPdfAnnotation (podtyp, obdĺžnik, farba, obsah, autor) a pripojí ho k aktuálnej stránke. Lístok s poznámkou (podtyp anText) je jednoduchý prípad: nastavíte pozíciu, obsah a autora a máte hotovo. Pri ručne kreslených anotáciách (Ink) sa však vývojári často pomýlia. Obdĺžnik v zázname ohraničuje iba kresbu; samotné ťahy sú polia bodov, ktoré sa musia pripojiť samostatne prostredníctvom volania FPDFAnnot_AddInkStroke s údajmi typu FS_POINTF, zachytenými zo vstupu myši alebo pera po jednotlivých ťahoch. Ak vytvoríte anotáciu typu Ink iba z obdĺžnika, získate prázdny čmáranec, ktorý sa vykreslí ako prázdne miesto, čo vyzerá ako chybný stav v jadre, ale v skutočnosti je to len nedokončená anotácia.

Zároveň vyriešte pravidlá autorstva. Každá značka, ktorú vaše používateľské rozhranie vytvorí, by mala obsahovať konzistentný text AuthorText, pretože filter recenzentov, ktorý vytvoríte o mesiac, bude len taký dobrý, ako mená, ktoré dnes priradíte ku komentárom. Prázdne alebo nekonzistentné reťazce autorov nie je možné opraviť spätne bez opätovného otvorenia každého súboru.

Exportovanie revízie z prehliadača

Údaje z revízie majú zmysel vtedy, keď môžu opustiť prehliadač, napríklad ako súhrn, ktorý si vedúci projektu prečíta bez otvorenia súboru, alebo ako súbor CSV pre sledovaciu tabuľku. Exportujte z indexu, ktorý ste už vytvorili, nikdy nie z novej analýzy, a zvoľte stabilný spôsob odkazovania na každú značku. Číslo stránky spárované s obdĺžnikom anotácie prežije zmeny, ktoré index poľa neprežije, pretože ďalšie vymazanie potichu prečísluje indexy a váš súbor CSV by začal ukazovať na nesprávne komentáre.

Riadok, ktorý stojí za to exportovať, obsahuje stránku, podtyp, autora, časovú pečiatku vytvorenia (ak ju súbor zaznamenáva), text obsahu a stĺpec stavu, ktorý spravujete vy, nie ten, ktorý poskytuje PDF. Rovnaký indexovací prechod je užitočný už skôr, počas príjmu dokumentov, keď dokument prichádza z externého prostredia a vy chcete vedieť, čo obsahuje, skôr než ho niekto začne recenzovať. Článok o spracovaní prijatých PDF popisuje toto triedenie a navigácia vo formulárových poliach sa zaoberá zrkadlovým problémom: revíziou dokumentov vytvorených na zber údajov namiesto komentárov.

Jeden prípad, ktorý vám pole neukáže

Jeden chybový stav si zaslúži pozornosť, pretože vyzerá ako chyba vo vašom kóde, no v skutočnosti ňou nie je. Zákazník nahlási viditeľné zvýraznenia po celej stránke, ale váš panel nezobrazuje nič a AnnotationCount vráti nulu. Zvyčajným vysvetlením je, že značky boli niekde predtým sploštené (flattened). Sploštenie zapečie vzhľad anotácie do bežného obsahu stránky, takže zvýraznenia sa stanú súčasťou grafiky stránky a úplne prestanú existovať ako objekty anotácií. Pre API na prácu s anotáciami nezostane nič, čo by sa dalo enumerovať, prefarbiť alebo vymazať. Keď vidíte vykreslené označenia s nulovým počtom, nehľadajte chybu vo svojom cykle prechádzania anotácií, ale opýtajte sa, ako bol súbor vytvorený.

Rozhranie pre anotácie popísané v tomto článku, od enumerácie a vytvárania až po prefarbovanie, mazanie a možnosti vykresľovania, ktoré zabezpečujú presné zobrazenie, je súčasťou PDFium Component pre Delphi, C++Builder a Lazarus/FPC.