Articolo tecnico

Esportazione di Fogli di Calcolo Sicura per Unicode in Delphi: RTF e HTML

Un foglio di calcolo contiene una colonna di nomi di clienti. Alcuni sono in cinese, alcuni in cirillico, un paio portano umlaut tedeschi o un accento francese. Lo esporti in CSV e apri il risultato, e ogni carattere è intatto. Esporti la stessa cartella di lavoro in RTF per un modello di stampa unione, lo apri in un elaboratore di testi, e i nomi non ASCII sono collassati in righe di punti interrogativi. I dati non sono mai cambiati. Ciò che è cambiato è il contratto di codifica del formato che hai scritto, e ogni percorso di esportazione ne comporta uno diverso

Questa è la trappola in cui cade una libreria che all'apparenza sembra completamente compatibile con Unicode. Il testo delle celle viene mantenuto internamente come WideString, quindi il modello non perde mai un carattere. La perdita avviene al confine, nello scrittore (writer) che deve serializzare quel testo in un formato con le proprie regole su quali byte siano validi e su come qualsiasi cosa al di fuori dell'intervallo valido debba essere codificata. Anche se azzecchi uno scrittore, potresti comunque fornirne un altro che storpia lo stesso testo. La soluzione non è un interruttore globale. È una decisione separata e corretta su ogni percorso

RTF è un formato sicuro a 7 bit per progettazione

Il formato Rich Text Format (RTF) è precedente a Unicode ed è stato specificato per sopravvivere ai trasporti che passano solo ASCII stampabile. Un documento RTF dichiara una code page nella sua intestazione, e qualsiasi carattere che lo scrittore non può rappresentare in quella code page deve essere emesso come sequenza di escape piuttosto che come byte grezzo. L'escape rilevante è \u, che trasporta un'unità di codice a 16 bit con segno seguita da un carattere ASCII di ripiego (fallback) per i lettori troppo vecchi per comprendere affatto l'escape

HotXLS scrive i file RTF in questo modo. L'intestazione del documento si apre dichiarando la code page, nella forma \ansi\ansicpg1252\uc1, e lo scrittore nell'unità lxRTF percorre ogni stringa emettendo qualsiasi carattere superiore al semplice ASCII come escape \u in modo che il flusso di byte rimanga pulito a 7 bit indipendentemente da ciò che la code page dichiarata può contenere. Un punto di codice come U+4E2D diventa la sequenza letterale \u20013?, non un byte grezzo che un visualizzatore cercherebbe poi di interpretare attraverso qualsiasi code page si trovi a presumere. Senza quella disciplina, qualsiasi cosa al di fuori della code page dichiarata non ha alcuna rappresentazione legale in byte, e uno scrittore che emette il valore grezzo produce i punti interrogativi con cui è iniziato questo articolo

Il dettaglio da tenere a mente è che la code page dichiarata e le sequenze di escape sono le due metà di un unico contratto. Dichiarare la sola code page non aiuta il testo che si trova al di fuori di essa. Emettere sequenze di escape senza una code page dichiarata lascia ambigui i caratteri di ripiego. Entrambi devono essere corretti contemporaneamente, motivo per cui uno scrittore che ne gestisce solo uno fallisce comunque alla prima cartella di lavoro multilingue

L'escape HTML riguarda ben più che le sole parentesi angolari

L'esportazione in HTML produce un documento a più fogli i cui frame di navigazione riportano i nomi dei fogli come testo visibile. Quei nomi sono stringhe controllate dall'autore che possono contenere qualsiasi carattere, compresi quelli significativi per il markup. Un foglio chiamato letteralmente Q1 & Q2 <draft> deve raggiungere la pagina come entità scappate (escaped entities), altrimenti le parentesi angolari aprono un tag fantasma e la e commerciale avvia un riferimento di entità che non era mai stato inteso. Questo è l'escape HTML ordinario, e ometterlo su un'etichetta del frame è il tipo di dimenticanza che supera ogni test costruito solo con nomi di fogli ASCII

La questione della codifica si trova a un livello sottostante. Quando i caratteri non ASCII arrivano in un contesto in cui non è garantito che siano serviti come UTF-8, la rappresentazione sicura è un riferimento numerico al carattere, quindi U+00E9 è scritto come é piuttosto che come un byte grezzo il cui significato dipende dal set di caratteri (charset) della risposta. L'immagine speculare di questa regola si applica in ingresso. Una cartella di lavoro riletta da XLSX trasporta stringhe condivise in cui un carattere potrebbe già essere archiviato come un'entità XML numerica, e quell'entità deve essere decodificata in un singolo carattere completo prima di entrare nel modello della cella. Decodificalo in modo negligente, dividendo un punto di codice in byte separati, e un singolo carattere riemergerà come due frammenti di mojibake che nessuna esportazione successiva potrà riparare

Il contenitore XLSX è uno ZIP, e lo ZIP ha la propria codifica dei nomi

Un file XLSX è un archivio ZIP e l'archivio memorizza un nome per ogni membro che contiene. Il formato ZIP è talmente vecchio che le sue specifiche originali non dicevano nulla sulla codifica di quei nomi, quindi un lettore che non trova alcun segnale presume la code page locale dell'archivio. Tale presupposto è errato nel momento in cui un nome di un membro contiene un carattere non ASCII, cosa che accade con i nomi localizzati delle parti del foglio di lavoro e con i media incorporati i cui nomi di file presentano accenti o caratteri non latini

La soluzione è un singolo bit. Il bit 11 per usi generali (general-purpose bit) nell'intestazione di ciascun file locale dichiara che il nome del membro è codificato in UTF-8. HotXLS controlla esattamente quel bit quando legge un archivio, testando i flag generali contro la maschera $0800, e un lettore o uno scrittore che lo ignora leggerà in modo errato un nome che un'implementazione corretta ha archiviato come UTF-8. Il bit è facile da impostare e facile da rispettare, ed è l'intera differenza tra un nome di un membro che sopravvive al ciclo di importazione-esportazione (round trip) e uno che arriva danneggiato ancor prima che il contenuto del foglio di calcolo venga persino parsato

Il case folding e la scansione dei numeri nascondono lo stesso rischio

La valutazione delle formule è il punto in cui la sicurezza Unicode smette di riguardare la serializzazione e inizia a riguardare il confronto. La funzione SEARCH (RICERCA) non fa distinzione tra maiuscole e minuscole, il che significa che deve effettuare il 'case folding' prima di cercare una sottostringa. Il modo sbagliato di effettuare il folding è tramite la code page ANSI, perché rendere maiuscolo un testo non ASCII in quel modo indirizza i caratteri attraverso una code page ristretta e corrompe tutto ciò che ne è al di fuori. Il modo corretto è tramite il passaggio alle maiuscole per stringhe larghe (wide-string uppercasing), che preserva l'intero intervallo UTF-16. HotXLS effettua il folding con WideUpperCase esattamente per questo motivo, così che una ricerca per un testo accentato o non latino corrisponda agli stessi caratteri che le sono stati forniti anziché a un'approssimazione storpiata dalla code page

Il tokenizer delle formule porta con sé un obbligo correlato che non ha nulla a che fare con le lettere e ha tutto a che fare con il punto in cui termina un token. La notazione scientifica come 1E3 o 2.5E-3 è un singolo valore letterale numerico, e lo scanner deve riconoscere la E, un segno opzionale, e le cifre seguenti come parte del numero anziché dividere l'input in un nome seguito da un numero separato. Uno scanner che gestisce male questa situazione trasforma una costante perfettamente valida in un errore di parsing o, peggio, in un'espressione silenziosamente sbagliata. L'argomento appartiene alla stessa discussione perché in entrambi i casi si tratta di un lettore che prende una decisione corretta a livello di carattere: una su come ripiegare (fold) un carattere per il confronto, l'altra su se un carattere prosegua o meno il token corrente

Creazione ed esportazione di una cartella di lavoro multilingue

Le API pubbliche non ti chiedono di pensare a niente di tutto questo. Costruisci la cartella di lavoro da valori di cella WideString e chiami il punto di ingresso per l'esportazione che desideri. Le decisioni sulla codifica avvengono all'interno di ogni scrittore. L'esempio seguente popola un foglio con testo in diverse lingue, poi scrive sia un file RTF sia un file HTML dalla stessa cartella di lavoro, in modo che i due percorsi vengano eseguiti a partire dallo stesso identico 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';

    // Il testo della cella è mantenuto come WideString, quindi ogni script sopravvive al modello.
    Sheet.Cells[2, 1].Value := '王伟';          // Cinese
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // Umlaut tedesco
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Cirillico
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // Accenti francesi
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: lo scrittore lxRTF dichiara la code page ed emette ogni
    // carattere non ASCII come escape \u, mantenendo il file pulito a 7 bit.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: i nomi dei fogli subiscono l'escape HTML e il testo non ASCII è scritto
    // in modo che non dipenda da un charset di risposta tirato a indovinare.
    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. Nulla nel codice chiamante dichiara una code page o esegue l'escape di un carattere, perché la responsabilità spetta allo scrittore che conosce il proprio formato. Il metodo a livello di cartella di lavoro SaveAsCSV segue la stessa forma se hai bisogno di un'esportazione delimitata dalla stessa identica fonte

// Stessa cartella di lavoro, un terzo percorso di esportazione con le proprie regole di codifica.
Book.SaveAsCSV('Customers.csv');

La sicurezza Unicode è per percorso, non per libreria

La lezione che vale la pena portarsi a casa è che non esiste un singolo posto in cui poter definire la sicurezza Unicode. L'RTF richiede una code page dichiarata più sequenze di escape \u. L'HTML richiede l'escape delle entità per i caratteri significativi nel markup e riferimenti numerici laddove il set di caratteri non sia garantito, oltre alla corretta decodifica delle entità che arrivano nelle stringhe condivise. Il contenitore ZIP necessita che il bit 11 per usi generali sia impostato, così che il nome di un membro UTF-8 venga letto come UTF-8. La valutazione delle formule necessita di un 'case folding' a stringa larga e di un tokenizer che mantenga unita in un solo blocco la notazione scientifica. Ciascuno di questi è un contratto diverso, e una libreria può soddisfarne uno violandone tranquillamente un altro. Questo è il motivo per cui uno strumento che azzecca i file CSV può comunque fornirti un file RTF pieno di punti interrogativi

Se le tue esportazioni si basano sui formati delimitati, i compromessi tra di essi sono trattati nella nostra guida all'esportazione in CSV, TSV e HTML, e quando la fonte è un result set piuttosto che un foglio costruito a mano, i pattern nell'esportazione di database per report Delphi si abbinano naturalmente con le regole di codifica descritte qui. Tutto questo viene fornito come parte del Componente HotXLS per Delphi e C++Builder, insieme alle API di lettura, di gestione delle formule e di formattazione trattate altrove in questo blog