2012年3月31日 星期六

使用 Flex 程式畫出 QRCode - 續

程式碼輸出的問題解決了,接著當然要把沒完成的文章打完。

上一篇分享的程式碼在輸出一般的英數字是沒有問題,但是當輸入的字是中文字時,反解的結果總是變成「?」,讓我覺得十分奇怪,明明在維基百科上看到 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>

沒有留言:

張貼留言