XPSドキュメントのサムネイル表示をする

XPSドキュメントのページをサムネイル表示します.
XPSドキュメントのサムネイルおよび注釈とコメント編集は,ドキュメントのサンプル (WPF) | Microsoft Docs以下にドキュメントのシリアル化のサンプル | Microsoft Docsというサンプルがあります.
サンプルのコードでは(XPSドキュメントがA4サイズの場合に)サムネイル1枚あたり5Mバイト程度のメモリを消費しており,実用的ではありませんでした.そこでメモリ消費量の少ないXPSドキュメントとそのサムネイル表示をしてみました.

方法

サンプルのスクリーンショットはこちらです.

コードを抜粋して要点をまとめます.

XPSドキュメントを開く

XPSドキュメントを開き,DocumentPginatorをサムネイルビューに渡します.

DocumentViewer _docViewer;

bool OpenDocument(string fileName)
{
    XpsDocument _xpsDocument  = new XpsDocument(fileName, FileAccess.Read);
    FixedDocumentSequence fds = _xpsDocument.GetFixedDocumentSequence();    
    _docViewer.Document = fds;
    _thumbView.DataContext = fds.DocumentPaginator;
}
Loadイベントでサムネイルを描画する

何千ページもあるドキュメントのサムネイルを逐次作成していては,表示までの処理時間がかかりすぎます.
ListBoxはデフォルトでVirtualizingPanel Class (System.Windows.Controls) | Microsoft Docsを使いますので,ListBoxに保持されたアイテムは表示の必要がなければLoadされません.
そこで,サムネイルビューのDataContextが変更されたときには,ListBoxにページ数分だけ空のBorderを用意します.実際のサムネイルの描画はLoadedイベントで処理します.

void redraw()
{
    this._listBox.Items.Clear();
    for (int index = 0; index < _paginator.PageCount; index++)
        _listBox.Items.Add(createEmptyBorder(index));
}

Border createEmptyBorder(int pageIndex)
{
    Border OuterBorder          = new Border();
    OuterBorder.BorderThickness = new Thickness(2);
    OuterBorder.Margin     = new Thickness(3);
    OuterBorder.Width      = _thumnailSize.Width  + _borderIncrement;
    OuterBorder.Height     = _thumnailSize.Height + _borderIncrement + 30;
    OuterBorder.Background = Brushes.White;
    OuterBorder.Tag        = pageIndex;
    OuterBorder.Loaded += new RoutedEventHandler(OuterBorder_Loaded);

    return OuterBorder;
}
RenderTargetBitmapクラスのかわりにVisualBrushクラスを使う

Loadedイベントでサムネイル描画処理を行います.DocumentPaginatorのメソッド GetPage()で,表示するDocumentPageを取得します.DocumentPageのVisualをVisualBrushを使いRectangleに描画します.

// _paginator はDataContextから設定する. _thumbnailSize にはサムネイル画像の大きさを設定する.
Size _thumnailSize;        
DocumentPaginator _paginator;

// 処理
int pageIndex = 0;
DocumentPage docPage = _paginator.GetPage(pageIndex);

VisualBrush vb = new VisualBrush(docPage.visual);
vb.ViewboxUnits = BrushMappingMode.Absolute;
vb.Viewbox = new Rect(_paginator.PageSize);

Rectangle rect = new Rectangle();
rect.BeginInit();
rect.Width  = _thumnailSize.Width;
rect.Height = _thumnailSize.Height;            
rect.Fill   = vb;
rect.EndInit();

MSDNから参照したサンプルは,サムネイルのレンダリングを下記のコードで処理していました.このコードでは,冒頭のとおり,メモリ消費量がかなりのものでした.

RenderTargetBitmap bitmap = new RenderTargetBitmap((int)_paginator.PageSize.Width, (int)_paginator.PageSize.Height , 96.0, 96.0, PixelFormats.Pbgra32);
bitmap.Render(visual);            

Image img  = new Image();
img.Source = bitmap;
img.Width  = _thumnailSize.Width;

結果,メモリ消費量の大雑把な評価

サムネイルをどんどん表示させていき,タスクマネージャーのページファイルの使用量増加分から,大雑把に消費メモリ量を見積もりました.
XPSドキュメントには,A4サイズ1000ページほどの(図表はほとんどない)文書を用いて,サムネイルのサイズの幅および高さをそれぞれ100および150としました.
オリジナルの方法ではサムネイル20ページほどでページファイル100Mバイトの増加,変更コードではサムネイル100ページで20Mバイトほどの増加でした.
サムネイル1枚あたりページファイルおよそ5Mバイトを消費していたものを,コード変更により,およそ200Kバイトにできました.

考察, おそらくRenderTargetBitmapがオリジナルのビットマップを保持しているためかと

RenderTargetBitmapのメモリ消費量が大きいのは,A4サイズのビットマップ描画そのものを保持しているためかと思います.DocumentPaginatorのSizeプロパティは,今回使用したA4のドキュメントだとWidth 816 Height 1056 なので1ピクセル3バイトとして2.5Mバイト.一方Rectangleに描画した場合だとサムネイルのサイズがWidth 100 Height 150なので同じく1ピクセル3バイトとして45 kバイトのメモリが必要なはずです.
したがって,先ほどのメモリ消費量の値とずいぶん食い違いますが,おそらく,Imageはそれのビットマップを縮小表示しているだけで,オリジナルの大きなビットマップはメモリに置かれたままなのでしょう.VisualBrushを使うと,Rectangleの背景に描画しているため,Rectangleの大きさのビットマップを保持するだけですむので,メモリ消費量が削減できたということかと思います.

サンプルコード

参照コードへのリンクはこちら.http://febhare.bake-neko.net/Src/XPSDocument_ThumnailView_Sample.zip
サムネイル描画の,サンプルオリジナルと変更したコードそれぞれは,ファイル Thumbnail.xaml.cs のメソッド createPageThumb() に記述しており,オリジナルのコードはコメントアウト,変更コードが有効にしてある.