深入理解Emoji(三) —— Emoji詳解

講故事的小黃瓜發表於2018-11-30

深入理解Emoji(一) —— 字符集,字符集編碼 深入理解Emoji(二) —— 位元組序和BOM

Emoji字元是Unicode字符集中一部分. 特定形象的Emoji表情符號對應到特定的Unicode位元組。常見的Emoji表情符號在Unicode字符集中的範圍和具體的位元組對映關係, 可通過Emoji Unicode Tables檢視到。

注:本篇文章在不同平臺下觀看效果會不一樣

問題引申

首先來看看我遇到的問題:

val smile  =  "?"
print("smile emoji length = ${smile.length}")

val flag = "??"
print("flag emoji length = ${flag.length}")

val portrait = "??‍?"
print("portrait emoji length = ${portrait.length}")

val family = "?‍?‍?‍?"
print("family emoji length = ${family.length}")
複製程式碼

輸出結果為:

smile emoji length = 2
flag emoji length = 4
portrait emoji length = 7
family emoji length = 11
複製程式碼

有沒有覺得很奇怪,按我們之前所說,一個emoji表情應該也是屬於一個字元,佔據著Unicode的一個碼點,為什麼會出現2、4甚至是7、11個字元長度的情況呢?我們去看看String.length()的原始碼:

public int length() {
        return value.length >> coder();
    }
複製程式碼

coder()這個方法是判斷當前的編碼獲取相應的值,預設是UTF-16,值為1,因為Java內部的預設編碼是UTF-16。也就是說,當字元的碼點在輔助平面時,String.length()的實現方式會將其判斷為長度為2。Emoji表情所有的碼點都在輔助平面上,那就解釋了第一個,為什麼長度為2,那大於2的那些又是怎麼回事呢?這就涉及到Unicode的一個很重要的特性:組合字元

組合字元

Unicode 包含一個系統,可以合併多個編碼點,動態組合字元。此係統用各種方式增加靈活性,而不引起編碼點的巨大組合膨脹。 例如,在歐洲語言中,組合標記出現在變音符和字母的使用中。 Unicode 支援各種各樣的變音符號,包括尖音符號的和重音符號、母音變音符號、變音符號等等。所有這些變音符可以被使用在任何字母表的字母中。事實上,多個變音符號可以被使用在一個字母上。

如果 Unicode 試圖為每個字母組合或變音符組合分配一個獨立的編碼點,事情會變得無法控制。相反,動態組合系統可以讓你構造你想要的任何字元,通過以一個基礎編碼點(字母)開始然後附加額外的編碼點,被稱作“組合標識”,來指定變音符。當一個文字渲染器看到字串中有這樣的序列時,它會自動堆疊變音符到基礎字母的上面或下面來造出一個組合字元。

例如,帶重音的字元“Á” 會被表示成由兩個編碼點組成的字串:U+0041 “A” 拉丁大寫字母 a 加上 U+0301 “◌́”組合尖音符號。這個字串自動被渲染成單個字元:“Á”。

有時候我們會看到某些人的簽名中有很奇怪的字元,其實他們就是利用了組合字元。比如Á́́ 就是多新增了幾個尖音符號:U+0041U+0301U+0301U+0301,是不是感覺挺有意思?

字位簇

如上所見,Unicode 包含多種情況,使用者認為的一個“字元” 事實上底下可能由多個編碼點組成。Unicode 使用「字位簇」的概念來表示這種情況。一個由一個或多個編碼點組成的字串構成一個 “使用者感知的字元”。

UAX #29 為字位叢定義了精確的規則。它大約是 “一個基本的編碼點接著任意數量的組合標記”,但是真實的定義有點複雜;它包含了朝鮮語字母,和 emoji ZWJ 序列。

字位簇主要被用在文字編輯:它們對游標和文字選擇來說是最明顯的單元。使用字位簇,確保在複製和貼上文字時不會突然丟掉一些符號,同時左右方向鍵也總是以一個可見字元的距離移動,等等。

另一個用到字位簇的地方是,執行字串長度限制——比如在資料庫域中。其實,底層的限制可能是類似 UTF-8 中的位元組長度之類的東西,你不能簡單的通過截斷位元組的方式來限制長度。至少,你得 “捨去” 最近的編碼點;但更好的是,捨去最近的字位簇。除此以外,你可以通過捨棄它的一個注音符號破壞一個字元,中斷一個 jamo 序列或 ZWJ 序列。

##Emoji組合規則 現在,我們知道了一個Emoji表情可能由多個碼點組成,這些碼點都遵循著一定的規則來組合成不同的Emoji表情,我們來看下幾種常見的規則:

  • #####單Unicode 最基本的Emoji表情,碼點位於輔助平面上。在UTF-16下通過String.length()會被判斷為2個長度,可以使用String.codePoints()通過碼點數來獲取正確的長度。

    單Unicode組成的笑臉

  • #####雙Unicode 最具代表性的就是旗幟序列(Flag Sequence),這類 Emoji 串是通過兩個地域指示符(regional_indicator)組合的方式來表示一個國家的國旗。總共有 26 個地域指示符(U+1F1E6~U+1F1FF),每個指示符又對應於一個英文字母含義,例如 U+1F1E8 為地域指示符 C, U+1F1F3 為地域指示符 N。這些指示符兩兩組合表示一個國旗CN即中國國旗(??),在不支援Emoji5.0的系統上,會被顯示為兩個字母Emoji表情(? ?)。並不是 26 x 26 種組合是全部合法的,合法的 Flag Sequence 只有 256 種。這種Emoji表情通過String.length()會被判斷為4個長度。

    旗幟序列組成的國旗

  • #####變數選擇器 在眾多Emoji中, 有一些特殊的Emoji 並沒有顯示的樣式, 只是起到了控制的作用。這些控制型的Emoji 與基礎Emoji 出現在一起, 可以展示更多的樣式。比如 變數選擇器

變數選擇器-15(VARIATION SELECTOR-15, 簡寫VS-15): <U+FE0E>, 作用是讓基礎Emoji 變成更接近文字樣式(text-style); 變數選擇器-16(VARIATION SELECTOR-16, 簡寫VS-16): <U+FE0F>, 作用則是讓基礎Emoji 變成更接近Emoji樣式(emoji-style).

VS-15 和 VS-16 加在基礎Emoji字元的後面, 可以起到控制作用(前提是必須系統支援, 否則會被忽略)。在UTF-16下通過String.length()會被判斷為2個長度。

變數選擇器樣式對比
而在VS-16的基礎上,還有一種鍵帽序列(KeyCap Sequence),這類 emoji 序列是將數字(0-9),* 與 # 通過一個 U+20E3 字元轉換為鍵帽的樣式。由於這種樣式要求必須以 emoji 風格展示,所有會在序列中新增樣式限制 U+FE0F。例如 U+0023 U+FE0F U+20E3 的 emoji 樣式即是 #️⃣,U+0030 U+FE0F U+20E3 的 emoji 樣式即是 0️⃣。其它與此類似。在UTF-16下通過String.length()會被判斷為3個長度。
鍵帽序列組成的Emoji
另外, 還有一些控制型的Emoji, 可以對人體膚色進行改變,改變物件僅限於"表示人身體部位的Emoji"。目前定義了五種修飾字元,分別表示顏色的由深及淺,它們分別是: U+1F3FB ~ U+1F3FF (?..?)共五個, 分別簡稱為: FITZ-1-2, FITZ-3, FITZ-4, FITZ-5, FITZ-6。例如,U+270D(✍️) 就是一個可以被修飾的 emoji 字元,那麼它被U+1F3FF修飾後就會變成U+270D U+1F3FF(✍️?)。
同個表情不同膚色

  • 無縫連線序列

上面說到,通過一些特定的Emoji組合,可以結合出不同膚色的表情,在增加Emoji的豐富度的同時,不需要增加過多的碼點。那性別,職業呢?是不是也可以用這種方式,答案是肯定的,只不過實現的方法有點不一樣。

通常,每一個emoji表情都是由特定的字元來展現的,新創造一個emoji表情意味著要新建一個符號來與之關聯。以膚色和性別為例,標準碼協會提出更多創造性的解決方案,比如選擇將多個程式碼結合在一起來建立一個新表情。

不同性別的表情所代表的職業如何來展現的呢?以一個標準的“男性”或是“女性”表情再新增個代表職業的表情,就能展現“男性”某職業或女性某職業這樣一個表情,而不是兩個表情。這種特殊不可見的排列方式被稱為“無縫連線”(“Zero-width joiner,即ZWJ”)。在iOS 10、Android N平臺支援這種組合表情,看到ZWJ就知道顯示一個表情而不是分離的兩個。

U+200D便是連線這些表情的字元。例如,U+1F468 U+200D U+1F469 U+200D U+1F467 (?‍?‍?) 這個 emoji 表示家庭即由三個emoji字元,U+1F468(?), U+1F469(?), U+1F467(?) 經 ZWJ 連線而成的。長度為8,而上面問題裡提到的?‍?‍?‍?,可以看到多了一個連線符和一個長度為2的基本Emoji表情,所以列印出來是11。

SWJ
當然不侷限於家庭人物,包括職業,運動等許多都是用這種方式組成的
SWJ職業
SWJ運動

標準碼協會利用ZWJ字元序列的方式(可以跨多平臺使用),使得各IT公司可以輕易地進行開發,不過同時也有個明顯的問題。**Emoji表情和ZWJ字串不需要標準碼協會批准就可以建立並在自有平臺上使用。**即使在不支援ZWJ的老版本中,最多也是顯示兩個或是兩個以上獨立的表情,新增新的程式碼不會破壞其他或是出現醜陋的問號塊。

不需要耗一個月甚至一年的時間等候審批,可以使表情開發變得更快,蘋果或是谷歌可以自主新增標誌或解決問題,而不會影響與其他平臺的相容。另一方面,這也使以ZWJ序列排列出的表現被跨平臺支援,但事實上卻沒能被支援。各個平臺都在開發屬於自己的表情,會導致不同平臺間的符號不相容,比如字元長度的問題,在IOS系統上,一個Emoji表情傳送到Android手機上,可能會出現4、5個,如果在有長度限制的條件下,便可能會出現截斷的問題。

Emoji的碎片化

標準碼協會提供所有表情符號的名稱和簡單的圖片,但任何Emoji文章展示,你通過手機和電腦看起來也有輕微的區別。不同的作業系統和程式開發者都想通過不同的emoji表情來達到更美觀,而不是用統一的通用字符集。如同我們的截圖,同一個Emoji表情碼,有不同的平臺上有各式各樣的表現形式。又因為SWJ的存在,導致各個平臺有屬於自己的一套表情,這就導致了Emoji的混亂,這點其實跟Unicode的“統一”多多少少是有點衝突的。但不管怎麼說,Emoji都是一個非常偉大且成功的發明。

相關文章