Загрузка и сохранение изображений в WinRT

55

Как вы уже видели, методу SetSource объекта WriteableBitmap можно задать поток со ссылкой на файл PNG; метод корректно распакует сжатый файл и преобразует его в массив. Вы можете подключиться к этому процессу при помощи классов из пространства имен Windows.Graphics.Imaging. Растровое изображение можно загрузить как массив битов пикселов, а можно действовать иначе: сохранить массив битов пикселов из объекта WriteableBitmap, созданный в программе, в файле одного из популярных графических форматов.

Форматы файлов растровой графики обычно различаются по используемому алгоритму сжатия (включая его отсутствие), и конечно, уникальным структурам данных, заголовкам и способам хранения сжатых данных. Блок кода, который может прочитать конкретный формат и преобразовывать его в массив пикселов, называется декодером (decoder). Декодеры обеспечивают загрузку графических файлов в приложения. Форматы, поддерживаемые классом BitmapDecoder из пространства имен Windows.Graphics.Imaging, перечислены в следующей таблице:

Форматы декодера .NET BitmapDecoder
Формат файла Типы MIME Расширение
Растровое изображение Windows image/bmp .bmp, .dib,.rle
Значки Windows image/ico, image/x-icon .ico, .icon
Файлы GIF image/gif .gif
JPEG image/jpeg, image/jpe, image/jpg .jpeg, .jpe, .jpg, .jnf, .exif
PNG image/png .png
TIFF image/tiff, image/tif .tiff, .tif
WMPhoto image/vnd.ms-photo .wdp, .jxr

Класс BitmapDecoder определяет тип файла, который он должен загрузить. Если ему это сделать не удается, выдается исключение.

Код, создающий файл конкретного формата по массиву битов пикселов, называется кодером (encoder). В Windows Runtime он представлен классом BitmapEncoder. Работа с кодером несколько отличается от работы с декодером. Декодер может определить, какой тип файла он должен загрузить, но кодер не может прочитать ваши мысли и определить формат файла для сохранения — эта информация должна быть передана явно. Класс BitmapEncoder поддерживает те же форматы, что и BitmapDecoder, кроме файлов значков Windows.

Иногда пара кодер-декодер объединяется общим названием кодек.

Семь форматов из таблицы идентифицируются глобально-уникальными идентификаторами (объекты типа Guid), определяемыми как статические свойства классов BitmapEncoder и BitmapDecoder. Впрочем, вам не придется жестко кодировать эти идентификаторы в своих программах.

Программа ImageFileIO демонстрирует, как загрузить в приложении растровый файл при помощи классов FileOpenPicker и BitmapDecoder и как выбрать формат файла и сохранить графический файл при помощи файлов FileSavePicker и BitmapEncoder. В строке приложения размещается пара кнопок для поворота изображения на 90°. Так как программа использует стандартные средства выбора файлов для получения объектов StorageFile, она не требует специальных разрешений для обращения к файлам пользователя.

Файл XAML определяет элемент Image, TextBlock для вывода информации о загруженных изображениях и AppBar:

<Page ...>

    <Grid Background="#999">
        <Grid.RowDefinitions>
            <RowDefinition Height="Auto" />
            <RowDefinition />
        </Grid.RowDefinitions>

        <TextBlock Name="txb" HorizontalAlignment="Center" FontSize="18" />

        <Image Name="image" Grid.Row="1" />
    </Grid>

    <Page.BottomAppBar>
        <AppBar IsOpen="True">
            <Grid>
                <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                    
                    <AppBarButton Name="rotateLeftButton"
                            IsEnabled="False"
                            Label="Повернуть влево"
                            Icon="Rotate"
                            Click="rotateLeftButton_Click" />

                    <AppBarButton Name="rotateRightButton"
                            IsEnabled="False"
                            Label="Повернуть вправо"
                            Icon="Rotate"
                            Click="rotateRightButton_Click" />
                </StackPanel>

                <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">

                    <AppBarButton Name="openFile" Icon="OpenFile" Click="openFile_Click" Label="Открыть файл" />

                    <AppBarButton Name="saveAsButton"
                            IsEnabled="False"
                            Icon="SaveLocal"
                            Label="Сохранить"
                            Click="saveAsButton_Click" />
                </StackPanel>
            </Grid>
        </AppBar>
    </Page.BottomAppBar>
</Page>

Обратите внимание: свойство IsOpen объекта AppBar инициализируется значением true. Программа не может ничего делать до тех пор, пока файл не будет загружен; остальные кнопки AppBar заблокированы.

Чтобы программа была относительно простой, она ограничивается минимумом хранимой информации. Любое растровое изображение, загружаемое программой с диска, содержится только в свойстве Source элемента Image. Поля, определяемые в файле фонового кода, предназначены для хранения информации о разрешении изображения, не критичной для работы приложения:

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices.WindowsRuntime;
using Windows.Graphics.Imaging;
using Windows.Storage;
using Windows.Storage.Pickers;
using Windows.Storage.Streams;
using Windows.UI.Popups;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media.Imaging;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        double dpiX, dpiY;

        public MainPage()
        {
            this.InitializeComponent();
        }

        private async void openFile_Click(object sender, RoutedEventArgs e)
        {
            // ...
        }

        private async void saveAsButton_Click(object sender, RoutedEventArgs e)
        {
            // ...
        }

        private async void rotateRightButton_Click(object sender, RoutedEventArgs e)
        {
            // ...
        }

        private void rotateLeftButton_Click(object sender, RoutedEventArgs e)
        {
            // ...
        } 

        private async void Rotate(Func<BitmapSource, int, int, int> calculateSourceIndex)
        {
            // ...
        }
    }
}

Когда пользователь нажимает кнопку Open в AppBar, программа создает FileOpenPicker, чтобы отобразить файлы из папки Pictures:

private async void openFile_Click(object sender, RoutedEventArgs e)
{
    // Создание объекта FileOpenPicker
    FileOpenPicker picker = new FileOpenPicker();
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

    // Инициализируется расширениями файлов
    IReadOnlyList<BitmapCodecInformation> codecInfos =
            BitmapDecoder.GetDecoderInformationEnumerator();

    foreach (BitmapCodecInformation codecInfo in codecInfos)
        foreach (string extension in codecInfo.FileExtensions)
            picker.FileTypeFilter.Add(extension);

    // Получение выбранного файла
    StorageFile storageFile = await picker.PickSingleFileAsync();

    if (storageFile == null)
        return;

    // ...
}

Статический метод BitmapDecoder.GetDecoderInformationEnumerator чрезвычайно полезен: он возвращает коллекцию из семи объектов BitmapCodecInformation, соответствующих семи форматам файлов из таблицы, приведенной несколько страниц назад. Каждый объект содержит коллекцию типов MIME и коллекцию расширений (кстати, именно так была получена информация, приведенная в таблице). Расширения могут передаваться прямо в объект FileOpenPicker, который выводит список всех файлов с указанными расширениями.

Если вызов PickSingleFileAsync возвращает объект StorageFile, отличный от null, следующим шагом становится создание объекта BitmapDecoder для этого файла:

private async void openFile_Click(object sender, RoutedEventArgs e)
{
    // ...
    
    // Открытие потока и создание декодера
    BitmapDecoder decoder = null;

    using (IRandomAccessStreamWithContentType stream = await storageFile.OpenReadAsync())
    {
        string exception = null;

        try
        {
            decoder = await BitmapDecoder.CreateAsync(stream);
        }
        catch (Exception exc)
        {
            exception = exc.Message;
        }

        if (exception != null)
        {
            MessageDialog msgdlg =
                new MessageDialog("Данный файл изображения не может быть загружен. Ошибка: " + exception);
            await msgdlg.ShowAsync();
            return;
        }

        // ...
    }

    // ...
}

Метод BitmapDecoder.CreateAsync выдает исключение при получении файла, не являющегося файлом изображения (или чего-то еще, что он не может обработать).

Как вам, вероятно, известно, файл GIF может содержать несколько изображений, воспроизводимых последовательно в виде простейшей анимации. Эти отдельные изображения называются кадрами (frames) и поддерживаются средой Windows Runtime. После создания объекта BitmapDecoder следующим шагом обычно становится извлечение кадров. Но если вы не хотите возиться с многокадровыми файлами GIF (и я вас за это не осуждаю!), просто извлеките первый кадр и считайте, что дело сделано. Именно так я поступаю в следующем фрагменте кода:

private async void openFile_Click(object sender, RoutedEventArgs e)
{
      // ...

        // Получение первого кадра
        BitmapFrame bitmapFrame = await decoder.GetFrameAsync(0);

        // Заполнение информационного заголовка
        txb.Text = String.Format("{0}: {1} x {2} {3} {4} x {5} DPI",
                            storageFile.Name,
                            bitmapFrame.PixelWidth, bitmapFrame.PixelHeight,
                            bitmapFrame.BitmapPixelFormat,
                            bitmapFrame.DpiX, bitmapFrame.DpiY);
        // Сохранить разрешение
        dpiX = bitmapFrame.DpiX;
        dpiY = bitmapFrame.DpiY;

        // Получение пикселов
        PixelDataProvider dataProvider =
            await bitmapFrame.GetPixelDataAsync(BitmapPixelFormat.Bgra8,
                                BitmapAlphaMode.Premultiplied,
                                new BitmapTransform(),
                                ExifOrientationMode.RespectExifOrientation,
                                ColorManagementMode.ColorManageToSRgb);

        byte[] pixels = dataProvider.DetachPixelData();

       // ...
}

Метод выводит информацию о первом кадре в TextBlock в верхней части страницы и сохраняет данные разрешения в полях.

Свойства BitmapPixelFormat и BitmapAlphaMode объекта BitmapFrame содержат важную информацию о формате пикселов. BitmapPixelFormat — перечисление с элементами Rgba16 (16-разрядные значения красного, зеленого, синего и альфа-канала), Rgba8 (8-разрядные значения красного, зеленого, синего и альфа-канала) или Bgra8 (8-разрядные значения синего, зеленого, красного и альфа-канала); последний элемент совместим с форматом, связанным с данными WriteableBitmap. Данные пикселов из файла всегда преобразуются в один из этих форматов. Свойство BitmapAlphaMode может принимать значения Ignore, Straight или Premultiplied.

Массив пикселов в формате кадра можно получить простым вызовом GetPixelDataAsync без аргументов. Но если вы хотите использовать данные растрового изображения для создания WriteableBitmap, лучше вызвать приведенную, более длинную версию GetPixelDataAsync для задания формата, совместимого с WriteableBitmap.

После того как GetPixelDataAsync получит массив байтов в формате, поддерживаемом WriteableBitmap, код создания и вывода растрового изображения аналогичен приводившемуся ранее:

private async void openFile_Click(object sender, RoutedEventArgs e)
{
     // ...
     
     // Создание объекта WriteableBitmap и заполнение пикселов
        WriteableBitmap bitmap = new WriteableBitmap((int)bitmapFrame.PixelWidth,
                     (int)bitmapFrame.PixelHeight);

        using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
        {
            await pixelStream.WriteAsync(pixels, 0, pixels.Length);
        }

        // Перерисовка WriteableBitmap и назначение объекта
        bitmap.Invalidate();
        image.Source = bitmap;
    }

    // Снятие блокировки с кнопок
    saveAsButton.IsEnabled = true;
    rotateLeftButton.IsEnabled = true;
    rotateRightButton.IsEnabled = true;
}

Вот и все, что касается кнопки Open. В двух словах, FileOpenPicker возвращает объект StorageFile, который открывается программой, а поток передается BitmapDecoder.CreateAsync. Объект BitmapDecoder предоставляет доступ к изображениям через объекты BitmapFrame, а метод GetPixelDataAsync получает массив байтов, который может использоваться для создания WriteableBitmap. Вот как выглядит программа:

Открытие графического файла в приложении Windows Runtime

Кнопка "Сохранить как" в строке приложения выполняет метод saveAsButton_Click, который начинается с создания объекта FileSavePicker. Метод BitmapEncoder.GetEncoderInformationEnumerator предоставляет информацию о форматах, поддерживаемых классом BitmapEncoder, но эта информация несколько отличается от той, которая используется FileOpenPicker.

Классу FileSavePicker должен передаваться список типов файлов с одним или несколькими расширениями для каждого типа. К сожалению, свойство FriendlyName объекта BitmapCodecInformation должно содержать строку вида «JPEG Encoder», поэтому я использую метод Split класса String для извлечения первого слова (например, «JPEG») и объединяю его с накопленными расширениями. Затем код строит словарь поддерживаемых типов MIME и объектов Guid, связанных с этими типами:

private async void saveAsButton_Click(object sender, RoutedEventArgs e)
{
    FileSavePicker picker = new FileSavePicker();
    picker.SuggestedStartLocation = PickerLocationId.PicturesLibrary;

    // Получение информации о кодере
    Dictionary<string, Guid> imageTypes = new Dictionary<string, Guid>();
    IReadOnlyList<BitmapCodecInformation> codecInfos =
            BitmapEncoder.GetEncoderInformationEnumerator();

    foreach (BitmapCodecInformation codecInfo in codecInfos)
    {
        List<string> extensions = new List<string>();

        foreach (string extension in codecInfo.FileExtensions)
            extensions.Add(extension);

        string filetype = codecInfo.FriendlyName.Split(' ')[0];
        picker.FileTypeChoices.Add(filetype, extensions);

        foreach (string mimeType in codecInfo.MimeTypes)
            imageTypes.Add(mimeType, codecInfo.CodecId);
    }

    // Получение выбранного объекта StorageFile
    StorageFile storageFile = await picker.PickSaveFileAsync();

    if (storageFile == null)
        return;

      // ...
}

Когда объект FileSavePicker отображается на экране, пользователь может выбрать нужный тип файлов во всплывающем окне:

Сохранение файлов в приложении Windows Runtime

Объект StorageFile, возвращаемый FileSavePicker, содержит поле ContentType. В этом поле хранится строка типа MIME, которая идентифицирует тип файла, выбранный пользователем во всплывающем окне. Программа использует ее со своим словарем для получения объекта Guid, ассоциированного с выбранным типом:

private async void saveAsButton_Click(object sender, RoutedEventArgs e)
{
    // ...
      
    // Открытие StorageFile
    using (IRandomAccessStream fileStream =
            await storageFile.OpenAsync(FileAccessMode.ReadWrite))
    {
        // Создать кодер изображения
        Guid codecId = imageTypes[storageFile.ContentType];
        BitmapEncoder encoder = await BitmapEncoder.CreateAsync(codecId, fileStream);

        // Получение пикселов из существующего объекта WriteableBitmap
        WriteableBitmap bitmap = image.Source as WriteableBitmap;
        byte[] pixels = new byte[4 * bitmap.PixelWidth * bitmap.PixelHeight];

        using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
        {
            await pixelStream.ReadAsync(pixels, 0, pixels.Length);
        }

        // Запись пикселов в первый кадр
        encoder.SetPixelData(BitmapPixelFormat.Bgra8, BitmapAlphaMode.Premultiplied,
             (uint)bitmap.PixelWidth, (uint)bitmap.PixelHeight,
             dpiX, dpiY, pixels);

        await encoder.FlushAsync();
    }
}

По заданному значению Guid статический метод BitmapEncoder.CreateAsync возвращает объект BitmapEncoder. Объект содержит метод SetPixelData, который может использоваться для передачи массива байтов в первый кадр нового файла. Вот и все, что относится к реализации "Сохранить как".

Оставшаяся часть кода обеспечивает поворот изображения на 90°. Класс BitmapEncoder предоставляет такую возможность — он определяет свойство Transform, при помощи которого при сохранении можно выполнять с изображением операции масштабирования, отражения, усечения и поворота. Но если вы хотите увидеть преобразованное изображение, логику придется реализовать самостоятельно. В повороте изображения на 90° задействованы три метода:

// ...

private async void rotateRightButton_Click(object sender, RoutedEventArgs e)
{
    Rotate((BitmapSource bitmap, int x, int y) =>
    {
        return 4 * (bitmap.PixelWidth * (bitmap.PixelHeight - x - 1) + y);
    });
}

private void rotateLeftButton_Click(object sender, RoutedEventArgs e)
{
    Rotate((BitmapSource bitmap, int x, int y) =>
    {
        return 4 * (bitmap.PixelWidth * x + (bitmap.PixelWidth - y - 1));
    });
}

private async void Rotate(Func<BitmapSource, int, int, int> calculateSourceIndex)
{
    // Получение пикселов исходного изображения
    WriteableBitmap srcBitmap = image.Source as WriteableBitmap;
    byte[] srcPixels = new byte[4 * srcBitmap.PixelWidth * srcBitmap.PixelHeight];

    using (Stream pixelStream = srcBitmap.PixelBuffer.AsStream())
    {
        await pixelStream.ReadAsync(srcPixels, 0, srcPixels.Length);
    }

    // Создание целевого изображения и массива пикселов
    WriteableBitmap dstBitmap =
            new WriteableBitmap(srcBitmap.PixelHeight, srcBitmap.PixelWidth);
            
    byte[] dstPixels = new byte[4 * dstBitmap.PixelWidth * dstBitmap.PixelHeight];

    // Перемещение пикселов
    int dstIndex = 0;
    for (int y = 0; y < dstBitmap.PixelHeight; y++)
        for (int x = 0; x < dstBitmap.PixelWidth; x++)
        {
            int srcIndex = calculateSourceIndex(srcBitmap, x, y);

            for (int i = 0; i < 4; i++)
                dstPixels[dstIndex++] = srcPixels[srcIndex++];
        }

    // Перенос пикселов в целевое изображение
    using (Stream pixelStream = dstBitmap.PixelBuffer.AsStream())
    {
        await pixelStream.WriteAsync(dstPixels, 0, dstPixels.Length);
    }
    dstBitmap.Invalidate();

    // Меняем местами разрешения
    double dpi = dpiX;
    dpiX = dpiY;
    dpiY = dpi;

    // Вывод нового изображения
    image.Source = dstBitmap;
}

// ...

Основная работа обеих операций выполняется методом Rotate, не считая того, что у метода имеется аргумент — функция для вычисления индекса источника по координатам x и y целевого изображения. Опробовав эту функцию на большом файле, вы увидите, что поворот занимает пару секунд; следовательно, такие операции не должны выполняться в потоке пользовательского интерфейса.

Поворот следует выполнять асинхронно, с перемещением блока вложенных циклов for в Task.Run и ожидании возврата управления. Однако асинхронный код не может обращаться к объекту WriteableBitmap напрямую. Вы должны получить ширину и высоту растрового изображения до выполнения асинхронного кода и переопределить calculateSourceIndex с передачей ширины и высоты изображения (вместо самого изображения). Также на это время стоит заблокировать кнопки строки приложения, чтобы предотвратить возможное вмешательство в выполнение операции до ее завершения.

Пройди тесты
Лучший чат для C# программистов