Tekninen artikkeli

Unicode-turvallinen taulukkovienti Delphissä: RTF ja HTML

Laskentataulukko (spreadsheet) sisältää sarakkeen asiakkaiden nimiä. Jotkut ovat kiinaksi, jotkut kyrillisillä, muutamassa on saksalaiset umlautit tai ranskalainen aksentti. Viet (export) sen CSV-muotoon, avaat tuloksen, ja jokainen merkki (character) on ehjä. Viet saman työkirjan (workbook) RTF-muotoon joukkopostitusmalliksi, avaat sen tekstinkäsittelyohjelmassa, ja ei-ASCII -nimet ovat romahtaneet kysymysmerkkiriveiksi. Data ei koskaan muuttunut. Se mikä muuttui, on kirjoittamasi muodon (format) koodaussopimus (encoding contract), ja jokaisella vientipolulla on eri sopimus

Tämä on ansa (trap), johon kirjasto jää kiinni, joka näyttää pinnalta täysin Unicode-tietoiselta. Solun teksti pidetään sisäisesti (internally) WideString-muodossa, joten malli (model) ei koskaan menetä yhtään merkkiä. Häviö (loss) tapahtuu rajapinnassa (boundary), kirjoittajassa (writer), jonka on sarjallistettava (serialise) teksti muotoon, jolla on omat sääntönsä siitä, mitkä tavut (bytes) ovat laillisia ja miten kaikki laillisen alueen ulkopuolella oleva täytyy koodata. Kun saat yhden kirjoittajan oikein, voit silti toimittaa (ship) toisen, joka turmelee saman tekstin. Korjaus ei ole globaali kytkin (global switch). Se on erillinen, oikea päätös jokaisella polulla

RTF on suunnitellusti 7-bittisen turvallinen muoto

Rich Text Format (RTF) edeltää Unicodea ja määriteltiin selviytymään siirroista (transports), jotka päästävät läpi vain tulostettavaa ASCII-koodia. RTF-asiakirja ilmoittaa otsikossaan (header) koodisivun (code page), ja mikä tahansa merkki, jota kirjoittaja ei voi esittää tuolla koodisivulla, on emittoitava (emitted) esikoodauksena (escape) eikä raakana tavuna. Olennainen (relevant) esikoodaus on \u, joka kantaa etumerkillistä (signed) 16-bittistä koodiyksikköä (code unit), mitä seuraa ASCII-varamerkki (fallback character) lukijoille, jotka ovat liian vanhoja ymmärtämään kyseistä esikoodausta ollenkaan

HotXLS kirjoittaa RTF:n tällä tavalla. Asiakirjan otsikko alkaa ilmoittamalla koodisivun muodossa \ansi\ansicpg1252\uc1, ja lxRTF-yksikön (unit) kirjoittaja (writer) käy läpi jokaisen merkkijonon emittoiden minkä tahansa tavallisen (plain) ASCII:n yläpuolella olevan merkin \u-esikoodauksena (escape), joten tavuvirta (byte stream) pysyy 7-bittisen puhtaana riippumatta siitä, mitä ilmoitettu (declared) koodisivu voi sisältää. Koodipisteestä (code point) kuten U+4E2D tulee kirjaimellinen (literal) sarja \u20013?, ei raaka (raw) tavu, jota katseluohjelma (viewer) sitten yrittäisi tulkita minkä tahansa olettamansa koodisivun läpi. Ilman tätä kurinalaisuutta (discipline) millään ilmoitetun koodisivun ulkopuolisella ei ole laillista tavuesitystä, ja kirjoittaja, joka emittoi raa'an arvon (raw value), tuottaa ne kysymysmerkit (question marks), joista tämä artikkeli alkoi

Muistettava yksityiskohta on, että ilmoitettu koodisivu ja esikoodaukset (escapes) ovat yhden sopimuksen kaksi puoliskoa. Pelkkä koodisivun ilmoittaminen ei auta sen ulkopuolelle jäävää tekstiä. Esikoodauksien emittointi ilman ilmoitettua koodisivua jättää varamerkit (fallback characters) moniselitteisiksi. Molempien on oltava oikein yhdessä, minkä vuoksi kirjoittaja, joka käsittelee (handles) vain toista niistä, epäonnistuu (fails) silti ensimmäisellä monikielisellä (multilingual) työkirjalla

HTML-esikoodauksessa on kyse muustakin kuin kulmasulkeista

HTML-vienti tuottaa monitaulukkoisen (multi-sheet) asiakirjan, jonka navigointikehykset (navigation frames) kantavat taulukoiden nimiä (sheet names) näkyvänä tekstinä. Nämä nimet ovat tekijän hallitsemia (author-controlled) merkkijonoja (strings), jotka voivat sisältää mitä tahansa merkkejä (characters), mukaan lukien merkinnän kannalta (markup-significant) olennaiset merkit. Taulukko, jonka nimi on kirjaimellisesti Q1 & Q2 <draft>, täytyy saapua sivulle esikoodattuina entiteetteinä (escaped entities), tai kulmasulkeet avaavat haamutagin (phantom tag) ja &-merkki (ampersand) aloittaa entiteettiviittauksen (entity reference), jota ei koskaan tarkoitettu. Tämä on tavallista (ordinary) HTML-esikoodausta (escaping), ja sen ohittaminen (skipping) kehyksen tunnisteessa (frame label) on sellaista laiminlyöntiä (omission), joka läpäisee jokaisen vain ASCII-merkeistä koostuville taulukkonimille rakennetun testin

Koodauskysymys (encoding question) asettuu yhtä kerrosta alemmas. Kun ei-ASCII -merkit laskeutuvat (land) kontekstiin, jota ei voida taata tarjottavan UTF-8-muodossa, turvallinen esitysmuoto (safe representation) on numeerinen merkkiviittaus (numeric character reference), joten U+00E9 kirjoitetaan é-muotoon mieluummin kuin raakana tavuna, jonka merkitys riippuu vastauksen merkistöstä (response charset). Tämän säännön peilikuva pätee sisääntulossa (on the way in). XLSX:stä takaisin luettu työkirja (workbook) kantaa jaettuja merkkijonoja (shared strings), joissa merkki voi jo olla tallennettuna numeerisena XML-entiteettinä, ja tuo entiteetti (entity) on purettava (decoded) yhdeksi kokonaiseksi merkiksi ennen kuin se saapuu solumalliin (cell model). Jos purat sen huolimattomasti (carelessly) jakaen koodipisteen (code point) erillisiksi tavuiksi, yksi merkki ilmaantuu uudelleen (re-emerges) kahtena roskatekstinä (mojibake), jota mikään myöhempi (later) vienti ei voi korjata

XLSX-säiliö (container) on ZIP, ja ZIP-muodolla on oma nimien koodauksensa

XLSX-tiedosto on ZIP-arkisto (archive), ja arkisto tallentaa nimen jokaiselle sisältämälleen jäsenelle (member). ZIP on tarpeeksi vanha, että sen alkuperäinen määrittely (specification) ei sanonut mitään noiden nimien koodauksesta (encoding), joten lukija, joka ei löydä signaalia, olettaa arkiston paikallisen koodisivun (local code page). Se oletus on väärä sillä hetkellä, kun jäsenen nimi sisältää ei-ASCII -merkin, mitä tapahtuu paikallistettujen (localized) laskentataulukon osanimien (part names) ja upotetun (embedded) median kanssa, jonka tiedostonimissä on aksentteja tai ei-latinalaista kirjoitusjärjestelmää

Korjaus on yksi ainoa bitti (bit). Yleiskäyttöinen (general-purpose) bitti 11 (bit 11) kussakin paikallisen tiedoston otsikossa (header) ilmoittaa, että jäsenen nimi (member name) on koodattu UTF-8-muotoon. HotXLS tarkistaa tarkalleen tämän bitin (bit) lukiessaan arkistoa testaten yleiskäyttöisiä lippuja (flags) maskia (mask) $0800 vastaan, ja lukija tai kirjoittaja, joka ohittaa (ignores) sen, lukee väärin (misread) nimen, jonka oikea (correct) toteutus on tallentanut UTF-8-muodossa. Bitti on halpa asettaa ja halpa kunnioittaa (honour), ja se on koko (whole) ero sen välillä, selviytyykö jäsenen nimi kierroksesta (round trip) vai saapuuko se korruptoituneena ennen kuin laskentataulukon sisältö on edes jäsennelty (parsed)

Kirjainkoon kääntö (case folding) ja numeroiden skannaus kätkevät saman vaaran

Kaavojen evaluointi (formula evaluation) on kohta, jossa Unicode-turvallisuus lakkaa olemasta sarjallistamista (serialisation) ja siitä tulee vertailua (comparison). SEARCH-funktio ei piittaa kirjainkoosta (case-insensitive), mikä tarkoittaa, että sen on käännettävä kirjainkoko (fold case) ennen kuin se etsii alimerkkijonoa (substring). Väärä tapa (wrong way) kääntää on ANSI-koodisivun kautta, koska ei-ASCII -tekstin muuttaminen isoiksi kirjaimiksi tällä tavalla (uppercasing) reitittää merkit kapean koodisivun läpi ja turmelee (corrupts) kaiken sen ulkopuolella olevan. Oikea tapa on leveiden merkkijonojen isojen kirjainten käyttö (wide-string uppercasing), joka säilyttää koko (full) UTF-16-alueen. HotXLS kääntää (folds) käyttämällä WideUpperCase-funktiota juuri tästä syystä, joten haku aksentoidulle (accented) tai ei-latinalaiselle tekstille täsmää (matches) samoihin merkkeihin, jotka sille on annettu, eikä niistä (them) tehtyyn koodisivun-turmelemaan (code-page-mangled) arvioon (approximation)

Kaavan tokenoijalla (tokenizer) on vastaava (related) velvoite, jolla ei ole mitään tekemistä (nothing to do) kirjainten kanssa ja kaikkea tekemistä sen kanssa, mihin (where) token (token) päättyy. Tieteellinen merkintätapa (scientific notation) kuten 1E3 tai 2.5E-3 on yksittäinen numeerinen literaali (literal), ja skannerin on tunnistettava (recognise) E, valinnainen (optional) etumerkki (sign) ja sitä seuraavat numerot osana (part of) numeroa sen sijaan, että syöte jaettaisiin nimeksi (name), jota seuraa erillinen numero. Skanneri, joka käsittelee tämän väärin (mishandles), muuttaa täysin pätevän (valid) vakion jäsennysvirheeksi (parse error) tai mikä pahempaa, hiljaisesti vääräksi lausekkeeksi (expression). Se kuuluu samaan keskusteluun, koska molemmissa tapauksissa (cases) on kyse siitä, että lukija tekee oikean merkkitason (character-level) päätöksen: yhdessä on kyse siitä, kuinka merkki käännetään (fold) vertailua varten, ja toisessa siitä, jatkaako merkki nykyistä tokenia (token)

Monikielisen työkirjan (workbook) rakentaminen ja vienti

Julkinen API (public API) ei pyydä sinua ajattelemaan mitään tästä (this). Rakennat työkirjan WideString-soluarvoista ja kutsut haluamaasi viennin aloituskohtaa (export entry point). Koodauspäätökset tapahtuvat jokaisen kirjoittajan sisällä. Alla oleva esimerkki kylvää (seeds) taulukkoon tekstiä useilla eri kirjoitusjärjestelmillä (scripts), ja kirjoittaa sitten sekä RTF-tiedoston että HTML-tiedoston samasta työkirjasta, jotta molemmat polut (paths) suoritetaan (run against) identtistä syötettä (input) vastaan

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';

    // Soluteksti pidetään WideString-muodossa, joten jokainen kirjoitusjärjestelmä selviytyy mallista.
    Sheet.Cells[2, 1].Value := '王伟';          // Kiina
    Sheet.Cells[2, 2].Value := '北京';
    Sheet.Cells[3, 1].Value := 'Müller';        // Saksalainen umlaut
    Sheet.Cells[3, 2].Value := 'Köln';
    Sheet.Cells[4, 1].Value := 'Иванов';        // Kyrillinen
    Sheet.Cells[4, 2].Value := 'Москва';
    Sheet.Cells[5, 1].Value := 'Désirée';       // Ranskalaiset aksentit
    Sheet.Cells[5, 2].Value := 'Montréal';

    // RTF: lxRTF-kirjoittaja ilmoittaa koodisivun ja emittoi jokaisen
    // ei-ASCII -merkin \u esikoodauksena, pitäen tiedoston 7-bittisen puhtaana.
    Book.SaveAsRTF('Customers.rtf');

    // HTML: taulukoiden nimet (sheet names) ovat HTML-esikoodattuja ja ei-ASCII -teksti kirjoitetaan
    // siten, ettei se riipu arvatuista (guessed) vastausmerkistöstä (response charset).
    Book.SaveAsHTML('Customers.html');
  finally
    Book := nil;
  end;
end;

Molemmat kutsut palauttavat Integer-statuksen, ja molemmat kuluttavat samaa muistissa olevaa tekstiä (in-memory text). Mikään kutsuvassa koodissa ei ilmoita koodisivua tai esikoodaa (escapes) merkkiä, koska vastuu (responsibility) on kirjoittajalla, joka tuntee oman muotonsa (format). Työkirjatason SaveAsCSV noudattaa samaa muotoa, jos tarvitset erotellun (delimited) viennin identtisestä lähteestä

// Sama työkirja, kolmas vientipolku omilla koodaussäännöillään.
Book.SaveAsCSV('Customers.csv');

Unicode-turvallisuus on polkukohtaista (per-path), ei kirjastokohtaista (per-library)

Pois vietävä (carrying away) oppitunti on se, että ei ole olemassa yhtä ainoaa (single) paikkaa, jossa voisi olla Unicode-turvallinen. RTF vaatii ilmoitetun koodisivun (declared code page) sekä \u esikoodaukset. HTML tarvitsee entiteettiesikoodauksen (entity escaping) merkinnän kannalta olennaisille merkeille ja numeerisille viittauksille (numeric references) silloin, kun merkistöä ei voida taata, sekä jaettuihin merkkijonoihin (shared strings) saapuvien entiteettien (entities) oikean dekoodauksen (decoding). ZIP-säiliö (container) tarvitsee yleiskäyttöisen bitin 11 asettamisen, jotta UTF-8-jäsenen nimi luetaan UTF-8-muodossa. Kaavojen (Formula) evaluointi vaatii leveiden merkkijonojen (wide-string) kirjainkoon kääntämisen (case folding) ja tokenoijan (tokenizer), joka pitää tieteellisen merkintätavan yhtenä kappaleena (in one piece). Jokainen näistä on eri (different) sopimus, ja kirjasto voi täyttää yhden (one) rikkoen samalla hiljaisesti (quietly violating) toista (another). Se on syy, miksi CSV:n oikein saava työkalu voi silti antaa sinulle kysymysmerkkejä täynnä olevan RTF:n

Jos vientisi (exports) nojaavat eroteltuihin (delimited) muotoihin, niiden väliset kompromissit (trade-offs) käydään läpi CSV-, TSV- ja HTML-viennin läpikäynnissämme, ja kun lähde on tulosjoukko (result set) eikä käsin rakennettu taulukko, kuviot tietokantaviennissä (database export) Delphi-raportteja varten parittuvat luonnollisesti (naturally) tässä kuvattujen koodaussääntöjen kanssa. Kaikki tämä (all of it) toimitetaan osana HotXLS Componentia Delphille ja C++Builderille, rinnakkain muualla tässä blogissa käsiteltyjen luku-, kaava- ja muotoilu-API:en (APIs) kanssa