2017年6月8日 星期四

使用 TRESTClient 與 TRESTRequest 作為 HTTP Client 之一 (POST 字串參數)

在 Delphi XE 推出以前的年代,Delphi的發展方向是筆直朝向資料庫連結Windows 應用程式這個目標不斷前進的,從Delphi 1開始,到Delphi 7,Delphi奠定了VB Killer的外號,主要依靠的就是與資料庫的連接功能超越其他開發工具,而且超越的距離不只一個世代。

在 .NET開始發展,Delphi 8, Delphi .NET 不斷延遲的時候,與資料庫連接功能的方便性,仍然讓許多ERP廠商、軟硬體廠商持續愛用 Delphi.

直到 Web 開發與 App 開發超越了 Windows 應用程式的需求,VC, VB, Delphi 也開始隨著這波潮流,漸漸不再像 1990年代那麼廣受愛戴了。

在1990到 2010年之間,Delphi的網路連線功能,主要是藉由第三方元件來提供的,其中知名度最高,全球使用人數也最多的,應該就是 Indy 這套元件了。

這套元件在 2000 年前,叫做 WinShose,從第八版之後才改名為Indy,全球投入這套元件開發的開發人員,前後超過40人,從最基礎的 TCP/IP 功能到各種協定的Client與Server 端元件,筆者從中得益非常多,也開發了當中的DNS Server元件,對通訊協定的深入了解,Indy團隊可說是我不可或缺的師長。

隨著Delphi 工具走入了多平台開發的領域,Indy的侷限性也在這兩三年凸顯了出來,主要是在各個作業系統上面對於SSL與加密功能的支援無法緊密結合到作業系統內建的功能所致。

由於這個局限性,Delphi XE6開始,REST Client系列元件漸漸開始成為 Delphi 團隊的重點開發項目之一,所以我們從 Delphi XE6, Delphi XE7 之後的版本,可以發現到,使用 TRESTClient, TRESTRequest, TRESTResponse 系列組合的應用程式越來越多了,原廠也不斷鼓勵大家使用這套元件來提供 REST API 的連線功能。

REST API 的基礎是 HTTP 協定,大多以 HTTP 的 POST 方法把 JSON 編碼形式的參數傳遞到 Server,而 Server 再以 JSON 形式的參數回傳。

有時作法也會稍有變化,例如以 POST 方法把 Web-Form 編碼形式的參數傳遞給 Server,Server 再以 JSON 形式把資料回傳。

形式不一而足,但相同的是 HTTP 協定,最常用的也是以 POST 方法把參數傳給 Server 端。

今天要跟大家分享的主題,則是如何『使用 TRESTClient 與 TRESTRequest 作為 HTTP Client』。

前面已經提到過,在沒有 TRESTClient 整組元件以前,我們通常用的是 Indy 系列的元件來提供網路傳輸的功能,而現在有了 TRESTClient 整組元件,我們在行動平台上面就可以不需要另外配置函式庫,也能夠直接使用 https 與 server 連線了,在勒索病毒氾濫的今天,使用 https 會讓使用者比較安心。

POST作業說明

在 HTTP 的 POST 作業當中,參數跟 GET 作業一樣,Client端需要以 name=value&name2=value2 這種形式進行字串連接,再傳送到 Server 端去。Get 跟 POST的差異,在於 Get 方法是把所有參數當做 URL 的一部分,發送 HTTP GET 指令的時候,參數連同 URL  一起傳送。

而 POST 作業則是發送完 POST 指令後,把所有的參數與資料隨之傳送。依照 HTTP 型定的規範,GET 作業的 URL 是無法加密的,而且長度也有限制。因此,當需要傳遞的資料比較多,或者有機敏性,透過 HTTPS 傳送,就是最直接,也最方便,更是目前最通用的資料保護方法。

透過 POST 傳遞的參數,除了字串以外,還常常包含了檔案傳遞。我們很常看到在網頁上面以按鈕提供使用者選擇要上傳的檔案,也常看見提供以拖拉的方式把檔案上傳到遠端系統,尤其網頁郵件系統最常見到這種作法。

過去以 TIdHTTP 元件的 POST 方法發送參數時,呼叫方式如下:
var
    httpClient : TIdHTTP;
    url, params, httpResultStr : string;
begin
    url := 'http://mytestURL.com/test.php';
    params := 'name=我的名字&test=測試';

    httpClient := TIdHTTP.Create(self);
    try
         httpResultStr := httpClient.Post(url, params);
         showMessage(httpResultStr);
    finally
        httpClient.Free;
    end;
end;
  
這樣就可以把 params 字串的眾參數傳到 server 去了。理論上是這樣沒錯,但事情並沒有這麼簡單,在 HTTP 協定當中要傳參數給 Server,如果這些字串包含了特殊字元,則必須要先經過編碼,而編碼,是我們一生都需要與之對抗的繁複程序。

在 HTTP GET 方法當中,所有的參數除了要以 name=value 對每一個參數做描述,以及需要用 & 來連接各組參數,所有的 value 都需要以 url encode 來擺脫 URL 保留字元的糾纏。name 是否需要編碼呢?筆者建議,name 就乖乖的用英文吧,可以省下很多問題,以及處理這些可避免的問題所需要的時間!

那麼同樣的功能,以 TRESTClient 跟 TRESTRequest 要怎麼達成呢? 也很容易,作法如下:
1. 在 form 裡面放上 TRESTClient 跟 TRESTRequest 元件各一。
2. 把要傳遞的參數加到 TRESTRequest 實體的 params 屬性裡面去,這個屬性的型別是 TArray,所以可以存放多組參數。
3. 設定 TRESTClient 要傳送參數的URL,注意,URL 是設定在TRESTClient 哦!
4. 設定 TRESTRequest 要使用的傳輸方法,要設定為POST(因為我們正在介紹的是POST方法,請按照您的需求調整)
5. 呼叫 TRESTRequest 實體的 execute 方法,就可以把資料送去 server 了。

寫成 Delphi 的程式碼,會像以下這樣:
self.RESTClient1.BaseURL :=
          'http://我的網址/acceptNewCard.php';
self.RESTRequest1.Params.Clear;
self.RESTRequest1.Method := rmPOST;

self.RESTRequest1.AddParameter('test', self.EditCardNo.Text);
self.RESTRequest1.AddParameter('name', self.EditName.Text);

是不是很容易呢?的確很容易,裡頭的問題我們等下再深入探討,先來看 server 端要怎麼接收這些個參數,我們用 PHP 當範例,需要用 C#,JSP的讀者朋友們請自行轉譯喔⋯⋯

PHP Server 端接收 POST 參數的方法
從 1994 年開始,筆者就陸續撰文說明 HTTP POST 方法如何接參數,包含了CGI 用C,perl等語言實作,也包含 ISAPI 以 Delphi 實作,近幾年比較流行的是 PHP,JSP,C#,但 PHP 程式碼讀起來比較簡潔易懂,所以我就選擇 PHP 來做範例了。

在 PHP 裡面,透過GET 跟 POST 方法傳遞的參數,會被分別存放在 $_GET 跟 $_POST 這兩個陣列變數裡面,如果要偷懶,不想區分 GET 或 POST 方法,也可以從 $_REQUEST 這個變數試著讀取,當中有些安全性考量,最好勤勞一點,把它們區分開來。

以剛剛的例子來看,我們傳了一個名為 name,以及一個名為 test 的字串,用的是 POST 方法,所以我們得用以下兩個變數來存取這兩個字串:
  • $_POST['name']  這個變數可以取得 Client 端發送出來的 name  
  • $_POST['test']  這個變數可以取得 Client 端發送出來的 test
所以在 server 端,我們可以這樣寫,來抓到這兩個資料:
$name = $_POST["name"];
$test = $_POST["test"];

這樣寫會不會出問題呢? 答案是不會!如果使用者不輸入中文的話!

中文資料的編碼處理 

Delphi的開發人員絕大多數都是英美語系的人,我推測因此對於亞洲語系的文字顯示與傳輸比較沒有辦法完整的測試,但對於我們以中文為母語的人來說,從電腦誕生的那個年代,中文的顯示在每個作業系統、每種通訊協定的設計都比英文協定來的困難。

以上面的例子來看,如果我們直接拿這個例子來測試,筆者寫的範例程式,執行傳輸資料時,Server 所抓到的文字並不是正確的中文字,如下圖所示:

可以看得出來,傳到 server 的時候,server 是讀不到資訊的。這是怎麼回事呢?筆者屬於不認輸的好奇寶寶,使出了渾身解數,終於解決了這個問題。

寫過 Web 程式的讀者們一定可以立刻推測出來,這絕對是文字編碼出問題了,然而,是什麼地方出問題?可能出問題的點我列出來跟大家分享:
  • HTTP Client 的 charset 設定錯了
  • HTTP Request 裡面的文字編碼出問題
檢查的方向也是從這兩個關鍵點出發,第一點的檢測很容易,從Object Inspector檢查一下 RESTRequest1的設定:

AcceptCharset 確定是 UTF-8,沒錯,所以設定不是問題。

接著,就要從 Client 端發出去的資料下手了。有讀者或許會問『你怎麼不懷疑Server端程式寫錯了?』這個問題很好,之所以排除了這個問題,是因為同一個 Server 端的 PHP 程式,我用了 Postman 做過比對測試,回傳的結果是正確的,因此判定是 Client 端程式的問題。

接著筆者從 TRESTRequest.AddParameter 的各種多載形式來嘗試,AddParameter 這個方法有以下幾種多載的形式:
  • procedure AddParameter(const AName, AValue: string); overload;
  • procedure AddParameter(const AName: string; AJsonObject: TJSONObject; AFreeJson: boolean = true); overload;
  • procedure AddParameter(const AName, AValue: string; const AKind: TRESTRequestParameterKind); overload;
三種形式我都測試過,從 AddParameter 的執行中 trace 進去看各個可能性,由於 TRESTRequest 的參數中,Get 跟 Post 的加入方法是混用的,在程式碼裡面編碼又會有點不同。

在 REST.Client.pas 裡面,我曾經懷疑過編碼錯誤,所以也在執行階段對各個變數都進行觀察,最後,找到了原因與解法,至於過程,就不多說了,花了我兩天咧。

原因:編碼錯誤

用HTTP傳遞中文的時候,務必用UTF-8編碼,但一定要記得,中文字在作業系統中,都是UCS32編碼,這個現象在Windows裡面如此,在Android裡面如此,在iOS跟Mac我不確定,但處理方法是一樣的。

直接以 AddParameter('name', '中文測試'); 把參數加進 TRESTRequest 的時候,REST.Client.pas 的程式碼是把 '中文測試' 這個字串直接抓 Ord 的資料來做編碼的,然而,這個作法,是錯的!!!!!!!!

在 HTTP 傳遞 UTF-8 資料的時候,我們要傳遞的是 UTF-8 文字的二進位資料,但直接把 '中文測試' 這個字串直接拿來轉成二進位? 當時編碼並不是 UTF-8 啊,當然怎麼編碼送到 server 都是錯的!!!!

解法:AddParameter之前先做 UTF-8 轉換

這個解法,筆者第一天就已經想到,只是很想像以前改 Indy 程式一樣,直接改好 REST.Client.pas 之後,回饋給原廠使用,所以花了不少時間找方法,最後發現這個方法不用動到 REST.Client.pas,又能正確處理,就直接這麼跟大家分享了,寫成 Delphi 程式碼如下:
var
     nameStr : String;
begin
   ...
   nameStr := TIdURI.ParamsEncode(nameStr, IndyTextEncoding_UTF8);
   self.RESTRequest1.AddParameter('name', nameStr,     
             TRESTRequestParameterKind.pkGETorPOST,    
             [TRESTRequestParameterOption.poDoNotEncode]);
   ...
end;
在把字串透過 AddParameter 加入參數陣列之前,我先把字串做個 UTF-8 轉換,在這裡用的是 TIdURI 的類別方法 ParamsEncode,這個方法只有兩個參數,第一個參數是字串內容,第二個參數則是要文字編碼的種類,在這裡我選擇了 UTF8,寫法就是上面範例程式的第一行。

接著,在呼叫 AddParameter 的時候,我使用了多載形式當中的第三種,要求 AddParameter 處理資料的時候不要再動我的編碼,因為我已經處理好了。

這麼修改過之後,在各個作業系統當中,執行結果都是正確的,上面兩個圖以 Windows 作業系統為例,現在我們拿 Android 截圖來做為例子:


讀者可以看到右邊的截圖裡面,server 回傳的資料已經是正確的中文字了。

最後,我把 PHP 程式碼也附上來給大家參考:
<?php
date_default_timezone_set("Asia/Taipei");
header('Content-Type: charset=utf-8');

include("public/DBClassPDO.php");
$objDBPDO = new DBClassPDO();

$cardNum = $_POST["cardNum"];
$floorIdx = $_POST["floorIdx"];
$name = $_POST["name"];
$houseNum = $_POST["houseNum"];

$params = array();
$params['name'] = $name;
$params['cardno'] = $cardNum;
$params['houseNum'] = $houseNum;
$params['floorIdx'] = $floorIdx;
$params['picFilename'] = $fileSaveName;
$params['created'] = 0;

$result["resultCode"] = "0";
$result["result"] = "成功";
$result["sqlcmd"] = $name;

$jsonStr = json_encode($result);
echo $jsonStr;
?>
這是最基本的傳遞字串,下次再給大家示範怎麼傳檔案,透過 TRESTRequest來做也是很簡單的,TRESTRequest 跟 TRESTClient 的確是取代 TIdHTTP 的好工具。

2017年5月21日 星期日

在 Android 的 Edit 中, 如何按下 Enter 即隱藏虛擬鍵盤

在 Windows 的應用程式中,我們常常為了讓使用者能夠快速輸入,在Edit元件中的onKeyUp或者 onKeyDown 事件中主動偵測使用者輸入的字元是否有換行符號 (Enter),當使用者按下了Enter,程式碼就主動把游標 Focus 帶到下個欄位,但在行動裝置中,又多了一個課題:『如果是多個欄位,就帶到下個欄位。但如果是單一欄位,或是最後一個欄位,就隱藏虛擬鍵盤』

這個課題筆者在 2014九月的筆記『Virtual Keyboard 的顯示與隱藏』裡面已經有介紹過一次,只是當時是聚焦在iOS系統上,因為當時筆者開發的平台大都聚焦在iOS,對Android系統的關切並不多。

最近在一些專案中,也把觸角伸到了Android平台,但筆者仍堅持『不到最後關頭,絕不輕言JAVA』的原則,認識筆者的朋友們也都知道,筆者會寫,但非不得已,絕不寫JAVA的原則。

加上Delphi從XE6之後,對Android的支援也很深入了,筆者就更能堅定這個立場。

言歸正傳,在TEdit元件中要在使用者按下虛擬鍵盤的Enter鍵時,隱藏虛擬鍵盤的話,要怎麼做呢?

顯然用2014九月那個作法(把focus轉移到別的元件), 對Android平台沒用,不然筆者也不用多寫這篇筆記了。

是的,熟習Android的使用者都知道,按下Android的實體Back鍵,就能隱藏虛擬鍵盤。所以這個作法也很簡單,就是『在onKeyUp事件中,把Key 改成 137 即可』

onKeyUp 事件中,有兩個 call by Reference 的參數,一個是 Key, 另一個是 KeyChar, 都讓我們可以在處理完鍵盤事件後,重新賦予新的值給系統。在Delphi Seattle, Berlin, Tokyo (前面的版本我已經移除了,無從驗證),這三個比較新的版本中,都定義有 vkHardwareBack這個值,其數值就是 137。

只需在 onKeyUp 事件中把 137 指派給 Key 這個變數即可隱藏鍵盤了,但通常我們程式碼可能會用於多種平台上,所以我的範例程式碼會加上 {$IFDEF Android} 這樣的判別式,以利多平台的程式維護:

procedure TFormMain.Edit_changeDeviceNameKeyUp(Sender: TObject; var Key: Word;
    var KeyChar: Char; Shift: TShiftState);
begin
   if Key = vkReturn then begin
      self.btn1.SetFocus;

{$IFDEF ANDROID}
      Key := vkHardwareBack
{$ENDIF}
   end;
end;
這樣應該很容易理解,對嗎?

2017年5月18日 星期四

在 iOS 10 之後, 使用手機相機與相簿應注意事項

iOS 的 SDK 每一年至少都會有一次大改版,從 2009 到 2016 年,版號已經到了第 10 版了,很輕易的就追上了 Mac OSX. 每一次的大改版都會有不少新的功能或新的規範,在 iOS 7的改版算的上是幅度最大的,把 iOS 1.0 到 iOS 6.0 以實物視覺與立體視覺的基礎來了個髮夾彎,轉向去跟 Android, Windows 一起走平面化極簡風。(不知道賈柏斯天上有靈會怎麼反應......)
  • iOS 8的改版中,要求使用地理資訊的App需要在 info.plist 當中自己寫出提示的文字
  • iOS 10 的改版中,更進一步要求使用相機、相簿的App也要自己提示。
如果沒有在 info.plist 裡面定義提示文字,App執行到該功能的時候,會直接發生閃退,這對於使用者來說很不方便,所以App開發的人員也只好為了使用者對 App 進行改版。

如果是使用 Xcode 來開發,就需要編輯 info.plist, 加入以下圖片中兩個灰色背景的設定,一個是使用相機,另一個是使用相簿。



這跟是否使用第三方的 Framework 無關喔,即使您使用了 Cocos2D, 或是 CocoaPad, 這些宣告也是跑不掉的。

如果使用的是 Delphi,則需要從專案設定選項 (Project Option)來設定,設定視窗如下圖所示:


叫出這個視窗的方法有兩個,一個是從Delphi的視窗中點選 Project 選項,選擇裡面的 Option。另一個則是用滑鼠右鍵點選視窗右邊的專案名稱,選擇 Menu 裡面的 Option 選項,都可以顯示出這個視窗。

設定好這兩個選項之後,在 Delphi 裡面呼叫 TTakePhotoFromCameraAction 或
TTakePhotoFromLibraryAction的時候,就不會閃退了.

要留意一點,透過 Delphi 取用相機或相簿的時候,OnDidFinishTaking 所取得的照片是 TBitMap 元件,我們可以先對圖片做一些簡單的處理,例如可以把圖片的 解析度弄小一點,或者改變圖片的大小,甚至是儲存成不同格式 (PNG或JPG)。

對 iOS 模擬器處理時,由於模擬器通常沒有相機,所以不用特別設定模擬器的相機使用描述。Delphi 的設定會依照不同的裝置平台做個別的設定,而且還分成 Debug/Release,所以我自己通常會直接編輯 All Configuration。

從 iOS 5S 之後,也已經沒有 32 bit 的新裝置了,所以如果您的 App 不打算提供給 32 bit 裝置使用的話,iOS Device - 32 bit platform 也可以不用理他了。



2017年5月12日 星期五

Delphi 快速使用 AES 加密的方法

從 1994 年開始,筆者就開始接觸加密與網路安全的世界,從魯立忠老師的指導當中獲益良多,後來在元智就讀研究所的時候,也以此為研究主題。

在當時,電子商務是顯學,Visa跟 Master Card還特別為了網路交易製作了厚厚三大本的商務通訊協定,命名為SET (Secure Electronic Transaction,安全電子交易),從客戶端、商店端、銀行端定義了綿綿密密的交易規範。

然而,網際網路的世界跟 Visa Master Card所熟悉的專用網路世界差的遠了,不是大狗們(Big dogs)說了算,很快的 SSL 128 被吹捧成『最安全的交易保護機制』,每年透過這『最安全的交易保護機制』成交的金額越攀越高。

破解網路而得逞的網路詐欺,始終維持在一個很低的比例,反而從商家端流出的詐欺資料年年創新高,SET也很快的成為一個歷史名詞。

但是,SET所本的一些加密基礎,並沒有就此被埋沒。X.509電子憑證、RC4, RC5, DES, 3-DES, RSA, SHA-1, SHA256, SHA-2, 還有我們這次要介紹的AES,也不斷的推陳出新,在世界上蓬勃發展。這些聽了令人打呵欠的主題跟名詞,在很多地方都會被用上,只是用了不同的面貌呈現給使用者而已。

像是在自然人憑證、健保卡裡面,都有個人電子憑證(X.509),每年五月我們都可以用這兩種憑證進行網路報稅。

或是像電子發票,當中就需要用到 AES 加密,依據財政部的『紙本電子發票二維條碼內容規範』第五頁所述:


左方二維條碼裡面,就需要用到 AES 對發票字軌10碼及隨機碼4碼以字串方式合併後使用AES加密,並採用Base64編碼轉換。

但是,在Delphi裡面好像沒有可以直接使用AES加密的單元可以使用。筆者在碩士論文的程式撰寫時,使用的是OpenSSL 0.4的函式庫,當時還叫做SSLeay呢。但是,這作法只能在Windows 平台上面順順的用,有沒有什麼方法可以讓我們在不同的作業系統下都能順利使用 AES 呢?

經過約莫兩三個小時上窮碧落下黃泉的搜尋,找到了一個在 SourceForge 上面的加密範例程式,更棒的是,它是用 Delphi + FireMonkey 寫的,不使用 DLL,而是使用純粹的Pascal 寫的 (感謝 Eldos 的 OpenSource, 但直接到 Eldos 網站的連結目前已經找不到了)。

換句話說,這是一個跨平台都可以正常運作的 Delphi 程式,不用依靠載入的 DLL 或 Lib,用這個範例來製作電子發票的驗證加密字串,就能夠很方便的達成了。

Source Forge 的範例程式可以從這個連結下載,下載之後,請看到裡面的範例專案『FlyUtilsAESCBC.dproj』,這個範例程式中,支援用字串作為AES加密金鑰(Key)對文字進行加解密,執行起來的畫面也很清楚,筆者做了一點點修改,修改後的執行畫面如下圖所示:
 

左圖是未執行加密作業前的畫面,右圖則是執行了加密作業之後的畫面。原始的範例程式中,只支援完整的字串作為 AES 金鑰,但我們常會用到二進位資訊來做金鑰,這種情形下,金鑰通常也會經過Base64編碼過。

所以筆者稍微改寫了一個function,新增了一個按鈕,就是畫面最底下的『AES加密with Byte Key』這個按鈕的event handler。

如果點選按鈕是 AES加密,則Key裡面的字串不會被做任何處理,直接會被當成AES金鑰,點選的如果是最底下的『AES加密with Byte Key』,則Key裡面的字串會先被做Base64 解碼,變成二進位資訊,AES金鑰就是這些二進位資訊了。

procedure TFormMain.Button1Click(Sender: TObject);
var
  KeyBit: TKeyBit;
  APaddingMode: TPaddingMode;
begin
   KeyBit := TKeyBit.kb256;
   APaddingMode := TPaddingMode.pmZeroPadding;
   Memo2.text := AESEncryptStrToBase64_Base64Key(Memo1.Text, Edit1.text, TEncoding.UTF8, KeyBit, '', APaddingMode, CheckBoxCBC.IsChecked,
      rlCRLF, rlCRLF, Process);
end;

這兒的 AESEncryptStrToBase64_Base64Key 是筆者照著原本 Eldos 的程式做了一點小手腳,方便大家把電子發票平台取得的金鑰直接貼上來就能用:
function AESEncryptStrToBase64_Base64Key(Value, Key: string; StrEncoding: TEncoding = nil;
  KeyBit: TKeyBit = kb128;
  InitVectorStr: string = '';
  APaddingMode: TPaddingMode = TPaddingMode.pmPKCS5or7RandomPadding; CBCMode: Boolean = True;
  ValueCRLFMode: TCRLFMode = rlCRLF;
  KeyCRLFMode: TCRLFMode = rlCRLF;
  OnProcessProc: TOnProcessProc = nil; ProcessProc: TProcessProc = nil): string;
var
   tStrm : TBytesStream;
   keyBytes: TBytes;
   IVBytes: TBytes;
   IdDecoderMIME1 : TIdDecoderMIME;
begin
   tStrm := TBytesStream.create;
   IdDecoderMIME1 := TIdDecoderMIME.Create(nil);
   try
      IdDecoderMIME1.DecodeBegin(tStrm);
      IdDecoderMIME1.Decode(Key);
      IdDecoderMIME1.DecodeEnd;

      keyBytes := tStrm.Bytes;
   finally
       tStrm.Free;
   end;

   tStrm := TBytesStream.create;
   try
      IdDecoderMIME1.DecodeBegin(tStrm);
      IdDecoderMIME1.Decode(InitVectorStr);
      IdDecoderMIME1.DecodeEnd;

      IVBytes := tStrm.Bytes;
   finally
       tStrm.Free;
   end;
   IdDecoderMIME1.Free;

  Result := EncodeBase64Bytes(AESEncryptStr_BytesKey(Value, keyBytes, IVBytes, TEncoding.UTF8, KeyBit, APaddingMode, CBCMode, ValueCRLFMode,
    KeyCRLFMode, OnProcessProc, ProcessProc));
end;

單獨使用這個 function 的話,ProcessProc 參數可以給 nil, 這是用來讓大家看到有進度列可以顯示處理進度用的,通常這些處理是在背景進行,沒有介面的時候直接給個 nil, 就可以不用管進度條了。

大家可以看到,上面的程式碼裡面,筆者使用了Indy的 Base64解碼元件『IdDecoderMIME』,因為一來它是原本 Delphi 安裝就內建的,使用上比較方便,二來筆者也熟悉這套元件,所以不另外找其他元件了。

AES 裡面除了 Key 之外,還可以指定 IV (initialization vector), 如果使用上需要使用二進位的 IV, 您可以把二進位的 IV 先做好 Base64 編碼,這個 function 也會將它先做 Base64 解碼之後,作為 IV 進行處理的。

寫到這裡,說明的差不多了,範例程式專案我也準備好了,有興趣的讀者請自行取用吧。範例在此



2016年4月8日 星期五

用遮罩來把 TBitmap 裁切成我們需要的形狀

緣起


以往在 VCL Framework 裡面, 我們可以使用 Windows GDI 的相關 API 來處理圖片, 讓原本方方正正的圖片依照我們的需要切成各種形狀。但這個功能在使用FireMonkey的時候, 卻因為Windows GDI無法跨平台而無法使用在其他裝置上面了,但把圖片切成各種形狀的需求還是很常出現,那怎麼辦呢?

在FireMonkey裡面,TBitmap這個 Class 提供了對 Canvas 處理的許多方法可以使用,其中的 CreateFromBitmapAndMask 方法,可以透過一個黑白的圖片當成遮罩來做這個自定形狀的裁切功能。

這個功能在許多網路的文章裡面都有提到過,所以我也不獻醜了。要跟大家分享的是在使用這個方法的時候,需要額外注意的地方。

在Embarcadero 的 docwiki 上面, 有關於 createFromBitmapAndMask 的範例程式,大家可以點連結去看看,但是這個範例程式沒有完整的專案,因此究竟什麼樣的 Mask 可以完美的讓我們把需要的區塊給截下來,還是得試一試。

在範例中,或者我們自己搜尋的網路範例中(山本隆的開發日誌示範的很好,我也從中獲益許多),都可以看到擷取圖片部分區塊的作法,是需要一個黑白圖片作為遮罩的,遮罩當中白色的區塊會被保留,黑色的區塊則會被刪掉。

問題來了

看到這裡,我好高興,感覺什麼都能做的出來了,但是,事情總是沒有想像中這麼順利啊,在實際操作上,我發現了以下的問題:

  • Runtime製作出來的 TBitmap 變數,怎麼都沒辦法弄出圖像啊!
  • 透過這個方法所截出來的圖片,黑色的區塊刪的不乾淨啊!還有矇矇看見原來的圖像,只是沒有很明顯,刪不乾淨怎麼辦啊!

問題一: Runtime製作出來的 TBitmap 怎麼都沒辦法弄出圖像

在 docwiki 的範例程式碼裡面,我看到需要傳入 Bitmap 變數當成參數,所以就想著自己寫個 procedure, 在當中建立 Bitmap, 用完再 Free 掉,相信大多數的人也會有相似的想法,所以我就寫成了這樣一段程式碼:

      LBmp := TBitmap.Create;
      TBmp := TBitmap.Create(127, 127);

      try
         LBmp.SetSize(127, 127);
         LBmp.CopyFromBitmap(source, rect, 0, 0);

         TBMP.Canvas.DrawBitmap(TBitMap.CreateFromBitmapAndMask(LBMP,
                self.Image2.Bitmap), rectF, fullRectF, 1);
         TBmp.Canvas.DrawBitmap(self.Image6.Bitmap, rectF, fullRectF, 1);
      finally
         LBmp.Free;
         TBmp.Free;
      end;

乍看之下,邏輯沒有問題,但大家可以試試看, 在 finally 之前把 TBmp 或 LBmp 指派給一個 TImage.Bitmap,保證完全透明,什麼都看不見......

要解決這個問題,需要把程式改成以下這樣,只加了兩行:
      LBmp := TBitmap.Create;
      TBmp := TBitmap.Create(127, 127);

      try
         LBmp.SetSize(127, 127);
         LBmp.CopyFromBitmap(source, rect, 0, 0);

         TBmp.Canvas.BeginScene;
         TBMP.Canvas.DrawBitmap(TBitMap.CreateFromBitmapAndMask(LBMP,
                self.Image2.Bitmap), rectF, fullRectF, 1);
         TBmp.Canvas.DrawBitmap(self.Image6.Bitmap, rectF, fullRectF, 1);
         TBmp.Canvas.EndScene;
      finally
         LBmp.Free;
         TBmp.Free;
      end;
也就是在進行對 Bitmap.Canvas 繪製的時候,一定要先呼叫該 Canvas 的 BeginScene方法,畫完了,要記得呼叫EndScene方法,不然這些DrawBitmap, DrawLine, DrawRect都不會生效。在沒有加入這兩行之前,在進入 finally時,大家可以試著設定一個 Breakpoint, 會發現TBmp.Canvas 居然是 nil......

因此,動態生成的 Bitmap 物件,在建立之後如果要對它的Canvas做編輯,記得一定要先呼叫 Bitmap.Canvas.BeginScene, 編輯結束也要記得呼叫 Bitmap.Canvas.EndScene, 這樣才不會白費工夫。

問題二: 透過遮罩做出來的裁切圖片,想裁切掉的地方弄不乾淨.

這個問題,筆者弄了一個晚上,想著以前用 windows GDI 相關 API的相似方法,找到了一個替代的方法,思路如下:
  1. 用遮罩切掉的周邊有切掉,只是切不乾淨,遺留著一些半透明的點。
  2. 那我用跟遮罩完全相同的一張圖,把遮罩原本白色的區塊用Photoshop 挖成透明,黑色區塊用特定的顏色填上,用這張圖畫上Canvas,原本有半透明點的區塊就會變成特定色,已裁好的區塊則不受影響。
  3. 再把這個特定顏色換成透明色,完成。
換成程式碼,就寫成這樣:
      try
         LBmp.SetSize(127, 127);
         LBmp.CopyFromBitmap(source, rect, 0, 0);

         TBmp.Canvas.BeginScene;
         TBmp.Canvas.Clear(TAlphaColor($00000000));

         TBMP.Canvas.DrawBitmap(TBitMap.CreateFromBitmapAndMask(LBMP,
                 self.Image2.Bitmap), rectF, fullRectF, 1);
         TBmp.Canvas.DrawBitmap(self.Image6.Bitmap, rectF, fullRectF, 1);

         TBmp.Canvas.EndScene;
         self.replaceBitMapColorWithAnotherColor(TBMP, TAlphaColor($FFFE00FE),
              TAlphaColor($00000000));

         self.Image8.Bitmap := TBmp;
      finally
         LBmp.Free;
         TBmp.Free;
      end;
   end;

在上述的程式碼當中,我還模仿了 Delphi FireMonkey 的 TBitmap.ReplaceOpaqueColor 方法,寫了一個 replaceBitMapColorWithAnothercolor 的方法,可以對參數中的 Bitmap 元件裡的特定顏色進行換色。

procedure TForm2.replaceBitMapColorWithAnotherColor(bitMap: TBitMap;
  originalColor, newColor: TAlphaColor);
var
  I, J: Integer;
  M: TBitmapData;
  C: PAlphaColorRec;
begin
  if bitMap.Map(TMapAccess.ReadWrite, M) then
  try
    for J := 0 to bitMap.Height - 1 do
      for I := 0 to bitMap.Width - 1 do
      begin
        C := @PAlphaColorArray(M.Data)[J * (M.Pitch div 4) + I];
        if C^.Color = originalColor then
           C^.Color := newColor;
      end;
  finally
    bitMap.Unmap(M);
  end;
end;

在本文的範例專案中,大家可以直接試試看效果,這個範例專案中,我提供了一張圖片放在最上方,我們可以用滑鼠游標點擊任何一個地方,以被點到的座標點為中心,會先複製出一張 127x127 大小的圖片。


上面的這張圖片,是尚未使用遮色片做顏色置換的版本,底下是遮罩圖片,上方則是使用遮罩圖片製作出的裁切結果。

以這張圖片,我用6張不同內容的圖片做遮罩,做出裁切的效果,範例專案最左邊的第一張圖片,已經套用了前述的圓形遮罩,並用#FE00FE 這個顏色作為塗色片,最後用換色的程式碼把該顏色換成 TAlphaColor($00000000),這麼一來,效果差強人意。

但圓形的遮罩,邊邊難免會有輕微的混色,所以建議大家在做遮色片的時候,選擇跟您程式外框比較接近的顏色來做遮色片,這樣就算邊邊稍微混了一點顏色,也不容易被發現喔。

這個作法, 可以用來作為 ListView 需要把方形圖片換成圓形顯示,也可以把方形圖片變成扇形、橢圓形,總之,只要有想要的遮罩,就可以裁切成各種形狀了。


2015年11月27日 星期五

如何建立自己的 Xcode .a 檔案

在使用 Delphi 建立自己的 app 時, 有時候會需要使用到第三方廠商的 source code, 這些 source 如果是用 Xcode objective-c 撰寫, 用來提供 iOS 的某些功能, 直接使用這些驗證過的程式碼, 會比自己照著原始碼重寫一次來的快、來的可靠。


所以,最快的方法就是用 Xcode 把這些原始碼 (通常會是 .m 跟 .h 檔案) 建立一個 .a 檔案, .a 檔案是靜態函式庫, 等同於在 C 語言裡面的 .lib 檔, 連結後就不能動態改變執行檔的內容了。

以下是建立 .a 檔案的簡單步驟說明:

1.     使用 XCode建立一個新的專案

      

2.     選擇 Framework & Library -> Cocoa Touch Static Library, 然後選Next












3.     填寫 library 的基本資料, 任填就行了.
 
4.     選擇一個 folder 來存放專案檔案, 我選擇在 iOS SDK8 目錄下, Xcode會自動建立專案目錄

5.     finder找到要加入的.m .h 檔案, 拖拉進 Xcode 專案中


6.     選擇編譯的專案組態, 必須對Device Simulator分別都進行 Build.



7.     Device Simulator 都編譯好了之後, 找到製作好的 .a 檔案, 快速找尋方法: Products 點選 lib, 然後用滑鼠右鍵點選它, 選擇 Show In Finder, 就會出現一個新的 Finder 視窗, 顯示該檔案的位置。





8.     上面的 Finder 是顯示 Debug-iphones Debug-iphonesimulator兩個目錄裡面的libmyPackage.a 檔案.
9.     iphoneos 目錄裡面的, 是給實際裝置使用的 .a 檔案, iphonesimulator目錄裡面的, 是給模擬器使用的 .a 檔案, 我們可以透過以下的 command line 來把兩個檔案合一, 檔案會變大, 但就不用選擇不同的檔案了, 這個指令的用法是 lipo –create 第一個.a 第二個.a… -output 合併檔案

Mac OSX 裡面, Terminal (終端機)有個很方便的設計, 就是可以從 finder 把檔案直接拖拉到終端機畫面, 可以直接把該檔案的完整路徑拖拉進去:




然後 key in –output, 再拖拉 folder 進去:

10. 輸入檔名, 剛剛指令已經完成輸入到 –output 目錄 , 我們把檔案名輸入一下, 本例中是 libmyPackage.a
11. 按下 Enter, lipo 就會執行, 把兩個 .a 檔案合併成同一個, 完成。

補充: 預設建立的 .a 檔案是 debug 模式的, 裡面會有 debug information, 如果要建立 Release 的版本, 則需要修改一下 Build Configuration:


點選 Edit Scheme, 會出現設定畫面:
下拉 Build Configuration, 選擇 Release 即可.
重複6-12步驟, 這次要選擇 Release-iphoneos Release-iphonesimulator當中的 .a檔案, 建立出來的 .a , 就會是 Release 版本的了.

注意事項: 如果第三方的 source code 裡面有使用到 Delphi IDE 預設匯入的 iOS framework 以外的 framework, 也要記得把他們透過 Delphi IDE 的 Option->Tool->SDK Manager 匯入, 才能正確運作喔.

2015年10月22日 星期四

Indy - TCP 之一 - 以 TIdTCPServer 自製檔案伺服器 (1)

緣起

自 2001年9月, 我的第四本書- Delphi/Kylix Indy 網際網路程式設計出版之後,忙工作,忙教學,不知不覺也過了十幾年。 (好可怕啊~~~ 時光艿苒就是這種感覺嗎?)

在這期間我也曾經發過幾次神經,想要把該書改版一下,當時撰寫的時候,那時空背景是 Delphi 7, Kylix 3, Indy 8.0.25 轉 Indy 9 的時候,現在 Delphi XE7 都舉行了預覽發表會,Kylix 已經緲緲不知所蹤,Indy 都到 10.5 以後的版本了。

但曾和幾家台灣地區較為知名的出版社接洽出版,得到的回音不外乎:『目前還有人在寫 Delphi 喔?』『現在出寫程式的書沒有人要買了,如果你寫 PhotoShop 怎麼修照片的書,我們就出』,而我對於 PhotoShop 的使用,只停留在剪貼、放大縮小、模糊、去背的基本使用,所以還是不獻醜了。

從 2001年到現在2014年,13個年頭過去了,對於Indy相關的應用不少,但真正用到Indy強大功能的不多。這幾年從沒間斷在程式寫作上的浸淫,Delphi 除了 2009-2011之間,我把重心稍微轉到 Objective-C 的階段之外,也從來沒放下過,但看到對Indy真的運用的很棒,居然只有 Embarcadero.

別說 Embarcadero 是原廠,所以他們就自然會對Indy 熟悉,這是牽強的說法,因為Indy從 WinShoes 開始,都是OpenSource 的軟體元件,只要你肯花點時間,一定能很快熟悉它,甚至寫些元件發佈給 Indy 用,君不見,TIdDNSServer 就是筆者所寫?

扯遠了,拉回來。

Embarcadero 這幾年在 Delphi XE3 之後新增的 Server 端元件,不僅是傳統的 DataSnap server 支援了 TCP/IP與 REST 兩種模式,用的就是 Indy 元件來寫的,大家可以花點時間進去 C:\Program Files (x86)\Embarcadero\Studio\14.0\source 這個目錄看看, 裡頭真的花了很多心思,用 Indy 用的非常精闢。

舜何人? 予何人? 有為者亦若是!

筆者可能算是比較老派的,所以老愛掉書包,我們也別落人後,今天就來個範例,讓大家寫寫 Indy 小程式,做做傳遞檔案的功能吧。

可能您會說:我用 FTP Server 就好了,我用 Apache 加 PHP 用 Upload File 來做就好了等等。

我的回應是:都很好,能達成功能,不傷身體就好,但多學一種方法也不錯,最近不是有個廣告,比別人多會一招,勝算就多一成?

就來多學這一招半式吧。

使用時機

什麼時候會用到這東西? Indy 會飛嗎?

別再亂鬧了,哪天你的專案需要傳檔案,你就等著哭吧!

如果哪天專案要把資料回傳到 Server, 而這些資料又是檔案,架設 FTP 是OK的,到時候你還是得寫個 FTP Client 來上傳, 而且設定也還是得花時間寫的。

不多說,來看一個簡單的 TCP Server 是怎麼運作的吧,FTP Server 跟 HTTP Server 都不例外喔。

TCP Server 的運作,就是從啟動開始,先建立一些等待的 Queue, 也就是上圖裡面的等待隊序,像 TIdTCPServer 預設的 ListenQueue 數量就是 15 個, 當然可以隨我們的需要來修改這個數字。

在等待的過程當中,一旦有使用者連線進來了,就會由其中一個線程來處理這個使用者的需求,等這個使用者的需求處理完畢,該連線就會由 Client 或 Server 端來切斷。

命令有哪些?

這個問題很好,既然現在我們正在製作一個新的 Server, 也就沒有傳統命令的包袱,我們可以自己來決定,以下我們假設要讓特定的 Client 端傳送檔案到 Server 來,所以可以來定義幾個簡單的命令,你也可以把這些個命令當成通訊協定,因為本來這就是一個自定的通訊協定。

我們定義有以下幾個簡單的命令:
filename
filereplace
fileresume
sendfile

命令的順序要正確,不然就從 server 端強制斷線,我們定義順序如下:
Client                                                          Server
 "sendfile Open-Sesume" -------------------->

             <----------------------------- "200, OK, Please send file name, size, data"

"filename 要存放在 server 端的檔案路徑"--->

             <----------------------------- "200, ok, ready for get file: 檔案路徑"

"filesize 檔案大小" ------------------------------>

"檔案內容" ---------------------------------------->

             <----------------------------- "200, ok, file accepted"

Server 端只存放 filesize 後面所帶的數字描述的資料量,多的全放棄,接著,Server 斷線。

講到這裡,概念說完了,就是 Client 跟 Server 之間把字串送來送去,如此而已,接著,我們要來看怎麼用 Indy 達成這個要求了,請接著看Part2.