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

Σχολιασμοί επισήμανσης κειμένου με PDFium QuadPoints στο Delphi

Το PDFium Component δημιουργεί σχολιασμούς επισήμανσης κειμένου (text markup annotations) — δηλαδή επισήμανση (highlight), υπογράμμιση (underline), διαγραφή (strikeout) και κυματιστή υπογράμμιση (squiggly) — μέσω της TPdf.CreateAnnotation: ορίζετε HasAttachmentPoints := True στην εγγραφή TPdfAnnotation και συμπληρώνετε το τετράπλευρο AttachmentPoints, και το συστατικό γράφει την καταχώριση QuadPoints που ορίζεται στο ISO 32000-1 §12.5.6.10. Αυτή είναι όλη η επιφάνεια του API. Ο λόγος που υπάρχει αυτό το άρθρο είναι αυτό που συμβαίνει από κάτω, επειδή η ακατέργαστη αλυσίδα κλήσεων του PDFium έχει μια κατάσταση αποτυχίας που παράγει το λιγότερο χρήσιμο σύμπτωμα: η FPDFAnnot_SetAttachmentPoints επιστρέφει false σε έναν νεοδημιουργηθέντα σχολιασμό, κάθε φορά, χωρίς κωδικό σφάλματος και χωρίς καμία ένδειξη. Αυτό είναι το συνοδευτικό άρθρο της πλευράς δημιουργίας για το άρθρο μας σχετικά με την ανάγνωση και την αναθεώρηση υπαρχόντων σχολιασμών, το οποίο διατρέχει τις ίδιες δομές προς την άλλη κατεύθυνση

Η σκηνή αποσφαλμάτωσης είναι πάντα η ίδια. Δημιουργείτε έναν σχολιασμό επισήμανσης, καλείτε τον setter των σημείων προσάρτησης με δείκτη 0, η συνάρτηση επιστρέφει false, και αρχίζετε να αμφιβάλλετε για τις συντεταγμένες σας. Μεταθέτετε τα σημεία, αναστρέφετε τον άξονα Y, ανταλλάσσετε το χώρο της σελίδας με το χώρο της συσκευής. Τίποτα από αυτά δεν βοηθά, επειδή οι συντεταγμένες δεν ήταν ποτέ το πρόβλημα. Το πρόβλημα είναι η σημασιολογία των δεικτών του C API, και μόλις την κατανοήσετε, η διόρθωση είναι δύο γραμμές

Tι σημαίνουν τα QuadPoints στο ISO 32000-1

Το QuadPoints είναι ένας πίνακας 8×n αριθμών που περιγράφει n τετράπλευρα, και το ISO 32000-1 §12.5.6.10 το απαιτεί σε κάθε σχολιασμό επισήμανσης κειμένου: κάθε τετράπλευρο επισημαίνει μια λέξη ή ομάδα συνεχόμενων λέξεων στις οποίες εφαρμόζεται η επισήμανση, η υπογράμμιση ή η διαγραφή. Η καταχώριση Rect του σχολιασμού εξακολουθεί να υπάρχει, αλλά για τους υποτύπους επισήμανσης οριοθετεί μόνο την περιοχή· τα quads είναι αυτά που σχεδιάζει πραγματικά η μηχανή απόδοσης. Χρησιμοποιείται τετράπλευρο αντί για ορθογώνιο επειδή το κείμενο μπορεί να είναι περιστραμμένο ή παραμορφωμένο, οπότε οι τέσσερις γωνίες αποθηκεύονται ως τέσσερα ανεξάρτητα σημεία: x1 y1 x2 y2 x3 y3 x4 y4

Η σειρά αυτών των τεσσάρων σημείων είναι το σημείο όπου η προδιαγραφή και η εγκατεστημένη βάση εφαρμογών διαφωνούν. Το κείμενο της προδιαγραφής περιγράφει τα σημεία ως σχεδίαση του τετραπλεύρου αριστερόστροφα, αλλά η ίδια η μηχανή απόδοσης της Adobe τα ερμήνευε πάντα σε μοτίβο Z: πρώτα η πάνω ακμή από αριστερά προς τα δεξιά, και στη συνέχεια η κάτω ακμή από αριστερά προς τα δεξιά. Επειδή κάθε δημιουργός έκανε δοκιμές έναντι του Acrobat, ουσιαστικά κάθε μηχανή απόδοσης, συμπεριλαμβανομένου του PDFium, ακολουθεί το μοτίβο Z, και αρχεία που ακολουθούν την κατά λέξη διατύπωση της προδιαγραφής αποδίδονται ως συμπτυγμένες ή στρεβλωμένες επισημάνσεις σε ορισμένες εφαρμογές προβολής. Η δομή FS_QUADPOINTSF του PDFium κωδικοποιεί ακριβώς αυτήν τη σύμβαση: το (x1,y1) είναι η πάνω αριστερή γωνία, το (x2,y2) η πάνω δεξιά, το (x3,y3) η κάτω αριστερή και το (x4,y4) η κάτω δεξιά, σε συντεταγμένες σελίδας όπου το Y αυξάνεται προς τα πάνω. Ακολουθήστε αυτήν τη σειρά και ξεμπερδέψτε· οι μηχανές απόδοσης είναι επιεικείς με πολλά πράγματα, αλλά ένα μπερδεμένο quad δεν είναι ένα από αυτά

Γιατί η FPDFAnnot_SetAttachmentPoints επιστρέφει false;

Η FPDFAnnot_SetAttachmentPoints αποτυγχάνει σε έναν νέο σχολιασμό επειδή το συμβόλαιό της είναι να αντικαταστήσει το τετράπλευρο σε έναν δεδομένο δείκτη, και ένας νεοδημιουργηθείς σχολιασμός έχει μηδέν τετράπλευρα προς αντικατάσταση. Η υπογραφή δέχεται μια λαβή σχολιασμού, έναν δείκτη quad_index και τα σημεία· ο δείκτης 0 δεν σημαίνει "η πρώτη θέση, δημιουργώντας την αν χρειάζεται", σημαίνει "το υπάρχον quad με αριθμό 0", και όταν η FPDFAnnot_CountAttachmentPoints αναφέρει 0, δεν υπάρχει τέτοιο quad και η κλήση επιστρέφει false. Η συνάρτηση που δημιουργεί μια θέση είναι η FPDFAnnot_AppendAttachmentPoints. Κάθε σχολιασμός που δημιουργείται μέσω της FPDFPage_CreateAnnot ξεκινά με μέτρηση μηδέν, οπότε η διαδρομή δημιουργίας πρέπει να καλέσει πρώτα την Append, και μόνο οι μεταγενέστερες ενημερώσεις μπορούν να καλέσουν τη Set

Αυτό επηρέασε και το ίδιο το PDFium Component. Έως την έκδοση v1.79.0, η εσωωτερική ρουτίνα που μοιράζονταν οι CreateAnnotation και SetAnnotation είχε σκληρά κωδικοποιημένη την κλήση FPDFAnnot_SetAttachmentPoints(Annotation, 0, ...), η οποία ήταν σωστή για την ενημέρωση ενός υπάρχοντος σχολιασμού επισήμανσης και εγγυημένα αποτυχημένη για έναν νέο, εμφανιζόμενη ως EPdfException με το μήνυμα 'Cannot set attachment points'. Η διόρθωση, η οποία στάλθηκε στην έκδοση v1.79.1, κάνει διακλάδωση ανάλογα με τη μέτρηση

// Inside the component's annotation writer (v1.79.1+):
// a new annotation has no quad slots yet, so Append creates
// the first one; Set only replaces a slot that already exists
if FPDFAnnot_CountAttachmentPoints(Annotation) = 0 then
  Check(FPDFAnnot_AppendAttachmentPoints(Annotation, QuadPoints) <> 0,
    'Cannot set attachment points')
else
  Check(FPDFAnnot_SetAttachmentPoints(Annotation, 0, QuadPoints) <> 0,
    'Cannot set attachment points');

Το ίδιο μοτίβο ισχύει αν καλέσετε απευθείας τις εξαγόμενες συναρτήσεις C, κάτι που σας επιτρέπει να κάνετε το συστατικό καθώς όλα τα σημεία εισόδου FPDFAnnot_* εμφανίζονται στο PDFium.pas. Κάθε φορά που κρατάτε μια λαβή FPDF_ANNOTATION και θέλετε να γράψετε quads, ρωτήστε πρώτα την FPDFAnnot_CountAttachmentPoints και δρομολογήστε ανάλογα. Εάν αναζητάτε τον λόγο που η "FPDFAnnot_SetAttachmentPoints επιστρέφει false", αυτή η διακλάδωση ελέγχου και στη συνέχεια προσάρτησης είναι σχεδόν σίγουρα η απάντησή σας

Δημιουργία επισήμανσης με την TPdf.CreateAnnotation

Με το συστατικό να εκτελεί τη δρομολόγηση Append έναντι Set για εσάς, η δημιουργία μιας επισήμανσης περιορίζεται στη συμπλήρωση μιας εγγραφής. Το παρακάτω παράδειγμα δημιουργεί μια σελίδα A4 και τοποθετεί μια ημιδιαφανή κίτρινη επισήμανση πάνω από μια περιοχή 200×20 σημείων· σημειώστε ότι το quad ακολουθεί τη σειρά Z που περιγράφεται παραπάνω, και ότι το Rectangle έχει οριστεί ώστε να εσωκλείει το quad, γεγονός που διατηρεί τη λογική συμπεριφορά των εφαρμογών προβολής που κάνουν hit-test έναντι του Rect

var
  Pdf: TPdf;
  A: TPdfAnnotation;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.CreateDocument;
    Pdf.AddPage(0, 595, 842);

    FillChar(A, SizeOf(A), 0);
    A.Subtype := anHighlight;
    A.HasColor := True;
    A.Color := clYellow;
    A.ColorAlpha := $80;                     // 50% opacity
    A.HasAttachmentPoints := True;
    A.AttachmentPoints[1].X := 50;  A.AttachmentPoints[1].Y := 700; // top-left
    A.AttachmentPoints[2].X := 250; A.AttachmentPoints[2].Y := 700; // top-right
    A.AttachmentPoints[3].X := 50;  A.AttachmentPoints[3].Y := 680; // bottom-left
    A.AttachmentPoints[4].X := 250; A.AttachmentPoints[4].Y := 680; // bottom-right
    A.Rectangle.Left := 50;  A.Rectangle.Top := 700;
    A.Rectangle.Right := 250; A.Rectangle.Bottom := 680;
    A.ContentsText := 'Highlighted region';
    Pdf.CreateAnnotation(A);

    Pdf.SaveAs('highlighted.pdf');
  finally
    Pdf.Free;
  end;
end;

Η αλλαγή υποτύπων κοστίζει μία γραμμή. Τα anUnderline, anStrikeout και anSquiggly λαμβάνουν την ίδια μορφή εγγραφής, quads και όλα, επειδή το ISO 32000-1 αντιμετωπίζει και τα τέσσερα ως την ίδια οικογένεια σχολιασμών που διακρίνονται μόνο από τον τρόπο διακόσμησης της περιοχής του quad. Οι υπότυποι που δεν είναι επισήμανση κειμένου, όπως οι anSquare, anCircle και anText, τοποθετούνται αποκλειστικά βάσει του Rectangle· αφήστε το HasAttachmentPoints στο False για αυτούς, και ο μηχανισμός των quads δεν εκτελείται ποτέ

Γιατί το AttachmentPoints[0] μεταγλωττίζεται στο Delphi αλλά αποτυγχάνει στο FPC;

Το TQuadrilateralPoint δηλώνεται ως array [1..4] of TPdfPoint, ένας πίνακας με βάση το 1, και αυτό μπερδεύει οποιονδήποτε έχει συνηθίσει σε ευρετήρια με βάση το μηδέν. Αν γράψετε A.AttachmentPoints[0], ο dcc32 του Delphi θα το μεταγλωττίσει χωρίς παράπονο, επειδή ο έλεγχος ορίων (range checking) είναι απενεργοποιημένος από προεπιλογή· κατά το χρόνο εκτέλεσης η έκφραση διαβάζει ή γράφει σιωπηρά τη μνήμη ακριβώς πριν από τον πίνακα, η οποία σε μια εγγραφή TPdfAnnotation είναι ένα διπλανό πεδίο. Η επισήμανσή σας λαμβάνει μια άχρηστη γωνία, ή ένα γειτονικό πεδίο καταστρέφεται, και τίποτα δεν εγείρει σφάλμα. Το Free Pascal εντόπισε αυτό ακριβώς το σφάλμα στις δικές μας πηγές επίδειξης κατά τη μεταφορά στο Lazarus: το fpc εκτελεί έλεγχο ορίων κατά το χρόνο μεταγλώττισης σε σταθερούς δείκτες και απέρριψε αμέσως το AttachmentPoints[0..3], και έτσι αποκαλύφθηκαν μαζί το σφάλμα index-off-by-one και το σφάλμα της βιβλιοθήκης Set-versus-Append

Δύο συνήθειες προκύπτουν. Δώστε δείκτη στο quad από το 1 έως το 4, που ταιριάζει με τη σειρά γωνιών στον παραπάνω κώδικα, και μεταγλωττίστε τον κώδικα σχολιασμών σας τουλάχιστον μία φορά με ενεργοποιημένο τον έλεγχο ορίων, είτε με {$R+} στο Delphi είτε σε οποιαδήποτε έκδοση fpc, πριν τον εμπιστευτείτε. Η επιτυχής μεταγλώττιση μιας προεπιλεγμένης έκδοσης dcc32 δεν αποτελεί απόδειξη ότι οι δείκτες είναι σωστοί· είναι μόνο απόδειξη ότι τίποτα δεν κατέρρευσε στη μνήμη που έτυχε να βρίσκεται εκεί

Λήψη συντεταγμένων quad από πραγματικό κείμενο

Τα σκληρά κωδικοποιημένα ορθογώνια είναι καλά για μια επίδειξη, αλλά οι επισημάνσεις παραγωγής ιχνηλατούν πραγματικές γλύφες, και οι συντεταγμένες θα πρέπει να προέρχονται από τη γεωμετρία της σελίδας κειμένου του PDFium και όχι από εικασίες. Οι ρουτίνες που καλύπτονται στον οδηγό μας για την εξαγωγή κειμένου με το PDFium Component σας δίνουν πλαίσια οριοθέτησης (bounding boxes) ανά χαρακτήρα στον ίδιο χώρο συντεταγμένων σελίδας που χρησιμοποιούν τα quads, οπότε ένα εύρημα αναζήτησης μετατρέπεται απευθείας σε σημεία γωνιών: αριστερά από τον πρώτο χαρακτήρα, δεξιά από τον τελευταίο, πάνω και κάτω από τα όρια της γραμμής. Εάν δημιουργείτε το κείμενο εσείς και πρέπει να γνωρίζετε πού θα πέσουν οι γραμμές πριν υπάρξουν, το άρθρο για τη μέτρηση κειμένου και την αναδίπλωση λέξεων καλύπτει τον υπολογισμό αυτών των ορίων εκ των προτέρων

Ένα ειλικρινές όριο: η εγγραφή TPdfAnnotation μεταφέρει ένα μόνο TQuadrilateralPoint, οπότε μία κλήση CreateAnnotation γράφει ένα τετράπλευρο. Μια επιλογή που εκτείνεται σε τρεις γραμμές χρειάζεται τρία quads, ένα ανά γραμμή, σύμφωνα με την §12.5.6.10, και έχετε δύο τρόπους να το πετύχετε. Ο απλός τρόπος είναι ένας σχολιασμός ανά γραμμή, ο οποίος αποδίδεται σωστά παντού και διατηρεί το API επιπέδου συστατικού. Ο συμπαγής τρόπος, ένας σχολιασμός που μεταφέρει τρία quads, σημαίνει τη δημιουργία του σχολιασμού μέσω του συστατικού και στη συνέχεια την κλήση της εξαγόμενης FPDFAnnot_AppendAttachmentPoints από εσάς για το δεύτερο και το τρίτο quad, κάτι που λειτουργεί ακριβώς επειδή η Append δημιουργεί θέσεις αντί να τις αντικαθιστά. Μην προσπαθήσετε να φτάσετε σε multi-quad μέσω επαναλαμβανόμενων κλήσεων της SetAttachmentPoints· κάθε δείκτης πέρα από την τρέχουσα μέτρηση απλώς επιστρέφει false, για τον ίδιο λόγο που το έκαε ο δείκτης 0 στον νέο σχολιασμό

Μετά τη συγγραφή, επαληθεύστε σε μια πραγματική εφαρμογή προβολής αντί να εμπιστεύεστε τους κωδικούς επιστροφής: ανοίξτε το αρχείο στο Acrobat ή σε οποιονδήποτε θεατή βασισμένο στο PDFium και επιβεβαιώστε ότι η επισήμανση προσγειώνεται στο κείμενο, διαβάζεται με την επιθυμητή αδιαφάνεια, και επιβιώνει από έναν κύκλο αποθήκευσης και επαναφόρτωσης. Οι τύποι σχολιασμών, ο χειρισμός των quads και ο count-aware writer που εμφανίζονται εδώ αποτελούν μέρος του τυπικού PDFium Component για Delphi, C++Builder και Lazarus· η σελίδα προϊόντος φέρει την πλήρη αναφορά API σχολιασμών μαζί με την υπόλοιπη βιβλιοθήκη