在這篇文章中,我要來紀錄一下 Code Point (字符代碼、碼位、編碼位置) 、Surrogate Pair (代理對)、HTML Unicode 編碼之間的關係,並且示範一下在 Java 中儲存的 UTF-16 字元要如何轉成可在 HTML 中正確顯示的 Unicode 編碼。
首先,先說明一下以下名詞的意義:
-
Code Point (字符代碼、碼位、編碼位置):
Code Point 是一個字符的 Unicode 編碼代碼。
參考 Unicode字元平面對映,Unicode 目前分成 17 組 平面 (Plane),每平面有 65536 (2 的 16 次方),編碼範圍從 0x0000 到 0x10FFFF ,每一個碼位代表某一個字符 (目前還沒有有全部使用到),例如
"我" 的 Code Point 是 25105 (也就是 0x6211)
"👍" 的 Code Point 是 128077 (也就是 0x1F44D)
-
Surrogate Pair (代理對):
Surrogate Pair 是一種儲存表示字符的一種方式,專門用來儲存 UTF-16 無法用 16 Bit 表示的字符,
以 "👍" 為例,
對於 Code Point 是 0xFFFF 以下的字符 UTF-16 會用 16 Bit (2 Byte) 去儲存,
而對於 "👍" 這種 Code Point 大於 0xFFFF 的字符則會使用 Surrogate Pair 的方式用 32 Bit (4 Byte) 來儲存。
規則如下,括弧中是以 "👍" 做的例子:
先將字符的 Code Point 減去 0x10000 (0x1F44D - 0x10000= 0xF44D),
把得到的值高位部份用 0 填充得到一個 20 Bit 的結果 (0000 1111 0100 0100 1101),
再將前 10 Bit 高位加上 110110 成為 High Surrogate (1101 1000 0011 1101 = 0xD83D = 55357),
將後 10 Bit 高位加上 110111 成為 Low Surrogate (1101 1100 0100 1101 = 0xDC4D = 56397),
最後 UTF-16 會用 High Surrogate + Low Surrogate 為一組的方式 (稱為 Surrogate Pair) 用 32 Bit (4 Byte) 來表示這樣一個字符 ,以 "👍" 來說就是 0xD83D 0xDC4D。
可以注意到,因為 1101 10 和 1101 11 已作為辨識 High/Low Surrogate 的用途,
所以 1101 1000 0000 0000 (0xD800) ~ 1101 1111 1111 1111 (0xDFFF) 之間的值不會對應到 任何一個 UTF-16 字符,
而在 Unicode 中,0xD800 ~ 0xDCFF 之中的 Code Point 也是設計給 UTF-16 的 Surrogate 使用的區塊,
也就沒有字符的 Code Point 在 0xD800 ~ 0xDCFF 之中。
可以參考 Unicode字元平面對映。
以 Java 來說,字符是用 UTF-16 來儲存的,
所以 "👍" 跟 "\ud83d\udc4d" 是等價的。
-
HTML Unicode 編碼 (或稱 HTML Character Entity 、HTML 字符實體編碼):
在 HTML 中可以使用 Unicode 表示特殊字符,可以直接使用 10 進位及 16 進位以 Code Point 表示,以 "👍" 為例,可以用 👍 或 👍 來表示,
注意這裡不需使用 Surrogate Pair ,因為 HTML Unicode 是支援大於 0xFFFF 的 Code Point 字符的。
同樣的,使用 CSS 的時候,也不需要使用 Surrogate Pair,例如:<style> div:after { content: "\1f44d"; } </style>
不過要注意的是,如果是使用 Javascript 的話,還是需要使用 Surrogate Pair 的表達方式,例如一樣以 "👍" 為例:console.log("\ud83d\udc4d");
最後,用 Java 示範一下上述的觀念,可以配合著看來更好的理解實際上的例子:
使用 JDK 11,
Maven Dependency :
<!-- https://mvnrepository.com/artifact/commons-lang/commons-lang --> <dependency> <groupId>commons-lang</groupId> <artifactId>commons-lang</artifactId> <version>2.4</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.14.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.commons/commons-text --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-text</artifactId> <version>1.11.0</version> </dependency>
Java:
package test; import java.util.HashMap; import java.util.Map; public class UTF16Test { public static void main(String[] args) { //👍 //Code Point (字符代碼、碼位、編碼位置): 0x1f44d = 128077 (10進位) //UTF-16 Surrogate Pair: \ud83d \udc4d = 55357 56397 (10進位) //High Surrogate: \ud83d //Low Surrogate: \udc4d //HTML Character Entity (HTML 字符實體編碼): &128077 String str = "👍"; //確認是 Code Point = 128077 是否就是 "👍" System.out.println(Character.toString(128077).equals(str)); // true //比較 "👍" 是否與 "\ud83d\udc4d" 等價 System.out.println("\ud83d\udc4d".equals(str)); // true //"👍" 字串長度為2,因為是用2個 16 Bit 表示,一個 16 Bit 被當成1個 Char System.out.println(str.length()); // 2 System.out.println(str.toCharArray().length); // 2 //"👍" 只含有一個 Code Point,所以字串的 Code Point 陣列長度是 1 System.out.println(str.codePoints().toArray().length); // 1 //org.apache.commons.lang.StringEscapeUtils.escapeHtml(String) //無法正確地將 "👍" 編碼成 HTML Character Entity,應該算是 Bug ? //輸出是 �� //這種編碼是無法在 HTML page 裡正常顯示的 System.out.println(org.apache.commons.lang.StringEscapeUtils.escapeHtml(str)); // �� //org.apache.commons.text.StringEscapeUtils 不會對 👍 進行編碼處理, //所以輸出不變一樣是 "👍" , //不過不編碼一樣是不能在 HTML page 裡正常顯示的 System.out.println(org.apache.commons.text.StringEscapeUtils.escapeHtml4(str)); //👍 //示範如何正確地將 "👍" 轉成可以在 HTML page 正常顯示的 HTML Character Entity, //也就是 👍 System.out.println(convertSurrogatePairToHtmlCode(str)); //👍 //示範如何從 Code Point 計算 Surrogate Pair (包括 High Surrogate 和 Low Surrogate) //以 "👍" 為例, Surrogate Pair 就是 \ud83d \udc4d = 55357 56397 (10進位) Map<String, Integer> surrogatePair = calculateSurrogatePairFromCodePoint(str.codePointAt(0)); int highSurrogate = surrogatePair.get("highSurrogate"); int lowSurrogate = surrogatePair.get("lowSurrogate"); System.out.println(highSurrogate); // 55357 System.out.println(lowSurrogate); // 56397 System.out.println(Character.isHighSurrogate((char) highSurrogate)); System.out.println(Character.isLowSurrogate((char) lowSurrogate)); //示範如何從 Surrogate Pair 計算 Code Point //以 "👍" 為例, Code Point 就是 0x1f44d = 128077 (10進位) int codePoint = calculateCodePointFromSurrogatePair(highSurrogate, lowSurrogate); System.out.println(codePoint); // 128077 System.out.println(Character.isSupplementaryCodePoint(codePoint)); // true } //將文字轉成可以正常顯示的 HTML 編碼 public static String convertSurrogatePairToHtmlCode(String str) { String htmlCode = ""; StringBuilder htmlCodeStringBuilder = new StringBuilder(); str.codePoints().forEach((int codePoint) -> { if (Character.isSupplementaryCodePoint(codePoint)) { htmlCodeStringBuilder.append(String.format("&#%d;", codePoint)); } else { htmlCodeStringBuilder.append(org.apache.commons.text.StringEscapeUtils.escapeHtml4(Character.toString(codePoint))); } }); htmlCode = htmlCodeStringBuilder.toString(); return htmlCode; } //計算一個 Code Point 的 Surrogate Pair (代理對), //Surrogate Pair 含有兩部份,分別是 High Surrogate 和 Low Surrogate public static Map<String, Integer> calculateSurrogatePairFromCodePoint(int codePoint) { Map<String, Integer> surrogatePair = new HashMap<>(); int s = codePoint - 0x10000; int highSurrogate = (s >> 10) + 0xd800; int lowSurrogate = (s & 0x3ff) + 0xdc00; surrogatePair.put("highSurrogate", highSurrogate); surrogatePair.put("lowSurrogate", lowSurrogate); return surrogatePair; } //計算一組 High Surrogate 和 Low Surrogate 代表的 Code Point public static int calculateCodePointFromSurrogatePair(int highSurrogate, int lowSurrogate) { return ((highSurrogate - 0xd800) << 10) + (lowSurrogate - 0xdc00) + 0x10000; } }
推薦一個線上有人寫的小工具,作者解釋了 Surrogate Pair 並寫了線上幫忙互轉 Code Point 和 Surrogate Pair 的功能:
Surrogate Pair Calculator etc.
參考資料:
沒有留言 :
張貼留言