Een spreadsheet bevat een kolom met klantnamen. Sommige zijn in het Chinees, sommige in het Cyrillisch, een paar bevatten Duitse umlauten of een Frans accent. U exporteert het naar CSV en opent het resultaat: elk teken is intact. U exporteert hetzelfde werkboek naar RTF voor een sjabloon voor afdruksamenvoeging (mail-merge), opent het in een tekstverwerker, en de niet-ASCII-namen zijn veranderd in reeksen vraagtekens. De gegevens zijn nooit gewijzigd. Wat is gewijzigd, is het coderingscontract (encoding contract) van het formaat dat u hebt geschreven, en elk exportpad heeft een ander contract.
Dit is de valkuil waar een bibliotheek in trapt die er aan de oppervlakte volledig Unicode-bewust uitziet. De celtekst wordt intern bewaard als WideString, zodat het model nooit een teken verliest. Het verlies vindt plaats aan de grens, in de writer die die tekst moet serialiseren naar een formaat met eigen regels over welke bytes zijn toegestaan en hoe alles buiten het toegestane bereik moet worden gecodeerd. Zelfs als één writer correct is, kunt u nog steeds een andere leveren die dezelfde tekst verminkt. De oplossing is geen wereldwijde schakelaar. Het is een afzonderlijke, correcte beslissing op elk pad.
RTF is van nature een 7-bits-veilig formaat
Rich Text Format (RTF) stamt van vóór Unicode en was ontworpen om transporten te overleven die alleen afdrukbare ASCII doorgeven. Een RTF-document declareert een codepagina in de header, en elk teken dat de writer niet in die codepagina kan weergeven, moet als een escape worden verzonden in plaats van als een ruwe byte. De relevante escape is \u, die een getekende 16-bits code-eenheid bevat, gevolgd door een ASCII-fallbackteken voor lezers die te oud zijn om de escape überhaupt te begrijpen.
HotXLS schrijft RTF op deze manier. De documentheader begint met het declareren van de codepagina, in de vorm \ansi\ansicpg1252\uc1, en the writer in de lxRTF-unit doorloopt elke string en verzendt elk teken boven gewone ASCII als een \u-escape, zodat de byte-stroom 7-bits schoon blijft, ongeacht wat de gedeclareerde codepagina kan bevatten. Een codepunt zoals U+4E2D wordt de letterlijke reeks 3?, niet een ruwe byte die een viewer vervolgens zou proberen te interpreteren via de codepagina die deze toevallig veronderstelt. Zonder die discipline heeft alles buiten de gedeclareerde codepagina geen geldige byte-vertegenwoordiging, en produceert een writer die de ruwe waarde verzendt 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. Alleen het declareren van de codepagina helpt tekst dat daarbuiten ligt niet. Het verzenden van escapes zonder een gedeclareerde codepagina maakt the fallbacktekens dubbelzinnig. Beide moeten samen correct zijn, en dat is waarom een writer die er slechts één afhandelt nog steeds faalt bij het eerste meertalige werkboek.
HTML-escaping gaat over meer dan alleen punthaakjes
De HTML-export produceert een document met meerdere bladen waarvan de navigatie-frames de bladnamen als zichtbare tekst dragen. Die namen zijn door de auteur beheerde strings die elk teken kunnen bevatten, inclusief tekens die belangrijk zijn voor de opmaak. Een blad dat letterlijk Q1 & Q2 <draft> heet, moet de pagina bereiken als geëscapede entiteiten, anders openen de punthaakjes een spook-tag en start het ampersand-teken een entiteitsreferentie die nooit de bedoeling was. Dit is gewone HTML-escaping, en het overslaan daarvan op een frame-label is het soort omissie dat elke test doorstaat die is gebouwd met alleen ASCII-bladnamen.
De coderingsvraag bevindt zich nog een niveau daaronder. Wanneer niet-ASCII-tekens terechtkomen in een context waarvan niet gegarandeerd is dat deze als UTF-8 wordt aangeboden, de veilige weergave is een numerieke tekenreferentie, zodat U+00E9 wordt geschreven als é in plaats van als een ruwe byte waarvan de betekenis afhangt van de responskarakterset. Het spiegelbeeld van deze regel geldt bij het inlezen. Een werkboek dat uit XLSX wordt teruggelezen, bevat gedeelde 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. Decodeert u dit onzorgvuldig, waarbij een codepunt in afzonderlijke bytes wordt gesplitst, dan verschijnt een enkel teken opnieuw als twee stukken mojibake die bij geen enkele latere export kunnen worden hersteld.
De XLSX-container is a ZIP, en ZIP heeft zijn eigen naamcodering
Een XLSX-bestand is een ZIP-archief, en het archief slaat een naam op voor elk element 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 van het archief. Die aanname is onjuist zodra een elementnaam een niet-ASCII-teken bevat, wat gebeurt bij gelokaliseerde namen van werkbladdelen en bij ingebedde media waarvan de bestandsnamen accenten of niet-Latijns schrift bevatten.
De oplossing is een enkele bit. Algemene bit 11 (general-purpose bit 11) in elke lokale bestandsheader verklaart dat de elementnaam is gecodeerd als UTF-8. HotXLS controleert exact die bit wanneer het een archief leest, door de algemene vlaggen te testen tegen het masker $0800. Een lezer of writer die dit negeert, zal een naam verkeerd lezen die door een correcte implementatie als UTF-8 was opgeslagen. De bit is goedkoop in te stellen en goedkoop te respecteren, en het is het hele verschil tussen een elementnaam die de heen- en terugreis overleeft en een naam die corrupt aankomt nog voordat de spreadsheetinhoud überhaupt is geparseerd.
Case folding en getallenscanning verbergen hetzelfde gevaar
Formule-evaluatie is het punt waar Unicode-veiligheid niet langer over serialisatie gaat, maar over vergelijking. De SEARCH-functie is hoofdletterongevoelig, wat betekent dat deze hoofdletters en kleine letters moet converteren (case folding) voordat hij zoekt naar een substring. De verkeerde manier om dit te doen is via de ANSI-codepagina, omdat het omzetten van niet-ASCII-tekst naar hoofdletters op die manier de tekens door een smalle codepagina leidt en alles daarbuiten verminkt. De juiste manier is hoofdletteromzetting via WideString, wat het volledige UTF-16-bereik behoudt. HotXLS converteert met WideUpperCase om precies die reden, zodat een zoekopdracht naar tekst met accenten of niet-Latijnse tekst overeenkomt met dezelfde tekens die werden opgegeven, in plaats van een door de codepagina verminkte benadering daarvan.
De formule-tokenizer draagt een verwante verplichting die niets te maken heeft met letters en alles met waar een token eindigt. Wetenschappelijke notatie zoals 1E3 of 2.5E-3 is een enkele numerieke literal, en the scanner moet de E, een optioneel teken en de volgende 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 dit verkeerd afhandelt, verandert een volkomen geldige constante in een parseringsfout of, erger nog, een geruisloos onjuiste expressie. Dit hoort in dezelfde discussie thuis omdat beide gevallen gaan over een lezer die een correcte beslissing op tekenniveau neemt: de ene over hoe een teken te converteren voor vergelijking, de andere over of een teken het huidige token voortzet.
Een meertalig werkboek bouwen en exporteren
Beide aanroepen retourneren een Integer-status, en beide verbruiken dezelfde tekst in het geheugen. Niets in de aanroepende code declareert een codepagina of escapet een teken, omdat de verantwoordelijkheid ligt bij de writer die het eigen formaat kent. Het werkboekniveau SaveAsCSV volgt dezelfde vorm als u een gescheiden export van de identieke bron hebt.
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 weerspiegelen dezelfde invoer. Niets in de aanroepende code declareert een codepagina of escapet een teken, omdat de verantwoordelijkheid ligt bij de writer die het eigen formaat kent. Het werkboekniveau SaveAsCSV volgt dezelfde vorm als u een gescheiden export van de identieke bron hebt.
// 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 u moet onthouden is dat er niet één enkele plek is om Unicode-veilig te zijn. RTF heeft een gedeclareerde codepagina plus \u-escapes nodig. HTML heeft entiteit-escaping nodig voor opmaak-relevante tekens en numerieke referenties waar de karakterset niet gegarandeerd is, plus correcte decodering van entiteiten die binnenkomen in gedeelde strings. De ZIP-container moet de algemene bit 11 hebben ingesteld, zodat een UTF-8-elementnaam als UTF-8 wordt gelezen. Formule-evaluatie vereist case folding via WideString en een tokenizer die wetenschappelijke notatie in één stuk houdt. Elk hiervan is un ander contract, en een bibliotheek kan aan de ene voldoen terwijl hij de andere stilletjes schendt. Dat is de reden waarom een hulpmiddel dat CSV goed doet, u toch een RTF vol vraagtekens kan overhandigen.
Als uw exports leunen op de gescheiden formaten, de afwegingen daartussen worden behandeld in onze handleiding over CSV-, TSV- en HTML-export. Wanneer de bron een resultaatset is in plaats van een handmatig gebouwd blad, sluiten de patronen in database-export voor Delphi-rapporten natuurlijk aan bij de hier beschreven coderingsregels. Dit alles wordt geleverd als onderdeel van de HotXLS Component voor Delphi en C++Builder, naast de API's voor lezen, formules en opmaak die elders op deze blog worden behandeld.