Articol tehnic

Evidențiere PDF non-distructivă în Delphi: stratul de revizuire HotPDF

Un dreptunghi desenat în jurul unui paragraf în timpul revizuirii nu trebuie să devină o marcă în interiorul PDF-ului. THPDFViewerModel al HotPDF expune AddHighlightRegion, o metodă care păstrează fiecare evidențiere ca o înregistrare în memorie, nu ca o modificare a documentului încărcat, astfel încât un revizor poate marca zeci de pagini în timp ce fișierul de pe disc rămâne octet-cu-octet exact cum a fost. Măriți la 6400%, rotiți pagina cu 90 de grade, treceți de la Fit Width la Fit Page, iar același dreptunghi tot ajunge pe același paragraf, pentru că matematica de coordonate rulează prin geometria reală de randare din momentul în care marca a fost desenată

Instrumentele de revizuire construite în jurul unui vizualizator PDF întâlnesc constant această problemă. Un ecran de redlining, o trecere QA peste facturi generate, un flux intern de aprobare: toate au nevoie să permită cuiva să atragă atenția asupra unei regiuni a unei pagini fără ca fiecare marcă provizorie să devină o modificare permanentă a fișierului, și fără a apela la un subsistem complet de adnotări doar pentru a afișa o casetă colorată în timp ce cineva încă decide dacă marca merită păstrată. HotPDF răspunde la asta cu un strat dedicat de evidențiere care stă în întregime pe partea de Model a separării descrise în construirea unui vizualizator PDF personalizat cu o arhitectură MVC în Delphi, motiv pentru care aceeași listă de evidențieri poate fi condusă și dintr-un test unitar, fără niciun handle de fereastră în raza vizuală

Ce stochează de fapt AddHighlightRegion al HotPDF?

AddHighlightRegion stochează exact trei lucruri per marcă: un index de pagină bazat pe zero, un THPDFRectangle în coordonate de spațiu-utilizator PDF și o TColor, toate ambalate ca o înregistrare THPDFViewerHighlight în interiorul THPDFViewerModel. Apelarea Viewer.HighlightRegion(PageIndex, PageRect, clYellow), sau echivalentul Model.AddHighlightRegion, adaugă una din aceste înregistrări la un tablou privat și returnează indexul său, iar acel index este singurul handle pe care îl primește un apelant înapoi: nu există niciun obiect separat, nicio interfață cu numărare de referințe, nimic de eliberat. Fiecare altă capabilitate din acest articol, desenarea mărcii, remaparea ei după o schimbare de zoom, ștergerea ei, este construită pe baza acelei singure înregistrări mici

Fiecare dreptunghi este normalizat și decupat înainte de a fi acceptat. AddHighlightRegion interschimbă marginile stânga și dreapta dacă un revizor trage de la dreapta la stânga, interschimbă sus și jos pentru o tragere în sus, apoi decupează rezultatul față de MediaBox-ul paginii, obținut prin GetLoadedPageBox. Un dreptunghi care ajunge cu lățime zero, înălțime zero sau complet în afara paginii este respins direct: metoda returnează -1 și nimic nu se adaugă la listă. Acea valoare de retur nu este decorativă: un lot de evidențieri reconstruit dintr-un fișier extern de revizuire, sau din coordonate perimate după ce o pagină a fost înlocuită, poate pierde silențios intrări dacă apelantul nu verifică asta

Cum rămâne o evidențiere aliniată după zoom sau rotație?

O evidențiere rămâne aliniată pentru că HotPDF o stochează în spațiul paginii PDF și o reproiectează în spațiul ecranului la fiecare redesenare, în loc să stocheze un dreptunghi de ecran care ar deveni perimat imediat ce nivelul de zoom se schimbă. THPDFViewerModel.PagePointToView și inversul său, ViewPointToPage, fac acea proiecție în două etape: mai întâi propria intrare /Rotate a paginii, apoi ViewRotation-ul independent al Viewer-ului, care nu este niciodată scris înapoi în PDF și afectează doar ceea ce afișează Viewer-ul. Anularea transformării la eliberarea mouse-ului rulează aceleași două etape în ordine inversă, ceea ce permite unei evidențieri desenate la zoom mare pe o pagină rotită cu 270 de grade să ajungă exact în locul corect după ce revizorul resetează vizualizarea înapoi la Fit Page

DPI-ul folosit pentru acea proiecție contează la fel de mult ca rotația. Viewer-ul HotPDF captează DPI-ul exact al bitmap-ului aflat curent pe ecran în FRenderedDPI imediat după fiecare randare, iar ImageMouseUp transmite aceeași valoare în ViewPointToPage, astfel încât o coordonată de mouse este întotdeauna convertită folosind rezoluția la care a fost efectiv desenată, nu o rezoluție recalculată din proprietatea curentă de zoom. CreatePageSnapshot și rudele sale plafonează DPI-ul la un interval de la 12 la 2400, dar calea de randare interactivă nu poartă un asemenea plafon: scara standard de zoom urcă până la 6400%, ceea ce se calculează la mult peste 2400 DPI la valoarea de bază implicită de 96 DPI, așa că refolosirea unei limite de stil snapshot pentru maparea coordonatelor ar deplasa fiecare evidențiere cu câțiva pixeli la partea de sus a intervalului de zoom. Două valori implicite mai mici completează interacțiunea: o tragere mai scurtă de doi pixeli pe oricare axă este tratată ca un clic și nu produce nicio evidențiere, iar evidențierea nu poate începe până când cel puțin o pagină nu s-a randat efectiv, pentru că FRenderedDPI pornește de la zero

Conectarea evidențierii interactive într-un ecran de revizuire

Activarea evidențierii interactive este o sarcină de trei proprietăți pe controlul THPDFViewer însuși: setați InteractionMode la vimHighlight în loc de implicitul vimBrowse, alegeți o HighlightColor, care este implicit clYellow, și gestionați OnMarqueeSelect pentru a afla ce tocmai a desenat revizorul. Orice altceva, capturarea mouse-ului, desenarea dreptunghiului punctat de selecție cât timp revizorul trage, conversia punctului de eliberare înapoi în spațiul paginii, apelarea AddHighlightRegion, se întâmplă în interiorul controlului înainte ca acel eveniment să se declanșeze

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 se declanșează doar pentru o tragere care a produs efectiv o evidențiere: un clic prea mic pentru a conta ca o tragere șterge imediat suprapunerea de selecție, iar o tragere care ajunge complet în afara paginii ajunge la AddHighlightRegion, dar este respinsă acolo la fel cum ar fi respinsă o chemare programatică, așa că evenimentul rămâne tăcut în ambele cazuri. Un detaliu de implementare care merită cunoscut dacă evidențierea pare vreodată să nu mai răspundă la marginile controlului: capturarea mouse-ului aparține THPDFViewer-ului însuși, un descendent de TScrollBox, nu TImage-ului intern care afișează bitmap-ul paginii, ceea ce permite unui revizor să tragă dincolo de marginea paginii randate și totuși să obțină o eliberare curată

Adăugarea, eliminarea și recitirea evidențierilor din cod

Evidențierile nu trebuie să provină deloc dintr-o tragere de mouse. Viewer.HighlightRegion(PageIndex, PageRect, Color), care se canalizează spre același Model.AddHighlightRegion pe care îl apelează intern tragerea interactivă, este public tocmai pentru ca un ecran de revizuire să poată reconstrui evidențieri din date pe care le are deja: comentarii încărcate dintr-o bază de date, rezultate dintr-o căutare de text, sau marcaje restaurate dintr-o sesiune anterioară. Pentru că acele coordonate sunt simple numere de spațiu-utilizator PDF, nimic din această cale nu depinde de faptul că o pagină a fost redată mai întâi, spre deosebire de tragerea interactivă, care are nevoie ca FRenderedDPI să conțină deja o valoare reală

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;

Eliminarea unei singure evidențieri este locul unde se vede stocarea pe bază de tablou. RemoveHighlightRegion șterge o înregistrare și mută fiecare înregistrare ulterioară cu o poziție mai jos pentru a închide golul, ceea ce înseamnă că orice index capturat anterior, dintr-un eveniment OnMarqueeSelect sau dintr-o enumerare anterioară, nu mai este de încredere odată ce ceva dinaintea sa în listă este eliminat. OnHighlightChange se declanșează la fiecare adăugare, eliminare și apel ClearHighlightRegions, dar nu poartă nicio informație despre ce s-a schimbat, așa că tiparul sigur este să îl tratați ca un semnal pentru a reconstrui orice listă afișează un panou de revizuire din HighlightCount și TryGetHighlightRegion, în loc să corectați pe loc un index din cache

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;

Când ar trebui ca o marcă să devină în schimb o adnotare Highlight reală?

O regiune de evidențiere ar trebui să devină o adnotare reală în momentul în care trebuie să supraviețuiască în afara acelei instanțe unice de THPDFViewer. HotPDF expune de asemenea AddHighlightAnnotation pentru o pagină nouă și AddLoadedHighlightAnnotation pentru un document deja încărcat, iar în ciuda numelui aproape identic, acesta este un mecanism complet diferit: ambele scriu o adnotare reală de marcare a textului conform ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, în tabloul /Annots al paginii, cu /QuadPoints marcând exact secvența de glife, iar orice vizualizator PDF conform o randează odată ce fișierul este salvat, nu doar HotPDF însuși. Aceeași limită de mecanism decide dacă o marcă face drumul dus-întors prin XFDF: o adnotare creată cu AddLoadedHighlightAnnotation este un obiect PDF normal, pe care ExportLoadedAnnotationsToXFDF îl preia și îl predă către Acrobat sau alt instrument de revizuire ca marcaj ISO 19444-1, acoperit în importul și exportul adnotărilor PDF ca XFDF în Delphi, în timp ce o regiune adăugată prin AddHighlightRegion este invizibilă pentru acel export, pentru că nu a fost niciodată scrisă în graful de obiecte deloc: există doar atâta timp cât există THPDFViewerModel-ul care a creat-o. Întreaga familie de tipuri de adnotări de marcaj și geometrice disponibile pe o pagină, și modul în care un dreptunghi plasează pe fiecare dintre ele, este acoperită în articolul despre adnotările PDF în Delphi cu HotPDF, iar regula practică este simplă: păstrați o marcă de unică folosință cât timp un document este încă în discuție și angajați-o ca adnotare abia odată ce o decizie este finală

Unde se oprește stratul de evidențiere

Stratul de evidențiere, la rândul său, nu încearcă deloc să semene cu un marker translucid: RefreshDocument desenează fiecare regiune ca un dreptunghi de contur de doi pixeli în propria culoare deasupra bitmap-ului de pagină din cache, la fel cum desenează rezultatele căutării, în loc să amestece o umplere colorată peste textul de dedesubt, așa că un aspect clasic de spălare galbenă trebuie pictat în codul aplicației sau amânat pentru fluxul de aspect propriu al unei adnotări promovate. O capabilitate care merită refolosită odată ce o regiune există este CreateCurrentPageRegionSnapshot, care preia același THPDFRectangle pe care îl poartă deja o evidențiere și randează exact acea zonă într-un bitmap, util pentru atașarea unei mici imagini de previzualizare la un comentariu de revizuire fără a exporta întreaga pagină. O construcție de revizuire nu trebuie să aleagă între cele două mecanisme de la început: setați implicit fiecare marcă nouă la o regiune de unică folosință THPDFViewerHighlight cât timp un fir de comentarii rămâne deschis, și apelați AddLoadedHighlightAnnotation abia odată ce un revizor îl rezolvă, ceea ce menține PDF-ul încărcat neatins în timpul du-te-vino-ului care produce cea mai mare fluctuație. Controlul de vizualizare descris aici face parte din componenta HotPDF standard pentru Delphi și C++Builder, alături de restul API-urilor de adnotare și formulare menționate mai sus