GDAL庫對於C#的支援問題還是蠻多的,對於中文路徑的支援就是其中之一(另一個就是通過OGR庫擷取圖形的座標資訊)。
關於C#支援中文路徑,看過我之前部落格的應該都不陌生,如果使用的是我修改過的GDAL庫,可以通過設定下面的代碼即可讓C#直接支援中文路徑。如果使用官方的庫,不用設定直接應該就可以支援中文路徑。
// 註冊所有的驅動 Ogr.RegisterAll(); // 為了支援中文路徑,請添加下面這句代碼 OSGeo.GDAL.Gdal.SetConfigOption("GDAL_FILENAME_IS_UTF8","YES"); // 為了支援shp屬性工作表欄位支援中文,請添加下面這句 OSGeo.GDAL.Gdal.SetConfigOption("SHAPE_ENCODING","");
昨天,一位朋友說,他測試C#版本,發現中文路徑有時候可以,有時候不可以,通過設定GDAL_FILENAME_IS_UTF8也無濟於事。
今天通過測試發現,只要是檔案名稱中的漢字個數是偶數,完全沒有影響,讀取和建立都正常,如果檔案名稱中的漢字個數是奇數,肯定不能讀取和建立。
比如下面的檔案名稱就是正常的:
D:\\建立檔案夾\\建立1.shpD:\\密雲資料\\線分離的0.shp
而下面的肯定就是不行:
D:\\建立檔案夾\\建立的1.shpD:\\密雲資料\\線分離0.shp
下面就通過C#程式調試GDAL庫,找找原因。按照上篇部落格中的跨語言調試的方式,在C#程式中的Open函數處設定斷點,然後啟動調試,程式在此處中斷。
首先用一個GDAL庫可以開啟的正常路徑進行測試,如所示。
接下來按F11鍵,進入swig封裝的C#代碼中,如所示。
在這裡,我們發現了這樣的代碼。
public static DataSourceOpen(string utf8_path, int update) { IntPtr cPtr =OgrPINVOKE.Open(System.Text.Encoding.Default.GetString(System.Text.Encoding.UTF8.GetBytes(utf8_path)),update); DataSource ret = (cPtr ==IntPtr.Zero) ? null : new DataSource(cPtr, true, ThisOwn_true()); if(OgrPINVOKE.SWIGPendingException.Pending) throwOgrPINVOKE.SWIGPendingException.Retrieve(); return ret; }
其中在調用OgrPINVOKE時,將路徑進行了編碼轉換,核心代碼如下:
System.Text.Encoding.Default.GetString(System.Text.Encoding.UTF8.GetBytes(utf8_path))
從代碼可以看出,Swig首先將C#預設的字串,使用UTF8的編碼轉換為預設的編碼。上面的路徑“D:\建立檔案夾\建立1.shp”通過這句轉換之後就變成了“D:\鏂板緩鏂囦歡澶筡鏂板緩1.shp”。而這個字串傳入GDAL庫後,在檔案gdal-1.10.0\port\cpl_vsil_win32.cpp中的函數VSIVirtualHandle*VSIWin32FilesystemHandler::Open( const char *pszFilename, const char *pszAccess )中又進行了一次編碼轉換。如所示。
通過,可以發現,如果設定了GDAL_FILENAME_IS_UTF8=YES時,系統先將編碼從UTF8轉為UCS2編碼。通過這句之後,發現路徑又編程了原來的,如:
這樣GDAL庫就可以正常開啟該檔案。下面再看一個GDAL不能開啟的路徑重複上面的步驟,下面只截取關鍵位置的。
首先是在開啟時設定斷點,檔案路徑為“D:\建立檔案夾\建立的1.shp”
然後傳入GDAL庫中的路徑通過轉碼變成了“D:\鏂板緩鏂囦歡澶筡鏂板緩鐨?.shp”。之後再通過GDAL庫中的函數轉為寬位元組時稱為了“D:\建立檔案夾\建立çš?.shp”。如所示。
只要路徑中出現了問號(?),這個路徑肯定有問題,不管是不是亂碼。所以這個路徑肯定就打不開了。
通過上面的步驟,我們可以確定,C#的路徑是好使的,而通過SWIG中的編碼轉換後就出現了問題,所以我們可以認為是編碼轉換出現的問題。
在SWIG封裝的介面中,使用System.Text.Encoding.Default.GetString(System.Text.Encoding.UTF8.GetBytes(utf8_path))進行轉換,下面針對此程式碼片段寫一個簡單的測試代碼進行驗證。
staticvoid Main(string[] args){ string strUtf8 = "D:\\建立檔案夾\\建立的1.shp"; byte[] byutf8 =System.Text.Encoding.UTF8.GetBytes(strUtf8); string strDefault =System.Text.Encoding.Default.GetString(byutf8); byte[] byDefault =System.Text.Encoding.Default.GetBytes(strDefault); string strUtf8n = System.Text.Encoding.UTF8.GetString(byDefault);}
首先看一個GDAL可以正常訪問的路徑,首先查看轉換後再轉回來,共三個字串的對比,如,可以看出,轉換為Default再轉為utf8之後,與原來的路徑一樣。所以GDAL庫可以正常訪問。
而轉換前後擷取的byte數組內容完全一致,如所示:
下面再使用一個GDAL不能訪問的路徑進行測試,查看轉換後再轉回來,共三個字串的對比,如,可以看出,轉換為Default再轉為utf8之後,與原來的路徑發生了變化。
下面比較兩次轉換的byte數組,按理說記憶體中的byte數組應該是一樣的,下面對比兩個byte數組中的內容,如所示,可以發現,數群組轉換前的27和28分別是132和49,而轉換後,這兩個位元組變成了一個位元組(63)。
從這裡可以看出,可以認為問題就出在此處。對應ASCII碼錶,將中的值轉為字串,可以得到下面的圖。英文字元佔用一個byte,而漢字佔用3個byte。而在轉碼的時候應該是兩個位元組為一組進行轉碼處理,也就是說對於偶數個漢字,轉成byte是3倍的偶數,結果肯定是偶數,所以按照兩個位元組轉碼剛好可以轉完;而漢字為奇數個,轉成byte是3倍的奇數,結果肯定是個奇數,按照兩個位元組轉碼,肯定會多出來一個,這多出來的一個系統可能不認識就用問號(?)來表示了。
所以,可以這麼認為,漢字是偶數的就正常,奇數的就會出現問題,與GDAL表現的結果完全一致。上面的最後這一段的是我個人的分析,不代表微軟內部就是這麼實現的。或許這可能算作C#的一個bug?不知道微軟有沒有發現這個問題。