こんにちは。開発のY.Mです。

C# の Windows アプリ開発案件で、印刷物のデータを Excel Creator で作成し、それを PDF に変換して印刷する要件があり下記のようなコードを作成しました。
使用している Excel Creator のバージョンは 10.0.5270.2401、PDF 印刷には PdfiumViewer を使用しています。

// Excelファイルを新規作成(ストリーム出力)
using var creator = new AdvanceSoftware.ExcelCreator.Xlsx.XlsxCreator();
using var msBook = new System.IO.MemoryStream();
creator.CreateBook(msBook, 1, AdvanceSoftware.ExcelCreator.xlsxVersion.ver2021);

// 印刷内容を作成する処理
// ・
// ・
// ・

// PDF をメモリストリームへ出力
using var msPdf = new System.IO.MemoryStream();
creator.CloseBook(true, msPdf, false);

// PdfiumViewer を用いて PDF メモリストリームを PDF ドキュメントに変換して印刷
using var pdf = PdfiumViewer.PdfDocument.Load(msPdf);
using var pdfDoc = pdf.CreatePrintDocument();
pdfDoc.Print();

PDFをワークファイルに出力したくなかったので、PDF の出力先に直接 MemoryStream を指定してやればメモリ内だけで処理できると考えました。

しかし、このコードは意図通りに動作しません。
16行目の PDF メモリストリームを PDF ドキュメントに変換する処理で下記の例外が発生するのです。

  System.ObjectDisposedException : 閉じているストリームにアクセスすることはできません。

CloseBook メソッドの実行時に PDF メモリストリームのオブジェクトが破棄されています。
Excel Creator 10.0 の仕様では、PDF 出力先のストリームには FileStream を指定することが前提になっており、ファイル出力後にストリームオブジェクトを破棄してしまうのです。
オブジェクトの破棄し忘れを防ぐ意図においては有り難い仕様なのですが、今回の用途では破棄されてしまうと後続の処理で使用できなくなってしまうので困ります。

PDF の出力先に FileStream を指定して一度ワークファイルに出力してから、MemoryStream にワークファイルを読み込んでやれば対応できますが、ファイルIOが発生するし、ワークファイルの管理をしなければいけなくなるしで気が進みません。

 

何とかメモリ上で処理する方法はないものかと、MemoryStream クラスのコードを掘っていくと見付けました。

MemoryStream の親クラスである Stream クラスには IDisposable インターフェースが付加されています。
オブジェクトが破棄される際に Dispose メソッドが実行されるため、そのタイミングで別の MemoryStream にデータを複製してやれば、オブジェクトが破棄された後もデータを保持できるのではないか?

そこで下記のカスタム MemoryStream クラスを作成しました。

public class RegenerableMemoryStream : System.IO.MemoryStream
{
    protected System.IO.MemoryStream _stream;

    protected bool _streamCopied = false;

    public RegenerableMemoryStream(System.IO.MemoryStream stream)
    {
        _stream = stream;
    }

    protected override void Dispose(bool disposing)
    {
        try
        {
            if (CanRead && !_streamCopied)
            {
                Seek(0, System.IO.SeekOrigin.Begin);
                CopyTo(_stream);
                _streamCopied = true;
            }
        }
        finally
        {
            base.Dispose(disposing);
        }
    }
}

オブジェクトの破棄時(Dispose メソッド実行時)に、コンストラクタの引数で指定した MemoryStream に自身のデータを複製して格納します。
上記カスタム MemoryStream クラスを用いて修正したコードがこちらです。

// Excelファイルを新規作成(ストリーム出力)
using var creator = new AdvanceSoftware.ExcelCreator.Xlsx.XlsxCreator();
using var msBook = new System.IO.MemoryStream();
creator.CreateBook(msBook, 1, AdvanceSoftware.ExcelCreator.xlsxVersion.ver2021);

// 印刷内容を作成する処理
// ・
// ・
// ・

// PDF をメモリストリームへ出力
var msPdf = new System.IO.MemoryStream();
using var rmsPdf = new RegenerableMemoryStream(msPdf);
// ↑ カスタム MemoryStream オブジェクトを生成
creator.CloseBook(true, rmsPdf, false);
// ↑ PDF 出力先をカスタム MemoryStream オブジェクトに変更

// PdfiumViewer を用いて PDF メモリストリームを PDF ドキュメントに変換して印刷
using var pdf = PdfiumViewer.PdfDocument.Load(msPdf);
using var pdfDoc = pdf.CreatePrintDocument();
pdfDoc.Print();

CloseBook メソッド実行時、rmsPdf に出力された PDF データが msPdf に複製され、無事ワークファイルを用いず PDF を MemoryStream へ出力できるようになりました。