Et regneark indeholder en kolonne med kundenavne. Nogle er på kinesisk, nogle på kyrillisk, et par stykker bærer tyske omlyd (umlauts) eller en fransk accent. Du eksporterer det til CSV og åbner resultatet, og hvert tegn er intakt. Du eksporterer den samme projektmappe til RTF til en brevfletningsskabelon (mail-merge template), åbner den i et tekstbehandlingsprogram, og de ikke-ASCII navne er kollapset til rækker af spørgsmålstegn. Dataene ændrede sig aldrig. Det, der ændrede sig, er kodningskontrakten (encoding contract) for det format, du skrev, og hver eksportsti bærer en forskellig én
Dette er den fælde, der fanger et bibliotek, som ser fuldt Unicode-bevidst ud på overfladen. Celleteksten holdes internt som WideString, så modellen mister aldrig et tegn. Tabet sker ved grænsen, i den writer, der skal serialisere den tekst til et format med dets egne regler om, hvilke bytes der er lovlige, og hvordan alt uden for det lovlige område skal kodes. Få én writer rigtig, og du kan stadig levere (ship) en anden, der skamferer (mangles) den samme tekst. Løsningen er ikke en global kontakt. Det er en særskilt, korrekt beslutning på hver sti
RTF er et 7-bit-sikkert format af design
Rich Text Format går forud for Unicode og blev specificeret til at overleve transporter, der kun passerer printbart ASCII. Et RTF-dokument erklærer en tegntabel (code page) i sin header, og ethvert tegn, writeren ikke kan repræsentere i den tegntabel, skal udsendes som en escape snarere end som en rå byte. Den relevante escape er \u, som bærer en 16-bit kodeenhed med fortegn efterfulgt af et ASCII-fallback-tegn til læsere, der er for gamle til overhovedet at forstå escapen
HotXLS skriver RTF på denne måde. Dokument-headeren åbner ved at erklære tegntabellen, i formen \ansi\ansicpg1252\uc1, og writeren i lxRTF-enheden gennemgår hver streng og udsender ethvert tegn over almindelig ASCII som en \u-escape, så bytestrømmen forbliver 7-bit ren uanset hvad den erklærede tegntabel kan holde. Et kodepunkt såsom U+4E2D bliver den bogstavelige sekvens \u20013?, ikke en rå byte, som en fremviser derefter ville forsøge at fortolke gennem hvilken som helst tegntabel, den tilfældigvis antog. Uden den disciplin har alt uden for den erklærede tegntabel ingen lovlig byterepræsentation, og en writer, der udsender den rå værdi, producerer de spørgsmålstegn, der startede denne artikel
Detaljen at huske på er, at den erklærede tegntabel og escapes er to halvdele af én kontrakt. At erklære tegntabellen alene hjælper ikke tekst, der ligger uden for den. At udsende escapes uden en erklæret tegntabel efterlader fallback-tegnene tvetydige. Begge skal være korrekte sammen, hvilket er grunden til, at en writer, der kun håndterer én af dem, stadig fejler på den første flersprogede projektmappe
HTML-escaping handler om mere end vinkelparenteser
HTML-eksporten producerer et flersidet (multi-sheet) dokument, hvis navigationsrammer bærer arknavnene som synlig tekst. Disse navne er forfatter-kontrollerede strenge, der kan indeholde ethvert tegn, inklusive de markup-betydende. Et ark bogstaveligt talt navngivet Q1 & Q2 <draft> skal nå siden som escaped entiteter (escaped entities), ellers åbner vinkelparenteserne et fantom-tag, og og-tegnet (ampersand) starter en entitetsreference, der aldrig var tilsigtet. Dette er almindelig HTML-escaping, og at springe det over på et ramme-label er den slags udeladelse, der passerer enhver test bygget fra arknavne med kun ASCII
Kodningsspørgsmålet sidder ét lag under det. Når ikke-ASCII-tegn lander i en kontekst, der ikke med garanti serveres som UTF-8, er den sikre repræsentation en numerisk tegnreference, så U+00E9 skrives som é i stedet for som en rå byte, hvis betydning afhænger af responstegnsættet (response charset). Spejlbilledet af denne regel gælder på vej ind. En projektmappe læst tilbage fra XLSX bærer delte strenge (shared strings), hvori et tegn allerede kan være gemt som en numerisk XML-entitet, og den entitet skal afkodes til ét helt tegn, før den kommer ind i cellemodellen. Afkod den skødesløst, ved at opdele et kodepunkt i separate bytes, og et enkelt tegn dukker op igen som to stykker mojibake, som ingen senere eksport kan reparere
XLSX-containeren er en ZIP, og ZIP har sin egen navnekodning
En XLSX-fil er et ZIP-arkiv, og arkivet gemmer et navn for hvert medlem (member), det indeholder. ZIP er gammelt nok til, at dets oprindelige specifikation intet sagde om kodningen af de navne, så en læser, der ikke finder noget signal, antager arkivets lokale tegntabel. Den antagelse er forkert i det øjeblik, et medlemsnavn indeholder et ikke-ASCII-tegn, hvilket sker med lokaliserede navne på regnearksdele (worksheet part names) og med indlejrede medier, hvis filnavne bærer accenter eller ikke-latinsk script
Løsningen er en enkelt bit. General-purpose bit 11 i hver lokal filheader erklærer, at medlemsnavnet er kodet som UTF-8. HotXLS tjekker præcis den bit, når det læser et arkiv, og tester de generelle flag (general-purpose flags) mod masken $0800, og en læser eller writer, der ignorerer det, vil fejllæse et navn, som en korrekt implementering gemte som UTF-8. Bitten er billig at sætte og billig at respektere, og det er hele forskellen mellem et medlemsnavn, der overlever rundturen, og et, der ankommer korrumperet, før regnearksindholdet overhovedet er parset
Store/små bogstaver-foldning (case folding) og tal-scanning skjuler den samme fare
Formelevaluering er der, hvor Unicode-sikkerhed stopper med at handle om serialisering og begynder at handle om sammenligning. SEARCH-funktionen skelner ikke mellem store og små bogstaver (case-insensitive), hvilket betyder, at den skal folde bogstaverne, før den leder efter en delstreng. Den forkerte måde at folde på er gennem ANSI-tegntabellen, fordi at gøre ikke-ASCII-tekst til store bogstaver på den måde leder tegnene gennem en smal tegntabel og korrumperer alt uden for den. Den rigtige måde er wide-string uppercasing, som bevarer hele UTF-16-området. HotXLS folder med WideUpperCase af præcis denne grund, så en søgning efter tekst med accenter eller ikke-latinsk tekst matcher de samme tegn, den fik, frem for en tegntabel-skamferet tilnærmelse af dem
Formel-tokenizeren bærer en relateret forpligtelse, der intet har at gøre med bogstaver og alt at gøre med, hvor et token slutter. Videnskabelig notation som 1E3 eller 2.5E-3 er et enkelt numerisk bogstav (numeric literal), og scanneren skal genkende E'et, et valgfrit fortegn, og de efterfølgende cifre som en del af tallet i stedet for at bryde inputtet ind i et navn efterfulgt af et separat tal. En scanner, der mishandler dette, omdanner en fuldstændig gyldig konstant til en parse-fejl eller, endnu værre, et lydløst forkert udtryk. Det hører hjemme i den samme diskussion, fordi begge tilfælde handler om en læser, der træffer en korrekt beslutning på tegnniveau: den ene om, hvordan man folder et tegn til sammenligning, den anden om, hvorvidt et tegn fortsætter det aktuelle token
Opbygning og eksport af en flersproget projektmappe
Det offentlige API beder dig ikke om at tænke på noget af dette. Du bygger projektmappen fra WideString celleværdier og kalder det eksport-indgangspunkt (entry point), du ønsker. Kodningsbeslutningerne sker inde i hver writer. Eksemplet nedenfor sår et ark med tekst i flere scripts (skriftsystemer), og skriver derefter både en RTF-fil og en HTML-fil fra den samme projektmappe, så de to stier kører mod identisk 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 (in-memory text). Intet i den kaldende kode erklærer en tegntabel eller escaper et tegn, fordi ansvaret sidder hos writeren, der kender sit eget format. SaveAsCSV på projektmappeniveau følger den samme form, hvis du har brug for en afgrænset (delimited) 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
Lektionen, der er værd at tage med sig, er, at der ikke er noget enkelt sted at være Unicode-sikker. RTF har brug for en erklæret tegntabel plus \u-escapes. HTML har brug for entitets-escaping for markup-betydende tegn og numeriske referencer, hvor tegnsættet ikke er garanteret, plus korrekt afkodning af entiteter, der ankommer i delte strenge. ZIP-containeren har brug for, at general-purpose bit 11 er sat, så et UTF-8-medlemsnavn læses som UTF-8. Formelevaluering har brug for wide-string case folding og en tokenizer, der holder videnskabelig notation i ét stykke. Hver af disse er en forskellig kontrakt, og et bibliotek kan tilfredsstille én, mens det stille overtræder en anden. Det er grunden til, at et værktøj, der får CSV rigtigt, stadig kan give dig en RTF fuld af spørgsmålstegn
Hvis dine eksporter støtter sig til de afgrænsede formater, er afvejningerne (trade-offs) mellem dem dækket i vores gennemgang af CSV-, TSV- og HTML-eksport, og når kilden er et resultatsæt i stedet for et manuelt bygget ark, parrer mønstrene i databaseeksport for Delphi-rapporter naturligt med de kodningsregler, der er beskrevet her. Alt dette leveres som en del af HotXLS-komponenten til Delphi og C++Builder, sammen med API'erne til læsning, formler og formatering, der dækkes andre steder på denne blog