C#でMemoryCacheクラスでWPFのBitmapSourceをキャッシュしてみる。2

C# コンピュータ
C#

以前の記事で試したプログラムを合わせてZIPファイルをキャッシュへ先読みさせてみます。

using System.Diagnostics;
using System.Runtime.Caching;
using System.IO;
using System.IO.Compression;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Collections.Concurrent;

namespace ImgCacheManager01;

class ImgCacheManager : IDisposable
{
    MemoryCache _cache = MemoryCache.Default;
    CacheItemPolicy _policy = new ()
    {
        SlidingExpiration = new TimeSpan(0, 5, 0),  // 有効期限を5分にセット
        RemovedCallback = arg =>
        {
            Debug.Print("key:{0}が削除された。", arg.CacheItem.Key);
        },
    };
    public void Dispose()
    {
        ReadLoopStop();
        _cache.Dispose();        
    }
    static async Task<BitmapSource> LoadImageFormStream(Stream s)
    {
        const int dpi = 96;

        MemoryStream ms = new();
        await s.CopyToAsync(ms);
        ms.Seek(0, SeekOrigin.Begin);

        BitmapSource bs = await Task.Run(()=>
        {
            BitmapSource bs = BitmapFrame.Create(ms);

            int stride = (bs.PixelWidth * bs.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[stride * bs.PixelHeight];
            bs.CopyPixels(pixels, stride, 0);

            bs = BitmapImage.Create(bs.PixelWidth, bs.PixelHeight, dpi, dpi, PixelFormats.Bgra32, null, pixels, stride);
            bs.Freeze();
            return bs;
        });
        
        ms.SetLength(0);
        ms.Close();

        return bs;
    }
    static public bool IsValidExtension(string path)
    {
        string pattern = @"\.(jpeg|jpg|bmp|tif|bmp|png|gif)$";
        return System.Text.RegularExpressions.Regex.IsMatch(path, pattern, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
    }
    public async Task<BitmapSource?> LoadImage(string location, string name)
    {
        string key = Path.Join(location, name);
        BitmapSource? bs = null;

        if (_cache.Contains(key))
        {
            bs = _cache[key] as BitmapSource;
            return bs;
        }

        string ext = System.IO.Path.GetExtension(location).ToUpper();
        
        if (ext == ".ZIP")
        {
            string zipFile = location;
            string entryName = name;
            ZipArchiveEntry? entry = ZipFile.OpenRead(zipFile).GetEntry(entryName);
            if (entry is null) return null;

            using Stream stream = entry.Open();
            bs = await LoadImageFormStream(stream);
        } else {
            string fullPath = key;
            FileStream fs = new (fullPath, FileMode.Open, FileAccess.Read);

            using Stream stream = fs;
            bs = await LoadImageFormStream(stream);
        }

        _cache.Set(key, bs, _policy);
        return bs;
    }




    //bool _loopFlag = true;
    CancellationTokenSource? _cts = new();
    Task? _task = null;
    ConcurrentQueue<string> _request = new();

    //public void ReadLoop()
    public Task ReadLoop(CancellationToken token)
    {
        return Task.Run(async ()=>
        {
            //while (_loopFlag)
            while(!token.IsCancellationRequested)
            {
                try
                {
                    // 処理を書く
                    //Debug.Print("{0}", Environment.TickCount64);
                    if (_request.Count() > 0)
                    {
                        string? request = "";
                        _request.TryDequeue(out request);
                        if (request is not null)
                        {
                            Debug.Print("Request: {0}", request);

                            string location = Path.GetDirectoryName(request) ?? "";
                            string name = Path.GetFileName(request);
                            if (location != "" && name != "")
                            {
                                var _ = await LoadImage(location, name);
                            }
                        }
                    }
                    await Task.Delay(10);
                }
                catch (Exception e)
                {
                    // 例外が発生した場合の処理
                    Debug.Print("{0}", e.Message);
                    throw;
                }
            }
            Debug.Print("Loop End");
        });
    }
    public void ReadLoopStop()
    {
        if (_task is not null)
        {
            _cts?.Cancel();
            _task.Wait();
        }
        Debug.Print("ReadLoopStop");
    }
    public async Task<List<string>> LoadFormZipAsync(string zipFile)
    {
        List<string> list = new();
        if (_cts is null) return list;

        if (_task is null)
        {
            _task = ReadLoop(_cts.Token);
        }
        await Task.Run(()=>
        {
            using var zip = ZipFile.OpenRead(zipFile);
            string location = zipFile;
            foreach(var entry in zip.Entries.Where(x => IsValidExtension(x.FullName)))
            {
                string name = entry.FullName;
                string request = Path.Join(location ,name);
                _request.Enqueue(request);
                list.Add(request);
            }
        });
        return list;
    }
}
using System.Windows;
using System.IO.Compression;
using System.IO;
using System.Windows.Input;

namespace ImgCacheManager01;

/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
    ImgCacheManager? _cache = new();
    List<string> _files = new();
    int _files_index = 0;
    public MainWindow()
    {
        InitializeComponent();


        Loaded += async (s, e) =>
        {
            string zipFile = @"H:\csharp\dotnet8\wpf\01.zip";
            var f = await _cache.LoadFormZipAsync(zipFile);
            _files.AddRange(f);

            await Task.Delay(1000);

            if (!_files.Any()) return;
            string location = Path.GetDirectoryName(_files[_files_index]) ?? ".";
            string name = Path.GetFileName(_files[_files_index]);
            Image1.Source = await _cache.LoadImage(location, name);
        };


        Closed += (s, e) =>
        {
            _cache?.Dispose();
        };
    }
    public async void Image1_MouseDownAsync(object? s, MouseButtonEventArgs e)
    {
        if (_cache is null) return;
        if (!_files.Any()) return;
        _files_index++;
        if (_files_index > (_files.Count-1)) _files_index = 0;

        string location = Path.GetDirectoryName(_files[_files_index]) ?? "";
        string name = Path.GetFileName(_files[_files_index]);
        Image1.Source = await _cache.LoadImage(location, name);
    }
}
<Window x:Class="ImgCacheManager01.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:ImgCacheManager01"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <Image x:Name="Image1"  MouseDown="Image1_MouseDownAsync"/>
    </Grid>
</Window>

以前の記事のプログラムを合体してみました。

LoadImage()でキャッシュがあればキャッシュから無ければ直接ストレージから読んでから画像を返します。
LoadFormZipAsync()でキャッシュさせたい画像ファイルがアーカイブされたzipファイルを渡してキャッシュに先読みさせます。

キャッシュ機能の確認として、zipファイル内の画像が表示されクリックすると次の画像が表示されます。

Loaded内でawait Task.Delay(1000);とウェイトを入れていますが、ウェイトを入れない場合、手元の環境ではキャッシュの先読みが間に合わず、直接画像を読み込むルーチンが動いているようです。

先読み用のルーチンはLoadFormZipAsync()を実行時taskが起動していない場合起動するするようにしてみました。

追記20241007:
本番用のプログラムに組み込んでみたところレスポンスが良くなりました。

良くなったおかげで、今度は画像ファイルによって表示までの時間がばらつくことが気になるようになりました。
キャッシュの段階で表示用の解像度に縮小(又は拡大)処理を挟むことで改善するのではないかと考えています。

あと、zipファイルのエントリーにサブディレクトリが含まれていると動作不良が発生します。
zipファイルのパス+エントリーファイル名の組み合わせて、パスの文字列から親ディレクトリ名とファイル名を取得する関数で、zipファイルとエントリーファイル名を分離出来ると考えていたのですが、エントリーファイル名にサブディレクトリ含まれると破綻することに後で気が付きました。分離は正規表現で.zipを目印に分割すると良さそうです。(パスに.zipが複数ある場合は考えていません)

コメント