Een werkblad bevat een kolom met klantnamen. Sommige zijn in het Chinees, sommige in het Cyrillisch, een paar dragen Duitse umlauts of een Frans accent. U exporteert het naar CSV en opent het resultaat, en elk teken is intact. U exporteert dezelfde werkmap (workbook) naar RTF voor een mail-merge sjabloon, opent het in een tekstverwerker, en de niet-ASCII namen zijn ingestort (collapsed) tot rijen vraagtekens. De gegevens zijn nooit veranderd. Wat veranderde is het coderingscontract (encoding contract) van het formaat dat u schreef, en elk exportpad draagt een ander contract
Dit is de valstrik die een bibliotheek vangt die aan de oppervlakte volledig Unicode-bewust lijkt. De celtekst wordt intern als WideString gehouden, dus het model verliest nooit een teken. Het verlies gebeurt op de grens (boundary), in de schrijver (writer) die die tekst moet serialiseren naar een formaat met zijn eigen regels over welke bytes legaal zijn en hoe alles buiten het legale bereik moet worden gecodeerd. Krijg de ene schrijver goed en u kunt nog steeds een andere leveren (ship) die dezelfde tekst verminkt. De oplossing is geen globale schakelaar (global switch). Het is een aparte, correcte beslissing op elk pad
RTF is by design een 7-bits-veilig formaat
Rich Text Format dateert van voor Unicode en werd gespecificeerd om transporten te overleven die alleen afdrukbare ASCII doorgeven. Een RTF-document declareert een codepagina (code page) in de koptekst (header), en elk teken dat de schrijver niet in die codepagina kan weergeven, moet worden verzonden (emitted) als een escape in plaats van als een onbewerkte (raw) byte. De relevante escape is \u, die een getekende (signed) 16-bits code unit draagt, gevolgd door een ASCII-fallback-teken voor lezers die te oud zijn om de escape überhaupt te begrijpen
HotXLS schrijft RTF op deze manier. De documentkoptekst opent met het declareren van de codepagina, in de vorm \ansi\ansicpg1252\uc1, en de schrijver in de lxRTF-eenheid (unit) doorloopt elke tekenreeks (string) en zendt (emits) elk teken boven gewone ASCII uit als een \u escape, zodat de bytestroom (byte stream) 7-bits schoon blijft, ongeacht wat de gedeclareerde codepagina kan bevatten. Een code point zoals U+4E2D wordt de letterlijke reeks \u20013?, geen onbewerkte byte die een viewer vervolgens zou proberen te interpreteren via de codepagina die deze toevallig aannam. Zonder die discipline heeft alles buiten de gedeclareerde codepagina geen legale byteweergave, en een schrijver die de onbewerkte waarde verzendt, produceert de vraagtekens waarmee dit artikel begon
Het detail om in gedachten te houden is dat de gedeclareerde codepagina en de escapes twee helften van één contract zijn. Het declareren van de codepagina alleen helpt de tekst niet die erbuiten ligt. Het verzenden van escapes zonder een gedeclareerde codepagina laat de fallback-tekens ambigu. Beide moeten samen correct zijn, en daarom faalt een schrijver die er slechts één van afhandelt nog steeds bij de eerste meertalige (multilingual) werkmap
HTML-escaping gaat over meer dan haakjes
De HTML-export produceert een document met meerdere bladen (multi-sheet) waarvan de navigatieframes de bladnamen als zichtbare tekst dragen. Die namen zijn door de auteur gecontroleerde tekenreeksen die elk teken kunnen bevatten, inclusief de mark-up-significante tekens. Een blad dat letterlijk Q1 & Q2 <draft> heet, moet de pagina bereiken als ontsnapte (escaped) entiteiten, anders openen de hoekige haakjes (angle brackets) een fantoom-tag en start de ampersand een entiteitreferentie (entity reference) die nooit de bedoeling was. Dit is gewone HTML-escaping, en het overslaan ervan op een frame-label is het soort weglating (omission) dat slaagt voor elke test gebouwd uit enkel-ASCII bladnamen
De coderingsvraag zit één laag daaronder. Wanneer niet-ASCII-tekens belanden in een context waarvan niet gegarandeerd is dat deze als UTF-8 wordt bediend (served), is de veilige weergave een numerieke tekenreferentie (numeric character reference), dus U+00E9 wordt geschreven als é in plaats van als een onbewerkte byte waarvan de betekenis afhangt van de respons-charset. Het spiegelbeeld van deze regel is van toepassing op de weg naar binnen. Een werkmap die wordt teruggelezen vanuit XLSX draagt gedeelde tekenreeksen (shared strings) waarin een teken mogelijk al is opgeslagen als een numerieke XML-entiteit, en die entiteit moet worden gedecodeerd tot één heel teken voordat deze het celmodel binnengaat. Decodeer het onzorgvuldig, door een code point in aparte bytes te splitsen, en een enkel teken duikt weer op als twee stukken mojibake die geen enkele latere export kan repareren
De XLSX container is een ZIP, en ZIP heeft zijn eigen naamcodering
Een XLSX-bestand is een ZIP-archief (archive), en het archief slaat een naam op voor elk lid dat het bevat. ZIP is oud genoeg dat de oorspronkelijke specificatie niets zei over de codering van die namen, dus een lezer die geen signaal vindt, gaat uit van de lokale codepagina (local code page) van het archief. Die aanname is verkeerd zodra een lidnaam (member name) een niet-ASCII-teken bevat, wat gebeurt bij gelokaliseerde namen van werkbladonderdelen en bij ingebedde media waarvan de bestandsnamen accenten of niet-Latijns schrift (non-Latin script) dragen
De oplossing is een enkele bit. General-purpose bit 11 in elke local file header verklaart dat de lidnaam is gecodeerd als UTF-8. HotXLS controleert exact die bit wanneer het een archief leest, waarbij de general-purpose flags worden getest tegen het masker $0800, en een lezer of schrijver die dit negeert, zal een naam verkeerd lezen die een correcte implementatie als UTF-8 heeft opgeslagen. De bit is goedkoop in te stellen en goedkoop te respecteren (honour), en het is het hele verschil tussen een lidnaam die de heen- en terugreis overleeft en een naam die corrupt aankomt voordat de inhoud van het spreadsheet überhaupt is ontleed
Vouwen van hoofdletters (case folding) en het scannen van getallen verbergen hetzelfde gevaar
Formule-evaluatie is waar Unicode-veiligheid stopt met over serialisatie te gaan en over vergelijking begint te gaan. De SEARCH-functie is niet hoofdlettergevoelig (case-insensitive), wat betekent dat deze hoofdletters/kleine letters moet vouwen (fold) voordat er naar een subtekenreeks wordt gezocht. De verkeerde manier om te vouwen is via de ANSI-codepagina, want het op die manier omzetten naar hoofdletters van niet-ASCII-tekst leidt de tekens door een smalle codepagina en verminkt alles daarbuiten. De juiste manier is het omzetten naar hoofdletters van wide-strings, wat het volledige UTF-16 bereik behoudt. HotXLS vouwt met WideUpperCase om precies deze reden, zodat een zoekopdracht naar geaccentueerde of niet-Latijnse tekst overeenkomt met dezelfde tekens die het kreeg, in plaats van een door de codepagina verminkte benadering (approximation) ervan
De formuletokenizer (formula tokenizer) draagt een gerelateerde verplichting die niets te maken heeft met letters en alles met waar een token eindigt. Wetenschappelijke notatie (scientific notation) zoals 1E3 of 2.5E-3 is één enkele numerieke literal, en de scanner moet de E, een optioneel teken (sign), en de daaropvolgende cijfers herkennen als onderdeel van het getal, in plaats van de invoer op te splitsen in een naam gevolgd door een apart getal. Een scanner die hiermee verkeerd omgaat, verandert een perfect geldige constante in een parse-fout of, erger nog, in een stilzwijgend verkeerde expressie. Het hoort in dezelfde discussie thuis, omdat beide gevallen gaan over een lezer (reader) die een correcte beslissing op tekenniveau neemt: de ene over hoe een teken te vouwen (fold) voor vergelijking, de andere over of een teken het huidige token (current token) voortzet
Het bouwen en exporteren van een meertalige (multilingual) werkmap
De publieke API vraagt u niet om over dit alles na te denken. U bouwt de werkmap (workbook) op uit WideString-celwaarden en roept het exporttoegangspunt (export entry point) aan dat u wilt. De coderingsbeslissingen vinden plaats binnen elke schrijver. Het onderstaande voorbeeld vult (seeds) een blad met tekst in verschillende scripts, en schrijft vervolgens zowel een RTF-bestand als een HTML-bestand vanuit dezelfde werkmap, zodat de twee paden tegen identieke invoer lopen
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 aanroepen retourneren een Integer status, en beide consumeren dezelfde tekst in het geheugen. Niets in de aanroepende code declareert een codepagina of ontsnapt (escapes) een teken, omdat de verantwoordelijkheid bij de schrijver ligt die zijn eigen formaat kent. De op werkmap-niveau geplaatste SaveAsCSV volgt dezelfde vorm als u een gescheiden export (delimited export) nodig heeft van de identieke bron
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Unicode-veiligheid is per-pad, niet per-bibliotheek
De les die de moeite waard is om mee te nemen, is dat er geen enkele (single) plek is om Unicode-veilig te zijn. RTF heeft een gedeclareerde codepagina nodig, plus \u escapes. HTML heeft entity-escaping nodig voor mark-up-significante tekens en numerieke referenties waar de charset niet gegarandeerd is, plus correcte decodering van entiteiten die aankomen in gedeelde tekenreeksen (shared strings). De ZIP-container heeft de general-purpose bit 11 nodig die is ingesteld, zodat een UTF-8-lidnaam wordt gelezen als UTF-8. Formule-evaluatie vereist wide-string case folding en een tokenizer die wetenschappelijke notatie intact houdt. Elk van deze is een ander contract, en een bibliotheek kan aan de ene voldoen terwijl deze een andere stilzwijgend overtreedt. Dat is de reden dat een tool die CSV goed doet, u nog steeds een RTF vol vraagtekens kan overhandigen
Als uw exports leunen op de gescheiden formaten (delimited formats), worden de afwegingen (trade-offs) daartussen behandeld in onze walkthrough van CSV-, TSV- en HTML-export, en wanneer de bron een result set (resultaatset) is in plaats van een met de hand gebouwd blad, combineren de patronen in database-export voor Delphi-rapporten op een natuurlijke wijze met de hier beschreven coderingsregels (encoding rules). Dit alles wordt meegeleverd als onderdeel van de HotXLS Component voor Delphi en C++Builder, naast de lees-, formule- en opmaak-API's (formatting APIs) die elders op deze blog worden behandeld