Et regneark indeholder en kolonne med kundenavne. Nogle er på kinesisk, nogle med kyrillisk, nogle få har tyske umlaut-tegn eller en fransk accent. Du eksporterer det til CSV og åbner resultatet, og hvert enkelt tegn er intakt. Du eksporterer samme projektmappe til RTF til en brevfletningsskabelon, åbner den i et tekstbehandlingsprogram, og de ikke-ASCII-navne er faldet sammen til rækker af spørgsmålstegn. Dataene ændrede sig aldrig. Det, der ændrede sig, er kodningskontrakten for det format, du skrev, og hver eksportsti bærer på en forskellig kontrakt.
Dette er fælden, der fanger et bibliotek, der ser ud til at være fuldt Unicode-kompatibelt på overfladen. Celleteksten gemmes internt som WideString, so modellen mister aldrig et tegn. Tabet sker ved grænsen, i den writer, der skal serialisere den pågældende tekst til et format med sine egne regler om, hvilke bytes der er tilladte, og hvordan alt uden for det tilladte område skal kodes. Få én writer rigtig, og du kan stadig levere en anden, der spolerer den samme tekst. Løsningen er ikke en global kontakt. Det er en separat, korrekt beslutning på hver enkelt sti.
RTF er et 7-bit-sikkert format som design
Rich Text Format ligger før Unicode og blev specificeret til at overleve transporter, der kun sender printbar ASCII. Et RTF-dokument erklærer en tegntabel (code page) i sit sidehoved, og ethvert tegn, som writeren ikke kan repræsentere i denne tegntabel, skal udsendes som en escape-sekvens frem for som en rå byte. Den relevante escape-sekvens er \u, som bærer en 16-bit kodeenhed med fortegn efterfulgt af et ASCII-erstatningstegn (fallback) til læsere, der er for gamle til overhovedet at forstå escape-sekvensen.
HotXLS skriver RTF på denne måde. Dokumenthovedet åbner med at erklære tegntabellen i formen \ansi\ansicpg1252\uc1, and writeren i lxRTF-enheden gennemgår hver streng og udsender ethvert tegn over almindelig ASCII som en \u-escape-sekvens, så byte-strømmen forbliver 7-bit-ren uanset hvad den erklærede tegntabel kan rumme. Et kodepunkt som U+4E2D bliver til den bogstavelige sekvens 3?, ikke en rå byte, som en fremviser derefter ville forsøge at fortolke gennem den tegntabel, den nu tilfældigvis antog. Uden den disciplin har alt uden for den erklærede tegntabel ingen lovlig byte-repræsentation, og en writer, der udsender den rå værdi, producerer de spørgsmålstegn, der startede denne artikel.
Detaljen, man skal huske, er, at den erklærede tegntabel og escape-sekvenserne er to halvdele af én kontrakt. At erklære tegntabellen alene hjælper ikke tekst, der ligger uden for den. At udsende escape-sekvenser uden en erklæret tegntabel efterlader erstatningstegnene tvetydige. Begge dele skal være korrekte sammen, hvilket er grunden til, at en writer, der kun håndterer den ene af dem, stadig fejler på den første flersprogede projektmappe.
HTML-escaping handler om mere end vinkelparenteser
HTML-eksporten producerer et dokument med flere ark, hvis navigationsrammer bærer arknavnene som synlig tekst. Disse navne er forfatterstyrede strenge, der kan indeholde ethvert tegn, inklusive dem, der har betydning for opmærkningen. Et ark, der bogstaveligt talt hedder Q1 & Q2 <draft>, skal nå siden som escaped entiteter, ellers åbner vinkelparenteserne et spøgelsestag, og ampersanden starter en entitetsreference, som aldrig var tilsigtet. Dette er almindelig HTML-escaping, og at springe det over på en rammeetiket er den slags udeladelser, der slipper igennem enhver test bygget med arknavne, der kun indeholder ASCII.
Kodningsspørgsmålet sidder et lag under dette. Når ikke-ASCII-tegn lander i en kontekst, der ikke med garanti leveres som UTF-8, den sikre repræsentation er en numerisk tegnreference, så U+00E9 skrives som é i stedet for som en rå byte, hvis betydning afhænger af svar-tegnsættet. Spejlbilledet af denne regel gælder på vejen ind. En projektmappe, der læses tilbage fra XLSX, bærer delte strenge (shared strings), hvori et tegn måske allerede er gemt som en numerisk XML-entitet, og den entitet skal afkodes til ét helt tegn, før det kommer ind i cellemodellen. Afkod det skødesløst ved at splitte et kodepunkt op i separate bytes, og et enkelt tegn dukker op igen som to stykker mojibake, som ingen senere eksport kan reparere.
XLSX-beholderen er en ZIP-fil, og ZIP har sin egen navnekodning
En XLSX-fil is et ZIP-arkiv, og arkivet gemmer et navn for hvert medlem, det indeholder. ZIP er gammelt nok til, at dets oprindelige specifikation intet sagde om kodningen af disse navne, så a læser, der ikke finder noget signal, antager arkivets lokale tegntabel. Den antagelse er forkert i samme øjeblik, et medlemsnavn indeholder et ikke-ASCII-tegn, hvilket sker med lokaliserede navne på regnearksdele og med indlejrede medier, hvis filnavne bærer accenter eller ikke-latinsk skrift.
Løsningen er et enkelt bit. General-purpose bit 11 i hvert lokalt filhoved erklærer, at medlemsnavnet er kodet som UTF-8. HotXLS kontrollerer præcis dette bit, når det læser et arkiv, idet det tester general-purpose-flagene mod masken $0800, og en læser eller writer, der ignorerer det, vil mislæse et navn, som en korrekt implementering gemte som UTF-8. Bit-værdien er billig at sætte og billig at respektere, og det er hele forskellen mellem et medlemsnavn, der overlever rundturen, og et, der ankommer beskadiget, før regnearksindholdet overhovedet er parset.
Case folding og nummerscanning gemmer på samme fare
Formelevaluering er der, hvor Unicode-sikkerhed holder op med at handle om serialisering og begynder at handle om sammenligning. SEARCH-funktionen er ikke versalfølsom (case-insensitive), hvilket betyder, at den skal folde versaler/minuskler (fold case), før den leder efter en understreng. Den forkerte måde at folde på er via ANSI-tegntabellen, fordi det at ændre ikke-ASCII-tekst til store bogstaver på den måde leder tegnene gennem en snæver tegntabel og ødelægger alt uden for den. Den rigtige måde er store bogstaver i Wide-streng (wide-string uppercasing), som bevarer hele UTF-16-intervallet. HotXLS folder med WideUpperCase af netop denne grund, så en søgning efter tekst med accent eller ikke-latin matcher de samme tegn, den fik, frem for en tilnærmelse ødelagt af tegntabellen.
Formel-tokenizeren bærer en relateret forpligtelse, der intet har at gøre med bogstaver, og alt at gøre med, hvor et token ender. Videnskabelig notation som 1E3 eller 2.5E-3 er en enkelt numerisk konstant (literal), og scanneren skal genkende E, et valgfrit fortegn og de efterfølgende cifre som en del af tallet i stedet for at opdele inputtet i et navn efterfulgt af et separat tal. En scanner, der mislykkes med dette, forvandler en fuldstændig gyldig konstant til en parserfejl eller, hvad der er værre, et lydløst forkert udtryk. Det hører til under samme diskussion, fordi begge tilfælde handler om, at en læser træffer en korrekt beslutning på tegnniveau: den ene om, hvordan et tegn foldes til sammenligning, og den anden om, hvorvidt et tegn fortsætter det aktuelle token.
Opbygning og eksport af en flersproget projektmappe
Den offentlige API beder dig ikke om at tænke på noget af dette. Du bygger projektmappen ud fra WideString-celleværdier og kalder det eksportindgangspunkt, du ønsker. Kodningsbeslutningerne sker inde i hver writer. Eksemplet nedenfor sår et ark med tekst i flere skrifttyper og skriver derefter både en RTF-fil og en HTML-fil fra den samme projektmappe, så de to stier kører mod identiske input.
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;
Begge kald returnerer en Integer-status, og begge forbruger den samme tekst i hukommelsen. Intet i den kaldende kode erklærer en tegntabel eller escaper et tegn, fordi ansvaret ligger hos den writer, der kender sit eget format. Metoden SaveAsCSV på projektmappeniveau følger samme form, hvis du har brug for en afgrænset eksport fra den identiske kilde.
// Same workbook, a third export path with its own encoding rules.
Book.SaveAsCSV('Customers.csv');
Unicode-sikkerhed er pr. sti, ikke pr. bibliotek
Læren værd at tage med sig er, at der ikke er et enkelt sted at være Unicode-sikker. RTF har brug for en erklæret tegntabel plus \u-escape-sekvenser. HTML har brug for entitets-escaping for opmærkningsrelevante tegn og numeriske referencer, hvor tegnsættet ikke er garanteret, plus korrekt afkodning af entiteter, der ankommer i delte strenge. ZIP-beholderen skal have sat general-purpose bit 11, så et UTF-8-medlemsnavn læses som UTF-8. Formelevaluering har brug for versal-foldning i Wide-streng og en tokenizer, der holder videnskabelig notation i ét stykke. Hver af disse er en forskellig kontrakt, og et bibliotek kan opfylde den ene, mens det stille og roligt overtræder en anden. Det er grunden til, at un værktøj, der klarer CSV rigtigt, stadig kan give dig en RTF fuld af spørgsmålstegn.
Hvis dine eksporter hælder til de afgrænsede formater, er afvejningerne mellem dem dækket i vores gennemgang af CSV-, TSV- og HTML-eksport, og når kilden er et resultatsæt frem for et manuelt opbygget ark, parres mønstrene i databaseeksport til Delphi-rapporter naturligt med de kodningsregler, der beskrives her. Alt dette leveres som en del af HotXLS Component til Delphi og C++Builder sammen med API'erne til læsning, formler og formatering, der er dækket andre steder på denne blog.