En rektangel ritad runt ett stycke under granskning måste inte bli en markering inuti PDF:en. HotPDF:s THPDFViewerModel exponerar AddHighlightRegion, en metod som håller varje markering som en post i minnet snarare än en ändring av det laddade dokumentet, så att en granskare kan märka upp dussintals sidor medan filen på disk förblir byte-för-byte densamma som den var. Zooma till 6400%, rotera sidan 90 grader, växla från Anpassa bredd till Anpassa sida, och samma rektangel hamnar fortfarande på samma stycke, eftersom koordinatberäkningen går via den faktiska rendergeometrin vid det ögonblick markeringen ritades
Granskningsverktyg byggda kring en PDF-visare stöter ständigt på det här problemet. En rödmarkeringsskärm, en QA-genomgång av genererade fakturor, ett internt godkännandeflöde: alla behöver låta någon rikta uppmärksamhet mot ett område på en sida utan att varje utkastmarkering blir en permanent ändring av filen, och utan att behöva ta till ett helt annoteringsundersystem bara för att visa en färgad ruta medan någon fortfarande bestämmer sig för om markeringen ska vara kvar. HotPDF svarar på det med ett dedikerat markeringslager som sitter helt på Model-sidan av uppdelningen som beskrivs i att bygga en anpassad PDF-visare med en MVC-arkitektur i Delphi, vilket också är varför samma markeringslista kan drivas från ett enhetstest utan ett enda fönsterhandtag i sikte
Vad lagrar HotPDF:s AddHighlightRegion egentligen?
AddHighlightRegion lagrar exakt tre saker per markering: ett nollbaserat sidindex, en THPDFRectangle i PDF-user-space-koordinater, och en TColor, allt paketerat som en THPDFViewerHighlight-post inuti THPDFViewerModel. Att anropa Viewer.HighlightRegion(PageIndex, PageRect, clYellow), eller motsvarande Model.AddHighlightRegion, lägger till en av dessa poster i en privat array och returnerar dess index, och det indexet är det enda handtag en anropare får tillbaka: det finns inget separat objekt, inget referensräknat gränssnitt, inget att frigöra. Varje annan förmåga i den här artikeln, att rita markeringen, mappa om den efter en zoomändring, ta bort den, är byggd ovanpå den där lilla posten
Varje rektangel normaliseras och beskärs innan den accepteras. AddHighlightRegion byter plats på vänster- och högerkant om en granskare drar från höger till vänster, byter plats på topp och botten för ett uppåtriktat drag, och beskär sedan resultatet mot sidans MediaBox hämtad via GetLoadedPageBox. En rektangel som slutar med noll bredd, noll höjd, eller helt utanför sidan avvisas rakt av: metoden returnerar -1 och inget läggs till i listan. Det returvärdet är inte dekorativt: en batch med markeringar återuppbyggda från en extern granskningsfil, eller från inaktuella koordinater efter att en sida ersatts, kan tyst tappa poster om anroparen inte kontrollerar det
Hur förblir en markering i linje efter zoom eller rotation?
En markering förblir i linje eftersom HotPDF lagrar den i PDF-sidrymden och projicerar om den till skärmrymden vid varje omritning, i stället för att lagra en skärmrektangel som skulle bli inaktuell i samma stund zoomnivån ändras. THPDFViewerModel.PagePointToView och dess invers, ViewPointToPage, gör den projektionen i två steg: först sidans egen /Rotate-post, sedan Visarens oberoende ViewRotation, som aldrig skrivs tillbaka till PDF:en och bara påverkar vad Visaren visar. Att ångra transformationen vid musuppsläpp kör samma två steg baklänges, vilket är vad som gör att en markering ritad vid hög zoom på en sida roterad 270 grader hamnar precis rätt efter att granskaren återställer vyn till Anpassa sida
DPI:n som används för den projektionen spelar lika stor roll som rotationen. HotPDF:s Visare fångar den exakta DPI:n för bitmappen som för närvarande visas på skärmen i FRenderedDPI direkt efter varje rendering, och ImageMouseUp skickar samma värde in i ViewPointToPage så att en muskoordinat alltid konverteras med den upplösning den faktiskt ritades vid, inte en upplösning omräknad från den aktuella zoom-egenskapen. CreatePageSnapshot och dess släktingar begränsar DPI till ett intervall 12 till 2400, men den interaktiva renderingsvägen har inget sådant tak: den vanliga zoomstegen toppar på 6400%, vilket beräknas till gott och väl över 2400 DPI vid standardbaslinjen 96 DPI, så att återanvända en snapshot-liknande gräns för koordinatmappning skulle flytta varje markering flera pixlar i toppen av zoomintervallet. Två mindre standardvärden avrundar interaktionen: ett drag kortare än två pixlar på endera axeln behandlas som ett klick och ger ingen markering, och markering kan inte börja förrän minst en sida faktiskt har renderats, eftersom FRenderedDPI börjar på noll
Att koppla in interaktiv markering i en granskningsskärm
Att slå på interaktiv markering är ett jobb med tre egenskaper på själva THPDFViewer-kontrollen: sätt InteractionMode till vimHighlight i stället för standardvärdet vimBrowse, välj en HighlightColor, som standardmässigt är clYellow, och hantera OnMarqueeSelect för att få reda på vad granskaren just ritade. Allt annat, att fånga musen, rita den prickade markeringsrektangeln medan granskaren drar, konvertera släpppunkten tillbaka till sidrymden, anropa AddHighlightRegion, sker inuti kontrollen innan den händelsen utlöses
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 utlöses bara för ett drag som faktiskt gav upphov till en markering: ett klick för litet för att räknas som ett drag rensar markeringsöverlägget omedelbart, och ett drag som hamnar helt utanför sidan når AddHighlightRegion men avvisas där på samma sätt som ett programmatiskt anrop skulle, så händelsen förblir tyst i båda fallen. En implementationsdetalj värd att känna till om markering någonsin verkar sluta svara vid kontrollens kanter: musfångst tillhör själva THPDFViewer, en TScrollBox-ättling, inte den interna TImage som visar sidbitmappen, vilket är vad som gör att en granskare kan dra förbi kanten på den renderade sidan och ändå få ett rent uppsläpp
Att lägga till, ta bort och läsa om markeringar från kod
Markeringar behöver inte alls komma från ett musdrag. Viewer.HighlightRegion(PageIndex, PageRect, Color), som leds in i samma Model.AddHighlightRegion som det interaktiva draget anropar internt, är publik specifikt så att en granskningsskärm kan återuppbygga markeringar från data den redan har: kommentarer laddade från en databas, resultat från en textsökning, eller markeringar återställda från en tidigare session. Eftersom koordinaterna är vanliga PDF-user-space-tal beror inget i den här vägen på att en sida redan renderats, till skillnad från det interaktiva draget, som behöver att FRenderedDPI redan innehåller ett riktigt värde
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;
Att ta bort en enskild markering är där den array-baserade lagringen syns igenom. RemoveHighlightRegion raderar en post och flyttar varje senare post ner en position för att stänga luckan, vilket innebär att alla index fångade tidigare, från en OnMarqueeSelect-händelse eller från en tidigare uppräkning, inte längre är pålitliga så fort något tidigare i listan tas bort. OnHighlightChange utlöses vid varje tillägg, borttagning och ClearHighlightRegions-anrop, men den bär ingen information om vad som ändrades, så det säkra mönstret är att behandla den som en signal att återuppbygga vilken lista en granskningspanel än visar från HighlightCount och TryGetHighlightRegion, snarare än att patcha ett cachat index på plats
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;
När ska en markering i stället bli en riktig Highlight-annotation?
En markeringsregion bör bli en riktig annotation i samma stund den behöver överleva utanför just den THPDFViewer-instansen. HotPDF exponerar också AddHighlightAnnotation för en ny sida och AddLoadedHighlightAnnotation för ett redan laddat dokument, och trots det nästan identiska namnet är detta en helt annan mekanism: båda skriver en riktig ISO 32000-1 §12.5.6.10-textmarkeringsannotation, PDF /Subtype /Highlight, in i sidans /Annots-array, med /QuadPoints som markerar exakt vilken glyf-sekvens, och varje regelrätt PDF-visare renderar den så fort filen sparats, inte bara HotPDF:s egen. Samma mekanismgräns avgör om en markering går tur-och-retur genom XFDF: en annotation skapad med AddLoadedHighlightAnnotation är ett vanligt PDF-objekt som ExportLoadedAnnotationsToXFDF plockar upp och lämnar till Acrobat eller ett annat granskningsverktyg som ISO 19444-1-markering, vilket täcks i att importera och exportera PDF-annoteringar som XFDF i Delphi, medan en region tillagd via AddHighlightRegion är osynlig för den exporten eftersom den aldrig skrevs till objektgrafen alls: den existerar bara så länge som den THPDFViewerModel som skapade den gör det. Hela familjen av markerings- och geometriska annotationstyper tillgängliga på en sida, och hur en rektangel placerar var och en, täcks i artikeln om PDF-annoteringar i Delphi med HotPDF, och den praktiska regeln är enkel: håll en markering engångs medan ett dokument fortfarande diskuteras, och befäst den till en annotation när ett beslut är slutgiltigt
Var markeringslagret slutar
Markeringslagret, för sin del, gör inget försök att se ut som en genomskinlig överstrykningspenna: RefreshDocument ritar varje region som en rektangel med tvåpixelskontur i sin egen färg ovanpå den cachade sidbitmappen, på samma sätt som den ritar söktreffar, i stället för att blanda en färgad fyllning över texten under, så en klassisk gul överstrykning måste målas i applikationskod eller skjutas upp till en befäst annotations egen utseendeström. En förmåga värd att återanvända när en region väl finns är CreateCurrentPageRegionSnapshot, som tar samma THPDFRectangle en markering redan bär och renderar just det området till en bitmapp, användbart för att bifoga en liten förhandsgranskningsbild till en granskningskommentar utan att exportera hela sidan. Ett granskningsbygge behöver inte välja mellan de två mekanismerna i förväg: låt varje ny markering som standard vara en engångs-THPDFViewerHighlight-region så länge en kommentartråd hålls öppen, och anropa AddLoadedHighlightAnnotation först när en granskare löser den, vilket håller den laddade PDF:en orörd under fram-och-tillbaka-processen som ger mest omsättning. Visarkontrollen som beskrivs här är en del av standardversionen av HotPDF-komponenten för Delphi och C++Builder, tillsammans med resten av annoterings- och formulär-API:erna som refereras ovan