С# Image.FromStream(): потерянные метаданные при работе в Windows 8/10

У меня есть приложение, которое извлекает изображение из веб-службы. Веб-служба будет внедрять некоторые метаданные в изображение перед отправкой на клиент С#.

Это часть метода. Он извлекает поток из объекта Response и создает изображение из потока. Обратите внимание, что я использую 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) описывается здесь здесь. Данные сохраняются в разделе "tEXt".

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), все становится по-другому. Я могу извлечь только 2 части метаданных из Image.

Поскольку мой метод GetData() преобразует Image в byte[], поэтому я попытался извлечь данные прямо из потока веб-службы. Я преобразовал поток в byte[] и использовал ту же технику для извлечения метаданных из byte[]. Мне удалось вернуть все 6 метаданных с помощью этого метода.

Итак, вопрос: Что изменилось? В Windows 7 он работает отлично, но не так в Windows 8 и 10. Я все еще могу вернуть данные, если не буду поток в Image. Где-то в этом процессе метаданные теряются. Он либо теряется, когда я конвертирую поток в Image, или когда я преобразовываю Image обратно в byte[]. В качестве примечания я попытался преобразовать строку byte[] в строку. Строковое представление byte[] из потока отличается от byte[] от Image. Используя правильный кодировщик, я мог видеть 4 метаданных, отсутствующих в более позднем byte[].

Ответ 1

Метаданные 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;

только для информации, можете ли вы разместить то, что является окном EncodingName в Windows 7/8/10

используйте команду powershell, чтобы знать:

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

Edit:

Я рассмотрел исходный код 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

Изменить 2:

Я провел эксперимент с файлом изображения PNG с данными TEXT:

Я разработал функцию для измерения размера изображения в байтах, который считывается функцией 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.

tEXT находится в позиции 66 (в десятичной форме), начиная с начала файла, и в обеих средах одинаково!

Я использовал свою собственную функцию чтения метаданных, и результат тот же и действителен как для окон 7, так и для окон 10 (NO LOSS OF DATA).

Официальный сайт формата PNG: https://www.w3.org/TR/PNG/

Заключение

Функция Image.FromStream не подходит для чтения метаданных, файл изображения следует читать в формате исходного байта не в формате изображения, потому что функция FromStream перераспределяет необработанные данные в таком состоянии, чтобы сохранить изображение и его данные без искажений (то есть внутренности функции в dotnet).

Чтобы прочитать метаданные, как описано в спецификациях PNG, вы должны прочитать поток в RAW BYTES с самого начала файла, как описано в спецификациях.

Я советую вам использовать библиотеку классов MetadataExtractor для чтения метаданных, и ее результат очень точен как в Windows 7, так и в Windows 10

Вы можете установить библиотеку из nuget.      install-Package MetadataExtractor

Изменить 3: Предлагаемое решение

Теперь проблема решена, и следующий класс действителен как для win 7, win 8

Основное изменение - чтение файла изображения в виде Raw bytes

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();

    }
}

Чтение метаданных из веб-службы:

Вы можете загрузить файл изображения с URL-адреса в виде потока и прочитать метаданные "на лету". Кроме того, вы можете создать экземпляр System.Drawing.Image и делать то, что когда-либо обрабатывалось на изображении. Вы можете найти полную демоверсию с исходным кодом по адресу:

Чтение метаданных из PNG, загруженных из Web Stream -TryIt