Artykuł techniczny

Nieniszczące wyróżnienia PDF w Delphi: warstwa recenzji HotPDF

Prostokąt narysowany wokół akapitu podczas recenzji nie musi stawać się znacznikiem wewnątrz PDF-a. THPDFViewerModel HotPDF udostępnia AddHighlightRegion, metodę, która przechowuje każde wyróżnienie jako rekord w pamięci, a nie zmianę we wczytanym dokumencie, więc recenzent może naznaczyć dziesiątki stron, podczas gdy plik na dysku pozostaje bajt w bajt taki sam, jaki był. Powiększ do 6400%, obróć stronę o 90 stopni, przełącz z Dopasuj szerokość na Dopasuj stronę — ten sam prostokąt nadal ląduje na tym samym akapicie, ponieważ matematyka współrzędnych przechodzi przez faktyczną geometrię renderowania w chwili, gdy znacznik został narysowany

Narzędzia recenzyjne zbudowane wokół przeglądarki PDF nieustannie napotykają ten problem. Ekran naniesionych poprawek, przejście QA po wygenerowanych fakturach, wewnętrzny proces akceptacji: wszystkie one potrzebują pozwolić komuś zwrócić uwagę na obszar strony bez zamieniania każdego roboczego znacznika w trwałą zmianę pliku, i bez sięgania po pełny podsystem adnotacji tylko po to, żeby pokazać kolorowy prostokąt, podczas gdy ktoś wciąż decyduje, czy znacznik ma zostać. HotPDF odpowiada na to dedykowaną warstwą wyróżnień, która siedzi całkowicie po stronie Modelu podziału opisanego w budowaniu własnej przeglądarki PDF z architekturą MVC w Delphi, co jest też powodem, dla którego ta sama lista wyróżnień może być sterowana z testu jednostkowego bez żadnego uchwytu okna w zasięgu wzroku

Co dokładnie przechowuje AddHighlightRegion w HotPDF?

AddHighlightRegion przechowuje dokładnie trzy rzeczy na znacznik: indeks strony liczony od zera, THPDFRectangle we współrzędnych przestrzeni użytkownika PDF oraz TColor, wszystko zapakowane jako rekord THPDFViewerHighlight wewnątrz THPDFViewerModel. Wywołanie Viewer.HighlightRegion(PageIndex, PageRect, clYellow), albo równoważnego Model.AddHighlightRegion, dopisuje jeden z tych rekordów do prywatnej tablicy i zwraca jego indeks, a ten indeks jest jedynym uchwytem, jaki wywołujący dostaje z powrotem: nie ma osobnego obiektu, żadnego interfejsu liczonego referencyjnie, niczego do zwolnienia. Każda inna możliwość opisana w tym artykule — rysowanie znacznika, przemapowanie go po zmianie powiększenia, usuwanie — jest zbudowana na bazie tego jednego małego rekordu

Każdy prostokąt jest normalizowany i przycinany, zanim zostanie zaakceptowany. AddHighlightRegion zamienia miejscami lewą i prawą krawędź, jeśli recenzent przeciąga od prawej do lewej, zamienia górę z dołem przy przeciągnięciu w górę, a następnie przycina wynik do MediaBox strony pobranego przez GetLoadedPageBox. Prostokąt, który kończy z zerową szerokością, zerową wysokością albo całkowicie poza stroną, jest wprost odrzucany: metoda zwraca -1 i nic nie zostaje dodane do listy. Ta wartość zwrotna nie jest dekoracyjna: partia wyróżnień odtworzona z zewnętrznego pliku recenzji, albo z nieaktualnych współrzędnych po wymianie strony, może po cichu stracić wpisy, jeśli wywołujący tego nie sprawdzi

Jak wyróżnienie pozostaje wyrównane po powiększeniu lub obrocie?

Wyróżnienie pozostaje wyrównane, ponieważ HotPDF przechowuje je w przestrzeni strony PDF i rzutuje ponownie na przestrzeń ekranu przy każdym przemalowaniu, zamiast przechowywać prostokąt ekranowy, który stałby się nieaktualny w chwili zmiany poziomu powiększenia. THPDFViewerModel.PagePointToView i jego odwrotność, ViewPointToPage, wykonują tę projekcję w dwóch etapach: najpierw własny wpis /Rotate strony, potem niezależny ViewRotation Przeglądarki, który nigdy nie jest zapisywany z powrotem do PDF-a i wpływa wyłącznie na to, co wyświetla Przeglądarka. Cofnięcie transformacji przy zwolnieniu myszy przebiega przez te same dwa etapy w odwrotnej kolejności, co pozwala wyróżnieniu narysowanemu przy wysokim powiększeniu na stronie obróconej o 270 stopni wylądować dokładnie we właściwym miejscu, gdy recenzent zresetuje widok z powrotem do Dopasuj stronę

DPI użyte do tej projekcji ma równie duże znaczenie co obrót. Przeglądarka HotPDF przechwytuje dokładne DPI bitmapy obecnie na ekranie w FRenderedDPI zaraz po każdym renderowaniu, a ImageMouseUp przekazuje tę samą wartość do ViewPointToPage, więc współrzędna myszy jest zawsze konwertowana przy użyciu rozdzielczości, w jakiej faktycznie została narysowana, a nie rozdzielczości przeliczonej na nowo z bieżącej właściwości powiększenia. CreatePageSnapshot i pokrewne metody ograniczają DPI do zakresu od 12 do 2400, ale interaktywna ścieżka renderowania nie ma takiego pułapu: standardowa drabinka powiększenia kończy się na 6400%, co przy domyślnym punkcie odniesienia 96 DPI daje znacznie ponad 2400 DPI, więc ponowne użycie limitu w stylu migawki do mapowania współrzędnych przesunęłoby każde wyróżnienie o kilka pikseli na górze zakresu powiększenia. Dwa mniejsze domyślne ustawienia dopełniają interakcję: przeciągnięcie krótsze niż dwa piksele na dowolnej osi jest traktowane jako kliknięcie i nie tworzy wyróżnienia, a wyróżnianie nie może się rozpocząć, dopóki przynajmniej jedna strona faktycznie się nie wyrenderowała, ponieważ FRenderedDPI zaczyna od zera

Podłączanie interaktywnego wyróżniania do ekranu recenzji

Włączenie interaktywnego wyróżniania to zadanie na trzy właściwości samej kontrolki THPDFViewer: ustaw InteractionMode na vimHighlight zamiast domyślnego vimBrowse, wybierz HighlightColor, który domyślnie wynosi clYellow, i obsłuż OnMarqueeSelect, aby dowiedzieć się, co recenzent właśnie narysował. Wszystko inne — przechwytywanie myszy, rysowanie kropkowanego prostokąta zaznaczenia podczas przeciągania przez recenzenta, konwersja punktu zwolnienia z powrotem na przestrzeń strony, wywołanie AddHighlightRegion — dzieje się wewnątrz kontrolki, zanim to zdarzenie zostanie zgłoszone

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 zgłasza się tylko dla przeciągnięcia, które faktycznie utworzyło wyróżnienie: kliknięcie zbyt małe, by liczyć się jako przeciągnięcie, natychmiast czyści nakładkę zaznaczenia, a przeciągnięcie, które ląduje całkowicie poza stroną, dociera do AddHighlightRegion, ale zostaje tam odrzucone tak samo, jak zostałoby odrzucone wywołanie programowe, więc zdarzenie w obu przypadkach milczy. Jeden szczegół implementacyjny wart poznania, jeśli wyróżnianie kiedykolwiek przestanie reagować na krawędziach kontrolki: przechwytywanie myszy należy do samego THPDFViewer, potomka TScrollBox, a nie do wewnętrznego TImage, który pokazuje bitmapę strony, co pozwala recenzentowi przeciągnąć poza krawędź wyrenderowanej strony i nadal otrzymać czyste zwolnienie

Dodawanie, usuwanie i ponowne odczytywanie wyróżnień z kodu

Wyróżnienia wcale nie muszą pochodzić z przeciągnięcia myszą. Viewer.HighlightRegion(PageIndex, PageRect, Color), który przekierowuje do tego samego Model.AddHighlightRegion, jakie wewnętrznie wywołuje interaktywne przeciągnięcie, jest publiczny właśnie po to, żeby ekran recenzji mógł odtworzyć wyróżnienia z danych, które już posiada: komentarzy wczytanych z bazy danych, wyników wyszukiwania tekstowego albo znaczników przywróconych z poprzedniej sesji. Ponieważ współrzędne to zwykłe liczby przestrzeni użytkownika PDF, nic w tej ścieżce nie zależy od tego, czy strona została wcześniej wyrenderowana, w przeciwieństwie do interaktywnego przeciągnięcia, które wymaga, aby FRenderedDPI zawierał już rzeczywistą wartość

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;

Usuwanie pojedynczego wyróżnienia to miejsce, gdzie przebija się przechowywanie oparte na tablicy. RemoveHighlightRegion usuwa jeden rekord i przesuwa każdy późniejszy rekord o jedną pozycję w dół, aby zamknąć lukę, co oznacza, że jakikolwiek indeks przechwycony wcześniej — ze zdarzenia OnMarqueeSelect albo z wcześniejszej enumeracji — przestaje być wiarygodny, gdy tylko coś przed nim na liście zostanie usunięte. OnHighlightChange zgłasza się przy każdym dodaniu, usunięciu i wywołaniu ClearHighlightRegions, ale nie niesie żadnej informacji o tym, co się zmieniło, więc bezpiecznym wzorcem jest traktowanie go jako sygnału do odbudowania jakiejkolwiek listy, którą pokazuje panel recenzji, z HighlightCount i TryGetHighlightRegion, zamiast łatania zbuforowanego indeksu w miejscu

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;

Kiedy znacznik powinien zamiast tego stać się prawdziwą adnotacją Highlight?

Obszar wyróżnienia powinien stać się prawdziwą adnotacją w chwili, gdy musi przetrwać poza jedną konkretną instancją THPDFViewer. HotPDF udostępnia też AddHighlightAnnotation dla nowej strony i AddLoadedHighlightAnnotation dla już wczytanego dokumentu, i mimo niemal identycznej nazwy jest to zupełnie inny mechanizm: obie zapisują rzeczywistą adnotację tekstową markup zgodną z ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, do tablicy /Annots strony, z /QuadPoints zaznaczającymi dokładny ciąg glifów, i każda zgodna z normą przeglądarka PDF renderuje ją po zapisaniu pliku, nie tylko własna przeglądarka HotPDF. Ta sama granica mechanizmu decyduje o tym, czy znacznik przechodzi przez XFDF w obie strony: adnotacja utworzona przez AddLoadedHighlightAnnotation jest zwykłym obiektem PDF, który ExportLoadedAnnotationsToXFDF odbiera i przekazuje do Acrobata lub innego narzędzia recenzji jako znacznik ISO 19444-1, opisany w imporcie i eksporcie adnotacji PDF jako XFDF w Delphi, podczas gdy obszar dodany przez AddHighlightRegion jest niewidoczny dla tego eksportu, ponieważ nigdy nie został zapisany do grafu obiektów: istnieje wyłącznie tak długo, jak długo istnieje THPDFViewerModel, który go utworzył. Pełna rodzina typów adnotacji markup i geometrycznych dostępnych na stronie oraz sposób, w jaki prostokąt umieszcza każdą z nich, jest opisana w artykule o adnotacjach PDF w Delphi z HotPDF, a praktyczna zasada jest prosta: trzymaj znacznik jednorazowy, dopóki dokument jest wciąż dyskutowany, i zatwierdź go jako adnotację dopiero, gdy decyzja jest ostateczna

Gdzie kończy się warstwa wyróżnień

Warstwa wyróżnień ze swojej strony nie próbuje wyglądać jak półprzezroczysty marker highlighterem: RefreshDocument rysuje każdy obszar jako dwupikselowy prostokąt konturowy we własnym kolorze na wierzchu buforowanej bitmapy strony, tak samo jak rysuje trafienia wyszukiwania, zamiast mieszać kolorowe wypełnienie z tekstem pod spodem, więc klasyczny żółty zmyw musi zostać namalowany w kodzie aplikacji albo odłożony do własnego strumienia wyglądu awansowanej adnotacji. Jedna możliwość warta ponownego wykorzystania, gdy obszar już istnieje, to CreateCurrentPageRegionSnapshot, który przyjmuje ten sam THPDFRectangle, jaki wyróżnienie już niesie, i renderuje tylko ten obszar do bitmapy, przydatne do dołączenia małego podglądu do komentarza recenzyjnego bez eksportowania całej strony. Konstrukcja recenzyjna nie musi wybierać między tymi dwoma mechanizmami z góry: domyślnie ustaw każdy nowy znacznik jako jednorazowy obszar THPDFViewerHighlight, dopóki wątek komentarza pozostaje otwarty, i wywołaj AddLoadedHighlightAnnotation dopiero, gdy recenzent go rozstrzygnie, co utrzymuje wczytany PDF nietknięty podczas wymiany, która generuje najwięcej zmian. Opisana tutaj kontrolka przeglądarki jest częścią standardowego komponentu HotPDF dla Delphi i C++Buildera, obok reszty API adnotacji i formularzy przywołanych powyżej