zoukankan      html  css  js  c++  java
  • 【C#】C#中使用GDAL3(二):Windows下读写Shape文件及超详细解决中文乱码问题

      转载请注明原文地址:https://www.cnblogs.com/litou/p/15035790.html

      本文为《C#中使用GDAL3》的第二篇,总目录地址:https://www.cnblogs.com/litou/p/15004877.html

    本文目录
    一、介绍
    二、读写数据内容
    三、中文乱码问题
    3.1、数据路径或数据文件名含中文时打开失败
    3.2、读取中文字符串显示乱码
    3.3、函数传入中文字符串参数报错

      一、介绍

      Shape文件是ESRI公司开发的一种空间数据开放格式,全称是ESRI Shapefile,该文件格式是由多个文件组成的,表示同一数据的一组文件的文件名必须相同。

      要组成一份Shapefile,有三个文件是必不可少的,它们分别是shp、shx和dbf文件。组成如下:

    必须文件 .shp 主文件,记录要素几何实体
    .shx 索引文件,记录每一个几何体在shp文件之中的位置
    .dbf 数据文件,以dBase IV的数据表格式存储每个几何形状的属性数据
    可选文件 .prj 投影文件,保存地理坐标系统与投影信息
    .sbx .sbn 其他文件

      二、读写数据内容

      GDAL库内置支持读写ESRI Shapefile文件,无需其他插件支持。

      示例Shapefile文件如下,存放在"C:shp数据"下,图层名称为"测试面",类型为面,自定义字段有"Id"、"名称"和"大小",有两条记录。

      

      以VS2015为例,修改自上一篇《C#中使用GDAL3(一):Windows下超详细编译C#版GDAL3.3.0(VS2015+.NET 4+32位/64位)》中第九部分"C#调用测试"的Demo程序。

      由于Shapefile文件属于矢量数据,所以只需注册OGR驱动。

      1、打开数据

      调用Ogr.Open打开数据获取DataSource。这里有两种打开方法:

      1)打开shp文件,即Ogr.Open的第一个参数是shp文件的路径,打开后得到的DataSource里面只含shp文件本身的一份数据。

      2)打开shp文件所在目录,即Ogr.Open的第一个参数是shp文件所在目录的路径,打开后得到的DataSource里面包含该目录下所有shp文件数据。

      另外,Open的第二个参数为打开方式,值0表示以只读方式打开,值1表示以读写方式打开。

      2、获取图层对象和图层名称

      调用DataSource.GetLayerByXXXXX获取图层对象,这里调用的是GetLayerByIndex,再调用Layer.GetName获取图层名称。

      3、获取要素定义、字段定义和字段名称

      调用Layer.GetLayerDefn获取要素定义,然后调用FeatureDefn.GetFieldDefn获取字段定义,再调用FieldDefn.GetName获取字段名称。

      4、遍历要素记录

      循环调用Layer.GetNextFeature获取每一条要素记录,直到获取的要素记录为null则循环结束。如需要重头开始遍历,需要调用Layer.ResetReading重置为开头位置。

      5、读取要素字段值

      调用Feature.GetFieldAsXXXXX获取要素字段值,这里调用的是GetFieldAsInteger、GetFieldAsString和GetFieldAsDouble的传入字段索引值的方法。

      6、设置要素字段值

      调用Feature.SetField写入要素字段值。

      7、更新要素

      调用Layer.SetFeature使要素修改生效。

    using OSGeo.OGR;
    using System;
    
    namespace GdalDemo
    {
        class Program
        {
            static void Main(string[] args)
            {
                Ogr.RegisterAll();
    
                ReadShapeFile();
    
                Console.ReadKey();
            }
    
            static void ReadShapeFile()
            {
                //打开数据
                string path = @"C:shp数据";
                DataSource ds = Ogr.Open(path, 1);  //以可写方式打开
                int lCount = ds.GetLayerCount();
                for (int i = 0; i < lCount; i++)
                {
                    //读取图层信息
                    Layer layer = ds.GetLayerByIndex(i);
                    string layerName = layer.GetName();
                    Console.WriteLine(String.Format("图层名:{0}", layerName));
    
                    //读取字段信息
                    FeatureDefn featureDefn = layer.GetLayerDefn();
                    int fCount = featureDefn.GetFieldCount();
                    for (int j = 0; j < fCount; j++)
                    {
                        FieldDefn fieldDefn = featureDefn.GetFieldDefn(j);
                        string fieldName = fieldDefn.GetName();
                        Console.WriteLine(String.Format("字段名:{0}", fieldName));
                    }
    
                    //遍历要素
                    Feature feature;
                    while ((feature = layer.GetNextFeature()) != null)
                    {
                        //读取要素信息
                        int id = feature.GetFieldAsInteger(0);
                        Console.WriteLine(String.Format("字段值-id:{0}", id));
                        string name = feature.GetFieldAsString(1);
                        Console.WriteLine(String.Format("字段值-名称:{0}", name));
                        double size = feature.GetFieldAsDouble(2);
                        Console.WriteLine(String.Format("字段值-大小:{0}", size));
    
                        //设置要素信息
                        feature.SetField(0, id + 1);
                        feature.SetField(1, name + "");
                        feature.SetField(2, size + 10.12);
    
                        //更新要素
                        layer.SetFeature(feature);
    
                        //读取修改后要素信息
                        Console.WriteLine(String.Format("字段值-修改后-id:{0}", feature.GetFieldAsInteger(0)));
                        Console.WriteLine(String.Format("字段值-修改后-名称:{0}", feature.GetFieldAsString(1)));
                        Console.WriteLine(String.Format("字段值-修改后-大小:{0}", feature.GetFieldAsDouble(2)));
    
                        //用字段名读取字段值
                        Console.WriteLine(String.Format("字段值-字段名值-id:{0}", feature.GetFieldAsInteger("id")));
                        try
                        {
                            Console.WriteLine(String.Format("字段值-字段名值-名称:{0}", feature.GetFieldAsString("名称")));
                        }
                        catch { }
                    }
                }
            }
        }
    }

      运行结果如下:

      1)数据读取正常

      2)中文图层名称和字段名称均显示为乱码

      3)读取字段值并显示中文内容正常

      4)写入中文内容到字段正常

      5)使用中文字段名获取字段值报错

      

      三、中文乱码问题

      要解决乱码问题,首先要理解为什么会出现乱码。根据GDAL的文档资料显示(https://gdal.org/development/rfc/rfc5_unicode.html),GDAL内部字符串使用UTF8编码,也就是说输入和输出的字符串均为UTF8编码,而我们使用的操作系统大部分都是简体中文版的Windows,其默认的字符串编码是GB2312(可通过C#下的System.Text.Encoding.Default.EncodingName得到),如果不做编码转换直接显示的话就会出现乱码问题

      3.1、数据路径或数据文件名含中文时打开失败

      该情况在GDAL 3.3.0的C#接口中是不存在的。以Ogr库为例,在Ogr.cs中可以找到Open方法,其方法内通过Ogr.StringToUtf8Bytes函数处理,把传入的路径字符串转化为UTF8编码的字节数组,再传入内部的Open方法,所以在调用Ogr.Open方法时,无需对传入的路径字符串进行编码处理,也能正常使用。

      另外在GDAL内部,参数GDAL_FILENAME_IS_UTF8的默认值是YES,所以无需显式重复设置为YES也能正常读取,设置为NO反而导致读取失败。

    //Ogr.cs
    public static DataSource Open(string utf8_path, int update)
    {
        IntPtr cPtr = OgrPINVOKE.Open(Ogr.StringToUtf8Bytes(utf8_path), update);
        DataSource ret = (cPtr == IntPtr.Zero) ? null : new DataSource(cPtr, true, ThisOwn_true());
        if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve();
        return ret;
    }
    
    internal static byte[] StringToUtf8Bytes(string str)
    {
        if (str == null)
            return null;
    
        int bytecount = System.Text.Encoding.UTF8.GetMaxByteCount(str.Length);
        byte[] bytes = new byte[bytecount + 1];
        System.Text.Encoding.UTF8.GetBytes(str, 0, str.Length, bytes, 0);
        return bytes;
    }

      3.2、读取中文字符串显示乱码

      同样是读取字符串,读取中文图层名称和字段名称显示乱码,而读取中文字段值则正常。

    //Layer.cs
    public string GetName()
    {
        string ret = OgrPINVOKE.Layer_GetName(swigCPtr);
        if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve();
        return ret;
    }
    
    //FieldDefn.cs
    public string GetName()
    {
        string ret = OgrPINVOKE.FieldDefn_GetName(swigCPtr);
        if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve();
        return ret;
    }
    
    //Feature.cs
    public string GetFieldAsString(int id)
    {
        IntPtr cPtr = OgrPINVOKE.Feature_GetFieldAsString__SWIG_0(swigCPtr, id);
        string ret = Ogr.Utf8BytesToString(cPtr);
    
        if (OgrPINVOKE.SWIGPendingException.Pending) throw OgrPINVOKE.SWIGPendingException.Retrieve();
        return ret;
    }
    
    //Ogr.cs
    internal unsafe static string Utf8BytesToString(IntPtr pNativeData)
    {
        if (pNativeData == IntPtr.Zero)
            return null;
    
        byte* pStringUtf8 = (byte*)pNativeData;
        int len = 0;
        while (pStringUtf8[len] != 0) len++;
        return System.Text.Encoding.UTF8.GetString(pStringUtf8, len);
    }

      对比GetName和GetFieldAsString两个函数可以很明显看出来,GetFieldAsString通过调用Ogr.Utf8BytesToString将返回的UTF8编码的字节数组以UTF8方式解码为字符串,所以能够正常显示;而GetName则直接返回字符串(实际上编译器隐性调用了System.Text.Encoding.Default.GetString解码为字符串),由于没有使用UTF8解码导致显示为乱码。

      不完美处理方法1:在C#中将乱码字符串还原为字节数组并重新以UTF8方式解码字符串

      具体方法为,将乱码的字符串先通过System.Text.Encoding.Default.GetBytes转换回乱码状态前的字节数组,再调用System.Text.Encoding.UTF8.GetString以UTF8的方式解码为系统识别的字符串。

      该方法处理偶数个中文字符时可以正常还原,但处理奇数个中文字符时最后一个中文字符还原失败。测试代码如下:

    using System;
    using System.Text;
    
    namespace Demo
    {
        class Program
        {
            static void Main(string[] args)
            {
                string sOdd = "测试";
                Console.WriteLine("原字符串:" + sOdd);
                string sOddUtf8 = Encoding.Default.GetString(Encoding.UTF8.GetBytes(sOdd));
                Console.WriteLine("UTF8字符串:" + sOddUtf8);
                string sOddURestore = Encoding.UTF8.GetString(Encoding.Default.GetBytes(sOddUtf8));
                Console.WriteLine("还原字符串:" + sOddURestore);
    
                Console.WriteLine();
    
                string sEven = "测试面";
                Console.WriteLine("原字符串:" + sEven);
                string sEvenUtf8 = Encoding.Default.GetString(Encoding.UTF8.GetBytes(sEven));
                Console.WriteLine("UTF8字符串:" + sEvenUtf8);
                string sEvenURestore = Encoding.UTF8.GetString(Encoding.Default.GetBytes(sEvenUtf8));
                Console.WriteLine("还原字符串:" + sEvenURestore);
    
                Console.ReadKey();
            }
        }
    }

      结果如下,"测试"可以正常还原,而"测试面"最后一个字还原失败。其原因是编码转换的问题,与平台无关,具体可参考该文章(https://blog.csdn.net/yuwenruli/article/details/6911401)。

      

      要解决字符串乱码问题,只需要将原始UTF8编码的字节数组正确的使用UTF8解码即可。

      前面提到GDAL中返回乱码字符串的函数(如GetName)已经把UTF8编码的字节数组返回为错误编码的字符串,且无法还原为完整的UTF8编码的字节数组,只能从源头开始处理。

      解决方法2:在GDAL的C#源码中修正返回乱码字符串的函数。

      以Layer.GetName为例,修改OgrPINVOKE.cs里面SWIGStringHelper的CreateString函数说明,并增加UTF8编码处理。(如没有找到.cs源码文件,执行一次nmake -f makefile.vc interface即可生成)

    //OgrPINVOKE.cs
    //修改前
    protected class SWIGStringHelper
    {
        public delegate string SWIGStringDelegate(string message);
        static SWIGStringDelegate stringDelegate = new SWIGStringDelegate(CreateString);
    
        [global::System.Runtime.InteropServices.DllImport("ogr_wrap", EntryPoint = "SWIGRegisterStringCallback_Ogr")]
        public extern static void SWIGRegisterStringCallback_Ogr(SWIGStringDelegate stringDelegate);
    
        static string CreateString(string cString)
        {
            return cString;
        }
    
        static SWIGStringHelper()
        {
            SWIGRegisterStringCallback_Ogr(stringDelegate);
        }
    }
    
    //修改后
    protected class SWIGStringHelper
    {
        public delegate string SWIGStringDelegate(IntPtr ptr);  //委托类型改为IntPtr
        static SWIGStringDelegate stringDelegate = new SWIGStringDelegate(CreateString);
    
        [global::System.Runtime.InteropServices.DllImport("ogr_wrap", EntryPoint = "SWIGRegisterStringCallback_Ogr")]
        public extern static void SWIGRegisterStringCallback_Ogr(SWIGStringDelegate stringDelegate);
    
        static string CreateString(IntPtr ptr)
        {
            return Ogr.Utf8BytesToString(ptr);  //返回UTF8解码的字符串
        }
    
        static SWIGStringHelper()
        {
            SWIGRegisterStringCallback_Ogr(stringDelegate);
        }
    }

       修改完毕后,重新执行nmake -f makefile.vc和nmake -f makefile.vc install,将新生成的ogr_csharp.dll替换原来引入到C#项目中的文件并重新运行,发现图层名已经能够正常显示外,且字段名也同样正常显示了。

       

      注:其他类库也需要同样修改,修改内容汇总如下:

    OgrPINVOKE.cs -> Ogr.Utf8BytesToString
    GdalPINVOKE.cs -> Gdal.Utf8BytesToString
    OsrPINVOKE.cs -> Osr.Utf8BytesToString
    GdalConst.cs 补充Utf8BytesToString函数
    GdalConstPINVOKE.cs -> GdalConst.Utf8BytesToString

      修改原理可参考下图:

      1)在Feature.GetFieldAsString方法的调用链中,用IntPtr表示C++返回的字符指针(橙色部分),然后将其用UTF8解码为字符串。

      2)在Layer.GetName方法的调用链中,C++将得到的字符指针回调至C#端处理(橙色部分),处理后的字符串回到C++中继续流转,最后返回到C#中。而回调的C#部分直接把字符指针返回为字符串,编译器隐性调用了System.Text.Encoding.Default.GetString解码为字符串,故后面得到的字符串都是解码错误的。

      

      所以Layer.GetName解决乱码的思路有两种:

      1)在SWIGStringHelper.CreateString处用UTF8解码字符串,也就是本解决方法。且除Layer.GetName之外,其他返回字符串的函数均调用了相同的回调函数,故其他返回乱码字符串的问题也一并解决了(如FieldDefn.GetName等)。

      2)跳过ogr_wrap的所有包装函数(包括C#回调),直接调用gdal的函数获取,因此引申出下面的解决方法。

      解决方法3:在C#中调用GDAL接口获取内容。

      以Layer.GetName为例,在C#中增加调用gdal303.dll的OGR_L_GetName接口,并使用UTF8编码处理。FieldDefn.GetName需要调用OGR_Fld_GetNameRef接口(接口名称可查阅https://gdal.org/python)。

    static string Utf8BytesToString(IntPtr ptr)
    {
        if (ptr == IntPtr.Zero)
            return null;
    
        MemoryStream ms = new MemoryStream();
        byte b;
        int ofs = 0;
        while ((b = Marshal.ReadByte(ptr, ofs++)) != 0)
        {
            ms.WriteByte(b);
        }
        return Encoding.UTF8.GetString(ms.ToArray());
    }
    
    //Layer.GetName
    [DllImport("gdal303.dll", EntryPoint = "OGR_L_GetName", CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr OGR_L_GetName(HandleRef handle);
    static string GetLayerName(Layer layer)
    {
        HandleRef handle = Layer.getCPtr(layer);
        IntPtr ptr = OGR_L_GetName(handle);
        return Utf8BytesToString(ptr);
    }
    
    //FieldDefn.GetName
    [DllImport("gdal303.dll", EntryPoint = "OGR_Fld_GetNameRef", CallingConvention = CallingConvention.Cdecl)]
    static extern IntPtr OGR_Fld_GetNameRef(HandleRef handle);
    static string GetFieldDfnName(FieldDefn fieldDefn)
    {
        HandleRef handle = FieldDefn.getCPtr(fieldDefn);
        IntPtr ptr = OGR_Fld_GetNameRef(handle);
        return Utf8BytesToString(ptr);
    }

      运行结果如下,图层名和字段名已经正常显示。

      

      3.3、函数传入中文字符串参数报错

      以Feature.GetFieldAsString(string field_name)为例,前面已通过枚举的方式列出所有字段名称且包含字段名"名称",但调用Feature.GetFieldAsString方法并传入"名称"作为参数时,却报错Invalid field name。

      参考其方法的调用链,C#中传入的字符串参数直接传递为C++的字符指针,编译器隐性调用了System.Text.Encoding.Default.GetBytes将传入的字符串编码为GB2312字节数组,故GDAL无法识别导致报错。

      

      解决方法:把传入的字符串做编码处理。

      根据上面的分析结果逆向处理,先把字符串用UTF8编码为字节数据,再用Default编码为字符串,把结果传入函数即可。

    static string Utf8String(string s)
    {
        if (!String.IsNullOrEmpty(s))
            return Encoding.Default.GetString(Encoding.UTF8.GetBytes(s));
        return s;
    }

       运行结果如下,已经可以识别中文字符串调用参数了。

      

  • 相关阅读:
    模拟赛总结
    2018.04.06学习总结
    2018.04.06学习总结
    Java实现 LeetCode 672 灯泡开关 Ⅱ(数学思路问题)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 671 二叉树中第二小的节点(遍历树)
    Java实现 LeetCode 670 最大交换(暴力)
    Java实现 LeetCode 670 最大交换(暴力)
    Java实现 LeetCode 670 最大交换(暴力)
  • 原文地址:https://www.cnblogs.com/litou/p/15035790.html
Copyright © 2011-2022 走看看