Et regneark har en kolonne med kundenavn. Noen er på kinesisk, noen er på kyrillisk, noen få bærer tyske omlyder (umlauts) eller en fransk aksent (accent). Du eksporterer det til CSV og åpner resultatet, og hvert tegn er intakt. Du eksporterer den samme arbeidsboken til RTF for en flettemal (mail-merge template), åpner den i et tekstbehandlingsprogram (word processor), og de ikke-ASCII-navnene har kollapset til rader med spørsmålstegn. Dataene endret seg aldri. Det som endret seg er kodingskontrakten (encoding contract) til formatet du skrev, og hver eksportvei (export path) bærer en forskjellig en
Dette er fellen (trap) som fanger et bibliotek (library) som på overflaten ser ut til å være fullstendig Unicode-bevisst (Unicode-aware). Celleteksten (cell text) holdes internt som WideString, så modellen (model) mister aldri et tegn (character). Tapet skjer ved grensen (boundary), i skriveren (writer) som må serialisere den teksten inn i et format med sine egne regler (rules) om hvilke byter (bytes) som er lovlige og hvordan alt utenfor det lovlige området (legal range) må kodes. Få én skriver (writer) riktig, og du kan fremdeles levere (ship) en annen som skader (mangles) den samme teksten. Løsningen (fix) er ikke en global bryter (global switch). Det er en egen, riktig beslutning på hver vei (path)
RTF er et 7-bits-sikkert format etter design
Rich Text Format kom før Unicode og ble spesifisert for å overleve transporter (transports) som bare sender utskrivbar ASCII (printable ASCII). Et RTF-dokument erklærer (declares) en kodeside (code page) i hodet (header), og ethvert tegn skriveren ikke kan representere i den kodesiden, må sendes ut som en unnslippelse (escape) fremfor som en rå byte (raw byte). Den relevante unnslippelsen (escape) er \u, som bærer en signert 16-bits kodeenhet (code unit) etterfulgt av et ASCII-reservetegn (fallback character) for lesere (readers) som er for gamle til i det hele tatt å forstå unnslippelsen
HotXLS skriver RTF på denne måten. Dokumenthodet (document header) åpner med å erklære (declaring) kodesiden (code page), i formen \ansi\ansicpg1252\uc1, og skriveren i lxRTF-enheten går gjennom hver streng og sender ut ethvert tegn over ren ASCII som en \u-unnslippelse slik at bytestrømmen (byte stream) forblir 7-bits ren uansett hva den erklærte kodesiden kan inneholde. Et kodepunkt (code point) som U+4E2D blir den bokstavelige (literal) sekvensen \u20013?, ikke en rå byte som et visningsprogram (viewer) deretter ville forsøkt å tolke gjennom hvilken som helst kodeside den tilfeldigvis antok (assume). Uten den disiplinen har alt utenfor den erklærte kodesiden ingen lovlig byte-representasjon, og en skriver (writer) som sender ut den rå verdien (raw value) produserer spørsmålstegnene som startet denne artikkelen
Detaljen å ha i bakhodet er at den erklærte kodesiden og unnslippelsene er to halvdeler av én kontrakt (contract). Å erklære (declaring) bare kodesiden hjelper ikke for tekst som ligger utenfor den. Å sende ut (emitting) unnslippelser (escapes) uten en erklært kodeside gjør reservetegnene (fallback characters) tvetydige (ambiguous). Begge må være riktige sammen, noe som er grunnen til at en skriver som bare håndterer én av dem fremdeles feiler (fails) på den første flerspråklige (multilingual) arbeidsboken
HTML-unnslippelse (escaping) handler om mer enn vinkelparenteser
HTML-eksporten produserer et flersidig dokument (multi-sheet document) hvis navigasjonsrammer (navigation frames) bærer arknavnene (sheet names) som synlig tekst. Disse navnene er forfatterkontrollerte strenger som kan inneholde ethvert tegn, inkludert de markup-signifikante. Et ark som bokstavelig talt (literally) heter Q1 & Q2 <utkast> må nå siden som unnslippede entiteter (escaped entities), eller vinkelparentesene (angle brackets) åpner en fantom-tagg (phantom tag) og og-tegnet (ampersand) starter en entitetsreferanse (entity reference) som aldri var tiltenkt (intended). Dette er vanlig HTML-unnslippelse (HTML escaping), og å hoppe over det på en rammeetikett (frame label) er den typen unnlatelse (omission) som passerer hver test bygget fra arknavn som kun inneholder ASCII
Kodingsspørsmålet (encoding question) sitter ett lag under det. Når ikke-ASCII-tegn lander i en kontekst (context) som ikke er garantert å bli servert som UTF-8, er den sikre representasjonen (safe representation) en numerisk tegnreferanse (numeric character reference), så U+00E9 skrives som é fremfor som en rå byte hvis betydning (meaning) avhenger (depends) av respons-tegnsettet (response charset). Speilbildet av denne regelen gjelder på veien inn. En arbeidsbok lest tilbake fra XLSX bærer delte strenger (shared strings) der et tegn allerede kan være lagret som en numerisk XML-entitet (numeric XML entity), og den entiteten må dekodes (decoded) til ett helt tegn (whole character) før den går inn i cellemodellen (cell model). Dekoder (decode) du den skjødesløst (carelessly), og deler (splitting) et kodepunkt (code point) i separate byter (separate bytes), og et enkelt tegn dukker opp igjen (re-emerges) som to biter med mojibake som ingen senere eksport (export) kan reparere (repair)
XLSX-beholderen er en ZIP, og ZIP har sin egen navnekoding
En XLSX-fil er et ZIP-arkiv, og arkivet lagrer (stores) et navn for hvert medlem det inneholder. ZIP er gammelt nok til at den opprinnelige spesifikasjonen ikke sa noe om kodingen (encoding) av disse navnene, så en leser (reader) som ikke finner noe signal antar (assumes) arkivets lokale kodeside (local code page). Den antagelsen (assumption) er feil det øyeblikket et medlemsnavn (member name) inneholder et ikke-ASCII-tegn, noe som skjer med lokaliserte (localized) delnavn for arbeidsark og med innebygde medier (embedded media) hvis filnavn bærer aksenter eller ikke-latinsk skrift (script)
Løsningen (fix) er én enkelt bit (bit). Den generelle formålsbiten 11 (general-purpose bit 11) i hvert lokale filhode (local file header) erklærer (declares) at medlemsnavnet er kodet (encoded) som UTF-8. HotXLS sjekker (checks) nøyaktig denne biten når den leser et arkiv, og tester de generelle formålsflaggene (general-purpose flags) mot masken $0800, og en leser eller skriver (writer) som ignorerer det vil feillese (misread) et navn som en riktig implementering (implementation) lagret som UTF-8. Biten er billig å sette (set) og billig å overholde (honour), og den er hele forskjellen mellom et medlemsnavn (member name) som overlever rundturen (round trip) og et som ankommer korrupt før regnearkinnholdet i det hele tatt er tolket (parsed)
Store og små bokstaver (Case folding) og tallsøking (number scanning) skjuler den samme faren (hazard)
Formelevaluering (Formula evaluation) er der Unicode-sikkerhet slutter å handle om serialisering (serialisation) og begynner å handle om sammenligning (comparison). SEARCH-funksjonen er uavhengig av store/små bokstaver (case-insensitive), noe som betyr at den må folde bokstaver (fold case) før den leter (looks) etter en delstreng (substring). Den feilaktige (wrong) måten å folde (fold) på er gjennom ANSI-kodesiden, fordi å gjøre ikke-ASCII-tekst om til store bokstaver på den måten ruter (routes) tegnene gjennom en smal kodeside (narrow code page) og korrumperer alt utenfor den. Den riktige (right) måten er oppskalering av bredstreng (wide-string uppercasing), som bevarer (preserves) hele UTF-16-området. HotXLS folder (folds) med WideUpperCase av akkurat denne grunnen, slik at et søk (search) etter aksentert (accented) eller ikke-latinsk tekst (non-Latin text) samsvarer med (matches) de samme tegnene den ble gitt, i stedet for en kodeside-lemlestet (code-page-mangled) tilnærming av dem
Formel-tokenisatoren (formula tokenizer) har en beslektet forpliktelse (obligation) som ikke har noe å gjøre med bokstaver, og alt å gjøre med hvor et token ender. Vitenskapelig notasjon (scientific notation) som 1E3 eller 2.5E-3 er en enkelt numerisk bokstavverdi (numeric literal), og skanneren må gjenkjenne E-en, et valgfritt fortegn (optional sign), og de følgende sifrene (digits) som en del av tallet, i stedet for å bryte (breaking) inndataene (input) inn i et navn etterfulgt av et separat tall. En skanner (scanner) som feilhåndterer (mishandles) dette gjør en helt gyldig konstant (valid constant) om til en tolkefeil (parse error) eller, enda verre, et lydløst feil (silently wrong) uttrykk (expression). Det hører hjemme i samme diskusjon fordi begge tilfellene handler om en leser som tar en riktig (correct) beslutning på tegnnivå: den ene om hvordan et tegn skal foldes for sammenligning (comparison), den andre om hvorvidt et tegn (character) fortsetter (continues) det nåværende (current) tokenet
Å bygge og eksportere en flerspråklig (multilingual) arbeidsbok
Den offentlige API-en (public API) ber deg ikke tenke på noe av dette. Du bygger (build) arbeidsboken (workbook) fra WideString celleverdier (cell values) og kaller (call) eksport-inngangspunktet (export entry point) du ønsker (want). Kodingsbeslutningene (encoding decisions) skjer (happen) inne (inside) i hver skriver (writer). Eksemplet nedenfor sår (seeds) et ark (sheet) med tekst i flere skript (scripts), og skriver deretter (then writes) både (both) en RTF-fil (RTF file) og en HTML-fil (HTML file) fra den samme (same) arbeidsboken, slik at de to banene (paths) kjører (run) mot identisk (identical) inndata (input)
uses
lxHandle;
procedure ExportMultilingualWorkbook;
var
Book: IXLSWorkbook;
Sheet: IXLSWorksheet;
begin
Book := TXLSWorkbook.Create;
try
Sheet := Book.Sheets.Add('Kunder');
Sheet.Cells[1, 1].Value := 'Navn';
Sheet.Cells[1, 2].Value := 'By';
// Celletekst holdes som WideString, så alle skript overlever modellen.
Sheet.Cells[2, 1].Value := '王伟'; // Kinesisk
Sheet.Cells[2, 2].Value := '北京';
Sheet.Cells[3, 1].Value := 'Müller'; // Tysk omlyd
Sheet.Cells[3, 2].Value := 'Köln';
Sheet.Cells[4, 1].Value := 'Иванов'; // Kyrillisk
Sheet.Cells[4, 2].Value := 'Москва';
Sheet.Cells[5, 1].Value := 'Désirée'; // Franske aksenter
Sheet.Cells[5, 2].Value := 'Montréal';
// RTF: lxRTF-skriveren erklærer kodesiden og sender ut hver
// ikke-ASCII karakter som en \u escape, som holder filen 7-bit ren.
Book.SaveAsRTF('Customers.rtf');
// HTML: arknavn er HTML-escapet og ikke-ASCII-tekst skrives
// så den er ikke avhengig av en gjettet respons-charset.
Book.SaveAsHTML('Customers.html');
finally
Book := nil;
end;
end;
Begge (Both) kallene (calls) returnerer (return) en Integer-status (status), og begge forbruker (consume) den samme teksten i minnet (in-memory). Ingenting (Nothing) i den kallende koden (calling code) erklærer (declares) en kodeside (code page) eller unnslipper (escapes) et tegn (character), fordi (because) ansvaret (responsibility) sitter (sits) hos skriveren (writer) som kjenner (knows) sitt eget format (own format). Arbeidsbok-nivået (workbook-level) SaveAsCSV følger den samme formen (shape) hvis (if) du trenger (need) en avgrenset (delimited) eksport (export) fra den identiske kilden (identical source)
// Samme arbeidsbok, en tredje eksportbane med sine egne kodingsregler.
Book.SaveAsCSV('Customers.csv');
Unicode-sikkerhet er per-bane, ikke per-bibliotek
Leksjonen verdt å ta med seg er at det ikke er noe enkelt sted å være Unicode-sikker. RTF trenger en erklært kodeside (code page) pluss \u unnslippelser (escapes). HTML trenger entitet-unnslippelse (entity escaping) for markup-signifikante tegn og numeriske referanser der tegnsettet (charset) ikke er garantert (guaranteed), pluss riktig dekoding (decoding) av entiteter som ankommer (arrive) i delte strenger (shared strings). ZIP-beholderen (ZIP container) trenger generell-formålsbit 11 satt (set) slik at et UTF-8 medlemsnavn (member name) leses (read) som UTF-8. Formelevaluering trenger foldning av store/små bokstaver i bred-streng (wide-string case folding) og en tokenisator (tokenizer) som holder vitenskapelig notasjon (scientific notation) i ett stykke. Hver av disse er en forskjellig kontrakt (contract), og et bibliotek (library) kan tilfredsstille (satisfy) en mens det stille (quietly) bryter (violating) en annen. Det er grunnen (reason) til at et verktøy (tool) som får CSV riktig fremdeles kan gi (hand) deg en RTF full av spørsmålstegn (question marks)
Hvis eksportene dine lener seg (lean) på de avgrensede formatene (delimited formats), er avveiningene (trade-offs) mellom dem dekket (covered) i vår gjennomgang (walkthrough) av CSV, TSV og HTML-eksport, og når kilden er et resultatsett (result set) snarere enn et håndbygget (hand-built) ark (sheet), passer mønstrene i databaseeksport for Delphi-rapporter naturlig med (pair naturally with) kodingsreglene (encoding rules) beskrevet her. Alt (All of it) leveres (ships) som en del av HotXLS-komponenten for Delphi og C++Builder, ved siden av lese-, formel- og formaterings-API-ene dekket (covered) andre steder (elsewhere) på denne bloggen