Un foglio di calcolo contiene una colonna di nomi di clienti. Alcuni sono in cinese, altri in cirillico, altri ancora presentano umlaut tedeschi o accenti francesi. Esportando il foglio in CSV e aprendo il file, ogni carattere risulta intatto. Esportando la stessa cartella di lavoro in RTF per un modello di stampa unione e aprendolo in un elaboratore di testi, i nomi non ASCII si sono ridotti a sequenze di punti interrogativi. I dati non sono mai cambiati. Ciò che è cambiato è il vincolo di codifica del formato scritto, e ciascun percorso di esportazione ne prevede uno differente.
Questa è l'insidia in cui incorre una libreria che all'apparenza sembra supportare pienamente Unicode. Il testo delle celle è gestito internamente come tipo WideString, perciò il modello non perde alcun carattere. La perdita avviene al confine, nel writer che deve serializzare quel testo in un formato dotato di regole proprie sui byte consentiti e sul modo in cui codificare gli elementi esterni all'intervallo valido. Gestire correttamente un writer non esclude la possibilità di distribuirne un altro che corrompe lo stesso testo. La soluzione non risiede in un'impostazione globale, ma in una decisione specifica e corretta per ciascun percorso.
RTF è un formato sicuro a 7 bit per progettazione
Il formato Rich Text Format precede lo standard Unicode ed è stato progettato per superare sistemi di trasporto che gestiscono solo caratteri ASCII stampabili. Un documento RTF dichiara una tabella codici (code page) nella sua intestazione, e qualsiasi carattere non rappresentabile dal writer in quella tabella deve essere emesso sotto forma di sequenza di escape anziché come byte grezzo. L'escape in questione è \u, che porta un'unità di codice a 16 bit con segno seguita da un carattere ASCII di riserva per i lettori troppo vecchi per comprendere l'escape.
HotXLS scrive i file RTF in questo modo. L'intestazione del documento inizia dichiarando la tabella codici nella forma \ansi\ansicpg1252\uc1, e il writer nell'unità lxRTF scorre ogni stringa emettendo qualsiasi carattere superiore all'ASCII standard come escape \u, così che il flusso di byte rimanga a 7 bit indipendentemente dai caratteri supportati dalla tabella codici dichiarata. Un punto di codice come U+4E2D diventa la sequenza letterale 3?, non un byte grezzo che un visualizzatore tenterebbe poi di interpretare tramite la tabella codici ipotizzata a runtime. Senza tale rigore, qualsiasi elemento esterno alla tabella codici dichiarata è privo di rappresentazione binaria valida, e un writer che emette il valore grezzo produce i punti interrogativi menzionati in apertura.
Il dettaglio da ricordare è che la tabella codici dichiarata e gli escape costituiscono le due parti di un unico accordo. Dichiarare la tabella codici non risolve la gestione del testo esterno ad essa. Emettere sequenze di escape senza dichiarare la tabella codici lascia ambigui i caratteri di riserva. Entrambi gli elementi devono essere corretti contemporaneamente, motivo per cui un writer che ne gestisce solo uno fallisce comunque alla prima cartella di lavoro multilingue.
L'escaping HTML non riguarda solo le parentesi angolari
L'esportazione HTML produce un documento multifoglio le cui cornici di navigazione portano i nomi dei fogli come testo visibile. Tali nomi sono stringhe gestite dall'autore che possono contenere qualsiasi carattere, inclusi quelli significativi per il markup. Un foglio chiamato letteralmente Q1 & Q2 <draft> deve raggiungere la pagina sotto forma di entità sottoposte a escape, altrimenti le parentesi angolari aprirebbero un tag inesistente e la e commerciale avvierebbe un riferimento a un'entità non previsto. Questo rientra nella normale gestione dell'escape HTML, e tralasciarlo sull'etichetta di una cornice è il tipo di omissione che supera ogni test basato su nomi di fogli composti da soli caratteri ASCII.
La questione della codifica si colloca un livello più in basso. Quando caratteri non ASCII rientrano in un contesto che non garantisce la fruizione in UTF-8, la rappresentazione sicura è un riferimento numerico di carattere, perciò U+00E9 viene scritto come é anziché come byte grezzo il cui significato dipenderebbe dal set di caratteri di risposta. La regola speculare si applica in fase di lettura. Una cartella di lavoro letta da un file XLSX porta con sé stringhe condivise in cui un carattere potrebbe essere memorizzato come entità XML numerica, e tale entità deve essere decodificata in un intero carattere prima di entrare nel modello della cella. Decodificandola senza cura, dividendo un punto di codice in byte separati, un singolo carattere riemerge sotto forma di caratteri corrotti (mojibake) che nessuna esportazione successiva potrà correggere.
Il contenitore XLSX è un archivio ZIP, che ha una propria codifica per i nomi
Un file XLSX è un archivio ZIP, che memorizza un nome per ciascun elemento contenuto. Il formato ZIP è abbastanza datato da non prevedere, nella specifica originale, alcuna indicazione sulla codifica di tali nomi, perciò un lettore che non rileva segnali assume la tabella codici locale dell'archivio. Questa assunzione è errata non appena il nome di un elemento contiene un carattere non ASCII, situazione che si verifica con nomi di fogli di lavoro localizzati o con file multimediali incorporati i cui nomi presentano accenti o caratteri non latini.
Il bit 11 per scopi generali (general-purpose bit 11) nell'intestazione di ciascun file locale dichiara che il nome dell'elemento è codificato in UTF-8. HotXLS verifica esattamente tale bit quando legge un archivio, testando i flag per scopi generali rispetto alla maschera $0800, e un lettore o writer che lo ignora leggerà in modo errato un nome che un'implementazione corretta ha memorizzato in UTF-8. Il bit è semplice da impostare e da rispettare, e costituisce la differenza tra un nome che supera indenne il percorso di lettura/scrittura e uno che risulta corrotto prima ancora che il contenuto del foglio di calcolo venga analizzato.
La conversione delle maiuscole/minuscole e la scansione dei numeri celano lo stesso pericolo
La valutazione delle formule è l'ambito in cui la sicurezza Unicode cessa di riguardare la serializzazione e inizia a riguardare il confronto delle stringhe. La funzione SEARCH non fa distinzione tra maiuscole e minuscole, il che significa che deve convertire i caratteri prima di cercare una sottostringa. Il modo errato di effettuare tale conversione è tramite la tabella codici ANSI, poiché convertire in maiuscolo il testo non ASCII in questo modo convoglia i caratteri in una tabella codici ristretta corrompendo gli elementi esterni ad essa. Il modo corretto consiste nella conversione in maiuscolo a livello di stringa ampia, che preserva l'intero intervallo UTF-16. HotXLS esegue la conversione con WideUpperCase per questo motivo, così che la ricerca di testo accentato o non latino trovi corrispondenza con i medesimi caratteri forniti anziché con un'approssimazione alterata dalla tabella codici.
Il tokenizer delle formule comporta un obbligo correlato slegato dalle lettere e legato al punto di termine di un token. La notazione scientifica come 1E3 o 2.5E-3 costituisce un singolo valore letterale numerico, e lo scanner deve riconoscere la E, il segno opzionale e le cifre successive come parte del numero anziché dividere l'input in un nome seguito da un numero separato. Uno scanner che gestisce male questo aspetto trasforma una costante valida in un errore di analisi o, peggio, in un'espressione silenziosamente errata. Rientra nella stessa discussione poiché entrambi i casi riguardano la corretta decisione a livello di carattere da parte del lettore: una su come convertire un carattere per il confronto, l'altra sulla prosecuzione del token corrente da parte di un carattere.
Creare ed esportare una cartella di lavoro multilingue
L'API pubblica non richiede di considerare nessuno di questi aspetti. Si crea la cartella di lavoro a partire da valori di cella in formato WideString e si chiama il punto di ingresso dell'esportazione desiderata. Le decisioni sulla codifica avvengono all'interno di ciascun writer. L'esempio seguente popola un foglio con testi in diverse grafie, per poi scrivere sia un file RTF sia uno HTML a partire dalla stessa cartella di lavoro, eseguendo i due percorsi sul medesimo input.
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;
Entrambe le chiamate restituiscono uno stato di tipo Integer ed entrambe consumano lo stesso testo in memoria. Nessun elemento nel codice chiamante dichiara una tabella codici o esegue l'escape di un carattere, poiché la responsabilità ricade sul writer che conosce il proprio formato. Il metodo SaveAsCSV a livello di cartella di lavoro segue la stessa logica qualora sia richiesta un'esportazione delimitata dalla medesima sorgente.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
La sicurezza Unicode è legata al percorso, non alla libreria
La lezione da trarre è che non esiste un unico punto in cui garantire la sicurezza Unicode. Il formato RTF richiede una tabella codici dichiarata e sequenze di escape \u. Il formato HTML richiede l'escape delle entità per i caratteri significativi del markup e riferimenti numerici laddove il set di caratteri non sia garantito, oltre alla corretta decodifica delle entità presenti nelle stringhe condivise. Il contenitore ZIP richiede l'impostazione del bit 11 per scopi generali affinché il nome di un elemento in UTF-8 sia letto correttamente. La valutazione delle formule richiede la conversione in maiuscolo/minuscolo a livello di stringa ampia e un tokenizer che mantenga unita la notazione scientifica. Ciascuno di questi rappresenta un vincolo differente, e una libreria può rispettarne uno violandone silenziosamente un altro. Questo spiega perché uno strumento che gestisce correttamente i file CSV possa comunque generare file RTF pieni di punti interrogativi.
Se le tue esportazioni si basano su formati delimitati, i relativi compromessi sono trattati nella nostra guida all'esportazione in CSV, TSV e HTML, e quando la sorgente corrisponde a un insieme di risultati anziché a un foglio creato manualmente, i pattern descritti nella sezione esportazione di database per report in Delphi si affiancano naturalmente alle regole di codifica qui illustrate. Tutto questo fa parte del HotXLS Component per Delphi e C++Builder, insieme alle API di lettura, gestione formule e formattazione trattate in altre sezioni di questo blog.