O foaie de calcul conține o coloană cu numele clienților. Unele sunt în chineză, altele în chirilică, câteva poartă umlaut-uri germane sau un accent francez. O exportați în CSV și deschideți rezultatul, iar fiecare caracter este intact. Exportați același registru de calcul în RTF pentru un șablon de îmbinare a corespondenței (mail-merge), îl deschideți într-un procesor de text, iar numele non-ASCII s-au prăbușit în rânduri de semne de întrebare. Datele nu s-au schimbat niciodată. Ceea ce s-a schimbat este contractul de codificare al formatului pe care l-ați scris, iar fiecare cale de export poartă unul diferit.
Aceasta este capcana în care cade o bibliotecă ce pare complet compatibilă cu Unicode la suprafață. Textul celulei este păstrat la nivel intern ca WideString, astfel încât modelul nu pierde niciodată vreun caracter. Pierderea are loc la graniță, în scriitorul care trebuie să serializeze acel text într-un format cu propriile reguli despre care octeți sunt legali și cum trebuie codificat orice se află în afara intervalului legal. Puteți scrie corect un scriitor și totuși să livrați altul care distruge același text. Soluția nu este un comutator global. Este o decizie separată și corectă pe fiecare cale.
RTF este un format sigur pe 7 biți prin design
Rich Text Format precede Unicode și a fost specificat pentru a supraviețui transporturilor care transmit doar ASCII imprimabil. Un document RTF declară o pagină de coduri în antetul său, iar orice caracter pe care scriitorul nu îl poate reprezenta în acea pagină de coduri trebuie emis ca o secvență de eludare (escape) în loc de un octet brut. Secvența de eludare relevantă este \u, care poartă o unitate de cod cu semn pe 16 biți urmată de un caracter ASCII de rezervă (fallback) pentru cititoarele prea vechi pentru a înțelege deloc secvența.
HotXLS scrie RTF în acest mod. Antetul documentului se deschide prin declararea paginii de coduri, sub forma \ansi\ansicpg1252\uc1, iar scriitorul din unitatea lxRTF parcurge fiecare șir emițând orice caracter de dincolo de ASCII simplu ca o secvență de eludare \u, astfel încât fluxul de octeți rămâne curat pe 7 biți, indiferent de ceea ce poate conține pagina de coduri declarată. Un punct de cod precum U+4E2D devine secvența literală \u20013?, nu un octet brut pe care un vizualizator ar încerca apoi să îl interpreteze prin orice pagină de coduri ar presupune întâmplător. Fără acea disciplină, orice se află în afara paginii de coduri declarate nu are o reprezentare legală de octeți, iar un scriitor care emite valoarea brută produce semnele de întrebare care au deschis acest articol.
Detaliul de reținut este că pagina de coduri declarată și secvențele de eludare sunt două jumătăți ale aceluiași contract. Declararea paginii de coduri singură nu ajută textul care se află în afara ei. Emiterea secvențelor de eludare fără o pagină de coduri declarată lasă caracterele de rezervă ambigue. Ambele trebuie să fie corecte împreună, motiv pentru care un scriitor care gestionează doar una dintre ele va eșua la primul registru de calcul multilingv.
Eludarea HTML înseamnă mai mult decât paranteze unghiulare
Exportul HTML produce un document cu mai multe foi ale cărui cadre de navigare poartă numele foilor ca text vizibil. Acele nume sunt șiruri controlate de autor care pot conține orice caracter, inclusiv cele semnificative pentru marcaj. O foaie numită la propriu Q1 & Q2 <draft> trebuie să ajungă pe pagină ca entități eludate, altfel parantezele unghiulare deschid o etichetă fantomă, iar ampersandul începe o referință de entitate care nu a fost intenționată niciodată. Aceasta este eludarea HTML obișnuită, iar omiterea ei pe o etichetă de cadru este genul de lipsă care trece de fiecare test construit cu nume de foi doar în ASCII.
Problema codificării se află cu un nivel mai jos de atât. Când caracterele non-ASCII ajung într-un context care nu este garantat să fie servit ca UTF-8, reprezentarea sigură este o referință de caracter numerică, astfel încât U+00E9 este scris ca é în loc de un octet brut a cărui semnificație depinde de setul de caractere al răspunsului. Imaginea în oglindă a acestei reguli se aplică la intrare. Un registru de calcul citit înapoi din XLSX poartă șiruri partajate în care un caracter poate fi deja stocat ca o entitate XML numerică, iar acea entitate trebuie decodificată într-un singur caracter complet înainte de a intra în modelul celulei. Decodificați-l neglijent, împărțind un punct de cod în octeți separați, iar un singur caracter reapare ca două bucăți de mojibake pe care niciun export ulterior nu le poate repara.
Containerul XLSX este un ZIP, iar ZIP are propria codificare de nume
Un fișier XLSX este o arhivă ZIP, iar arhiva stochează un nume pentru fiecare membru pe care îl conține. ZIP este suficient de vechi încât specificația sa originală nu spunea nimic despre codificarea acelor nume, așa că un cititor care nu găsește niciun semnal presupune pagina de coduri locală a arhivei. Acea presupunere este greșită în momentul în care numele unui membru conține un caracter non-ASCII, ceea ce se întâmplă cu numele localizate ale foilor de calcul și cu media încorporate ale căror nume de fișiere poartă accente sau scriere non-latină.
Soluția este un singur bit. Bitul 11 de uz general din fiecare antet de fișier local declară că numele membrului este codificat ca UTF-8. HotXLS verifică exact acel bit când citește o arhivă, testând flag-urile de uz general în raport cu masca $0800, iar un cititor sau scriitor care îl ignoră va citi greșit un nume pe care o implementare corectă l-a stocat ca UTF-8. Bitul este ieftin de setat și ieftin de respectat și reprezintă întreaga diferență între un nume de membru care supraviețuiește călătoriei dus-întors și unul care ajunge corupt înainte ca conținutul foii de calcul să fie măcar parsat.
Plierea cazului (case folding) și scanarea numerelor ascund același pericol
Evaluarea formulelor este momentul în care siguranța Unicode nu se mai referă la serializare, ci la comparare. Funcția SEARCH este insensibilă la majuscule/minuscule, ceea ce înseamnă că trebuie să plieze cazul înainte de a căuta un sub-șir. Modul greșit de a plia este prin intermediul paginii de coduri ANSI, deoarece transformarea textului non-ASCII în majuscule în acest mod direcționează caracterele printr-o pagină de coduri îngustă și corupe tot ce se află în afara ei. Modul corect este transformarea în majuscule a șirurilor largi (wide-string), care păstrează întregul interval UTF-16. HotXLS pliază cu WideUpperCase exact din acest motiv, astfel încât o căutare de text cu accente sau non-latin se potrivește cu aceleași caractere care i-au fost date, mai degrabă decât cu o aproximare deformată prin pagina de coduri.
Tokenizatorul de formule poartă o obligație conexă care nu are nicio legătură cu literele și are legătură în întregime cu locul unde se termină un token. Notația științifică, cum ar fi 1E3 sau 2.5E-3, este un singur literal numeric, iar scanerul trebuie să recunoască caracterul E, un semn opțional și cifrele următoare ca parte a numărului, în loc să împartă intrarea într-un nume urmat de un număr separat. Un scaner care gestionează greșit acest lucru transformă o constantă perfect validă într-o eroare de parsare sau, mai rău, într-o expresie greșită în mod silențios. Acesta aparține aceleiași discuții deoarece ambele cazuri se referă la un cititor care ia o decizie corectă la nivel de caracter: una despre cum să plieze un caracter pentru comparare, cealaltă despre dacă un caracter continuă tokenul curent.
Construirea și exportarea unui registru de calcul multilingv
API-ul public nu vă cere să vă gândiți la nimic din toate acestea. Construiți registrul de calcul din valori de celule WideString și apelați punctul de intrare de export dorit. Deciziile de codificare au loc în interiorul fiecărui scriitor. Exemplul de mai jos inițializează o foaie cu text în mai multe scrieri, apoi scrie atât un fișier RTF, cât și un fișier HTML din același registru de calcul, astfel încât cele două căi rulează în raport cu intrări identice.
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;
Ambele apeluri returnează o stare de tip Integer și ambele consumă același text din memorie. Nimic din codul de apelare nu declară o pagină de coduri sau eludează un caracter, deoarece responsabilitatea revine scriitorului care își cunoaște propriul format. Metoda SaveAsCSV la nivel de registru de calcul urmează aceeași formă dacă aveți nevoie de un export delimitat din aceeași sursă.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Siguranța Unicode este specifică fiecărei căi, nu întregii biblioteci
Lecția care merită reținută este că nu există un singur loc în care să fim siguri din punct de vedere Unicode. RTF are nevoie de o pagină de coduri declarată plus secvențe de eludare \u. HTML are nevoie de eludarea entităților pentru caracterele semnificative din marcaj și de referințe numerice acolo unde setul de caractere nu este garantat, plus o decodificare corectă a entităților care ajung în șiruri partajate. Containerul ZIP are nevoie de setarea bitului 11 de uz general pentru ca un nume de membru UTF-8 să fie citit ca UTF-8. Evaluarea formulelor are nevoie de plierea cazului pentru șiruri largi și de un tokenizator care păstrează notația științifică într-o singură piesă. Fiecare dintre acestea reprezintă un contract diferit, iar o bibliotecă poate satisface unul în timp ce îl încalcă discret pe altul. Acesta este motivul pentru care un instrument care face CSV-ul corect vă poate oferi totuși un RTF plin de semne de întrebare.
Dacă exporturile dumneavoastră se bazează pe formate delimitate, compromisurile dintre ele sunt acoperite în ghidul nostru pentru exportul CSV, TSV și HTML, iar când sursa este un set de rezultate în loc de o foaie creată manual, modelele din exportul bazei de date pentru rapoarte Delphi se asociază în mod natural cu regulile de codificare descrise aici. Totul este livrat ca parte a HotXLS Component pentru Delphi și C++Builder, alături de API-urile de citire, formule și formatare acoperite în alte părți ale acestui blog.