C# 图像.FromStream() : Lost metadata when running in Windows 8/10

标签 c# image web-services png platform

我有一个从 Web 服务检索图像的应用程序。在发送到 C# 客户端之前,Web 服务会将一些元数据嵌入到图像中。

这是方法的一部分。它从 Response 对象中检索 Stream,并从该流中创建一个 Image。请注意,我使用的是 System.Drawing.Image,而不是 System.Windows.Controls.Image - 这意味着我不能使用任何 ImageSource 或 BitmapSource。

System.Drawing.Image img = null;
using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
    Stream stream = response.GetResponseStream();
    img = System.Drawing.Image.FromStream(stream);
    .......
}
return img;

图像看起来非常好,但里面嵌入了元数据。图像是 PNG 格式,还有另一种方法可以从 Image 中提取信息。总共嵌入了六个元数据。描述了 PNG 格式(PNG block )here .数据保存在“tEXt” block 下。

public static Hashtable GetData(Image image)
{
    Hashtable metadata = null;
    data = new Hashtable();

    byte[] imageBytes;
    using (MemoryStream stream = new MemoryStream())
    {
        image.Save(stream, image.RawFormat);
        imageBytes = new byte[stream.Length];
        imageBytes = stream.ToArray();
    }

    if (imageBytes.Length <= 8)
    {
        return null;
    }

    // Skipping 8 bytes of PNG header
    int pointer = 8;

    while (pointer < imageBytes.Length)
    {
        // read the next chunk
        uint chunkSize = GetChunkSize(imageBytes, pointer);
        pointer += 4;
        string chunkName = GetChunkName(imageBytes, pointer);
        pointer += 4;

        // chunk data -----
        if (chunkName.Equals("tEXt"))
        {
            byte[] data = new byte[chunkSize];
            Array.Copy(imageBytes, pointer, data, 0, chunkSize);
            StringBuilder stringBuilder = new StringBuilder();
            foreach (byte t in data)
            {
                stringBuilder.Append((char)t);
            }

            string[] pair = stringBuilder.ToString().Split(new char[] { '\0' });
            metadata[pair[0]] = pair[1];
        }

        pointer += (int)chunkSize + 4;

        if (pointer > imageBytes.Length)
            break;
    }
    return data;
}

private static uint GetChunkSize(byte[] bytes, int pos)
{
    byte[] quad = new byte[4];
    for (int i = 0; i < 4; i++)
    {
        quad[3 - i] = bytes[pos + i];
    }

    return BitConverter.ToUInt32(quad);
}

private static string GetChunkName(byte[] bytes, int pos)
{
    StringBuilder builder = new StringBuilder();
    for (int i = 0; i < 4; i++)
    {
        builder.Append((char)bytes[pos + i]);
    }

    return builder.ToString();
}

在 Windows 7 中,所有六个元数据都被检测并提取出来。所以简而言之,在 Windows 7 环境中,我设法得到了我需要的一切。

当我将其移至 Windows 10 终端(也尝试过 Windows 8)时,情况变得不同了。我只能从 Image 中提取 2 条元数据。

因为我的 GetData() 方法将 Image 转换为 byte[],所以我尝试直接从网络服务流中提取数据.我将流转换为 byte[],并使用相同的技术从 byte[] 中提取元数据。我设法使用这种方法取回了所有 6 个元数据。

所以问题是:发生了什么变化?它在 Windows 7 中完全可以正常工作,但在 Windows 8 和 10 中却不是这样。我仍然可以取回数据,前提是我不转将流转换为 Image。在此过程中的某个地方,元数据丢失了。它要么在我将流转换为 Image 时丢失,要么在我将 Image 转换回 byte[] 时丢失。作为旁注,我尝试将 byte[] 转换为字符串。流中 byte[] 的字符串表示看起来与 Image 中的 byte[] 不同。使用正确的编码器,我可以看到后面的 byte[] 中缺少 4 个元数据。

最佳答案

元数据 tEXt:在 ISO/IEC 8859-1 中表示

尝试在提出请求之前添加以下内容:

 request.Headers.Add(HttpRequestHeader.AcceptCharset, "ISO-8859-1");

所以,修改你的代码:

System.Drawing.Image img = null;

 //accept Charset "ISO-8859-1"
 request.Headers.Add(HttpRequestHeader.AcceptCharset, "ISO-8859-1");

using (HttpWebResponse response = request.GetResponse() as HttpWebResponse)
{
 Stream stream = response.GetResponseStream();
 img = System.Drawing.Image.FromStream(stream);
  .......
}
 return img;

仅供引用,您能发布一下 windows 7/8/10 中的 windows EncodingName 是什么吗

使用powershell命令可知:

[System.Text.Encoding]::Default.EncodingName

编辑:

我查看了 DOTNet System.Drawing.Image.FromStream 的源代码 并发现该语句:

  // [Obsolete("Use Image.FromStream(stream, useEmbeddedColorManagement)")]
    public static Image FromStream(Stream stream) { 
        return Image.FromStream(stream, false);
    }

尝试使用:

  Image.FromStream(stream, true); 
  or
 Image.FromStream(stream, true,true);

参数详情:

  public static Image FromStream(
  Stream stream,
  bool useEmbeddedColorManagement,////true to use color management  information embedded in the data stream; otherwise, false. 
  bool validateImageData //true to validate the image data; otherwise, false.
  )

Image.FromStream Method

编辑 2:

我用文本数据对 PNG 图像文件做了一个实验:

我开发了一个函数来测量图像的大小(以字节为单位),该函数由函数 FromStream() 读取并在 win7/win 10 上执行。

下表,代表了两种环境下图像的实际大小(以字节为单位):

 The file size: 502,888 byte (real size on disk).     

 win 7         win10        function used
 569674        597298      Image.FromStream(stream, true,true)
 597343        597298      Image.FromStream(stream, true)
 597343        597298      Image.FromStream(stream, false)

你发现大小在两种环境中都不同,并且不同于 磁盘中的实际大小。

因此,您希望元数据的位置发生变化(但不会丢失,只是重新分配)

我使用十六进制编辑器工具查看 tTEXT block 。

tEXT 位于文件开头的第 66 位(十进制),并且在两种环境下都是相同的!!!

我使用了自己的元数据读取器功能,结果对于 Windows 7 或 Windows 10 都是相同且有效的(没有数据丢失)。

PNG 格式的官方网站是:https://www.w3.org/TR/PNG/

结论

Image.FromStream 函数不适合读取元数据,图像文件应该以原始字节格式而不是图像格式读取,因为函数 FromStream 重新分配原始数据以保持图像及其数据没有失真(即 dotnet 中函数的内部结构)。

要读取 PNG 规范描述的元数据,您应该按照规范描述从文件开头读取 RAW BYTES 格式的流。

我建议你使用类库MetadataExtractor来读取元数据,它的结果在windows 7和windows 10中都非常准确

您可以从 nuget 安装库。 安装包元数据提取器

编辑 3:建议的解决方案

现在问题已经解决,下面的类对 win 7 和 win 8 都有效

主要变化是以原始字节读取图像文件

class MetaReader 
{
    public static Hashtable GetData(string fname)
    {
        using (FileStream image = new FileStream(fname, FileMode.Open, FileAccess.Read))
        {
            Hashtable metadata = new Hashtable();
            byte[] imageBytes;

            using (var memoryStream = new MemoryStream())
            {
                image.CopyTo(memoryStream);
                imageBytes = memoryStream.ToArray();
                Console.WriteLine(imageBytes.Length);
            }

            if (imageBytes.Length <= 8)
            {
                return null;
            }

            // Skipping 8 bytes of PNG header
            int pointer = 8;

            while (pointer < imageBytes.Length)
            {
                // read the next chunk
                uint chunkSize = GetChunkSize(imageBytes, pointer);
                pointer += 4;
                string chunkName = GetChunkName(imageBytes, pointer);
                pointer += 4;

                // chunk data -----
                if (chunkName.Equals("tEXt"))
                {
                    byte[] data = new byte[chunkSize];
                    Array.Copy(imageBytes, pointer, data, 0, chunkSize);
                    StringBuilder stringBuilder = new StringBuilder();
                    foreach (byte t in data)
                    {
                        stringBuilder.Append((char)t);
                    }

                    string[] pair = stringBuilder.ToString().Split(new char[] { '\0' });
                    metadata[pair[0]] = pair[1];
                    Console.WriteLine(metadata[pair[0]]);
                }

                pointer += (int)chunkSize + 4;

                if (pointer > imageBytes.Length)
                    break;
            }
            return metadata;
        }
    }

    private static uint GetChunkSize(byte[] bytes, int pos)
    {
        byte[] quad = new byte[4];
        for (int i = 0; i < 4; i++) { quad[3 - i] = bytes[pos + i]; }

        return BitConverter.ToUInt32(quad, 0);

    }

    private static string GetChunkName(byte[] bytes, int pos)
    {
        StringBuilder builder = new StringBuilder(); for (int i = 0; i < 4; i++) { builder.Append((char)bytes[pos + i]); }

        return builder.ToString();

    }
}

从 Web 服务读取元数据:

您可以从 url 作为流加载图像文件,并动态读取元数据。 此外,您可以创建 System.Drawing.Image 的实例并对图像进行任何处理。 您可以在以下位置找到带有源代码的完整演示:

Reading Metadata from PNG loaded from Web Stream -TryIt

关于C# 图像.FromStream() : Lost metadata when running in Windows 8/10,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/38693292/

相关文章:

c# - 创建实例化类对象的数组

web-services - 为 IIS 应用程序池帐户安装客户端证书

android - Web 服务、Android 客户端、SSL

android - 如何让移动网页在所有设备上显示相同

swift - 在 TableView 行中显示来自 URL 的图像

web-services - 尝试解析JSON响应时得到<null>

c# - 为什么我在 Web 客户端中得到结果,即使服务器已关闭?

c# - SQL BulkCopy 与 OPENDATASOURCE 哪个更快

c# - 如何将 targetname 与用户控件和触发器一起使用

JavaFX Canvas 扭曲图像