Odborný článok

Nedeštruktívne zvýrazňovanie PDF v Delphi: vrstva na revíziu v HotPDF

Obdĺžnik nakreslený okolo odseku počas revízie sa nemusí stať značkou vnútri PDF. THPDFViewerModel v HotPDF sprístupňuje AddHighlightRegion, metódu, ktorá drží každé zvýraznenie ako záznam iba v pamäti, nie ako zmenu načítaného dokumentu, takže recenzent môže označiť desiatky stránok, zatiaľ čo súbor na disku zostáva bajt po bajte rovnaký ako predtým. Priblížte na 6400 %, otočte stránku o 90 stupňov, prepnite z Prispôsobiť šírke na Prispôsobiť stránke a rovnaký obdĺžnik stále skončí na tom istom odseku, pretože výpočet súradníc prebieha cez skutočnú geometriu vykreslenia v okamihu, keď bola značka nakreslená

Nástroje na revíziu postavené okolo prehliadača PDF na tento problém narážajú neustále. Obrazovka na revízne pripomienky, kontrola kvality generovaných faktúr, interný schvaľovací proces: všetky potrebujú umožniť niekomu upozorniť na oblasť stránky bez toho, aby sa každá koncepčná značka zmenila na trvalú zmenu súboru, a bez toho, aby bolo treba siahnuť po celom subsystéme anotácií len na zobrazenie farebného rámčeka, kým sa ešte len rozhoduje, či značka do dokumentu vôbec patrí. HotPDF na to odpovedá vyhradenou vrstvou zvýraznenia, ktorá celá sedí na strane Modelu z rozdelenia opísaného v stavbe vlastného prehliadača PDF s architektúrou MVC v Delphi, a práve preto sa aj rovnaký zoznam zvýraznení dá riadiť z jednotkového testu bez toho, aby bolo kdekoľvek vidieť handle okna

Čo presne ukladá AddHighlightRegion v HotPDF?

AddHighlightRegion ukladá pre každú značku presne tri veci: index stránky počítaný od nuly, THPDFRectangle v súradniciach používateľského priestoru PDF a TColor, všetko zabalené ako záznam THPDFViewerHighlight vnútri THPDFViewerModel. Volanie Viewer.HighlightRegion(PageIndex, PageRect, clYellow), alebo ekvivalentné Model.AddHighlightRegion, pripojí jeden z týchto záznamov do súkromného poľa a vráti jeho index, a tento index je jediný handle, ktorý volajúci dostane späť: neexistuje samostatný objekt, žiadne rozhranie s počítaním referencií, nič, čo treba uvoľniť. Každá ďalšia schopnosť v tomto článku, kreslenie značky, jej prepočítanie po zmene priblíženia, jej odstránenie, je postavená na tomto jedinom malom zázname

Každý obdĺžnik sa pred prijatím normalizuje a orezáva. AddHighlightRegion vymení ľavý a pravý okraj, ak recenzent ťahá sprava doľava, vymení horný a dolný okraj pre ťah smerom nahor, a potom výsledok oreže podľa MediaBoxu stránky získaného cez GetLoadedPageBox. Obdĺžnik, ktorý skončí s nulovou šírkou, nulovou výškou, alebo celý mimo stránky, sa rovno odmietne: metóda vráti -1 a nič sa do zoznamu nepridá. Táto návratová hodnota nie je len ozdobná: dávka zvýraznení znovu zostavená z externého revízneho súboru, alebo zo zastaraných súradníc po tom, čo bola stránka nahradená, môže ticho stratiť položky, ak ich volajúci neskontroluje

Ako zvýraznenie zostáva zarovnané po priblížení alebo otočení?

Zvýraznenie zostáva zarovnané preto, lebo HotPDF ho ukladá v priestore stránky PDF a pri každom prekreslení ho znova premieta do priestoru obrazovky, namiesto toho, aby ukladalo obdĺžnik obrazovky, ktorý by zastaral vo chvíli, keď sa zmení úroveň priblíženia. THPDFViewerModel.PagePointToView a jej inverzia ViewPointToPage vykonávajú toto premietanie v dvoch krokoch: najprv vlastnú položku stránky /Rotate, potom nezávislé ViewRotation prehliadača, ktoré sa nikdy nezapisuje späť do PDF a ovplyvňuje iba to, čo prehliadač zobrazuje. Zrušenie transformácie pri uvoľnení tlačidla myši prebehne cez tie isté dva kroky v opačnom poradí, čo je to, čo umožňuje, aby zvýraznenie nakreslené pri vysokom priblížení na stránke otočenej o 270 stupňov skončilo presne na správnom mieste aj potom, čo recenzent resetuje pohľad späť na Prispôsobiť stránke

DPI použité pre toto premietanie je rovnako dôležité ako otočenie. Prehliadač HotPDF zachytáva presné DPI bitmapy práve zobrazenej na obrazovke do FRenderedDPI hneď po každom vykreslení, a ImageMouseUp odovzdáva rovnakú hodnotu do ViewPointToPage, takže súradnica myši sa vždy prevádza pomocou rozlíšenia, pri ktorom bola skutočne nakreslená, nie rozlíšenia prepočítaného z aktuálnej vlastnosti priblíženia. CreatePageSnapshot a jej príbuzné metódy obmedzujú DPI na rozsah od 12 do 2400, no interaktívna cesta vykresľovania takýto strop nemá: štandardný rebrík priblíženia vrcholí na 6400 %, čo pri predvolenom základe 96 DPI vychádza výrazne nad 2400 DPI, takže opätovné použitie limitu v štýle snímky pre mapovanie súradníc by posunulo každé zvýraznenie o niekoľko pixelov na hornom konci rozsahu priblíženia. Dve menšie predvolené hodnoty dopĺňajú túto interakciu: ťah kratší ako dva pixely na ktorejkoľvek osi sa považuje za klik a nevytvorí žiadne zvýraznenie, a zvýrazňovanie nemôže začať, kým sa aspoň jedna stránka skutočne nevykreslila, keďže FRenderedDPI začína na nule

Zapojenie interaktívneho zvýrazňovania do revíznej obrazovky

Zapnutie interaktívneho zvýrazňovania je záležitosťou troch vlastností na samotnom ovládacom prvku THPDFViewer: nastavte InteractionMode na vimHighlight namiesto predvoleného vimBrowse, vyberte HighlightColor, ktorá je predvolene clYellow, a ošetrite OnMarqueeSelect, aby ste zistili, čo recenzent práve nakreslil. Všetko ostatné, zachytenie myši, kreslenie bodkovaného výberového obdĺžnika počas ťahania recenzenta, prevod bodu uvoľnenia späť do priestoru stránky, volanie AddHighlightRegion, sa deje vnútri ovládacieho prvku ešte predtým, než sa táto udalosť vyvolá

type
  TReviewForm = class(TForm)
    Viewer: THPDFViewer;
    ReviewLog: TMemo;
    procedure FormCreate(Sender: TObject);
  private
    procedure ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
      PageIndex: Integer; const PageRect: THPDFRectangle;
      HighlightIndex: Integer);
  end;

// PdfDoc is a THotPDF already loaded elsewhere on the form
procedure TReviewForm.FormCreate(Sender: TObject);
begin
  Viewer.PDFDocument := PdfDoc;
  Viewer.InteractionMode := vimHighlight;
  Viewer.HighlightColor := clLime;
  Viewer.OnMarqueeSelect := ViewerMarqueeSelect;
end;

procedure TReviewForm.ViewerMarqueeSelect(Sender: TObject; Shift: TShiftState;
  PageIndex: Integer; const PageRect: THPDFRectangle; HighlightIndex: Integer);
begin
  ReviewLog.Lines.Add(Format('page %d, mark #%d at (%.1f, %.1f)-(%.1f, %.1f)',
    [PageIndex + 1, HighlightIndex, PageRect.Left, PageRect.Bottom,
     PageRect.Right, PageRect.Top]));
end;

OnMarqueeSelect sa vyvolá iba pre ťah, ktorý skutočne vytvoril zvýraznenie: klik príliš malý na to, aby sa počítal ako ťah, okamžite zruší prekryvný výber, a ťah, ktorý skončí celý mimo stránky, sa dostane až do AddHighlightRegion, no tam sa odmietne rovnako, ako by sa odmietlo programové volanie, takže udalosť zostane ticho v oboch prípadoch. Jeden implementačný detail sa oplatí poznať, ak sa niekedy zdá, že zvýrazňovanie prestáva reagovať na okrajoch ovládacieho prvku: zachytenie myši patrí samotnému THPDFViewer, potomkovi TScrollBox, nie internému TImage, ktorý zobrazuje bitmapu stránky, a práve to umožňuje recenzentovi ťahať za hranu vykreslenej stránky a stále dostať čisté uvoľnenie

Pridávanie, odstraňovanie a spätné čítanie zvýraznení z kódu

Zvýraznenia vôbec nemusia pochádzať z ťahania myšou. Viewer.HighlightRegion(PageIndex, PageRect, Color), ktorá smeruje do rovnakého Model.AddHighlightRegion, aké interne volá aj interaktívne ťahanie, je verejná práve preto, aby revízna obrazovka mohla znova zostaviť zvýraznenia z dát, ktoré už má: komentárov načítaných z databázy, výsledkov textového vyhľadávania alebo značiek obnovených z predchádzajúcej relácie. Keďže súradnice sú obyčajné čísla v používateľskom priestore PDF, nič na tejto ceste nezávisí od toho, či bola stránka najprv vykreslená, na rozdiel od interaktívneho ťahania, ktoré potrebuje, aby FRenderedDPI už držala skutočnú hodnotu

var
  I: Integer;
  Item: TPriorComment;    // your own record: PageIndex + PageRect
  NewIndex: Integer;
begin
  for I := 0 to PriorComments.Count - 1 do
  begin
    Item := TPriorComment(PriorComments[I]);
    NewIndex := Viewer.HighlightRegion(Item.PageIndex, Item.PageRect, clAqua);
    if NewIndex < 0 then
      LogWarning('comment %d fell outside the page and was dropped', [I]);
  end;
end;

Odstránenie jediného zvýraznenia je miesto, kde sa prejaví úložisko podložené poľom. RemoveHighlightRegion vymaže jeden záznam a posunie každý neskorší záznam o jednu pozíciu nižšie, aby uzavrel medzeru, čo znamená, že akýkoľvek index zachytený skôr, či už z udalosti OnMarqueeSelect alebo z predchádzajúcej enumerácie, už nie je dôveryhodný vo chvíli, keď sa niečo pred ním v zozname odstráni. OnHighlightChange sa vyvolá pri každom pridaní, odstránení a volaní ClearHighlightRegions, no neprináša žiadnu informáciu o tom, čo sa zmenilo, takže bezpečný vzor je považovať ju za signál na opätovné zostavenie akéhokoľvek zoznamu, ktorý revízny panel zobrazuje, z HighlightCount a TryGetHighlightRegion, namiesto opravovania uloženého indexu na mieste

procedure TReviewForm.ViewerHighlightChange(Sender: TObject);
var
  I: Integer;
  Mark: THPDFViewerHighlight;
begin
  MarkList.Items.Clear;
  for I := 0 to Viewer.Model.HighlightCount - 1 do
    if Viewer.Model.TryGetHighlightRegion(I, Mark) then
      MarkList.Items.AddObject(Format('page %d', [Mark.PageIndex + 1]),
        TObject(I));
end;

Kedy by sa mala značka namiesto toho stať skutočnou anotáciou Highlight?

Oblasť zvýraznenia by sa mala stať skutočnou anotáciou vo chvíli, keď potrebuje prežiť mimo tejto jednej inštancie THPDFViewer. HotPDF tiež sprístupňuje AddHighlightAnnotation pre novú stránku a AddLoadedHighlightAnnotation pre už načítaný dokument, a napriek takmer identickému názvu ide o úplne odlišný mechanizmus: obe zapisujú skutočnú textovú značkovaciu anotáciu podľa ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, do poľa /Annots stránky, s /QuadPoints označujúcimi presný priebeh glyfov, a akýkoľvek normu rešpektujúci prehliadač PDF ju po uložení súboru vykreslí, nielen samotný HotPDF. Rovnaká hranica mechanizmu rozhoduje aj o tom, či sa značka prenesie cez XFDF: anotácia vytvorená pomocou AddLoadedHighlightAnnotation je normálny objekt PDF, ktorý ExportLoadedAnnotationsToXFDF zachytí a odovzdá Acrobatu alebo inému revíznemu nástroju ako značkovanie podľa ISO 19444-1, čo je opísané v importe a exporte anotácií PDF ako XFDF v Delphi, zatiaľ čo oblasť pridaná pomocou AddHighlightRegion je pre tento export neviditeľná, pretože sa nikdy nezapísala do grafu objektov: existuje iba dovtedy, kým existuje THPDFViewerModel, ktorý ju vytvoril. Celá rodina značkovacích a geometrických typov anotácií dostupných na stránke, a to, ako obdĺžnik umiestňuje každú z nich, je opísaná v článku o anotáciách PDF v Delphi s HotPDF, a praktické pravidlo je jednoduché: nechajte značku jednorazovú, kým sa o dokumente ešte diskutuje, a potvrďte ju ako anotáciu vo chvíli, keď je rozhodnutie konečné

Kde vrstva zvýraznenia končí

Vrstva zvýraznenia sa zo svojej strany vôbec nesnaží vyzerať ako priesvitné pero zvýrazňovača: RefreshDocument nakreslí každú oblasť ako dvojpixelový obrysový obdĺžnik vo vlastnej farbe navrch na bitmapu stránky uloženú vo vyrovnávacej pamäti, rovnako ako kreslí výsledky vyhľadávania, namiesto toho, aby prelínal farebnú výplň cez text pod ňou, takže klasický žltý nádych je treba domaľovať v kóde aplikácie alebo ponechať na vlastný vzhľadový prúd povýšenej anotácie. Jedna schopnosť, ktorú sa oplatí znovu využiť, keď oblasť už existuje, je CreateCurrentPageRegionSnapshot, ktorá zoberie ten istý THPDFRectangle, aký už zvýraznenie nesie, a vykreslí presne túto oblasť do bitmapy, čo je užitočné na pripojenie malého náhľadového obrázka k revíznemu komentáru bez exportu celej stránky. Revízny build si nemusí vopred vybrať medzi týmito dvoma mechanizmami: predvolene nastavte každú novú značku ako jednorazovú oblasť THPDFViewerHighlight, kým vlákno komentárov zostáva otvorené, a volajte AddLoadedHighlightAnnotation až vo chvíli, keď ju recenzent uzavrie, čo drží načítané PDF nedotknuté počas tam-a-späť výmeny, ktorá produkuje najviac zmien. Ovládací prvok prehliadača opísaný tu je súčasťou štandardného komponentu HotPDF pre Delphi a C++Builder, spolu so zvyškom API pre anotácie a formuláre, na ktoré sa vyššie odkazuje