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

Εξαγωγή Φύλλων Υπολογισμών με Ασφάλεια Unicode στο Delphi: RTF και HTML

Ένα υπολογιστικό φύλλο διατηρεί μια στήλη ονομάτων πελατών. Ορισμένα είναι στα Κινέζικα, κάποια σε Κυριλλικά, και μερικά φέρουν γερμανικά umlaut ή γαλλική προφορά. Το εξάγετε σε CSV και ανοίγετε το αποτέλεσμα, και κάθε χαρακτήρας είναι ανέπαφος. Εξάγετε το ίδιο βιβλίο εργασίας (workbook) σε RTF για ένα πρότυπο συγχώνευσης αλληλογραφίας (mail-merge template), το ανοίγετε σε έναν επεξεργαστή κειμένου, και τα ονόματα που δεν είναι ASCII έχουν καταρρεύσει σε σειρές ερωτηματικών. Τα δεδομένα δεν άλλαξαν ποτέ. Αυτό που άλλαξε είναι το συμβόλαιο κωδικοποίησης της μορφής (format) που γράψατε, και κάθε διαδρομή εξαγωγής φέρει ένα διαφορετικό

Αυτή είναι η παγίδα που πιάνει μια βιβλιοθήκη η οποία φαίνεται πλήρως συμβατή με το Unicode (Unicode-aware) στην επιφάνεια. Το κείμενο του κελιού διατηρείται εσωτερικά ως WideString, οπότε το μοντέλο ποτέ δεν χάνει κανέναν χαρακτήρα. Η απώλεια συμβαίνει στα όρια, στον writer που πρέπει να μετατρέψει (serialise) αυτό το κείμενο σε μια μορφή με τους δικούς της κανόνες σχετικά με το ποια byte είναι νόμιμα και πώς πρέπει να κωδικοποιηθεί οτιδήποτε βρίσκεται εκτός του νόμιμου εύρους. Κάντε τον έναν writer σωστό και θα μπορείτε ακόμα να παραδώσετε έναν άλλον που παραμορφώνει το ίδιο κείμενο. Η λύση δεν είναι ένας καθολικός διακόπτης (global switch). Είναι μια ξεχωριστή, σωστή απόφαση σε κάθε διαδρομή

Το RTF είναι μια μορφή με ασφάλεια 7-bit εκ κατασκευής

Το Rich Text Format προηγείται του Unicode και καθορίστηκε ώστε να επιβιώνει από μεταφορές που επιτρέπουν μόνο εκτυπώσιμο ASCII. Ένα έγγραφο RTF δηλώνει μια κωδικοσελίδα (code page) στην κεφαλίδα του, και κάθε χαρακτήρας που ο writer δεν μπορεί να αναπαραστήσει σε αυτήν την κωδικοσελίδα πρέπει να εκπεμφθεί ως αλληλουχία διαφυγής (escape) και όχι ως ωμό byte (raw byte). Η σχετική αλληλουχία διαφυγής είναι το \u, το οποίο φέρει μια προσημασμένη (signed) 16-bit μονάδα κώδικα (code unit) ακολουθούμενη από έναν εναλλακτικό χαρακτήρα ASCII (fallback character) για αναγνώστες (readers) υπερβολικά παλιούς για να κατανοήσουν καν το escape

Το HotXLS γράφει RTF με αυτόν τον τρόπο. Η κεφαλίδα του εγγράφου ανοίγει δηλώνοντας την κωδικοσελίδα, με τη μορφή \ansi\ansicpg1252\uc1, και ο writer στη μονάδα (unit) lxRTF διατρέχει κάθε συμβολοσειρά εκπέμποντας οποιονδήποτε χαρακτήρα πάνω από το απλό ASCII ως \u escape, έτσι ώστε η ροή byte (byte stream) να παραμένει 7-bit καθαρή ανεξάρτητα από το τι μπορεί να χωρέσει η δηλωμένη κωδικοσελίδα. Ένα σημείο κώδικα (code point) όπως το U+4E2D γίνεται η κυριολεκτική αλληλουχία \u20013?, και όχι ένα ωμό byte που ένας προβολέας (viewer) θα προσπαθούσε στη συνέχεια να ερμηνεύσει μέσα από όποια κωδικοσελίδα έτυχε να υποθέσει. Χωρίς αυτήν την πειθαρχία, οτιδήποτε εκτός της δηλωμένης κωδικοσελίδας δεν έχει νόμιμη αναπαράσταση byte, και ένας writer που εκπέμπει την ωμή τιμή παράγει τα ερωτηματικά που ξεκίνησαν αυτό το άρθρο

Η λεπτομέρεια που πρέπει να έχετε κατά νου είναι ότι η δηλωμένη κωδικοσελίδα και οι αλληλουχίες διαφυγής (escapes) αποτελούν δύο μισά ενός συμβολαίου. Η δήλωση της κωδικοσελίδας από μόνη της δεν βοηθά το κείμενο που βρίσκεται εκτός αυτής. Η εκπομπή escapes χωρίς δηλωμένη κωδικοσελίδα αφήνει τους εναλλακτικούς χαρακτήρες mơρpτους (ambiguous). Και τα δύο πρέπει να είναι σωστά μαζί, γι' αυτό και ένας writer που χειρίζεται μόνο το ένα από αυτά εξακολουθεί να αποτυγχάνει στο πρώτο πολύγλωσσο βιβλίο εργασίας

Η διαφυγή (escaping) στο HTML αφορά κάτι περισσότερο από τις γωνιακές αγκύλες

Η εξαγωγή HTML παράγει ένα έγγραφο πολλαπλών φύλλων (multi-sheet) του οποίου τα πλαίσια πλοήγησης (navigation frames) φέρουν τα ονόματα των φύλλων ως ορατό κείμενο. Αυτά τα ονόματα είναι συμβολοσειρές ελεγχόμενες από τον δημιουργό (author-controlled strings) που μπορεί να περιέχουν οποιονδήποτε χαρακτήρα, συμπεριλαμβανομένων εκείνων που είναι σημαντικοί για το markup. Ένα φύλλο με το κυριολεκτικό όνομα Q1 & Q2 <draft> πρέπει να φτάσει στη σελίδα ως escaped οντότητες (entities), διαφορετικά οι γωνιακές αγκύλες ανοίγουν μια ετικέτα-φάντασμα (phantom tag) και το σύμβολο ampersand ξεκινά μια αναφορά οντότητας (entity reference) που ποτέ δεν ήταν σκόπιμη. Πρόκειται για συνηθισμένο escaping HTML, και η παράλειψή του σε μια ετικέτα πλαισίου (frame label) είναι το είδος της αβλεψίας που περνάει κάθε τεστ το οποίο έχει κατασκευαστεί από ονόματα φύλλων μόνο με ASCII

Το ζήτημα της κωδικοποίησης (encoding) βρίσκεται ένα επίπεδο κάτω από αυτό. Όταν χαρακτήρες εκτός ASCII καταλήγουν σε ένα πλαίσιο (context) που δεν είναι εγγυημένο ότι εξυπηρετείται ως UTF-8, η ασφαλής αναπαράσταση είναι μια αναφορά αριθμητικού χαρακτήρα (numeric character reference), έτσι το U+00E9 γράφεται ως é και όχι ως ένα ωμό byte του οποίου το νόημα εξαρτάται από το σύνολο χαρακτήρων (charset) της απόκρισης. Η αντίστροφη εικόνα αυτού του κανόνα ισχύει και κατά την είσοδο. Ένα βιβλίο εργασίας που διαβάζεται ξανά από το XLSX μεταφέρει κοινόχρηστες συμβολοσειρές (shared strings) στις οποίες ένας χαρακτήρας μπορεί να είναι ήδη αποθηκευμένος ως μια αριθμητική οντότητα XML, και αυτή η οντότητα πρέπει να αποκωδικοποιηθεί (decoded) σε έναν ολόκληρο χαρακτήρα πριν εισέλθει στο μοντέλο του κελιού. Αν το αποκωδικοποιήσετε απρόσεκτα, διασπώντας ένα σημείο κώδικα (code point) σε ξεχωριστά byte, ένας μεμονωμένος χαρακτήρας επανεμφανίζεται ως δύο κομμάτια mojibake που καμία μεταγενέστερη εξαγωγή δεν μπορεί να επισκευάσει

Ο περιέκτης (container) XLSX είναι ένα ZIP, και το ZIP έχει τη δική του κωδικοποίηση ονομάτων

Ένα αρχείο XLSX είναι μια αρχειοθήκη (archive) ZIP, και η αρχειοθήκη αποθηκεύει ένα όνομα για κάθε μέλος (member) που κρατάει. Το ZIP είναι αρκετά παλιό ώστε η αρχική του προδιαγραφή να μην έλεγε τίποτα για την κωδικοποίηση αυτών των ονομάτων, οπότε ένας reader που δεν βρίσκει σήμα υποθέτει την τοπική κωδικοσελίδα της αρχειοθήκης. Αυτή η υπόθεση είναι λανθασμένη τη στιγμή που το όνομα ενός μέλους περιέχει έναν χαρακτήρα εκτός ASCII, κάτι που συμβαίνει με τα τοπικοποιημένα (localized) ονόματα των μερών (parts) του φύλλου εργασίας και με τα ενσωματωμένα μέσα των οποίων τα ονόματα αρχείων φέρουν τόνους ή μη λατινική γραφή

Η λύση είναι ένα και μόνο bit. Το bit 11 γενικού σκοπού (general-purpose bit 11) σε κάθε τοπική κεφαλίδα (local file header) δηλώνει ότι το όνομα μέλους (member name) είναι κωδικοποιημένο ως UTF-8. Το HotXLS ελέγχει ακριβώς αυτό το bit όταν διαβάζει μια αρχειοθήκη, δοκιμάζοντας τις σημαίες (flags) γενικού σκοπού έναντι της μάσκας $0800, και ένας reader ή writer που το αγνοεί θα παρερμηνεύσει (misread) ένα όνομα που μια σωστή υλοποίηση αποθήκευσε ως UTF-8. Το bit είναι φθηνό να οριστεί και φθηνό να τηρηθεί, και κάνει όλη τη διαφορά ανάμεσα σε ένα όνομα μέλους που επιβιώνει από το ταξίδι μετάβασης και επιστροφής (round trip) και σε ένα που φτάνει κατεστραμμένο πριν καν αναλυθεί (parsed) το περιεχόμενο του υπολογιστικού φύλλου

Η αναδίπλωση πεζών/κεφαλαίων (case folding) και η σάρωση αριθμών (number scanning) κρύβουν τον ίδιο κίνδυνο

Η αξιολόγηση των τύπων (formula evaluation) είναι εκεί όπου η ασφάλεια Unicode παύει να αφορά τη σειριοποίηση (serialisation) και αρχίζει να αφορά τη σύγκριση (comparison). Η συνάρτηση SEARCH δεν κάνει διάκριση πεζών-κεφαλαίων (case-insensitive), πράγμα που σημαίνει ότι πρέπει να κάνει αναδίπλωση πεζών/κεφαλαίων (fold case) προτού αναζητήσει μια υποσυμβολοσειρά (substring). Ο λάθος τρόπος αναδίπλωσης είναι μέσω της κωδικοσελίδας ANSI, επειδή η μετατροπή σε κεφαλαία κειμένου εκτός ASCII με αυτόν τον τρόπο δρομολογεί τους χαρακτήρες μέσα από μια στενή κωδικοσελίδα και καταστρέφει οτιδήποτε βρίσκεται εκτός αυτής. Ο σωστός τρόπος είναι η μετατροπή σε κεφαλαία χρησιμοποιώντας wide-string, η οποία διατηρεί το πλήρες εύρος του UTF-16. Το HotXLS αναδιπλώνει με το WideUpperCase ακριβώς για αυτόν τον λόγο, ώστε μια αναζήτηση για τονισμένο (accented) ή μη λατινικό κείμενο να ταιριάζει με τους ίδιους χαρακτήρες που της δόθηκαν αντί για μια παραμορφωμένη-από-κωδικοσελίδα προσέγγισή τους

Το tokenizer των τύπων φέρει μια σχετική υποχρέωση που δεν έχει καμία σχέση με γράμματα και έχει άμεση σχέση με το πού τελειώνει ένα token. Η επιστημονική σημειογραφία όπως το 1E3 ή 2.5E-3 είναι ένα μεμονωμένο αριθμητικό literal (literal), και ο σαρωτής (scanner) πρέπει να αναγνωρίσει το E, ένα προαιρετικό πρόσημο, και τα ψηφία που ακολουθούν ως τμήμα του αριθμού αντί να σπάσει την είσοδο σε ένα όνομα που ακολουθείται από έναν ξεχωριστό αριθμό. Ένας σαρωτής που χειρίζεται λάθος αυτό το σημείο μετατρέπει μια απολύτως έγκυρη σταθερά σε σφάλμα ανάλυσης (parse error) ή, χειρότερα, σε μια αθόρυβα λανθασμένη έκφραση. Ανήκει στην ίδια συζήτηση επειδή και οι δύο περιπτώσεις αφορούν έναν reader που παίρνει μια σωστή απόφαση σε επίπεδο χαρακτήρων: η μία σχετικά με το πώς να αναδιπλώσει έναν χαρακτήρα για σύγκριση, και η άλλη σχετικά με το αν ένας χαρακτήρας συνεχίζει το τρέχον token

Κατασκευή και εξαγωγή ενός πολύγλωσσου βιβλίου εργασίας

Το δημόσιο API δεν σας ζητά να σκεφτείτε τίποτα από όλα αυτά. Χτίζετε το βιβλίο εργασίας από τιμές κελιών WideString και καλείτε το σημείο εισόδου εξαγωγής (export entry point) που θέλετε. Οι αποφάσεις κωδικοποίησης λαμβάνουν χώρα μέσα σε κάθε writer. Το παρακάτω παράδειγμα γεμίζει ένα φύλλο με κείμενο σε διάφορες γραφές (scripts), και στη συνέχεια γράφει τόσο ένα αρχείο RTF όσο και ένα αρχείο HTML από το ίδιο βιβλίο εργασίας, οπότε και οι δύο διαδρομές εκτελούνται πάνω σε πανομοιότυπη είσοδο

uses
  lxHandle;

procedure ExportMultilingualWorkbook;
var
  Book: IXLSWorkbook;
  Sheet: IXLSWorksheet;
begin
  Book := TXLSWorkbook.Create;
  try
    Sheet := Book.Sheets.Add('Customers');

    Sheet.Cells[1, 1].Value := 'Name';
    Sheet.Cells[1, 2].Value := 'City';

    // Cell text is held as WideString, so every script survives the model.
    Sheet.Cells[2, 1].Value := '王伟';          // Chinese
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // German umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Cyrillic
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // French accents
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: the lxRTF writer declares the code page and emits every
    // non-ASCII character as a \u escape, keeping the file 7-bit clean.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: sheet names are HTML-escaped and non-ASCII text is written
    // so it does not depend on a guessed response charset.
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Και οι δύο κλήσεις επιστρέφουν μια κατάσταση (status) Integer, και οι δύο καταναλώνουν το ίδιο κείμενο στη μνήμη (in-memory text). Τίποτα στον κώδικα κλήσης (calling code) δεν δηλώνει κάποια κωδικοσελίδα ούτε κάνει escape κάποιον χαρακτήρα, επειδή η ευθύνη εναπόκειται στον writer που γνωρίζει τη δική του μορφή. Το επίπεδο βιβλίου εργασίας (workbook-level) SaveAsCSV ακολουθεί το ίδιο μοτίβο (shape) αν χρειάζεστε μια οριοθετημένη εξαγωγή (delimited export) από την ίδια πηγή

// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');

Η ασφάλεια Unicode ισχύει ανά διαδρομή (per-path), όχι ανά βιβλιοθήκη

Το μάθημα που αξίζει να κρατήσετε είναι ότι δεν υπάρχει ένα μεμονωμένο μέρος για να είστε ασφαλείς με το Unicode. Το RTF χρειάζεται μια δηλωμένη κωδικοσελίδα συν τα escapes \u. Η HTML χρειάζεται το escaping των οντοτήτων (entity escaping) για χαρακτήρες σημαντικούς για το markup καθώς και αριθμητικές αναφορές (numeric references) όπου το σύνολο χαρακτήρων (charset) δεν είναι εγγυημένο, συν τη σωστή αποκωδικοποίηση των οντοτήτων που φτάνουν σε κοινόχρηστες συμβολοσειρές (shared strings). Ο περιέκτης (container) ZIP χρειάζεται το γενικού σκοπού bit 11 ρυθμισμένο έτσι ώστε ένα όνομα μέλους UTF-8 να διαβάζεται ως UTF-8. Η αξιολόγηση τύπων χρειάζεται αναδίπλωση πεζών-κεφαλαίων wide-string και ένα tokenizer που διατηρεί την επιστημονική σημειογραφία ενωμένη σε ένα κομμάτι. Κάθε ένα από αυτά είναι ένα διαφορετικό συμβόλαιο, και μια βιβλιοθήκη μπορεί να ικανοποιεί το ένα ενώ παραβιάζει αθόρυβα ένα άλλο. Αυτός είναι ο λόγος που ένα εργαλείο που χειρίζεται σωστά το CSV μπορεί να σας δώσει ακόμα ένα RTF γεμάτο ερωτηματικά

Αν οι εξαγωγές σας στηρίζονται σε οριοθετημένες (delimited) μορφές, οι συμβιβασμοί (trade-offs) μεταξύ τους καλύπτονται στην αναλυτική μας παρουσίαση της εξαγωγής CSV, TSV και HTML, και όταν η πηγή είναι ένα σύνολο αποτελεσμάτων (result set) αντί για ένα χειροκίνητα κατασκευασμένο φύλλο, τα μοτίβα στην εξαγωγή βάσης δεδομένων για αναφορές στο Delphi συνδυάζονται φυσικά με τους κανόνες κωδικοποίησης που περιγράφονται εδώ. Όλα αυτά διατίθενται ως μέρος του HotXLS Component για Delphi και C++Builder, μαζί με τα API ανάγνωσης, τύπων (formula) και μορφοποίησης που καλύπτονται αλλού σε αυτό το ιστολόγιο