Ένα ορθογώνιο σχεδιασμένο γύρω από μια παράγραφο κατά τη διάρκεια ανασκόπησης δεν χρειάζεται να γίνει σημάδι μέσα στο PDF. Το THPDFViewerModel του HotPDF εκθέτει το AddHighlightRegion, μια μέθοδο που κρατά κάθε επισήμανση ως εγγραφή στη μνήμη αντί για αλλαγή στο φορτωμένο έγγραφο, ώστε ένας αναθεωρητής να μπορεί να σημειώνει δεκάδες σελίδες ενώ το αρχείο στον δίσκο παραμένει byte προς byte ό,τι ήταν. Κάντε zoom στο 6400%, περιστρέψτε τη σελίδα κατά 90 μοίρες, αλλάξτε από Fit Width σε Fit Page, και το ίδιο ορθογώνιο εξακολουθεί να προσγειώνεται στην ίδια παράγραφο, επειδή τα μαθηματικά συντεταγμένων περνούν μέσα από την πραγματική γεωμετρία απόδοσης τη στιγμή που σχεδιάστηκε το σημάδι
Τα εργαλεία ανασκόπησης χτισμένα γύρω από έναν PDF viewer πέφτουν συνεχώς σε αυτό το πρόβλημα. Μια οθόνη redlining, ένα πέρασμα QA πάνω από τιμολόγια που δημιουργήθηκαν αυτόματα, μια εσωτερική ροή εργασίας έγκρισης: όλα χρειάζονται να επιτρέπουν σε κάποιον να τραβήξει την προσοχή σε μια περιοχή μιας σελίδας χωρίς κάθε πρόχειρο σημάδι να γίνεται μόνιμη αλλαγή στο αρχείο, και χωρίς να καταφεύγουν σε ένα πλήρες υποσύστημα σχολίων μόνο για να δείξουν ένα έγχρωμο κουτί ενώ κάποιος ακόμη αποφασίζει αν το σημάδι ανήκει. Το HotPDF απαντά σε αυτό με ένα αφιερωμένο επίπεδο επισήμανσης που βρίσκεται εξ ολοκλήρου στην πλευρά Model του διαχωρισμού που περιγράφεται στο χτίσιμο ενός προσαρμοσμένου PDF viewer με αρχιτεκτονική MVC σε Delphi, κάτι που εξηγεί επίσης γιατί η ίδια λίστα επισημάνσεων μπορεί να οδηγηθεί από ένα unit test χωρίς κανένα window handle σε αναμμα
Τι αποθηκεύει πραγματικά το AddHighlightRegion του HotPDF;
Το AddHighlightRegion αποθηκεύει ακριβώς τρία πράγματα ανά σημάδι: έναν δείκτη σελίδας βασισμένο στο μηδέν, ένα THPDFRectangle σε συντεταγμένες χώρου χρήστη PDF, και ένα TColor, όλα συσκευασμένα ως εγγραφή THPDFViewerHighlight μέσα στο THPDFViewerModel. Η κλήση Viewer.HighlightRegion(PageIndex, PageRect, clYellow), ή το ισοδύναμο Model.AddHighlightRegion, προσαρτά μία από αυτές τις εγγραφές σε έναν ιδιωτικό πίνακα και επιστρέφει τον δείκτη της, και αυτός ο δείκτης είναι ο μόνος χειριστής που παίρνει πίσω ένας καλών: δεν υπάρχει ξεχωριστό αντικείμενο, καμία διεπαφή μετρημένη με αναφορές, τίποτα να ελευθερωθεί. Κάθε άλλη δυνατότητα σε αυτό το άρθρο, η σχεδίαση του σημαδιού, η επαναχαρτογράφησή του μετά από αλλαγή zoom, η διαγραφή του, χτίζεται πάνω σε αυτή τη μία μικρή εγγραφή
Κάθε ορθογώνιο κανονικοποιείται και αποκόπτεται πριν γίνει αποδεκτό. Το AddHighlightRegion εναλλάσσει τις αριστερή και δεξιά ακμές αν ένας αναθεωρητής σύρει από δεξιά προς αριστερά, εναλλάσσει πάνω και κάτω για ένα σύρσιμο προς τα πάνω, έπειτα αποκόπτει το αποτέλεσμα έναντι του MediaBox της σελίδας που ανακτάται μέσω GetLoadedPageBox. Ένα ορθογώνιο που καταλήγει με μηδενικό πλάτος, μηδενικό ύψος, ή εντελώς έξω από τη σελίδα απορρίπτεται εντελώς: η μέθοδος επιστρέφει -1 και τίποτα δεν προστίθεται στη λίστα. Αυτή η τιμή επιστροφής δεν είναι διακοσμητική: μια παρτίδα επισημάνσεων που ανακατασκευάζεται από ένα εξωτερικό αρχείο ανασκόπησης, ή από παρωχημένες συντεταγμένες μετά την αντικατάσταση μιας σελίδας, μπορεί σιωπηλά να χάσει καταχωρίσεις αν ο καλών δεν το ελέγξει
Πώς παραμένει μια επισήμανση ευθυγραμμισμένη μετά από zoom ή περιστροφή;
Μια επισήμανση παραμένει ευθυγραμμισμένη επειδή το HotPDF την αποθηκεύει σε χώρο σελίδας PDF και την επαναπροβάλλει σε χώρο οθόνης σε κάθε επανασχεδίαση, αντί να αποθηκεύει ένα ορθογώνιο οθόνης που θα παρωχούσε τη στιγμή που αλλάζει το επίπεδο zoom. Τα THPDFViewerModel.PagePointToView και το αντίστροφό του, ViewPointToPage, κάνουν αυτή την προβολή σε δύο στάδια: πρώτα τη δική της εγγραφή /Rotate της σελίδας, έπειτα την ανεξάρτητη ViewRotation του Viewer, η οποία ποτέ δεν εγγράφεται πίσω στο PDF και επηρεάζει μόνο ό,τι εμφανίζει ο Viewer. Η αναίρεση του μετασχηματισμού κατά την απελευθέρωση του ποντικιού εκτελεί τα ίδια δύο στάδια αντίστροφα, κάτι που επιτρέπει σε μια επισήμανση σχεδιασμένη σε υψηλό zoom σε μια σελίδα περιστραμμένη κατά 270 μοίρες να προσγειώνεται ακριβώς στη σωστή θέση αφού ο αναθεωρητής επαναφέρει την όψη σε Fit Page
Το DPI που χρησιμοποιείται για αυτή την προβολή έχει εξίσου μεγάλη σημασία με την περιστροφή. Ο Viewer του HotPDF συλλαμβάνει το ακριβές DPI του bitmap που βρίσκεται αυτή τη στιγμή στην οθόνη στο FRenderedDPI αμέσως μετά από κάθε απόδοση, και το ImageMouseUp περνά αυτή την ίδια τιμή στο ViewPointToPage ώστε μια συντεταγμένη ποντικιού να μετατρέπεται πάντα χρησιμοποιώντας την ανάλυση με την οποία σχεδιάστηκε πράγματι, όχι μια ανάλυση επανυπολογισμένη από την τρέχουσα ιδιότητα zoom. Τα CreatePageSnapshot και οι συγγενείς του περιορίζουν το DPI σε εύρος 12 έως 2400, αλλά η διαδραστική διαδρομή απόδοσης δεν φέρει τέτοιο ανώτατο όριο: η τυπική κλίμακα zoom φτάνει έως 6400%, που υπολογίζεται σε πάνω από 2400 DPI στην προεπιλεγμένη βάση 96 DPI, οπότε η επαναχρησιμοποίηση ενός ορίου τύπου snapshot για τη χαρτογράφηση συντεταγμένων θα μετατόπιζε κάθε επισήμανση κατά αρκετά pixel στην κορυφή του εύρους zoom. Δύο μικρότερες προεπιλογές ολοκληρώνουν την αλληλεπίδραση: ένα σύρσιμο μικρότερο από δύο pixel σε οποιονδήποτε άξονα αντιμετωπίζεται ως κλικ και δεν παράγει επισήμανση, και η επισήμανση δεν μπορεί να ξεκινήσει μέχρι τουλάχιστον μία σελίδα να έχει πράγματι αποδοθεί, αφού το FRenderedDPI ξεκινά από μηδέν
Καλωδίωση διαδραστικής επισήμανσης σε μια οθόνη ανασκόπησης
Η ενεργοποίηση διαδραστικής επισήμανσης είναι μια δουλειά τριών ιδιοτήτων στο ίδιο το στοιχείο ελέγχου THPDFViewer: ορίστε το InteractionMode σε vimHighlight αντί για το προεπιλεγμένο vimBrowse, επιλέξτε ένα HighlightColor, που εξ ορισμού είναι clYellow, και χειριστείτε το OnMarqueeSelect για να μάθετε τι μόλις σχεδίασε ο αναθεωρητής. Όλα τα υπόλοιπα, η σύλληψη του ποντικιού, η σχεδίαση του διάστικτου ορθογωνίου επιλογής ενώ σέρνει ο αναθεωρητής, η μετατροπή του σημείου απελευθέρωσης πίσω σε χώρο σελίδας, η κλήση του AddHighlightRegion, συμβαίνουν μέσα στο στοιχείο ελέγχου πριν πυροδοτηθεί εκείνο το συμβάν
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 πυροδοτείται μόνο για ένα σύρσιμο που πράγματι παρήγαγε μια επισήμανση: ένα κλικ πολύ μικρό για να μετρήσει ως σύρσιμο καθαρίζει αμέσως το επίστρωμα επιλογής, και ένα σύρσιμο που προσγειώνεται εντελώς έξω από τη σελίδα φτάνει στο AddHighlightRegion αλλά απορρίπτεται εκεί με τον ίδιο τρόπο που θα απορριπτόταν μια προγραμματιστική κλήση, οπότε το συμβάν παραμένει σιωπηλό και στις δύο περιπτώσεις. Μια λεπτομέρεια υλοποίησης που αξίζει να γνωρίζετε αν η επισήμανση φαίνεται ποτέ να σταματά να ανταποκρίνεται στις άκρες του στοιχείου ελέγχου: η σύλληψη ποντικιού ανήκει στο ίδιο το THPDFViewer, έναν απόγονο TScrollBox, όχι στο εσωτερικό TImage που δείχνει το bitmap της σελίδας, κάτι που επιτρέπει σε έναν αναθεωρητή να σύρει πέρα από την άκρη της αποδοθείσας σελίδας και να πάρει και πάλι μια καθαρή απελευθέρωση
Προσθήκη, αφαίρεση, και επανανάγνωση επισημάνσεων από κώδικα
Οι επισημάνσεις δεν χρειάζεται καθόλου να προέρχονται από σύρσιμο ποντικιού. Το Viewer.HighlightRegion(PageIndex, PageRect, Color), το οποίο κατευθύνεται στο ίδιο Model.AddHighlightRegion που καλεί εσωτερικά το διαδραστικό σύρσιμο, είναι δημόσιο ειδικά ώστε μια οθόνη ανασκόπησης να μπορεί να ανακατασκευάσει επισημάνσεις από δεδομένα που ήδη έχει: σχόλια φορτωμένα από μια βάση δεδομένων, αποτελέσματα από μια αναζήτηση κειμένου, ή σημάδια αποκατεστημένα από μια προηγούμενη συνεδρία. Επειδή οι συντεταγμένες είναι απλοί αριθμοί χώρου χρήστη PDF, τίποτα σε αυτή τη διαδρομή δεν εξαρτάται από το να έχει αποδοθεί πρώτα μια σελίδα, σε αντίθεση με το διαδραστικό σύρσιμο, που χρειάζεται το FRenderedDPI να κρατά ήδη μια πραγματική τιμή
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;
Η αφαίρεση μιας μεμονωμένης επισήμανσης είναι εκεί όπου διαφαίνεται η αποθήκευση βασισμένη σε πίνακα. Το RemoveHighlightRegion διαγράφει μία εγγραφή και μετατοπίζει κάθε μεταγενέστερη εγγραφή κατά μία θέση προς τα κάτω για να κλείσει το κενό, κάτι που σημαίνει ότι οποιοσδήποτε δείκτης συλλήφθηκε νωρίτερα, από ένα συμβάν OnMarqueeSelect ή από μια προηγούμενη απαρίθμηση, δεν είναι πλέον αξιόπιστος μόλις αφαιρεθεί κάτι πριν από αυτόν στη λίστα. Το OnHighlightChange πυροδοτείται σε κάθε προσθήκη, αφαίρεση, και κλήση ClearHighlightRegions, αλλά δεν φέρει καμία πληροφορία για το τι άλλαξε, οπότε το ασφαλές μοτίβο είναι να το αντιμετωπίζετε ως σήμα για να ανακατασκευάσετε οποιαδήποτε λίστα δείχνει ένα πάνελ ανασκόπησης από τα HighlightCount και TryGetHighlightRegion, αντί να επιδιορθώνετε επιτόπου έναν αποθηκευμένο σε 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;
Πότε πρέπει ένα σημάδι να γίνει πραγματική σχολιαστική επισήμανση (annotation) αντ' αυτού;
Μια περιοχή επισήμανσης πρέπει να γίνει πραγματική annotation τη στιγμή που χρειάζεται να επιβιώσει έξω από αυτό το ένα στιγμιότυπο THPDFViewer. Το HotPDF εκθέτει επίσης το AddHighlightAnnotation για μια νέα σελίδα και το AddLoadedHighlightAnnotation για ένα ήδη φορτωμένο έγγραφο, και παρά το σχεδόν πανομοιότυπο όνομα, αυτός είναι ένας εντελώς διαφορετικός μηχανισμός: και τα δύο γράφουν μια πραγματική annotation επισήμανσης κειμένου κατά ISO 32000-1 §12.5.6.10, PDF /Subtype /Highlight, στον πίνακα /Annots της σελίδας, με /QuadPoints να σημειώνει την ακριβή ακολουθία γλυφών, και οποιοσδήποτε συμμορφούμενος PDF viewer την αποδίδει μόλις αποθηκευτεί το αρχείο, όχι μόνο ο δικός του viewer του HotPDF. Το ίδιο όριο μηχανισμού αποφασίζει αν ένα σημάδι κάνει πήγαινε-έλα μέσω XFDF: μια annotation που δημιουργήθηκε με AddLoadedHighlightAnnotation είναι ένα κανονικό αντικείμενο PDF που το ExportLoadedAnnotationsToXFDF το παίρνει και το παραδίδει στο Acrobat ή σε άλλο εργαλείο ανασκόπησης ως σήμανση ISO 19444-1, που καλύπτεται στο εισαγωγή και εξαγωγή σχολίων PDF ως XFDF σε Delphi, ενώ μια περιοχή που προστέθηκε μέσω AddHighlightRegion είναι αόρατη σε αυτή την εξαγωγή επειδή ποτέ δεν γράφτηκε καθόλου στο γράφημα αντικειμένων: υπάρχει μόνο όσο υπάρχει το THPDFViewerModel που τη δημιούργησε. Η πλήρης οικογένεια τύπων σχολιαστικών και γεωμετρικών annotation διαθέσιμων σε μια σελίδα, και πώς ένα ορθογώνιο τοποθετεί καθέναν από αυτούς, καλύπτεται στο το άρθρο για τα annotation PDF σε Delphi με το HotPDF, και ο πρακτικός κανόνας είναι απλός: κρατήστε ένα σημάδι αναλώσιμο όσο ένα έγγραφο συζητείται ακόμη, και δεσμεύστε το σε annotation μόλις μια απόφαση είναι οριστική
Πού σταματά το επίπεδο επισήμανσης
Το επίπεδο επισήμανσης, από την πλευρά του, δεν κάνει καμία προσπάθεια να μοιάζει με έναν διάφανο μαρκαδόρο επισήμανσης: το RefreshDocument σχεδιάζει κάθε περιοχή ως ορθογώνιο περιγράμματος δύο pixel στο δικό του χρώμα πάνω στο αποθηκευμένο σε cache bitmap της σελίδας, με τον ίδιο τρόπο που σχεδιάζει τα ευρήματα αναζήτησης, αντί να αναμειγνύει μια έγχρωμη γέμιση πάνω από το κείμενο από κάτω, οπότε μια κλασική εμφάνιση κίτρινης επικάλυψης πρέπει να ζωγραφιστεί σε κώδικα εφαρμογής ή να αναβληθεί για τη δική της ροή εμφάνισης μιας αναβαθμισμένης annotation. Μια δυνατότητα που αξίζει να επαναχρησιμοποιηθεί μόλις υπάρχει μια περιοχή είναι το CreateCurrentPageRegionSnapshot, το οποίο παίρνει το ίδιο THPDFRectangle που ήδη φέρει μια επισήμανση και αποδίδει ακριβώς εκείνη την περιοχή σε bitmap, χρήσιμο για την επισύναψη μιας μικρής εικόνας προεπισκόπησης σε ένα σχόλιο ανασκόπησης χωρίς εξαγωγή ολόκληρης της σελίδας. Ένα build ανασκόπησης δεν χρειάζεται να διαλέξει μεταξύ των δύο μηχανισμών εκ των προτέρων: ορίστε εξ ορισμού κάθε νέο σημάδι ως αναλώσιμη περιοχή THPDFViewerHighlight όσο ένα νήμα σχολίων παραμένει ανοιχτό, και καλέστε το AddLoadedHighlightAnnotation μόνο μόλις το επιλύσει ένας αναθεωρητής, κάτι που κρατά το φορτωμένο PDF ανέγγιχτο κατά τη διάρκεια του πηγαινέλα που παράγει τη μεγαλύτερη εναλλαγή. Το στοιχείο ελέγχου viewer που περιγράφεται εδώ είναι μέρος του τυπικού εξαρτήματος HotPDF για Delphi και C++Builder, μαζί με το υπόλοιπο των API annotation και φορμών που αναφέρονται παραπάνω