Τραβήξτε emoji ή ένα ιαπωνικό όνομα οικογενειακού μητρώου από ένα PDF ως κείμενο, και η έξοδος δείχνει ένα κουτί, ένα ερωτηματικό, ή τίποτα καθόλου εκεί όπου θα έπρεπε να είναι ο χαρακτήρας. Η ιδιότητα Character[] του PDFium Component είναι συνήθως ο λόγος: διαβάζει κάθε γλύφο μέσω FPDFText_GetUnicode, το οποίο επιστρέφει ένα πλήρες σημείο κώδικα Unicode ως τιμή 32-bit χωρίς πρόσημο, έπειτα το εκθέτει στη Delphi ως ένα μοναδικό WideChar 16-bit. Οποιοδήποτε σημείο κώδικα πέρα από U+FFFF δεν μπορεί να κάνει εκείνο το ταξίδι σε ένα κομμάτι, και η αλλοίωση ποτέ δεν εμφανίζεται ενώ κοιτάτε την αποδοθείσα σελίδα, επειδή η απόδοση και η εξαγωγή κειμένου τρέχουν μέσα από ξεχωριστές διαδρομές κώδικα στο PDFium — ένα έγγραφο μπορεί να εμφανίζει τα emoji του τέλεια και ακόμη να σας δίνει σκουπίδια τη στιγμή που διαβάζετε το Character[] σε βρόχο και χτίζετε μια συμβολοσειρά από αυτό
Το Βασικό Πολυγλωσσικό Επίπεδο και γιατί το WideChar σταματά στο U+FFFF
Το WideChar της Delphi είναι ένας τύπος 16-bit που μπορεί να κρατήσει μόνο μία μονάδα κώδικα UTF-16. Το Βασικό Πολυγλωσσικό Επίπεδο του Unicode, το εύρος U+0000 έως U+FFFF, χωρά ακριβώς μέσα σε αυτό, γι' αυτό τα λατινικά, τα κυριλλικά, τα ελληνικά, και το κοινό μπλοκ CJK Unified Ideographs κάνουν όλα πήγαινε-έλα μέσω ενός μοναδικού WideChar χωρίς περιστατικό. Δύο οικογένειες χαρακτήρων τακτικά πέφτουν έξω από αυτό σε πραγματικά έγγραφα: τα emoji, πολλά από αυτά στο μπλοκ Emoticons που ξεκινά στο U+1F600, και σπάνια ιδεογράμματα CJK από το CJK Unified Ideographs Extension B, το εύρος U+20000 έως U+2A6DF δεσμευμένο για λιγότερο συνηθισμένους κινεζικούς, ιαπωνικούς, και κορεατικούς χαρακτήρες που περιλαμβάνουν πολλά προσωπικά ονόματα και τοπωνύμια. Το UTF-16 χειρίζεται οτιδήποτε πάνω από U+FFFF με ένα ζεύγος υποκατάστασης (surrogate pair) — δύο μονάδες κώδικα 16-bit, ένα υψηλό υποκατάστατο στο εύρος $D800 έως $DBFF ακολουθούμενο από ένα χαμηλό υποκατάστατο στο $DC00 έως $DFFF, που μαζί κωδικοποιούν ένα σημείο κώδικα — και τα μαθηματικά πίσω από εκείνο το ζευγάρωμα είναι αρκετά σταθερά ώστε να τα δείξουμε απευθείας σε Pascal
function ToSurrogatePair(CodePoint: LongWord; out Hi, Lo: WideChar): Boolean;
var
V: LongWord;
begin
Result := CodePoint > $FFFF;
if Result then
begin
V := CodePoint - $10000;
Hi := WideChar($D800 + (V shr 10));
Lo := WideChar($DC00 + (V and $3FF));
end;
end;
Δώστε το U+1F600, το emoji χαμογελαστού προσώπου, μέσα από εκείνη τη συνάρτηση και το αποτέλεσμα είναι ένα υψηλό υποκατάστατο $D83D και ένα χαμηλό υποκατάστατο $DE00, δύο τιμές 16-bit, όχι μία. Κανένα από τα δύο μισά δεν σημαίνει τίποτα από μόνο του· ένα μοναχικό $D83D που κάθεται σε μια συμβολοσειρά χωρίς κανένα $DE00 πίσω του είναι ένα κρεμασμένο υποκατάστατο, και ο περισσότερος κώδικας χειρισμού κειμένου που το συναντά είτε το ρίχνει, είτε το αντικαθιστά με έναν γλύφο αντικατάστασης, είτε εγείρει σφάλμα
Γιατί το FPDFText_GetUnicode επιστρέφει μια τιμή που το Character[] δεν μπορεί να κρατήσει;
Το FPDFText_GetUnicode επιστρέφει ένα LongWord, μια πλήρη τιμή 32-bit, επειδή η κωδικοποίηση κειμένου PDF ήδη φέρει την πλήρη τιμή βαθμωτού Unicode για κάθε γλύφο. Ένα CMap ToUnicode ενός PDF αντιστοιχίζει κωδικούς χαρακτήρων σε κείμενο Unicode, και όταν ένας γλύφος αναπαριστά αυτό που ανεπίσημα αποκαλείται χαρακτήρας αστρικού επιπέδου — οτιδήποτε πέρα από το Βασικό Πολυγλωσσικό Επίπεδο — εκείνη η αντιστοίχιση είναι ένα πλήρες σημείο κώδικα, όχι ένα κομμάτι 16-bit. Το PDFium το αποκωδικοποιεί πίσω σε βαθμωτή τιμή εσωτερικά και το επιστρέφει πέρα από το όριο DLL μέσω FPDFText_GetUnicode, και εκείνο το όριο είναι ακριβώς εκεί όπου μια τιμή 32-bit πρέπει να γίνει κάτι που μια ιδιότητα Delphi μπορεί να δώσει πίσω στον κώδικά σας
Η προφανής υλοποίηση είναι WideChar(FPDFText_GetUnicode(TextPage, Index)), και είναι επίσης η λάθος. Μια σκληρή μετατροπή τύπου από μια τιμή 32-bit σε έναν τύπο 16-bit κρατά μόνο τα χαμηλά 16 bit και πετά τα υπόλοιπα σιωπηλά, χωρίς εξαίρεση και χωρίς έλεγχο εύρους. Για το U+1F600 αυτό σημαίνει να κρατήσετε το $F600 και να χάσετε το γεγονός ότι η αληθινή τιμή ήταν ποτέ πάνω από U+FFFF, κάτι που παράγει μια μονάδα κώδικα που δεν είναι καν έγκυρο κρεμασμένο υποκατάστατο, απλώς έναν ασύνδετο χαρακτήρα του Βασικού Πολυγλωσσικού Επιπέδου που τυχαίνει να μοιράζεται εκείνα τα χαμηλά bit. Συνενώστε μερικές χιλιάδες από αυτά σε μια συμβολοσειρά και ο κατάντη κώδικας δεν έχει πλέον κανέναν τρόπο να ξεχωρίσει έναν αλλοιωμένο χαρακτήρα από έναν νόμιμο
Τι επιστρέφουν τώρα τα Character[] και Charcode[] για σημεία κώδικα αστρικού επιπέδου
Οι ιδιότητες Character[] και Charcode[] του PDFium Component επιστρέφουν U+FFFD, τον χαρακτήρα αντικατάστασης Unicode, όποτε το υποκείμενο σημείο κώδικα υπερβαίνει το U+FFFF, αντί να το περικόπτουν σιωπηλά. Εκείνη η φρουρά κάθεται απευθείας μέσα στον getter ιδιότητας πίσω από το Character[]
function TPdf.GetCharacter(Index: Integer): WideChar;
var
Code: LongWord;
begin
LoadTextPage;
Code := FPDFText_GetUnicode(FTextPage, Index);
if Code > $FFFF then
Result := #$FFFD // astral-plane code point: cannot fit in one WideChar
else
Result := WideChar(Code);
end;
Η επιστροφή U+FFFD αντί για ένα περικομμένο θραύσμα είναι μια σκόπιμη, στενή διόρθωση αντί για επανασχεδιασμό. Τα Character[] και Charcode[] είναι τυποποιημένα ως WideChar τόσο στο TPdf όσο και στο TPdfView, και η διεύρυνση εκείνου του τύπου επιστροφής για να φέρει ένα πλήρες σημείο κώδικα θα έσπαγε κάθε υπάρχοντα καλούντα που αναμένει ένας γλύφος ανά δείκτη να σημαίνει μία τιμή 16-bit. Το U+FFFD είναι το δικό του ορισμένο σύμβολο κράτησης θέσης του ίδιου του Προτύπου Unicode ακριβώς για αυτή τη κατάσταση, οπότε ένας καλών που ελέγχει για αυτό παίρνει ένα καθορισμένο, τεκμηριωμένο σήμα αντί για σιωπηλά λάθος δεδομένα. Μία οριακή περίπτωση που αξίζει να γνωρίζετε: το U+FFFD είναι επίσης ένας νόμιμος χαρακτήρας από μόνος του, οπότε στο σπάνιο έγγραφο που ήδη περιέχει έναν γνήσιο γλύφο χαρακτήρα-αντικατάστασης, εκείνος ο δείκτης είναι μη διακρίσιμος από έναν περικομμένο αστρικό χαρακτήρα μόνο από την τιμή
Πώς εξάγετε σωστά κείμενο emoji και CJK Extension B σε Delphi;
Καλέστε το Text αντί να διατρέξετε το Character[] όποτε το πραγματικό κειμενικό περιεχόμενο έχει σημασία, επειδή το Text διαβάζει μέσω FPDFText_GetText και επιστρέφει ένα πλήρες WString με σωστά ζεύγη υποκατάστασης για κάθε χαρακτήρα αστρικού επιπέδου στο εύρος, αντί για μία τιμή σταθερού πλάτους ανά δείκτη. Το Pdf.Text(0, MaxInt), ή η σύντμηση Pdf.Text, εξάγει μια ολόκληρη σελίδα σωστά σε μία κλήση, και το Pdf.Text(StartIndex, Count) τραβά ένα μικρότερο εύρος με τον ίδιο τρόπο. Το Character[] εξακολουθεί να κερδίζει τη θέση του όταν χρειάζεστε μόνο δεδομένα θέσης, γραμματοσειράς, ή σημαίας σε έναν δείκτη και ποτέ δεν αγγίζετε το ίδιο το σημείο κώδικα — τα CharacterOrigin[], FontSize[], και CharacterMapError[] δεν νοιάζονται αν ο υποκείμενος γλύφος ήταν αστρικός
function ExtractLineSafely(Pdf: TPdf): WString;
var
I: Integer;
begin
Result := '';
for I := 0 to Pdf.CharacterCount - 1 do
if not (Pdf.CharacterGenerated[I] or Pdf.CharacterMapError[I]) then
Result := Result + Pdf.Text(I, 1); // full code point, never a truncated WideChar
end;
Ο έλεγχος παράλειψης-δημιουργημένου-και-μη-αντιστοιχισμένου σε εκείνον τον βρόχο είναι το ίδιο μοτίβο που χρησιμοποιείται για την εξαγωγή απλού κειμένου στο εξαγωγή κειμένου από έγγραφα PDF με το PDFium Component· η μόνη αλλαγή είναι η τελευταία γραμμή, η οποία ανταλλάσσει μια άμεση προσάρτηση Character[I] για μια κλήση ενός δείκτη στο Text ώστε οι αστρικοί χαρακτήρες να φτάνουν ως πλήρη ζεύγη υποκατάστασης αντί για σύμβολα κράτησης θέσης αντικατάστασης
Πού πράγματι δαγκώνει αυτό: εξαγωγές συνομιλιών, προσωπικά ονόματα, και ενσωματωμένες γραμματοσειρές CJK
Τα emoji εμφανίζονται όπου κι αν ένα PDF συλλαμβάνει άτυπη επικοινωνία: εξαγόμενα αρχεία καταγραφής συνομιλιών, εκφορτώσεις κριτικών από app store, απομαγνητοφωνήσεις συστημάτων ticketing αποθηκευμένες σε PDF για αρχείο συμμόρφωσης. Το CJK Extension B εμφανίζεται σε μια στενότερη αλλά υψηλότερου κινδύνου θέση, προσωπικά ονόματα και τοπωνύμια, επειδή τα ιαπωνικά οικογενειακά μητρώα, τα κινεζικά αρχεία οικογενειακής καταγραφής, και τα ταϊβανέζικα έγγραφα ταυτότητας είναι κλασικές πηγές χαρακτήρων που ποτέ δεν έφτασαν στο κοινό μπλοκ CJK. Μια διοχέτευση μισθοδοσίας ή επαλήθευσης ταυτότητας που εξάγει ονόματα από σαρωμένα κρατικά χαρτιά είναι ακριβώς το είδος φόρτου εργασίας όπου ένας σιωπηλά αλλοιωμένος χαρακτήρας μετατρέπεται σε αποτυχημένο ταίριασμα αντί για ένα διακοσμητικό σφάλμα
Τα σπάνια ιδεογράμματα CJK επίσης τείνουν να ταξιδεύουν με προβλήματα γραμματοσειράς, όχι μόνο κωδικοποίησης, επειδή μια γραμματοσειρά πρέπει να φέρει έναν γλύφο για ένα σημείο κώδικα εύρους U+20000 προτού μπορέσει να αποδοθεί καθόλου, και λίγες εγκατεστημένες γραμματοσειρές συστήματος το κάνουν. Όποιος ήδη διατρέχει το FontIsEmbedded[] ανά χαρακτήρα με τον τρόπο που περιγράφει το ανάγνωση ιδιοτήτων γραμματοσειράς PDF με το PDFium Component πρέπει να ελέγξει τον ίδιο δείκτη και για τα δύο προβλήματα μαζί: ένας δείκτης που επιστρέφει U+FFFD από το Character[] και αναφέρει μη ενσωματωμένη γραμματοσειρά είναι ένα έγγραφο που δεν θα εξάγει ούτε θα εκτυπώσει σωστά εκείνον τον χαρακτήρα, και η διόρθωση ανήκει ανάντη στο πώς παρήχθη το PDF, όχι στον κώδικα εξαγωγής σας
Οι ιδιότητες Character[], Charcode[], και Text που περιγράφονται εδώ είναι μέρος του τυπικού εξαρτήματος PDFium για Delphi και C++Builder