Unicode - není to jednoduché
Posted: 04 Jul 2019, 02:53
Původně jsem tento příspěvek chtěl napsat do jednoho z vláken, které tu řeší vývoj Salamandera, ale pak narostl, tak raději zakládám vlastní vlákno. Problematiku Unicode a jeho složitosti jsme tady řešili před lety. Mezitím vývoj Unicode pokročil a já získal nové zkušenosti (Osobně jsem psal editor využívající API Uniscribe a ani to nestačí na podchycení všech okolností...)
Pokusím se trochu nastínit, co je součástí Unicode a co je nutné vzít v potaz. Rozhodně to není tak jednoduché, jak si představujeme. Tímto však nechci říct, že Salamander musí podporovat všechny záležitosti, je však dobré o nich vědět a zaujmout k nim nějaké stanovisko.
1) Když zůstaneme u češtiny, tak narážíme na to, že jeden znak lze zapsat několika způsoby:
Ř = U+0158 LATIN CAPITAL LETTER R WITH CARON
Ř = U+0052 LATIN CAPITAL LETTER R, U+030C COMBINING CARON
- při zobrazení čekáte, že se oba způsoby zobrazí stejně
- při hledání čekáte, že oba způsoby budou ekvivalentní (a to ještě nepočítám hledání s ignorováním malých a velkých písmen)
- při řazení čekáte, že oba způsoby budou ekvivalentní
- pro Vietnamštinu a Korejštinu je toto obzvláště důležité
2) Určitě jste si všimli, že v poslední době náš život doprovází emotikony a ty se také staly součástí Unicode:
= U+1F600 GRINNING FACE
= U+1F1E8 REGIONAL INDICATOR SYMBOL LETTER C, U+1F1FF REGIONAL INDICATOR SYMBOL LETTER Z (vlajka České republiky)
= U+1F476 BABY, U+1F3FF EMOJI MODIFIER FITZPATRICK TYPE-6 (dítě tmavé pleti)
= U+1F469 WOMAN, U+1F3FF EMOJI MODIFIER FITZPATRICK TYPE-6, U+200D ZERO WIDTH JOINER [ZWJ], U+1F91D HANDSHAKE, U+200D ZERO WIDTH JOINER [ZWJ], U+1F468 MAN, U+1F3FB EMOJI MODIFIER FITZPATRICK TYPE-1-2 (paní tmavé pletí drží za ruku pána světlé pleti)
= U+1F3F4 WAVING BLACK FLAG, U+E0067 TAG LATIN SMALL LETTER G, U+E0062 TAG LATIN SMALL LETTER B, U+E0073 TAG LATIN SMALL LETTER S, U+E0063 TAG LATIN SMALL LETTER C, U+E0074 TAG LATIN SMALL LETTER T, U+E007F CANCEL TAG (vlajka Skotska)
- barevné znaky myslím umí až Windows 10 a zmíněný Uniscribe na to nestačí (DirectWrite už ano)
- k tomu, abyste správně složili některé sekvence potřebujete je znát - součástí Unicode standardu je seznam všech povolených
3) RTL jazyky přináší do Unicode jednu z nejšílenějších záležitostí, co může být (Unicode bidirectional algorithm).
- některé znaky jsou LTR (latinka)
- některé znaky jsou RTL (hebrejština, arabština)
- některé znaky jsou slabě LTR (římské číslice)
- některé znaky se řídí tím, kde se zrovna nachází
- některé znaky mění tvar podle toho, zda jsou v RTL nebo LTR sekvenci
- existují znaky na změnu z RTL na LTR a opačně (U+200E LEFT-TO-RIGHT MARK [LRM], U+200F RIGHT-TO-LEFT MARK [RLM])
- označení kusu texty v kombinovaném textu vypadá pro nás chaoticky:
بعد 14 يناير 2020، لن توفر Microsoft التحديثات الأمنية أو الدعم التقني لأجهزة الكمبيوتر التي تعمل
--- (tento text je špatně! protože je text v LTR bloku a přestože jsem použil RLM a LRM, tak jsou slova přeházena )
4) Některé jazyky jsou ještě více citlivější na tvar znaků a jejich ovlivnění znaky v okolí.
- Arabština je "jednoduchá"
- Dévanágarí (používá například hindština ~350 milionů lidí) je značně složitější:
क् = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA]
क् = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA] [U+200D ZERO WIDTH JOINER [ZWJ]]
क्ष = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA] [ṣa ष - U+0937 DEVANAGARI LETTER SSA]
क्ष = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA] [U+200D ZERO WIDTH JOINER [ZWJ]] [ṣa ष - U+0937 DEVANAGARI LETTER SSA]
5) Co se týče karetu (textového kurzoru), tak je nutné rozlišovat mezi "znakem", "symbolem" a "pozicí ve stringu (codepoint)"
- počet pozic ve stringu vůbec nesouvisí s délkou řetězce z pohledu jazyka. Již jsem uvedl příklad, kdy Ř bylo uloženo jako dvě pozice (R a háček)
- jeden symbol (glyf) může obsahovat více znaků z hlediska jazyka. V latince se týká například:
fi = U+FB01 LATIN SMALL LIGATURE FI
… = U+2026 HORIZONTAL ELLIPSIS
- zatímco v latince je toto neobvyklé v jiných jazycích je vazba mezi znaky a pozicemi ve stringu mnohem volnější
6) Podobně jako v bodě 1) jsou některé znaky z hlediska jazyka stejné, přestože vypadají jinak a to je nutné zohlednit při řazení a hledání:
Z = U+005A LATIN CAPITAL LETTER Z
Z = U+FF3A FULLWIDTH LATIN CAPITAL LETTER Z
𝐙 = U+1D419 MATHEMATICAL BOLD CAPITAL Z
𝖹 = U+1D5B9 MATHEMATICAL SANS-SERIF CAPITAL Z
7) A to jsme se ještě nedostali k problematice neplatných sekvencí
- UTF-8 a UTF-16 jsou náchylné na chyby již na úrovni dat, kdy se může stát, že sekvence nelze převést na číslo Unicode znaku.
--- Jak se s tím vypořádat? Z hlediska účelu vieweru by bylo podle mne nevhodné nějaká data přeskočit.
--- Je nutné pak dekodér sesynchronizovat (naštěstí UTF-8 i UTF-16 mají jasné, které byte/word jsou první a které následující)
- pokud už dekódujeme čísla znaků, tak na úrovni Unicode jsou některé symboly definovány sekvencí, která nemusí být platná.
--- třeba vlajky států jsou kódované pomocí právě dvou znaků "REGIONAL INDICATOR SYMBOL" a jen některé kombinace mají smysl. Ostatní se musí zobrazit nějakým alternativním způsobem.
8 ) Zalamování textu je další kapitola. V mnoha jazycích se používají mezery mezi slovy a tak není tak velký problém text zalomit v mezeře za slovem. Ale třeba v čínštině mezery v textu nenajdeme. Algoritmus, který zalamuje na základě mezer tak selže. "Naštěstí" je v Unicode pro každý znak definováno, zda je možné zalomit text před ním či za ním. Při nedávném testování jsem však narazil na to, že ani známé textové editory některé situace neřeší správně.
A pak přijde thajština, kterou raději neřeší ani Unicode standard, protože nepoužívá mezery a zalomit je možné jen mezi slabikami, takže je nutné provést sémantickou analýzu textu...
9) Někdy se stane, že je potřeba zobrazit jen část textu, a tak je nutné ho oříznout. Teď už víme, že mnoho znaků je složeno z více pozic v řetězci a tak nelze jednoduše odřezávat pozice odzadu dokud se text nevejde, protože to změní význam nebo vytvoří blbost. Zkracovací algoritmus by tak měl odřezávat spíše po symbolech než po znacích.
10) Fonty - když už máme nějaký Unicode znak, který chceme zobrazit, tak potřebujeme font, pomocí kterého ho nakreslíme. Neexistuje žádný font, který obsahuje všechny znaky (pokus Arial Unicode MS ukázal, že to ani nelze).
- musíme tedy text skládat z několika fontů a napasovat je k sobě tak, aby to vypadalo rozumně
- ne každý font se hodí k druhému - například BabelPad tak nabízí velký konfigurační dialog, kde každému Unicode bloku můžete určit vlastní font
- ani když poskládáme veškeré fonty v systému, tak nezískáme 100% pokrytí Unicode
- pro potřeby Vieweru je tedy nutné chybějící znaky zobrazit nějak alternativně (osobně bych rád viděl kód znaku místo pověstného čtverečku)
Osobní komentář:
- Zobrazení Internal Viewer: Vzhledem k tomu, jakým způsobem používám Internal Viewer (analýza dat souborů), tak by mi vyhovovalo, kdyby jednoduše zobrazil jeden symbol pro každou pozici ve stringu (codepoint). Tedy Ř uložené pomocí dvou znaků by se mi zobrazilo jako dva symboly. Šílené řetězce znaků, které jsem zde v příkladech uvedl, by se zobrazily rozloženě po jednotlivých znacích. Chyby v samotném kódování bych zobrazil speciálně zvýrazněné. Neplatné sekvence Unicode znaků se nemusí řešit, protože je zde každý znak za sebe...
--- Tento způsob by však mohl být asi dost matoucí pro ne-latinkové jazyky, kdy jednotlivé části znaku nemusí vypadat jako části složeného symbolu (například Dévanágarí, Arabština, …). BabelPad toto řeší přepínačem "Simple Rendering"/"Complex Rendering"
- Zobrazení v panelu: Zde bych naopak uvítal plnou podporu Unicode, protože nechci luštit, jaký je skutečný název souboru na základě komponent symbolů.
- Hledání: Tady by se hodila logika, která by umožnila najít všechny varianty znaků řetězce, které jsou sémanticky shodné nezávisle na to, jakým způsobem jsou uloženy. Při nalezení by se ve Vieweru pak označil odpovídající počet znaků který v souboru kóduje hledaný řetězec.
- Řazení v panelu: S tímto snad pomohou Windows, protože již pro aktuální Locale nabízejí funkce pro porovnání dvou Unicode řetězců, které by měly brát v potaz veškerá pravidla řazení v daném Locale.
Pokusím se trochu nastínit, co je součástí Unicode a co je nutné vzít v potaz. Rozhodně to není tak jednoduché, jak si představujeme. Tímto však nechci říct, že Salamander musí podporovat všechny záležitosti, je však dobré o nich vědět a zaujmout k nim nějaké stanovisko.
1) Když zůstaneme u češtiny, tak narážíme na to, že jeden znak lze zapsat několika způsoby:
Ř = U+0158 LATIN CAPITAL LETTER R WITH CARON
Ř = U+0052 LATIN CAPITAL LETTER R, U+030C COMBINING CARON
- při zobrazení čekáte, že se oba způsoby zobrazí stejně
- při hledání čekáte, že oba způsoby budou ekvivalentní (a to ještě nepočítám hledání s ignorováním malých a velkých písmen)
- při řazení čekáte, že oba způsoby budou ekvivalentní
- pro Vietnamštinu a Korejštinu je toto obzvláště důležité
2) Určitě jste si všimli, že v poslední době náš život doprovází emotikony a ty se také staly součástí Unicode:
= U+1F600 GRINNING FACE
= U+1F1E8 REGIONAL INDICATOR SYMBOL LETTER C, U+1F1FF REGIONAL INDICATOR SYMBOL LETTER Z (vlajka České republiky)
= U+1F476 BABY, U+1F3FF EMOJI MODIFIER FITZPATRICK TYPE-6 (dítě tmavé pleti)
= U+1F469 WOMAN, U+1F3FF EMOJI MODIFIER FITZPATRICK TYPE-6, U+200D ZERO WIDTH JOINER [ZWJ], U+1F91D HANDSHAKE, U+200D ZERO WIDTH JOINER [ZWJ], U+1F468 MAN, U+1F3FB EMOJI MODIFIER FITZPATRICK TYPE-1-2 (paní tmavé pletí drží za ruku pána světlé pleti)
= U+1F3F4 WAVING BLACK FLAG, U+E0067 TAG LATIN SMALL LETTER G, U+E0062 TAG LATIN SMALL LETTER B, U+E0073 TAG LATIN SMALL LETTER S, U+E0063 TAG LATIN SMALL LETTER C, U+E0074 TAG LATIN SMALL LETTER T, U+E007F CANCEL TAG (vlajka Skotska)
- barevné znaky myslím umí až Windows 10 a zmíněný Uniscribe na to nestačí (DirectWrite už ano)
- k tomu, abyste správně složili některé sekvence potřebujete je znát - součástí Unicode standardu je seznam všech povolených
3) RTL jazyky přináší do Unicode jednu z nejšílenějších záležitostí, co může být (Unicode bidirectional algorithm).
- některé znaky jsou LTR (latinka)
- některé znaky jsou RTL (hebrejština, arabština)
- některé znaky jsou slabě LTR (římské číslice)
- některé znaky se řídí tím, kde se zrovna nachází
- některé znaky mění tvar podle toho, zda jsou v RTL nebo LTR sekvenci
- existují znaky na změnu z RTL na LTR a opačně (U+200E LEFT-TO-RIGHT MARK [LRM], U+200F RIGHT-TO-LEFT MARK [RLM])
- označení kusu texty v kombinovaném textu vypadá pro nás chaoticky:
بعد 14 يناير 2020، لن توفر Microsoft التحديثات الأمنية أو الدعم التقني لأجهزة الكمبيوتر التي تعمل
--- (tento text je špatně! protože je text v LTR bloku a přestože jsem použil RLM a LRM, tak jsou slova přeházena )
4) Některé jazyky jsou ještě více citlivější na tvar znaků a jejich ovlivnění znaky v okolí.
- Arabština je "jednoduchá"
- Dévanágarí (používá například hindština ~350 milionů lidí) je značně složitější:
क् = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA]
क् = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA] [U+200D ZERO WIDTH JOINER [ZWJ]]
क्ष = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA] [ṣa ष - U+0937 DEVANAGARI LETTER SSA]
क्ष = [ka क - U+0915 DEVANAGARI LETTER KA] [virāma ् - U+094D DEVANAGARI SIGN VIRAMA] [U+200D ZERO WIDTH JOINER [ZWJ]] [ṣa ष - U+0937 DEVANAGARI LETTER SSA]
5) Co se týče karetu (textového kurzoru), tak je nutné rozlišovat mezi "znakem", "symbolem" a "pozicí ve stringu (codepoint)"
- počet pozic ve stringu vůbec nesouvisí s délkou řetězce z pohledu jazyka. Již jsem uvedl příklad, kdy Ř bylo uloženo jako dvě pozice (R a háček)
- jeden symbol (glyf) může obsahovat více znaků z hlediska jazyka. V latince se týká například:
fi = U+FB01 LATIN SMALL LIGATURE FI
… = U+2026 HORIZONTAL ELLIPSIS
- zatímco v latince je toto neobvyklé v jiných jazycích je vazba mezi znaky a pozicemi ve stringu mnohem volnější
6) Podobně jako v bodě 1) jsou některé znaky z hlediska jazyka stejné, přestože vypadají jinak a to je nutné zohlednit při řazení a hledání:
Z = U+005A LATIN CAPITAL LETTER Z
Z = U+FF3A FULLWIDTH LATIN CAPITAL LETTER Z
𝐙 = U+1D419 MATHEMATICAL BOLD CAPITAL Z
𝖹 = U+1D5B9 MATHEMATICAL SANS-SERIF CAPITAL Z
7) A to jsme se ještě nedostali k problematice neplatných sekvencí
- UTF-8 a UTF-16 jsou náchylné na chyby již na úrovni dat, kdy se může stát, že sekvence nelze převést na číslo Unicode znaku.
--- Jak se s tím vypořádat? Z hlediska účelu vieweru by bylo podle mne nevhodné nějaká data přeskočit.
--- Je nutné pak dekodér sesynchronizovat (naštěstí UTF-8 i UTF-16 mají jasné, které byte/word jsou první a které následující)
- pokud už dekódujeme čísla znaků, tak na úrovni Unicode jsou některé symboly definovány sekvencí, která nemusí být platná.
--- třeba vlajky států jsou kódované pomocí právě dvou znaků "REGIONAL INDICATOR SYMBOL" a jen některé kombinace mají smysl. Ostatní se musí zobrazit nějakým alternativním způsobem.
8 ) Zalamování textu je další kapitola. V mnoha jazycích se používají mezery mezi slovy a tak není tak velký problém text zalomit v mezeře za slovem. Ale třeba v čínštině mezery v textu nenajdeme. Algoritmus, který zalamuje na základě mezer tak selže. "Naštěstí" je v Unicode pro každý znak definováno, zda je možné zalomit text před ním či za ním. Při nedávném testování jsem však narazil na to, že ani známé textové editory některé situace neřeší správně.
A pak přijde thajština, kterou raději neřeší ani Unicode standard, protože nepoužívá mezery a zalomit je možné jen mezi slabikami, takže je nutné provést sémantickou analýzu textu...
9) Někdy se stane, že je potřeba zobrazit jen část textu, a tak je nutné ho oříznout. Teď už víme, že mnoho znaků je složeno z více pozic v řetězci a tak nelze jednoduše odřezávat pozice odzadu dokud se text nevejde, protože to změní význam nebo vytvoří blbost. Zkracovací algoritmus by tak měl odřezávat spíše po symbolech než po znacích.
10) Fonty - když už máme nějaký Unicode znak, který chceme zobrazit, tak potřebujeme font, pomocí kterého ho nakreslíme. Neexistuje žádný font, který obsahuje všechny znaky (pokus Arial Unicode MS ukázal, že to ani nelze).
- musíme tedy text skládat z několika fontů a napasovat je k sobě tak, aby to vypadalo rozumně
- ne každý font se hodí k druhému - například BabelPad tak nabízí velký konfigurační dialog, kde každému Unicode bloku můžete určit vlastní font
- ani když poskládáme veškeré fonty v systému, tak nezískáme 100% pokrytí Unicode
- pro potřeby Vieweru je tedy nutné chybějící znaky zobrazit nějak alternativně (osobně bych rád viděl kód znaku místo pověstného čtverečku)
Osobní komentář:
- Zobrazení Internal Viewer: Vzhledem k tomu, jakým způsobem používám Internal Viewer (analýza dat souborů), tak by mi vyhovovalo, kdyby jednoduše zobrazil jeden symbol pro každou pozici ve stringu (codepoint). Tedy Ř uložené pomocí dvou znaků by se mi zobrazilo jako dva symboly. Šílené řetězce znaků, které jsem zde v příkladech uvedl, by se zobrazily rozloženě po jednotlivých znacích. Chyby v samotném kódování bych zobrazil speciálně zvýrazněné. Neplatné sekvence Unicode znaků se nemusí řešit, protože je zde každý znak za sebe...
--- Tento způsob by však mohl být asi dost matoucí pro ne-latinkové jazyky, kdy jednotlivé části znaku nemusí vypadat jako části složeného symbolu (například Dévanágarí, Arabština, …). BabelPad toto řeší přepínačem "Simple Rendering"/"Complex Rendering"
- Zobrazení v panelu: Zde bych naopak uvítal plnou podporu Unicode, protože nechci luštit, jaký je skutečný název souboru na základě komponent symbolů.
- Hledání: Tady by se hodila logika, která by umožnila najít všechny varianty znaků řetězce, které jsou sémanticky shodné nezávisle na to, jakým způsobem jsou uloženy. Při nalezení by se ve Vieweru pak označil odpovídající počet znaků který v souboru kóduje hledaný řetězec.
- Řazení v panelu: S tímto snad pomohou Windows, protože již pro aktuální Locale nabízejí funkce pro porovnání dvou Unicode řetězců, které by měly brát v potaz veškerá pravidla řazení v daném Locale.