Ett kalkylblad innehåller en kolumn med kundnamn. Vissa är på kinesiska, vissa på kyrilliska, några bär tyska omljud eller en fransk accent. Du exporterar det till CSV och öppnar resultatet, och varje tecken är intakt. Du exporterar samma arbetsbok till RTF för en e-postkopplingsmall, öppnar den i en ordbehandlare, och de icke-ASCII-namnen har kollapsat till rader av frågetecken. Data ändrades aldrig. Det som ändrades är kodningskontraktet för formatet du skrev, och varje exportväg bär ett eget annorlunda kontrakt
Detta är fällan som fångar ett bibliotek som ser fullt Unicode-medvetet ut på ytan. Celltexten hålls internt som WideString, så modellen förlorar aldrig ett tecken. Förlusten sker vid gränsen, i skribenten som måste serialisera den texten till ett format med sina egna regler om vilka byte som är lagliga och hur allt utanför det lagliga intervallet måste kodas. Få till en skribent rätt, och du kan fortfarande leverera en annan som manglar samma text. Lösningen är inte en global strömbrytare. Det är ett separat, korrekt beslut på varje väg
RTF är ett 7-bitars säkert format genom sin design
Rich Text Format föregår Unicode och var specificerat för att överleva transporter som bara passerar utskrivbar ASCII. Ett RTF-dokument deklarerar en teckentabell (code page) i sin rubrik, och alla tecken som skribenten inte kan representera i den teckentabellen måste avges som en flyktsekvens (escape) snarare än som en rå byte. Den relevanta flyktsekvensen är \u, som bär en signerad 16-bitars kodenhet följd av ett ASCII-reservtecken för läsare som är för gamla för att förstå flyktsekvensen överhuvudtaget
HotXLS skriver RTF på detta sätt. Dokumentrubriken öppnas genom att deklarera teckentabellen, i formen \ansi\ansicpg1252\uc1, och skribenten i enheten lxRTF går igenom varje sträng och emitterar alla tecken över vanlig ASCII som en \u-flyktsekvens så att byteströmmen förblir 7-bitars ren oavsett vad den deklarerade teckentabellen kan rymma. En kodpunkt som U+4E2D blir den bokstavliga sekvensen \u20013?, inte en rå byte som ett visningsprogram sedan skulle försöka tolka genom den teckentabell den råkade anta. Utan den disciplinen har inget utanför den deklarerade teckentabellen en laglig byterepresentation, och en skribent som emitterar det råa värdet producerar de frågetecken som inledde denna artikel
Detaljen att hålla i åtanke är att den deklarerade teckentabellen och flyktsekvenserna är två halvor av ett kontrakt. Att enbart deklarera teckentabellen hjälper inte text som ligger utanför den. Att emittera flyktsekvenser utan en deklarerad teckentabell lämnar reservtecknen tvetydiga. Båda måste vara korrekta tillsammans, vilket är anledningen till att en skribent som bara hanterar en av dem fortfarande misslyckas på den första flerspråkiga arbetsboken
HTML-kodning handlar om mer än vinkelparenteser
HTML-exporten producerar ett flersidigt dokument vars navigeringsramar bär arkets namn som synlig text. Dessa namn är författarkontrollerade strängar som kan innehålla vilket tecken som helst, inklusive de markeringsbetydelsefulla. Ett blad som bokstavligen heter Q1 & Q2 <draft> måste nå sidan som formaterade entiteter, annars öppnar vinkelparenteserna en fantomtagg och ampersanden startar en entitetsreferens som aldrig var avsedd. Detta är vanlig HTML-kodning, och att hoppa över det på en ram-etikett är den typen av utelämnande som klarar varje test byggt från endast ASCII-bladnamn
Kodningsfrågan sitter ett lager under det. När icke-ASCII-tecken landar i en kontext som inte garanterat serveras som UTF-8, är den säkra representationen en numerisk teckenreferens, så U+00E9 skrivs som é snarare än som en rå byte vars betydelse beror på svarsteckenuppsättningen. Spegelbilden av denna regel gäller på vägen in. En arbetsbok som läses tillbaka från XLSX bär delade strängar där ett tecken redan kan vara lagrat som en numerisk XML-entitet, och den entiteten måste avkodas till ett helt tecken innan den går in i cellmodellen. Avkoda det oförsiktigt, dela upp en kodpunkt i separata byte, och ett enda tecken dyker upp igen som två bitar mojibake som ingen senare export kan reparera
XLSX-behållaren är en ZIP, och ZIP har sin egen namnkodning
En XLSX-fil är ett ZIP-arkiv, och arkivet lagrar ett namn för varje medlem det innehåller. ZIP är tillräckligt gammalt för att dess ursprungliga specifikation inte sa något om kodningen av de namnen, så en läsare som inte hittar någon signal antar arkivets lokala teckentabell. Det antagandet är fel i samma ögonblick som ett medlemsnamn innehåller ett icke-ASCII-tecken, vilket händer med lokaliserade arbetsblad-delnamn och med inbäddade medier vars filnamn bär accenter eller icke-latinska tecken
Lösningen är en enda bit. Generell ändamålsbit 11 i varje lokal filrubrik deklarerar att medlemsnamnet är kodat som UTF-8. HotXLS kontrollerar exakt den biten när den läser ett arkiv, testar de allmänna flaggorna mot masken $0800, och en läsare eller skribent som ignorerar den kommer att felläsa ett namn som en korrekt implementering lagrade som UTF-8. Biten är billig att ställa in och billig att respektera, och det är hela skillnaden mellan ett medlemsnamn som överlever tur och retur-resan och ett som anländer korrupt innan kalkylbladets innehåll ens har tolkats
Skiftlägesändring (Case folding) och nummerskanning döljer samma fara
Formelutvärdering är där Unicode-säkerhet slutar att handla om serialisering och istället handlar om jämförelse. Funktionen SEARCH är skiftlägesokänslig, vilket innebär att den måste ändra skiftläge innan den letar efter en delsträng. Fel sätt att ändra är genom ANSI-teckentabellen, eftersom att omvandla icke-ASCII-text till versaler på det sättet leder tecknen genom en snäv teckentabell och korrumperar allt utanför den. Det rätta sättet är wide-string versalomvandling, som bevarar hela UTF-16-intervallet. HotXLS ändrar med WideUpperCase av exakt denna anledning, så en sökning efter accentuerad eller icke-latinsk text matchar samma tecken som den gavs snarare än en teckentabell-manglad approximation av dem
Formeltokern (tokenizer) bär en relaterad skyldighet som inte har något att göra med bokstäver och allt att göra med var ett tecken(token) slutar. Vetenskaplig notation som 1E3 eller 2.5E-3 är en enda numerisk bokstav (literal), och skannern måste känna igen E:et, ett valfritt tecken, och de följande siffrorna som en del av talet snarare än att bryta ner inmatningen till ett namn följt av ett separat tal. En skanner som felhanterar detta förvandlar en fullt giltig konstant till ett tolkfel eller, ännu värre, ett tyst felaktigt uttryck. Det hör hemma i samma diskussion eftersom båda fallen handlar om att en läsare fattar ett korrekt beslut på teckennivå: det ena om hur man ändrar ett tecken för jämförelse, det andra om huruvida ett tecken fortsätter den nuvarande token
Att bygga och exportera en flerspråkig arbetsbok
Det publika API:et ber dig inte att tänka på något av detta. Du bygger arbetsboken från WideString-cellvärden och anropar den export-ingångspunkt du vill ha. Kodningsbesluten sker inuti varje skribent. Exemplet nedan sår ett kalkylblad med text i flera skript, och skriver sedan både en RTF-fil och en HTML-fil från samma arbetsbok, så att de två vägarna körs mot identisk inmatning
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;
Båda anropen returnerar en Integer-status, och båda konsumerar samma in-memory-text. Inget i den anropande koden deklarerar en teckentabell eller kodar ett tecken, eftersom ansvaret ligger hos skribenten som känner till sitt eget format. Arbetsboks-nivå SaveAsCSV följer samma form om du behöver en avgränsad export från samma källa
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Unicode-säkerhet är per väg, inte per bibliotek
Lektionen som är värd att ta med sig är att det inte finns någon enskild plats för att vara Unicode-säker. RTF behöver en deklarerad teckentabell plus \u-flyktsekvenser. HTML behöver entitetskodning för markeringsbetydelsefulla tecken och numeriska referenser där teckenuppsättningen inte garanteras, plus korrekt avkodning av entiteter som anländer i delade strängar. ZIP-behållaren behöver ha allmänändamålsbit 11 satt så att ett UTF-8-medlemsnamn läses som UTF-8. Formelutvärdering behöver wide-string skiftlägesändring och en tokeniserare (tokenizer) som håller vetenskaplig notation i ett stycke. Var och en av dessa är ett annorlunda kontrakt, och ett bibliotek kan tillfredsställa ett medan det i tysthet bryter mot ett annat. Det är anledningen till att ett verktyg som får CSV rätt fortfarande kan ge dig en RTF full av frågetecken
Om dina exporter stöder sig på de avgränsade formaten, täcks kompromisserna mellan dem i vår genomgång av CSV, TSV och HTML-export, och när källan är en resultatuppsättning (result set) snarare än ett handbyggt blad, passar mönstren i databasexport för Delphi-rapporter naturligt ihop med de kodningsregler som beskrivs här. Allt detta levereras som en del av HotXLS-komponenten för Delphi och C++Builder, tillsammans med de läs-, formel- och formaterings-API:er som täcks på andra ställen på denna blogg