上一篇分享的程式碼在輸出一般的英數字是沒有問題,但是當輸入的字是中文字時,反解的結果總是變成「?」,讓我覺得十分奇怪,明明在維基百科上看到 QR Code 的說明是支援 UTF-8 的文字,而 Flash 本身也支援多國語系,難不成是因為 Flash 實際上走的是 UTF-16 ,造成文字因為編碼不同造成無法反解?
在沒有什麼文件的狀態下,我開始程式碼的 hack 旅程…
首先當然是由實際運作編碼的 QRCodeWriter 開始尋找,在 encode 方法下,看到了使用 com.google.zxing.qrcode.encoder.Encoder 再次編碼方法。
Encoder.encode(contents, errorCorrectionLevel, code);再去看看 Encoder 的程式嗎,依然之傳入作為編碼對象的「content」變數作依據,在 encode 方法的程式碼看到一些東西。
public static function encode(content:String , ecLevel:ErrorCorrectionLevel, qrCode:QRCode,hints:HashTable=null ):void { var encoding:String = hints == null ? null : (hints._get(EncodeHintType.CHARACTER_SET) as String); // 後面會提及,這裡的 encoding 就是表示輸入文字所用的編碼 if (encoding == null) { encoding = DEFAULT_BYTE_MODE_ENCODING; // 如果沒有指定編碼,就設定為預設值 } // Step 1: Choose the mode (encoding). var mode:Mode = chooseMode(content, encoding); // 這裡沒什麼好說的,就是 QR Code Mode // Step 2: Append "bytes" into "dataBits" in appropriate encoding. var dataBits:BitVector = new BitVector(); appendBytes(content, mode, dataBits, encoding); // 這裡是重點,看起來文字是由這裡編碼後再轉換成 QR Code 資訊 // 後略… }
再繼續查,查到 appendBytes 這個方法…
/** * Append "bytes" in "mode" mode (encoding) into "bits". On success, store the result in "bits". */ public static function appendBytes(content:String , mode:Mode , bits:BitVector , encoding:String ):void { if (mode == Mode.NUMERIC) { appendNumericBytes(content, bits); } else if (mode == Mode.ALPHANUMERIC) { appendAlphanumericBytes(content, bits); } else if (mode == Mode.BYTE) { append8BitBytes(content, bits, encoding); // 以中斷點進行測試,表示 QR Code 的編碼會落在這個部分… } else if (mode == Mode.KANJI) { appendKanjiBytes(content, bits); } else { throw new WriterException("Invalid mode: " + mode); } }
最後找到 appendBitBytes 方法…
public static function append8BitBytes(content:String , bits:BitVector , encoding:String ):void { var bytes:ByteArray = new ByteArray(); try { //bytes = content.getBytes(encoding); if ((encoding == "Shift_JIS") || (encoding == "SJIS")) { bytes.writeMultiByte(content, "shift-jis");} else if (encoding == "Cp437") { bytes.writeMultiByte(content, "IBM437"); } else if (encoding == "ISO8859_2") { bytes.writeMultiByte(content, "iso-8859-2"); } else if (encoding == "ISO8859_3") { bytes.writeMultiByte(content, "iso-8859-3"); } else if (encoding == "ISO8859_4") { bytes.writeMultiByte(content, "iso-8859-4"); } else if (encoding == "ISO8859_5") { bytes.writeMultiByte(content, "iso-8859-5"); } else if (encoding == "ISO8859_6") { bytes.writeMultiByte(content, "iso-8859-6"); } else if (encoding == "ISO8859_7") { bytes.writeMultiByte(content, "iso-8859-7"); } else if (encoding == "ISO8859_8") { bytes.writeMultiByte(content, "iso-8859-8"); } else if (encoding == "ISO8859_9") { bytes.writeMultiByte(content, "iso-8859-9"); } else if (encoding == "ISO8859_11"){ bytes.writeMultiByte(content, "iso-8859-11"); } else if (encoding == "ISO8859_15"){ bytes.writeMultiByte(content, "iso-8859-15"); } else if ((encoding == "ISO-8859-1") || (encoding == "ISO8859-1")) { bytes.writeMultiByte(content, "iso-8859-1"); } else if ((encoding == "UTF-8") || (encoding == "UTF8")) { bytes.writeMultiByte(content, "utf-8"); } // 真多編碼,重點在這裡有「UTF-8」的存在,所以要確保文字傳入是以這個模式轉換 // 看到了嗎?就是用 encoding 這個參數來表示編碼 else { //other encodings not supported throw new Error("Encoding "+ encoding + " not supported"); } bytes.position = 0; } catch (uee:Error) { throw new WriterException(uee.toString()); } for (var i:int = 0; i < bytes.length; ++i) { bits.appendBits(bytes[i], 8); } }
但是以中斷點查詢,並不是「UTF-8」而是「ISO-8859-1」這個編碼,它並不代表任何一種編碼,而是表示「依所在作業系統的預設編碼」,而 Windows 7 的預設編碼為?雖然號稱支援多國語系,但是仍然很神奇地使用傳統的 Big5 中文編碼,也難怪中文字編碼後解析不出來。
既然找到問題,再接下來就開始解決它。由前文中提到,encoding 表編碼,而編碼如何被傳入?是由下面這段而來…
var encoding:String = hints == null ? null : (hints._get(EncodeHintType.CHARACTER_SET) as String);由 hints 這個參數傳入資訊,它是一個由作者自得開發,一個類似 Java 的 Map 類別,叫作 HashTable,使用字串常入 EncodeHintType.CHARACTER_SET 作 Key。
因此我修改了我的程式,將…
var writer:Writer = new QRCodeWriter(); var mb:BitMatrix = writer.encode(content.text, BarcodeFormat.QR_CODE, 0, 0) as BitMatrix;改成…
var writer:Writer = new QRCodeWriter(); var codeinfo:HashTable = new HashTable(); codeinfo._put(EncodeHintType.CHARACTER_SET, "UTF-8"); var mb:BitMatrix = writer.encode(content.text, BarcodeFormat.QR_CODE, 0, 0, codeinfo) as BitMatrix;
滿心期望的執行測試,結果什麼都沒有發生。再次利用中斷點查詢程式中變數內容的變化,後來發現在 QRCodeWriter 的程式中…
Encoder.encode(contents, errorCorrectionLevel, code);並沒有將我傳入的 hints 再 pass 給後面的 Encoder 類別,所以又變成以預設值來進行編碼。
所以我修改它,將它改成…
Encoder.encode(contents, errorCorrectionLevel, code, hints); // hints 是本來函數呼叫就有的戔數,不是我多加的
自此,這個程式也可以將中文字編碼,順利被手機掃描出結果。
後記︰
QR Code 雖然仍然有資訊承截量的上限,不過已經讓我吃了驚,可以將新聞網站上兩段文字貼入,在幾秒內就讀出並反解。雖然到第三段就無法解析,還不知道是因為字數過多,還是因為遇到第三個「換行字元」,但是由這中間也看到不少可能應用。
最後分享我測試使用的程式碼。(官方的函數請自行到 google code 下載)
<?xml version="1.0" encoding="utf-8"?> <s:WindowedApplication xmlns:fx="http://ns.adobe.com/mxml/2009" xmlns:s="library://ns.adobe.com/flex/spark" xmlns:mx="library://ns.adobe.com/flex/mx"> <fx:Declarations> <!-- Place non-visual elements (e.g., services, value objects) here --> </fx:Declarations> <fx:Script> <![CDATA[ import com.google.zxing.BarcodeFormat; import com.google.zxing.EncodeHintType; import com.google.zxing.Writer; import com.google.zxing.common.BitMatrix; import com.google.zxing.common.flexdatatypes.HashTable; import com.google.zxing.qrcode.QRCodeWriter; protected override function initializationComplete():void { super.initializationComplete(); } private function createQRCode():void { var writer:Writer = new QRCodeWriter(); var codeinfo:HashTable = new HashTable(); codeinfo._put(EncodeHintType.CHARACTER_SET, "UTF-8"); var mb:BitMatrix = writer.encode(content.text, BarcodeFormat.QR_CODE, 0, 0, codeinfo) as BitMatrix; var width:int = mb.width; var height:int = mb.height; var color:uint; var cw:Number = canvas.width; var ch:Number = canvas.height; var sx:Number; var sy:Number; if(cw > ch) { sx = (cw - ch)/2; sy = 0; cw = ch; } else { sy = (ch - cw)/2; sx = 0; ch = cw; } canvas.graphics.clear(); for(var y:int=0; y<height; y++) { for(var x:int=0; x<width; x++) { color = mb._get(x, y)?0:0xFFFFFF; canvas.graphics.beginFill(color, 1); canvas.graphics.drawRect( sx+cw/width*x, sy+ch/height*y, cw/width, ch/height ); canvas.graphics.endFill(); } } } ]]> </fx:Script> <s:layout> <s:VerticalLayout horizontalAlign="center" /> </s:layout> <s:Group id="canvas" width="100%" height="100%"> </s:Group> <s:HGroup> <s:TextInput id="content" width="100%" /> <s:Button label="Create QRCode" click="createQRCode()" /> </s:HGroup> </s:WindowedApplication>
沒有留言:
張貼留言