Eine Tabelle enthält eine Spalte mit Kundennamen. Einige sind auf Chinesisch, andere in kyrillischer Schrift, einige enthalten deutsche Umlaute oder einen französischen Akzent. Sie exportieren sie nach CSV und öffnen das Ergebnis – jedes Zeichen ist intakt. Sie exportieren dieselbe Arbeitsmappe nach RTF für eine Serienbrief-Vorlage, öffnen sie in einer Textverarbeitung, und die Nicht-ASCII-Namen sind zu Reihen von Fragezeichen zerfallen. Die Daten haben sich nie geändert. Geändert hat sich der Kodierungsvertrag des von Ihnen geschriebenen Formats, und jeder Exportpfad bringt einen anderen Vertrag mit sich.
Dies ist die Falle, in die eine Bibliothek tappt, die oberflächlich betrachtet vollständig Unicode-fähig zu sein scheint. Der Zellentext wird intern als WideString gehalten, sodass das Modell niemals ein Zeichen verliert. Der Verlust passiert an der Schnittstelle – in der Routine (Writer), die diesen Text in ein Format serialisieren muss, das eigene Regeln darüber hat, welche Bytes zulässig sind und wie alles außerhalb des zulässigen Bereichs kodiert werden muss. Selbst wenn Sie einen Writer fehlerfrei hinbekommen, können Sie immer noch einen anderen ausliefern, der denselben Text beschädigt. Die Lösung ist kein globaler Schalter. Es ist eine separate, korrekte Entscheidung für jeden einzelnen Pfad.
RTF ist konstruktionsbedingt ein 7-Bit-sicheres Format
Das Rich Text Format (RTF) ist älter als Unicode und wurde so konzipiert, dass es Transporte übersteht, die nur druckbare ASCII-Zeichen durchlassen. Ein RTF-Dokument deklariert eine Codepage in seinem Header, und jedes Zeichen, das der Writer in dieser Codepage nicht darstellen kann, muss als Escape-Sequenz anstatt als rohes Byte ausgegeben werden. Die entsprechende Escape-Sequenz lautet \\u, welche eine vorzeichenbehaftete 16-Bit-Codeeinheit trägt, gefolgt von einem ASCII-Fallback-Zeichen für Reader, die zu alt sind, um die Escape-Sequenz überhaupt zu verstehen.
HotXLS schreibt RTF auf diese Weise. Der Dokumenten-Header beginnt mit der Deklaration der Codepage in der Form \\ansi\\ansicpg1252\\uc1, und der Writer in der Unit lxRTF durchläuft jede Zeichenfolge und gibt jedes Zeichen oberhalb von einfachem ASCII als \\u-Escape-Sequenz aus. Dadurch bleibt der Bytestrom unabhängig von dem, was die deklarierte Codepage aufnehmen kann, 7-Bit-sauber. Ein Codepunkt wie U+4E2D wird zur literalen Sequenz \u20013?, nicht zu einem rohen Byte, das ein Viewer dann über eine zufällig angenommene Codepage zu interpretieren versuchen würde. Ohne diese Vorgehensweise hat alles außerhalb der deklarierten Codepage keine gültige Byte-Darstellung, und ein Writer, der den rohen Wert ausgibt, erzeugt die Fragezeichen, die eingangs erwähnt wurden.
Das Detail, das man im Auge behalten muss, ist, dass die deklarierte Codepage und die Escape-Sequenzen zwei Hälften eines einzigen Vertrags sind. Die Deklaration der Codepage allein hilft Texten, die außerhalb dieser liegen, nicht weiter. Das Ausgeben von Escape-Sequenzen ohne deklarierte Codepage lässt die Fallback-Zeichen zweideutig. Beide müssen zusammen korrekt sein, weshalb ein Writer, der nur eines von beiden handhabt, bei der ersten mehrsprachigen Arbeitsmappe scheitert.
HTML-Escaping betrifft mehr als nur spitze Klammern
Der HTML-Export erzeugt ein Dokument mit mehreren Tabellenblättern, deren Navigationsrahmen die Blattnamen als sichtbaren Text tragen. Diese Namen sind vom Autor definierte Zeichenfolgen, die jedes beliebige Zeichen enthalten können, einschließlich der für das Markup wichtigen Zeichen. Ein Tabellenblatt, das buchstäblich Q1 & Q2 <draft> heißt, muss die Seite als escaped Entities erreichen, andernfalls öffnen die spitzen Klammern ein Phantom-Tag und das Kaufmanns-Und startet eine Entity-Referenz, die niemals beabsichtigt war. Dies ist normales HTML-Escaping, und dieses bei einer Rahmenbeschriftung zu überspringen ist die Art von Versäumnis, die jeden Test mit rein ASCII-basierten Blattnamen problemlos besteht.
Die Frage der Kodierung liegt noch eine Ebene tiefer. Wenn Nicht-ASCII-Zeichen in einem Kontext landen, bei dem eine Auslieferung als UTF-8 nicht garantiert ist, ist die sichere Darstellung eine numerische Zeichenreferenz, sodass U+00E9 als é geschrieben wird und nicht als rohes Byte, dessen Bedeutung vom Zeichensatz der Antwort abhängt. Das Spiegelbild dieser Regel gilt beim Einlesen. Eine aus XLSX zurückgelesene Arbeitsmappe enthält gemeinsame Zeichenfolgen (Shared Strings), in denen ein Zeichen möglicherweise bereits als numerische XML-Entity gespeichert ist. Diese Entity muss in ein vollständiges Zeichen dekodiert werden, bevor sie in das Zellenmodell einfließt. Dekodiert man sie unvorsichtig und zerlegt einen Codepunkt in separate Bytes, entsteht aus einem einzelnen Zeichen Zeichensalat (Mojibake), den kein späterer Export mehr reparieren kann.
Der XLSX-Container ist eine ZIP, und ZIP hat eine eigene Namenskodierung
Eine XLSX-Datei ist ein ZIP-Archiv, und das Archiv speichert einen Namen für jedes enthaltene Element. ZIP ist so alt, dass seine ursprüngliche Spezifikation nichts über die Kodierung dieser Namen aussagte. Ein Reader, der kein Signal findet, nimmt daher die lokale Codepage des Archivs an. Diese Annahme ist in dem Moment falsch, in dem der Name eines Elements ein Nicht-ASCII-Zeichen enthält, was bei lokalisierten Arbeitsblattnamen und bei eingebetteten Medien vorkommt, deren Dateinamen Akzente oder nicht-lateinische Schriftzeichen tragen.
Die Lösung ist ein einzelnes Bit. Das Mehrzweck-Bit 11 (General-Purpose Bit 11) in jedem lokalen Datei-Header deklariert, dass der Name des Elements als UTF-8 kodiert ist. HotXLS prüft genau dieses Bit beim Lesen eines Archivs, indem es die Mehrzweck-Flags gegen die Maske $0800 testet. Ein Reader oder Writer, der dies ignoriert, liest einen Namen falsch, den eine korrekte Implementierung als UTF-8 gespeichert hat. Das Bit ist kostengünstig zu setzen und einfach zu berücksichtigen. Es macht den gesamten Unterschied aus, ob ein Elementname den Rundlauf übersteht oder bereits beschädigt ankommt, bevor der Inhalt der Tabelle überhaupt geparkt wird.
Case-Folding und Ziffern-Scanning bergen dieselbe Gefahr
Bei der Formelauswertung geht es bei der Unicode-Sicherheit nicht mehr um Serialisierung, sondern um Vergleiche. Die Funktion SEARCH arbeitet ohne Unterscheidung von Groß- und Kleinschreibung, was bedeutet, dass sie die Groß-/Kleinschreibung vereinheitlichen (folden) muss, bevor sie nach einer Teilzeichenfolge sucht. Der falsche Weg für das Folding führt über die ANSI-Codepage, da die Großschreibung von Nicht-ASCII-Text auf diese Weise Zeichen über eine enge Codepage leitet und alles außerhalb dieser beschädigt. Der richtige Weg ist die Großschreibung über Wide-Strings, die den gesamten UTF-16-Bereich beibehält. HotXLS konvertiert genau aus diesem Grund mit WideUpperCase, sodass eine Suche nach Text mit Akzenten oder nicht-lateinischen Zeichen genau mit den übergebenen Zeichen übereinstimmt und nicht mit einer durch die Codepage verfälschten Annäherung.
Der Formel-Tokenizer hat eine ähnliche Pflicht, die nichts mit Buchstaben zu tun hat, sondern ganz damit, wo ein Token endet. Die wissenschaftliche Notation wie 1E3 oder 2.5E-3 ist ein einzelnes numerisches Literal, und the Scanner muss das E, ein optionales Vorzeichen und die folgenden Ziffern als Teil der Zahl erkennen, anstatt die Eingabe in einen Namen gefolgt von einer separaten Zahl aufzuteilen. Ein Scanner, der dies falsch handhabt, macht aus einer völlig gültigen Konstante einen Parse-Fehler oder, schlimmer noch, einen unbemerkt falschen Ausdruck. Dies gehört in dieselbe Diskussion, da es in beiden Fällen darum geht, dass ein Reader eine korrekte Entscheidung auf Zeichenebene trifft: einmal darüber, wie ein Zeichen für den Vergleich vereinheitlicht wird, und einmal darüber, ob ein Zeichen das aktuelle Token fortsetzt.
Erstellen und Exportieren einer mehrsprachigen Arbeitsmappe
Die öffentliche API verlangt nicht, dass Sie über all das nachdenken. Sie erstellen die Arbeitsmappe aus WideString-Zellwerten und rufen den gewünschten Export-Einstiegspunkt auf. Die Kodierungsentscheidungen treffen die jeweiligen Writer intern. Das folgende Beispiel befüllt eine Tabelle mit Texten in verschiedenen Schriften und schreibt dann sowohl eine RTF-Datei als auch eine HTML-Datei aus derselben Arbeitsmappe, sodass beide Pfade mit identischen Eingaben arbeiten.
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;
Beide Aufrufe geben einen Integer-Status zurück und konsumieren denselben Text im Speicher. Nichts im aufrufenden Code deklariert eine Codepage oder escaped ein Zeichen, da die Verantwortung beim Writer liegt, der sein eigenes Format kennt. Die Methode SaveAsCSV auf Arbeitsmappenebene folgt derselben Struktur, falls Sie einen Export mit Trennzeichen aus identischer Quelle benötigen.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Unicode-Sicherheit gilt pro Pfad, nicht pro Bibliothek
Die wichtigste Lektion ist, dass es keinen einzelnen Ort gibt, um Unicode-sicher zu sein. RTF benötigt eine deklarierte Codepage plus \u-Escape-Sequenzen. HTML erfordert das Escaping von Entities für markup-relevante Zeichen und numerische Referenzen, wenn der Zeichensatz nicht garantiert ist, sowie das korrekte Dekodieren von Entities, die in gemeinsamen Zeichenfolgen ankommen. Der ZIP-Container benötigt das gesetzte Mehrzweck-Bit 11, damit ein UTF-8-Elementname als UTF-8 gelesen wird. Die Formelauswertung erfordert ein Wide-String-Case-Folding und einen Tokenizer, der die wissenschaftliche Notation zusammenhält. Jede dieser Anforderungen ist ein anderer Vertrag, und eine Bibliothek kann eine erfüllen, während sie eine andere unbemerkt verletzt. Das ist der Grund, warum ein Tool, das CSV fehlerfrei beherrscht, Ihnen immer noch eine RTF-Datei voller Fragezeichen liefern kann.
If your exports lean on the delimited formats, the trade-offs between them are covered in our walkthrough of CSV, TSV and HTML export, and when the source is a result set rather than a hand-built sheet, the patterns in database export for Delphi reports pair naturally with the encoding rules described here. All of it ships as part of the HotXLS Component for Delphi and C++Builder, alongside the reading, formula, and formatting APIs covered elsewhere on this blog.