2011年12月10日土曜日

wpf : 色んな形式の画像ファイルをPbgra32形式に変換してWriteableBitmapに読み込む

色んな形式の画像ファイルをPbgra32形式に変換してWriteableBitmapに読み込むコードのサンプルです。 まず、BitmapDecoderを使ってそのままの形式で画像ファイルを読み込み、FormatConvertedBitmapでPbgra32形式に変換します。

private WriteableBitmap LoadBitmap(string path)
{
    using (FileStream stream = new FileStream(
        path,
        FileMode.Open,
        FileAccess.Read,
        FileShare.ReadWrite | FileShare.Delete) ) // ロックしない
    {
        BitmapDecoder decoder = BitmapDecoder.Create(
            stream,
            BitmapCreateOptions.IgnoreImageCache, // キャッシュしない
            BitmapCacheOption.Default
        );

        FormatConvertedBitmap bmpSrc = new FormatConvertedBitmap(
            decoder.Frames[0],
            PixelFormats.Pbgra32,
            null,
            0
        );

        return new WriteableBitmap(bmpSrc);
    }
}

デコーダーにUriではなくFileStreamを渡しているのは読み込んだ画像ファイルをロックしないようにするためです。 Uriを渡してしまうとBitmapDecoderは画像ファイルを読み込むときにロックしてしまいます。 作るアプリケーションによってはロックされると不便なことがあるのでこうしてみました。

「こういうのってセキュリティ的にはどうかなぁ?」などとチラッと考えてしまったけど、詳しく掘り下げる気は起きなかったり。 セキュリティに配慮をするなら、ロックした方が良いかもしれませんね。

BitmapDecoder.CreateではBitmapCreateOptions.IgnoreImageCacheを指定してキャッシュを無効にしています。 WriteableBitmapを使うくらいなんだから、作りたいのは画像処理ソフトですよね? 画像処理ソフトを使うユーザーは、いくつかのソフトを使い分ける人が多いです。 「あのソフトで加工してこのソフトで読み直して...」とやっているときにキャッシュがあると不便になりそうなので無効にしてみました。

FormatConvertedBitmapでPbgra32形式に変換されたものをBitmapSourceにしてWriteableBitmapを作れば完成。 「色んなファイルを画像処理にかけたいけど、形式ごとにコード書くのは面倒だなぁ」ってときはコレで。

以下、サンプルコード全体を載せておきます。 MainWindowに画像ファイルをドラッグ&ドロップしたら、その1部を半透明のバッテンに描き変えて表示するコードです。 MainWindowの背景を市松模様にして、スクロールしたりウィンドウサイズ変更したりで透過がわかるようになっています。

まずはMainWindow.xaml。

<Window x:Class="ReadImageTest.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="350" Width="525"
        AllowDrop="True"
        DragOver="OnDragOver"
        Drop="OnDrop"
        >
    <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
        <ScrollViewer.Background>
            <DrawingBrush Viewport="0,0,0.25,0.25" TileMode="Tile">
                <DrawingBrush.Drawing>
                    <DrawingGroup>
                        <GeometryDrawing Brush="White">
                            <GeometryDrawing.Geometry>
                                <RectangleGeometry Rect="0,0,100,100" />
                            </GeometryDrawing.Geometry>
                        </GeometryDrawing>
                        <GeometryDrawing Brush="Black">
                            <GeometryDrawing.Geometry>
                                <GeometryGroup>
                                    <RectangleGeometry Rect="0,0,50,50" />
                                    <RectangleGeometry Rect="50,50,50,50" />
                                </GeometryGroup>
                            </GeometryDrawing.Geometry>
                        </GeometryDrawing>
                    </DrawingGroup>
                </DrawingBrush.Drawing>
            </DrawingBrush>
        </ScrollViewer.Background>
        <Image Name="mainImage" Stretch="None"/>
    </ScrollViewer>
</Window>

そしてMainWindow.xaml.csです。

using System;
using System.IO;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace ReadImageTest
{
    public partial class MainWindow : Window
    {
        private WriteableBitmap _bmp;

        public MainWindow()
        {
            InitializeComponent();
        }

        private WriteableBitmap LoadBitmap(string path)
        {
            using (FileStream stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.Read,
                FileShare.ReadWrite | FileShare.Delete) ) // ロックしない
            {
                BitmapDecoder decoder = BitmapDecoder.Create(
                    stream,
                    BitmapCreateOptions.IgnoreImageCache, // キャッシュしない
                    BitmapCacheOption.Default
                );

                FormatConvertedBitmap bmpSrc = new FormatConvertedBitmap(
                    decoder.Frames[0],
                    PixelFormats.Pbgra32,
                    null,
                    0
                );

                return new WriteableBitmap(bmpSrc);
            }
        }

        // 適当にバッテンを描いてどんな絵を読んでもPixelFormats.Pbgra32になっていることを確認
        private void PaintTest()
        {
            int width = _bmp.PixelWidth;
            int height = _bmp.PixelHeight;
            int lstride = _bmp.BackBufferStride / 4; // (注)WriteableBitmapのアラインメントは8byteのはず
            int size = (width < height ? width : height);
            Int32Rect rect = new Int32Rect(0, 0, size, size);

            _bmp.Lock();
            unsafe
            {
                uint* p = (uint*)_bmp.BackBuffer;

                for (int i = 0; i < size; i++)
                {
                    p[lstride * i + i] = 0x60000000;
                    p[lstride * i + (size - i - 1)] = 0x60000000;
                }
            }

            _bmp.AddDirtyRect(rect);
            _bmp.Unlock();
        }

        private void LoadImage(string path)
        {
            try
            {
                _bmp = LoadBitmap(path);
                mainImage.Source = _bmp;
                PaintTest();
                Title = path;
            }
            catch(Exception exc)
            {
                MessageBox.Show(this, "[ファイル読み込み失敗]\n\n" + exc.Message);
            }
        }

        private void OnDragOver(object sender, DragEventArgs e)
        {
            bool fileDropped = e.Data.GetData(DataFormats.FileDrop) != null;
            if (fileDropped)
            {
                e.Effects = DragDropEffects.Move;
                e.Handled = true;
            }
            else
            {
                e.Effects = DragDropEffects.None;
            }
        }

        private void OnDrop(object sender, DragEventArgs e)
        {
            string[] files = e.Data.GetData(DataFormats.FileDrop) as string[];
            if (files == null)
                return;

            LoadImage(files[0]);
        }
    }
}

プロジェクト名を「Read」ImageTestにしてメソッド名を「Load」Imageにしてしまった。 そういうところがダメなんだよなぁ。

でも面倒で直さなかったり...