Ett kalkylblad innehåller en kolumn med kundnamn. Vissa är på kinesiska, andra på kyrilliska, några har tyska umlaut eller franska accenter. Du exporterar det till CSV och öppnar resultatet, och varje tecken är intakt. Du exporterar samma arbetsbok till RTF för en kopplad dokumentmall (mail-merge template), öppnar den i en ordbehandlare, och de icke-ASCII-namnen har kollapsat till rader av frågetecken. Datan ändrades aldrig. Det som ändrades är kodningskontraktet för det format du skrev, och varje exportväg har ett eget.
Detta är fällan som fångar ett bibliotek som ser helt Unicode-medvetet ut på ytan. Celltexten sparas internt som WideString, så modellen förlorar aldrig ett tecken. Förlusten sker vid gränsen, i den skrivare som måste serialisera den texten till ett format med sina egna regler om vilka byte som är tillåtna och hur allt utanför det tillåtna intervallet måste kodas. Får du en skrivare rätt kan du ändå leverera en annan som fördärvar samma text. Lösningen är inte en global switch. Det är ett separat, korrekt beslut på varje sökväg.
RTF är ett 7-bitars säkert format av design
Rich Text Format föregår Unicode och specificerades för att överleva transporter som endast skickar utskrivbar ASCII. Ett RTF-dokument deklarerar en teckentabell (code page) i sitt sidhuvud, och alla tecken som skrivaren inte kan representera i den teckentabellen måste skickas ut som en escape-sekvens snarare än som en rå byte. Den relevanta escape-sekvensen är \u, som bär en tecknad 16-bitars kodenhet följd av ett ASCII-ersättningstecken (fallback) för läsare som är för gamla för att överhuvudtaget förstå escape-sekvensen.
HotXLS skriver RTF på detta sätt. Dokumentets sidhuvud öppnas genom att deklarera teckentabellen, i formen \ansi\ansicpg1252\uc1, och skrivaren i lxRTF-enheten går igenom varje sträng och skickar ut alla tecken över vanlig ASCII som en \u-escape så att byte-strömmen förblir 7-bitarsren 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 som det råkar anta. Utan den disciplinen har allt utanför den deklarerade teckentabellen ingen giltig byte-representation, och en skrivare som skickar ut råvärdet producerar frågetecken som startade den här artikeln.
Detaljen att hålla i minnet är att den deklarerade teckentabellen och escape-sekvenserna är två halvor av ett kontrakt. Att enbart deklarera teckentabellen hjälper inte text som ligger utanför den. Att skicka ut escape-sekvenser utan en deklarerad teckentabell lämnar ersättningstecknen tvetydiga. Båda måste vara korrekta tillsammans, vilket är anledningen till att en skrivare som bara hanterar den ena fortfarande misslyckas på den första flerspråkiga arbetsboken.
HTML-escaping handlar om mer än vinkelparenteser
HTML-exporten producerar ett dokument med flera blad vars navigeringsramar bär bladnamnen som synlig text. De namnen är användarkontrollerade strängar som kan innehålla alla tecken, inklusive de som är betydelsefulla för uppmärkningen. Ett blad som bokstavligen heter Q1 & Q2 <draft> måste nå sidan som escapade entiteter, annars öppnar vinkelparenteserna en fantomtagg och et-tecknet (ampersand) startar en entitetsreferens som aldrig var avsedd. Detta är vanlig HTML-escaping, och att hoppa över det på en rametikett är den typ av utelämnande som klarar varje test byggt på enbart ASCII-bladnamn.
Kodningsfrågan ligger ett lager under det. När icke-ASCII-tecken landar i ett sammanhang 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å svarets teckenuppsättning (charset). Spegelbilden av denna regel gäller på vägen in. En arbetsbok som läses tillbaka från XLSX bär delade strängar i vilka ett tecken redan kan vara lagrat som en numerisk XML-entitet, och den entiteten måste avkodas till ett helt tecken innan det kommer in i cellmodellen. Avkoda det oförsiktigt, genom att dela upp en kodpunkt i separata byte, och ett enskilt tecken återuppstår 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 sparar ett namn för varje medlem det innehåller. ZIP är så gammalt att dess ursprungliga specifikation inte sade något om kodningen av dessa namn, så en läsare som inte hittar någon signal antar arkivets lokala teckentabell. Det antagandet är felaktigt i det ögonblick ett medlemsnamn innehåller ett icke-ASCII-tecken, vilket händer med lokaliserade kalkylbladsdelars namn och med inbäddade media vars filnamn bär accenter eller icke-latinska skrift.
Lösningen är en enskild bit. General-purpose bit 11 i varje lokalt filhuvud deklarerar att medlemsnamnet är kodat som UTF-8. HotXLS kontrollerar exakt den biten när den läser ett arkiv, genom att testa general-purpose-flaggorna mot masken $0800, och en läsare eller skrivare som ignorerar den kommer att missförstå ett namn som en korrekt implementering lagrade som UTF-8. Biten är billig att sätta och billig att respektera, och det är hela skillnaden mellan ett medlemsnamn som överlever rundresan och ett som anländer korrupt innan kalkylbladets innehåll ens har tolkats.
Skiftlägesmatchning och sifferskanning döljer samma fara
Formelutvärdering är där Unicode-säkerhet slutar handla om serialisering och börjar handla om jämförelse. Funktionen SEARCH är skiftlägesoberoende, vilket innebär att den måste matcha skiftläge (case fold) innan den söker efter en delsträng. Det felaktiga sättet att matcha är via ANSI-teckentabellen, eftersom versalisering av icke-ASCII-text på det sättet leder tecknen genom en smal teckentabell och förstör allt utanför den. Det rätta sättet är versalisering av breda strängar (wide-string), vilket bevarar hela UTF-16-intervallet. HotXLS matchar med WideUpperCase av exakt denna anledning, så att en sökning efter text med accenter eller icke-latinska tecken matchar samma tecken som den fick snarare än en teckentabellsfördärvad approximation av dem.
Formel-tokenizern har en relaterad skyldighet som inte har något med bokstäver att göra och allt att göra med var en token slutar. Vetenskaplig notation som 1E3 eller 2.5E-3 är en enskild numerisk literal, och skannern måste känna igen E, ett valfritt tecken, och de följande siffrorna som en del av talet snarare än att bryta upp indatan i ett namn följt av ett separat tal. En skanner som misshandlar detta förvandlar en helt giltig konstant till ett tolkningsfel 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å: ett om hur man matchar ett tecken för jämförelse, det andra om huruvida ett tecken fortsätter den aktuella 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 exportstartpunkt du vill ha. Kodningsbesluten sker inuti varje skrivare. Exemplet nedan sår ett blad med text i flera olika skrifter, och skriver sedan både en RTF-fil och en HTML-fil från samma arbetsbok, so att de två sökvägarna körs mot identiska indata.
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 text i minnet. Ingenting i den anropande koden deklarerar en teckentabell eller escapar ett tecken, eftersom ansvaret ligger hos den skrivare som känner till sitt eget format. Arbetsboks-nivåns SaveAsCSV följer samma mönster om du behöver en avgränsad export från identisk källa.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Unicode-säkerhet är per sökväg, inte per bibliotek
Lärdomen 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-escape-sekvenser. HTML behöver entitets-escaping för tecken som är betydelsefulla för uppmärkningen och numeriska referenser där teckenuppsättningen (charset) inte är garanterad, plus korrekt avkodning av entiteter som anländer i delade strängar. ZIP-behållaren behöver general-purpose bit 11 satt så att ett UTF-8-medlemsnamn läses som UTF-8. Formelutvärdering behöver versalisering av breda strängar (wide-string) och en tokenizer som håller ihop vetenskaplig notation i ett stycke. Var och en av dessa är ett separat kontrakt, och ett bibliotek kan uppfylla ett medan det tyst bryter mot ett annat. Det är anledningen till att ett verktyg som gör CSV rätt ändå kan ge dig en RTF full av frågetecken.
If you are 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.