Τεχνικό Άρθρο

Αξιολόγηση σημειώσεων PDF στο Delphi με το PDFium Component

Μια σχόλιο-σημείωση (annotation) PDF είναι ένα λεξικό προσαρτημένο σε μια σελίδα, όχι ένα σημάδι ζωγραφισμένο πάνω της. Το ISO 32000-1 §12.5 ορίζει περίπου δύο ντουζίνες υποτύπους, και καθένας φέρει ένα /Subtype, ένα ορθογώνιο σε συντεταγμένες σελίδας, ένα σύνολο σημαιών, και συνήθως μια ροή εμφάνισης (appearance stream) που καθορίζει τι πραγματικά ζωγραφίζει ένας viewer. Οι υπότυποι δεν σημαίνουν όλοι το ίδιο πράγμα για κάποιον που εξετάζει ένα έγγραφο. Ένα Highlight και μια πινελιά Ink είναι σχόλια· ένα Link είναι πλοήγηση· ένα Popup είναι το μικρό παράθυρο που ανοίγει όταν κάνετε κλικ σε μια αυτοκόλλητη σημείωση, αποθηκευμένο ως δικό του αντικείμενο και δείχνεται από έναν γονέα. Οι απαντήσεις είναι πλήρεις σχολιασμοί Text που αναφέρονται στο σχόλιο που απαντούν μέσω μιας καταχώρισης in-reply-to. Έτσι, ο πίνακας annotations σε επίπεδο σελίδας δεν είναι η λίστα σχολίων του ελεγκτή. Είναι μια επίπεδη τσάντα που περιέχει σχόλια, την υδραυλική εγκατάσταση που τα συνδέει, και αρκετά πράγματα που κανένας ελεγκτής δεν θα αποκαλούσε σχόλιο. Ένα πάνελ που αντιμετωπίζει τον πίνακα ως τη λίστα σχολίων θα διαφωνήσει με κάθε άλλον viewer που τρέχει ο πελάτης

Η δημιουργία μιας ροής εργασίας ελέγχου annotations πάνω στο PDFium Component, το component VCL/LCL βασισμένο σε PDFium για Delphi, C++Builder και Lazarus, σημαίνει να συγκεντρωθείτε στα σημεία όπου αυτό το χάσμα ανάμεσα στον ακατέργαστο πίνακα και την ανθρώπινη όψη προκαλεί προβλήματα: καταμέτρηση, ευρετηρίαση, επαναχρωματισμός σημαδιών που η μηχανή έχει ήδη παγώσει, διαγραφή χωρίς να μένουν φαντάσματα, και προσθήκη δικών σας σημαδιών

Διάγραμμα που δείχνει πώς ένα πάνελ αναθεώρησης PDFium Delphi φιλτράρει τον ακατέργαστο πίνακα σχολιασμών σελίδας από σχόλια, popups, απαντήσεις και συνδέσμους στην επιμελημένη λίστα σχολίων που βλέπει ο αναθεωρητής
Ο πίνακας σχολιασμών σελίδας ανακατεύει σχόλια με popups, απαντήσεις, συνδέσμους και κρυφές σημάνσεις, οπότε ένας πίνακας αναθεώρησης χρειάζεται κανόνα μέτρησης πριν δείξει σύνολο

Γιατί η μέτρησή σας ποτέ δεν ταιριάζει με το πάνελ σχολίων του Acrobat

Ανοίξτε ένα σχολιασμένο συμβόλαιο στον viewer σας και στο Acrobat δίπλα-δίπλα και τα σύνολα σπάνια συμφωνούν. Το Acrobat δείχνει μια επιμελημένη όψη: σχολιασμός ομαδοποιημένος σε νήματα απαντήσεων, popups διπλωμένα μέσα στις σημειώσεις στις οποίες ανήκουν, links και widgets φόρμας παραλειπόμενα. Ο ακατέργαστος πίνακας κρατά όλα αυτά αδιαφοροποίητα, οπότε μια αφελής μέτρηση βγαίνει ψηλή με έναν τρόπο και χαμηλή με άλλον ταυτόχρονα

Τα popups διογκώνουν το σύνολο, επειδή κάθε αυτοκόλλητη σημείωση συνοδεύεται από ξεχωριστό αντικείμενο Popup και η καταμέτρηση και των δύο διπλασιάζει τη σημείωση. Οι απαντήσεις μειώνουν το σύνολο αν φιλτράρετε βάσει ορατών σημαδιών, αφού μια απάντηση είναι ένα annotation Text χωρίς τίποτα ζωγραφισμένο μέχρι κάποιος να αναπτύξει το νήμα, και η παράλειψή της χάνει τη συζήτηση. Οι σημαίες Hidden και NoView αφαιρούν ένα annotation από την οθόνη χωρίς να το αφαιρούν από τον πίνακα, οπότε μια μέτρηση τυφλή στις σημαίες περιλαμβάνει σημάδια που ο χρήστης δεν μπορεί να δει. Τα annotations Link κάθονται στον ίδιο πίνακα με τα σχόλια και δεν ανήκουν ούτε στη μέτρηση ούτε στη λίστα. Αποφασίστε τον κανόνα μέτρησης πριν γράψετε τον βρόχο, και καταγράψτε την απόφαση, επειδή το «γιατί το πάνελ σας δείχνει διαφορετικό αριθμό από το Acrobat» είναι το πρώτο ticket που κερδίζει ένα χαρακτηριστικό ελέγχου

Ευρετηριάστε τα πάντα μία φορά, και μετά ποτέ μην ξαναναλύσετε μια σελίδα

Ένας κανόνας σχεδιασμού καθοδηγεί όλα όσα ακολουθούν: το φιλτράρισμα ανά συγγραφέα, τύπο ή σελίδα δεν πρέπει ποτέ να ξαναναλύει αντικείμενα σελίδας. Σε ένα έγγραφο 300 σελίδων με βαρύ σχολιασμό, η επανανάλυση σε κάθε αλλαγή dropdown μετατρέπει το πάνελ σε κάτι που κολλάει για δευτερόλεπτα κάθε φορά. Το component εκθέτει την AnnotationCount και την ευρετηριασμένη ιδιότητα Annotation[], και οι δύο με εμβέλεια την τρέχουσα φορτωμένη σελίδα, και η εγγραφή TPdfAnnotation που επιστρέφουν φέρει ό,τι χρειάζεται μια προβολή λίστας: Subtype, Flags, Color, Rectangle, ContentsText, AuthorText. Η σωστή κίνηση είναι να σαρώσετε κάθε σελίδα μία φορά κατά το άνοιγμα και να κρατήσετε το δικό σας επίπεδο ευρετήριο:

procedure TReviewPanel.BuildIndex;
var
  PageNo, i: Integer;
  A: TPdfAnnotation;
begin
  FItems.Clear;
  for PageNo := 1 to Pdf.PageCount do
  begin
    Pdf.PageNumber := PageNo;
    for i := 0 to Pdf.AnnotationCount - 1 do
    begin
      A := Pdf.Annotation[i];
      // Κρατήστε μόνο τους υποτύπους που ενδιαφέρουν τον ελεγκτή· καταγράψτε το ζεύγος
      // σελίδας και δείκτη, επειδή κάθε μεταγενέστερη επεξεργασία απευθύνεται μέσω αυτού
      if A.Subtype in [anText, anHighlight, anInk] then
        FItems.Add(TReviewItem.Create(PageNo, i,
          A.AuthorText, A.ContentsText, A.Rectangle, A.Color));
    end;
  end;
end;

Το ζεύγος που αξίζει να υπογραμμιστεί είναι το (PageNo, i). Κάθε μεταγενέστερη μεταβολή, είτε επαναχρωματισμός είτε διαγραφή, απευθύνεται μέσω αριθμού σελίδας συν δείκτη annotation, και ο δείκτης είναι εύθραυστος: η αφαίρεση ενός annotation επαναριθμεί όλα όσα ακολουθούν σε εκείνη τη σελίδα. Οπότε σχεδιάστε να ξαναχτίσετε τις καταχωρίσεις της επηρεαζόμενης σελίδας μετά από κάθε διαγραφή αντί να επιδιορθώνετε αριθμούς δεικτών επιτόπου. Η ανακατασκευή κοστίζει ένα χιλιοστό του δευτερολέπτου. Ένας μπαγιάτικος δείκτης, αντίθετα, διαγράφει το σχόλιο του λάθος ελεγκτή, κάτι που είναι το είδος σφάλματος που διαβρώνει την εμπιστοσύνη σε ολόκληρο το χαρακτηριστικό

Το threading αξίζει μια θέση στο ευρετήριο ακόμα κι αν η πρώτη σας έκδοση απλώς μετρά τις απαντήσεις αντί να τις δείχνει. Ομαδοποιήστε στοιχεία βάσει της αναφοράς γονέα τους ενώ έχετε τη σελίδα ανοιχτή, ώστε το πάνελ να μπορεί αργότερα να διπλώσει ένα νήμα όπως κάνει το Acrobat. Η ανακατασκευή αυτής της ομαδοποίησης τεμπέλικα κατά την κύλιση ματαιώνει ολόκληρο το νόημα της μία φορά ευρετηρίασης, επειδή ξανανοίγει σελίδες που ήδη πληρώσατε για να αναλυθούν. Η γεωμετρία θέλει την ίδια πειθαρχία. Το Rectangle σε κάθε εγγραφή είναι σε χώρο σελίδας, και η μετατροπή του σε συντεταγμένες view ανήκει σε έναν κοινό helper, όχι σκόρπια μέσα στον κώδικα. Τα πάνελ αναπτύσσουν σφάλματα συντεταγμένων όταν η επιλογή, το hit-testing και η ζωγραφική εφευρίσκουν το καθένα δικά του μαθηματικά zoom και περιστροφής· δρομολογήστε και τα τρία μέσα από μία μόνο μετατροπή και μια επισήμανση, η γραμμή της στη λίστα, και ο στόχος κλικ της παραμένουν καρφωμένα στο ίδιο σημάδι

Επαναχρωματισμός σχολιασμού και το βέτο της ροής εμφάνισης

Η αλλαγή μιας επισήμανσης από κίτρινο σε κεχριμπαρί ακούγεται σαν μια γραμμή κώδικα, και μερικές φορές είναι. Η παγίδα είναι το ISO 32000-1 §12.5.5. Όταν ένα annotation φέρει μια ροή εμφάνισης /AP, ένας συμμορφούμενος viewer ζωγραφίζει εκείνη την προχτισμένη ροή και αντιμετωπίζει την καταχώριση χρώματος στο λεξικό ως νεκρά μεταδεδομένα. Το Acrobat γράφει ροές εμφάνισης για ουσιαστικά όλα όσα δημιουργεί, οπότε τα περισσότερα annotations που φτάνουν από πελάτες βρίσκονται ήδη σε αυτή την κατάσταση, και το χρώμα που ορίσατε τόσο σίγουρα ποτέ δεν φτάνει στην οθόνη. Ο επαναχρωματισμός είναι ένα read-modify-write μέσω της ιδιότητας Annotation[], και το component είναι ειλικρινές για τη σύγκρουση: όταν η μηχανή αρνείται να αφήσει ένα χρώμα λεξικού να παρακάμψει μια ενσωματωμένη εμφάνιση, η εγγραφή εγείρει EPdfError

Διάγραμμα της διαδρομής επαναχρωματισμού ανάγνωσης-τροποποίησης-εγγραφής σε component Delphi PDFium όπου μια προψημένη ροή εμφάνισης θετοίζει το χρώμα του λεξικού και εγείρει EPdfError
Όταν ένας σχολιασμός κουβαλά προκατασκευασμένη ροή /AP η μηχανή αρνείται το χρώμα του λεξικού και προκαλεί EPdfError, οπότε ο πίνακας χρωματίζει ξανά το δικό του overlay ή σημαίνει τη γραμμή ως κλειδωμένη σε εμφάνιση
A := Pdf.Annotation[Item.Index];
A.HasColor := True;
A.Color := $0000B0FF;       // κεχριμπαρί
A.ColorAlpha := 160;
try
  Pdf.Annotation[Item.Index] := A;
except
  on EPdfError do
  begin
    // Το annotation κατέχει μια προαποδοσμένη ροή /AP· το χρώμα του λεξικού
    // από μόνο του δεν μπορεί να αλλάξει τι ζωγραφίζουν οι viewers
    Item.AppearanceLocked := True;
    StatusBar.SimpleText := 'Color is fixed by the annotation appearance';
  end;
end;

Πιάστε αυτή την εξαίρεση κάθε φορά, και αντιμετωπίστε την ως πληροφορία παρά ως αποτυχία. Παραλείψτε τη διασφάλιση και το πάνελ σας δείχνει χαρούμενα κεχριμπαρί στη δική του λίστα ενώ η σελίδα συνεχίζει να ζωγραφίζει κίτρινο· ο χρήστης το καταγγέλλει εβδομάδες αργότερα ως «ο viewer σας αγνοεί τις επεξεργασίες μου», και εσείς περνάτε ένα απόγευμα αποτυγχάνοντας να το αναπαράγετε σε ένα αρχείο που τυχαίνει να μην έχει ροή εμφάνισης. Μόλις ξέρετε ότι η εμφάνιση είναι κλειδωμένη, έχετε δύο έντιμες απαντήσεις: επαναχρωματίστε το δικό σας overlay επιλογής αντί για το annotation, ώστε ο ελεγκτής να βλέπει τουλάχιστον την επισήμανση που διάλεξε, ή σημειώστε τη γραμμή ως κλειδωμένη εμφάνιση ώστε κανείς να μην περιμένει ότι η αλλαγή θα κολλήσει

Διαγραφή annotations χωρίς να μένουν φαντάσματα

Η DeleteAnnotation αφαιρεί το αντικείμενο από το δέντρο annotations της τρέχουσας σελίδας, αλλά αφήνει ανέγγιχτο το raster σελίδας που βρίσκεται σε cache. Ζωγραφίστε αμέσως μετά την κλήση και η διαγραμμένη επισήμανση βρίσκεται ακόμα στην οθόνη, καθισμένη σε ένα bitmap που πλέον δεν ταιριάζει με το μοντέλο εγγράφου από πίσω του. Η λύση είναι να αντιμετωπίσετε την επανα-απόδοση ως μέρος της διαγραφής, όχι ως βήμα που ο καλών μπορεί να ξεχάσει:

Διάγραμμα του κύκλου διαγραφής PDFium Delphi τριών βημάτων που αφαιρεί τον σχολιασμό, επανα-αποδίδει τη σελίδα με reAnnotations και ξαναχτίζει τον δείκτη σελίδας
Η διαγραφή αγγίζει μόνο το δέντρο σχολιασμών, οπότε ο πίνακας πρέπει να ξανα-αποδώσει με reAnnotations και να ξαναχτίσει τις εγγραφές σελίδας πριν η οθόνη και ο δείκτης ξαναγίνουν ειλικρινείς
Pdf.PageNumber := Item.PageNo;
Pdf.DeleteAnnotation(Item.Index);   // εγείρει EPdfError σε αποτυχία
Bmp := Pdf.RenderPage(0, 0, ViewWidth, ViewHeight, ro0, [reAnnotations]);
try
  PaintPageBitmap(Bmp);
finally
  Bmp.Free;  // η RenderPage παραδίδει την κυριότητα του bitmap στον καλούντα
end;
RebuildPageEntries(Item.PageNo);  // οι δείκτες μετά το Item.Index έχουν μετατοπιστεί

Δύο λεπτομέρειες σε εκείνο το block είναι εύκολο να τις κάνετε λάθος. Η επιλογή reAnnotations πρέπει να είναι παρούσα, αλλιώς το νέο raster ρίχνει κάθε εναπομείναν annotation και η σελίδα φαίνεται σαν να σβήσατε ολόκληρο το σύνολο σχολίων αντί για ένα σημάδι. Και το Bmp.Free δεν είναι προαιρετικό: η υπερφόρτωση της RenderPage τύπου συνάρτησης παραδίδει την κυριότητα του bitmap στον καλούντα, οπότε μια χαμένη απελευθέρωση διαρρέει ένα raster ολόκληρης σελίδας σε κάθε μεμονωμένη διαγραφή, κάτι που ένας ελεγκτής που δουλεύει σε ένα μακρύ έγγραφο θα το μετατρέψει σε πραγματική πίεση μνήμης μέσα σε λεπτά

Προσθήκη σημαδιών ελεγκτή από το δικό σας UI

Η δημιουργία annotations περνά μέσα από την CreateAnnotation, η οποία δέχεται μια γεμάτη εγγραφή TPdfAnnotation (υπότυπος, ορθογώνιο, χρώμα, περιεχόμενο, συγγραφέας) και την προσαρτά στην τρέχουσα σελίδα. Μια αυτοκόλλητη σημείωση, υπότυπος anText, είναι η εύκολη περίπτωση: ορίστε τη θέση, το περιεχόμενο και τον συγγραφέα και έχετε τελειώσει. Τα annotations Ink είναι εκεί όπου πιάνονται οι άνθρωποι. Το ορθογώνιο της εγγραφής μόνο οριοθετεί το σχέδιο· οι ίδιες οι πινελιές είναι πίνακες σημείων που πρέπει να προσαρτηθούν ξεχωριστά μέσω της κλήσης ink-stroke της μηχανής, της FPDFAnnot_AddInkStroke τροφοδοτούμενης με δεδομένα FS_POINTF, καταγεγραμμένα από είσοδο ποντικιού ή πένας μία πινελιά τη φορά. Χτίστε ένα annotation Ink από ένα ορθογώνιο και τίποτα άλλο και παίρνετε ένα άδειο μουτζούρωμα που αποδίδεται ως κενός χώρος, κάτι που μοιάζει με σφάλμα στη μηχανή και στην πραγματικότητα είναι ένα μισοτελειωμένο annotation

Καθορίστε την πολιτική συγγραφέα στην ίδια ανάσα. Κάθε σημάδι που δημιουργεί το UI σας θα πρέπει να φέρει ένα συνεπές AuthorText, επειδή το φίλτρο ελεγκτή που θα χτίσετε τον επόμενο μήνα είναι τόσο καλό όσο τα ονόματα που σφραγίζετε στα σχόλια σήμερα. Κενές ή ασυνεπείς συμβολοσειρές συγγραφέα δεν μπορούν να επιδιορθωθούν αναδρομικά χωρίς να ξανανοίξετε κάθε αρχείο

Βγάζοντας τον έλεγχο έξω από τον viewer

Τα δεδομένα ελέγχου δικαιολογούν την ύπαρξή τους μόλις μπορέσουν να φύγουν από τον viewer, ως μια περίληψη που διαβάζει ο επικεφαλής έργου χωρίς να ανοίξει το αρχείο ή ως CSV που τροφοδοτεί ένα φύλλο παρακολούθησης. Εξάγετε από το ευρετήριο που ήδη χτίσατε, ποτέ από μια φρέσκια ανάλυση, και διαλέξτε έναν σταθερό τρόπο να αναφέρεστε ξανά σε κάθε σημάδι. Ένας αριθμός σελίδας σε ζεύγος με το ορθογώνιο του annotation επιβιώνει σε round-trips που ένας δείκτης πίνακα δεν επιβιώνει, επειδή η επόμενη διαγραφή αθόρυβα επαναριθμεί τους δείκτες και το CSV σας αρχίζει να δείχνει στα λάθος σχόλια

Μια γραμμή που αξίζει να κρατηθεί φέρει τη σελίδα, τον υπότυπο, τον συγγραφέα, τη χρονική σφραγίδα δημιουργίας όταν το αρχείο καταγράφει μία, το κείμενο περιεχομένου, και μια στήλη κατάστασης που κατέχετε εσείς αντί για μία που παρέχει το PDF. Το ίδιο πέρασμα ευρετηρίασης είναι χρήσιμο νωρίτερα, κατά την εισαγωγή, όταν ένα έγγραφο φτάνει από έξω από την ομάδα και θέλετε να ξέρετε τι περιέχει προτού κάποιος το ελέγξει. Το άρθρο για τον πάγκο εργασίας εισαγωγής και ελέγχου PDF εξηγεί εκείνη τη διαλογή, και η πλοήγηση πεδίων φόρμας καλύπτει το κατοπτρικό πρόβλημα: τον έλεγχο εγγράφων φτιαγμένων για να συλλέγουν δεδομένα αντί για σχόλια

Μια περίπτωση που ο πίνακας δεν θα σας δείξει

Ένας τρόπος αποτυχίας αξίζει μια σημείωση επειδή μοιάζει με ελάττωμα στον κώδικά σας και δεν είναι. Ένας πελάτης αναφέρει ορατές επισημάνσεις σε ολόκληρη μια σελίδα, αλλά το πάνελ σας δεν παραθέτει τίποτα, και η AnnotationCount επιστρέφει μηδέν. Η συνήθης εξήγηση είναι ότι τα σημάδια ισοπεδώθηκαν (flattened) κάπου νωρίτερα στη ροή. Το flattening ψήνει τις εμφανίσεις annotations μέσα στο συνηθισμένο περιεχόμενο σελίδας, οπότε οι επισημάνσεις γίνονται μέρος των γραφικών της σελίδας και σταματούν να υπάρχουν ως αντικείμενα annotation εντελώς. Δεν μένει τίποτα για ένα API annotation να απαριθμήσει, να επαναχρωματίσει ή να διαγράψει. Όταν βλέπετε ζωγραφισμένο σχολιασμό με μηδενική μέτρηση, σταματήστε να ψάχνετε το σφάλμα στον βρόχο απαρίθμησής σας και ρωτήστε πώς παράχθηκε το αρχείο

Η επιφάνεια annotations που χρησιμοποιήθηκε εδώ, από την απαρίθμηση και τη δημιουργία μέχρι τον επαναχρωματισμό, τη διαγραφή, και τις επιλογές απόδοσης που κρατούν την εμφάνιση ειλικρινή, συνοδεύει το PDFium Component για Delphi, C++Builder και Lazarus/FPC