C# で表計算シートを PDF にする — Office もプリンタドライバも使わずに ReoGrid だけで出力する

· unvell team
C# で表計算シートを PDF にする — Office もプリンタドライバも使わずに ReoGrid だけで出力する

「帳票を PDF にしてメールで送るだけ」。チケットには 1 行で書かれていて、開発機ではどの方法でも動きます。ところがサーバーに載せた途端、それぞれ別の理由で止まります。Excel が入っていない、コンテナに印刷スプーラが無い、そして出てきた PDF は誰かが半日かけて整えた列幅を失っている。

厄介なのは、シート自身は紙の上でどう見えるべきかを既に知っているという点です。列幅も、結合も、罫線も、表示形式も、印刷範囲も、ページ設定も持っている。それなのに大半の PDF 生成パイプラインは、それを一度捨ててから組み直します。

ReoGrid V5 は、画面にグリッドを描くのと同じ描画エンジンで PDF を出力します。PDF 出力に必要な機能は ReoGrid に搭載されているため、外部ライブラリに依存せずに PDF の出力を完結できます。この記事では呼び出し方、ページ設定、日本語の扱い、そしてヘッドレス運用で必ず引っかかる 1 点を扱います。


よくある 4 つの方法と、その代償

方法引っかかる点
Office Interop — Excel の ExportAsFixedFormat を自動操作サーバーに Excel が必要で、しかもサーバーサイドでの Office 自動操作は Microsoft が推奨もサポートもしていません。Windows 専用。EXCEL.EXE が残留する不具合はそれ自体でひとつのジャンル
Microsoft Print to PDF — 仮想プリンタに印刷Windows 専用で、しかも中身はプリンタドライバです。印刷スプーラ、ユーザーセッション、書き込み可能なスプールディレクトリを要求します。コンテナにはどれもありません
HTML を描いてヘッドレス Chromium で撮るイメージにブラウザを丸ごと同梱することになり、ページ分割を CSS で再実装する羽目になります。@media print の改ページは Excel の改ページとは別物です
PDF ライブラリ(iText / QuestPDF / PdfSharp)どれも優れたライブラリですが、描くのは自分で書いたレイアウトであってシートのレイアウトではありません。列幅も結合も罫線もコード側で書き直すことになり、ブックが編集された瞬間にズレ始めます

どれも「ある問題」に対しては正解です。ただし「このワークシートを、設定されているとおりに、サーバーで描いてほしい」に対する答えはひとつもありません。


V5 のやり方

V5 では、WinForms・WPF・Avalonia の画面表示と PDF 出力が同じ描画エンジンを共有しています。PDF は、画面と並ぶもう 1 つの出力先です。

ここから出てくる帰結は 2 つです。

  1. PDF は「グリッドが描いたもの」そのものです。 条件付き書式、リッチテキスト、結合セル、点線・二重罫線、表示形式 — 画面と同じ描画処理で描くので、見たとおりに PDF に出ます。
  2. 必要な機能をすべて内蔵しています。 PDF 出力に必要な機能は ReoGrid に搭載されているため、外部ライブラリに依存せずに PDF の出力を完結できます。Windows と同じように Linux / macOS でも動作します。

改ページは印刷と同じ処理で計算し、行と列を単位のまま印刷可能領域に詰めます。Excel と同じく、各行は 1 ページの中に収めて出力します。

サーバー/バッチ用途には、UI を含まないパッケージ unvell.ReoGrid.One.Core(.NET 10)を使います。

dotnet add package unvell.ReoGrid.One.Core

WinForms / WPF / Avalonia 版のパッケージは、数式・XLSX / PDF 入出力を含むすべての機能を内蔵しています。デスクトップアプリでは、各プラットフォームのパッケージ 1 つで PDF まで出力できます。


呼び出し

using unvell.ReoGrid.Core;
using unvell.ReoGrid.IO.Pdf;

PdfExporter.Export(wb, "book.pdf");              // ブックの全シート
PdfExporter.Export(ws, "sheet.pdf");             // 1 シートだけ

byte[] bytes = PdfExporter.ExportToBytes(wb);    // ファイルを経由しない

Web API で使うのは ExportToBytes のほうです。ファイルを経由せずに、PDF をメモリ上のバイト列として受け取れます。

サーバー側の処理は結局 3 ステップになります。

using unvell.ReoGrid.Core;
using unvell.ReoGrid.IO.Excel;
using unvell.ReoGrid.IO.Pdf;

var workbook = XlsxReader.Read("input.xlsx");

// XLSX には Excel が保存した計算結果が入っており、読み込み時はそれをそのまま使います。
// 値を変更したときだけ Recalculate を呼びます。
var sheet = workbook[0];
sheet.SetNumber(1, 1, 500);
sheet.Recalculate();

PdfExporter.Export(workbook, "output.pdf");

出力は PDF 1.7 です。空のブックも、どの PDF リーダーでも開ける 1 ページの PDF として出力します。


ページ設定

どのオーバーロードも PrintSettings を省略できます。省略した場合、各シートはそのシート自身の ws.PrintSettings にフォールバックします。これは XLSX 読み込み時にファイルから復元されたものです。集計シートが A4 縦、データシートが A3 横というブックも、コードを書かずに作られたとおりに出力できます。

using unvell.ReoGrid.Core.Printing;

var settings = new PrintSettings
{
    Paper = PaperKind.A4,
    Orientation = PageOrientation.Landscape,
    ShowGridLines = false,
    FitToPagesWide = 1,
};

PdfExporter.Export(ws, "sheet.pdf", settings);

主な設定項目です。

プロパティ既定値補足
PaperA4A5〜A3、JIS B5 / B4、Letter、Legal、Tabloid、Executive、Custom
OrientationPortrait横向きは用紙の縦横を入れ替えます
MarginLeft / Right / Top / Bottom54単位は ポイントです。72 pt = 1 インチなので、54 は Excel の「標準」0.75 インチ
PrintAreanullRangePosition.Parse("A1:H60")。null なら使用範囲全体
Scale1.00.1〜4.0(Excel の 10%〜400%)。フィット指定があると無視されます
FitToPagesWide / Tallnull実務で多いのは Wide = 1, Tall = null — 横にはみ出さず、縦は必要なだけ
CenterHorizontally / Verticallyfalse印刷可能領域の中央に寄せます
OrderDownThenOver縦横ともに複数ページになるときのページ番号順(Excel の既定)
ShowGridLinestrue
ShowHeadersfalse行番号・列見出し。Excel と同じく既定はオフ
ShowImagestrueオフにすると文字だけの出力になります

余白がポイント単位なのは間違えやすいので明示しておきます。MarginLeft = 54 は 0.75 インチ、20 mm にしたいなら 20 * 72 / 25.4 です。

ページ数だけ知りたい場合 — 進捗表示のため、あるいは 400 ページの要求を描画前に弾くため — 描画せずにページ分割だけ行えます。

PrintLayout layout = Paginator.Paginate(ws, ws.PrintSettings);
Console.WriteLine($"{layout.Pages.Count} ページ");

見出し行の繰り返しとページ番号

200 行の表が 6 ページに渡るなら、見出し行は 6 ページ全部に必要です。Excel の「印刷タイトル」で、プロパティ 2 つです。

// 1〜2 行目を各ページの上端に、A 列を各ページの左端に繰り返す
ws.PrintSettings.RepeatRows = new LineSpan(0, 1);
ws.PrintSettings.RepeatColumns = new LineSpan(0, 0);

繰り返し部分は本文とは別の固定帯として描くため、見出し行を重複させずに各ページの先頭へ配置できます。

ヘッダー/フッターは Excel の & コードをそのまま使います。Excel で作られたブックがそのまま動きます。

var hf = ws.PrintSettings.HeaderFooter;
hf.Header.Center = "&A";                 // シート名
hf.Footer.Right  = "&P / &N";            // 2 / 5
hf.Footer.Left   = "&D &T";              // 日付と時刻
hf.FontSizePt    = 9;

// 1 ページ目だけ別の見出しにする
hf.DifferentFirst = true;
hf.FirstHeader.Center = "四半期レポート";
コード展開結果
&Pページ番号。&P+2 / &P-1 でオフセット可
&N総ページ数
&Aシート名
&F元ファイル名
&Z元フォルダのパス
&D / &T日付 / 時刻(現在のカルチャ)
&&& 自身

&F と &Z は出力先パスではなく、第 3 引数から取ります。

PdfExporter.Export(wb, "out.pdf", settings, documentName: "/srv/reports/Q3.xlsx");

省略した場合、どちらも空欄として出力します。

装飾コード(&B 太字、&I 斜体、&K 色、&"フォント名")は PDF には反映せず、ヘッダー/フッターは 1 つのスタイルで描きます。コード自体は設定に残るので、XLSX に保存し直しても元の指定を保てます。


日本語

サーバーサイドの PDF 生成が破綻しやすいのがここです。コンテナにフォントが入っていないので日本語が豆腐になる、あるいは出力自体が落ちる。

日本語フォント(IPAexゴシック)を内蔵しています。 サーバーにフォントをインストールせずに、日本語を含むシートをそのまま PDF に出力できます。

見た目以上に効いてくる点が 3 つあります。

  • 文字は文字のまま PDF に入ります。 出力した PDF は検索もコピーもでき、「商品名」は PDF から「商品名」としてそのまま取り出せます。
  • フォントは必要なときだけ埋め込みます。 日本語フォントを埋め込むのは日本語を含むシートだけで、欧文だけのシートは PDF 標準の Helvetica で描くため、ファイルを小さく保てます。日本語を含む場合は、フォントの分としてファイルサイズが約 4 MB 増えると見ておいてください。
  • 折り返しは禁則処理に従います。 句読点が行頭に来る位置では 1 文字前で改行するため、。 や 、 は前の行の末尾に収まります。

見込んでおくべき点が 2 つあります。

  • 日本語フォントは書体全体を埋め込みます。 帳票 1 通なら十分なサイズですが、小さな PDF を 1 万通生成すると同じフォントが合計 40 GB 分になるので、結合や後処理を検討してください
  • 日本語の太字・斜体は通常のウェイトで描きます。 欧文は Helvetica の 4 書体(標準・太字・斜体・太字斜体)で描き分けます

画像

セルに配置した画像は PDF にそのまま埋め込みます。PNG(透過を含む)と JPEG に対応し、JPEG は元の画質を保ったまま埋め込みます。

繰り返しヘッダーに置いたロゴのように 12 ページに登場する画像も、PDF には1 回だけ埋め込んで各ページから参照するため、ファイルサイズを抑えられます。読み込めない画像はスキップして出力を続けるので、ロゴが 1 つ壊れていても夜間バッチは最後まで完了します。


ASP.NET Core のエンドポイントに載せる

ExportToBytes を使えば、ハンドラはこれだけで書けます。

using unvell.ReoGrid.Core;
using unvell.ReoGrid.Core.License;
using unvell.ReoGrid.IO.Excel;
using unvell.ReoGrid.IO.Pdf;

var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();

// 起動時に 1 回だけ — 出力処理が走る前に。
ReoGridLicense.SetLicense(builder.Configuration["ReoGrid:License"]!);

app.MapPost("/reports/pdf", async (IFormFile xlsx) =>
{
    await using var stream = xlsx.OpenReadStream();
    var workbook = XlsxReader.Read(stream);

    byte[] pdf = PdfExporter.ExportToBytes(workbook, documentName: xlsx.FileName);

    return Results.File(pdf, "application/pdf",
        Path.ChangeExtension(xlsx.FileName, ".pdf"));
}).DisableAntiforgery();

app.Run();

Docker イメージも、mcr.microsoft.com/dotnet/aspnet:10.0 にアプリを載せるだけで動作します。フォントパッケージやブラウザ、印刷スプーラを追加せずに、そのまま PDF を出力できます。

入力が非常に大きい場合は、XlsxReader.OpenVirtual("large.xlsx", SheetLoadMode.OnDemand) でブック全体を読み込まずにシート単位で遅延読み込みできます。


押さえておくべき挙動 — ライセンス未適用時は例外になる

ここが引っかかりどころで、しかも意図的な設計です。

有効なライセンスキーが無い場合、PDF 出力は透かし入りの PDF を作らずに ReoGridLicenseException を投げます。

using unvell.ReoGrid.Core.License;

// ソース中の文字列リテラルではなく、設定ファイルか環境変数から読みます。
ReoGridLicense.SetLicense(Environment.GetEnvironmentVariable("REOGRID_LICENSE")!);

理由は書いておく価値があります。画面上のグリッドは逆の挙動をするからです。未ライセンスのコントロールは読み取り専用のまま使え、透かしが表示されます。画面を見ている人間は、何がおかしいかを見て対処できるためです。ヘッドレスのサービスにはその経路がありません。ファイル I/O が静かに劣化したら、透かしを最初に読むのは請求書を受け取った顧客になります。だから ファイル I/O のほうは明示的に失敗します — XLSX / CSV / PDF / reogrid-json のすべてが、出力せずに拒否します。

評価版キーの期限切れも同じです。期限を過ぎた日から、稼働中のサービスは出力時に例外を投げ始めます。月末に 500 で気付くのではなく、起動時に ReoGridLicense.IsLicensed を見るヘルスチェックを入れておくのが安全です。

if (!ReoGridLicense.IsLicensed)
    throw new InvalidOperationException("ReoGrid のライセンスが未適用または期限切れです — PDF 出力は失敗します。");

キーは 1 つで全バージョン・全プラットフォームをカバーし、買い切りです。アップデート期間が切れても購入済みのキーは動き続け、V4 用に発行されたキーはそのまま V5 を有効化します。


V4 の場合

ReoGrid V4 は 4.6 から、WinForms のアプリで PDF を直接出力できます。ページは印刷と同じに分けます。

using unvell.ReoGrid.PDF;

PDFExporter.Export(sheet, "report.pdf");

デスクトップアプリの中で動作する機能です(PDF としてエクスポート)。4.6 より前の V4 では、印刷機能の出力先を仮想プリンタに向けて PDF を作成します(C# で表計算シートを印刷するで扱っています)。サーバーやコンテナで PDF を出力するなら、UI なしで動作する V5 をお使いください。


まとめ

  • PDF 出力に必要な機能は ReoGrid に搭載されているため、Office・プリンタドライバ・ヘッドレスブラウザ・PDF ライブラリを使わずに、サーバーでワークシートを PDF にできます。サーバー用途には unvell.ReoGrid.One.Core(.NET 10)を使います
  • PDF は画面表示と同じ描画エンジンで出力するので、結合・罫線・条件付き書式・表示形式といったシート自身のレイアウトがそのまま乗ります
  • PdfExporter.Export(...) はファイルに書き出し、API エンドポイントでは ExportToBytes(...) でバイト列を受け取ります
  • PrintSettings を省略すれば各シートは XLSX から復元された自前のページ設定を使います。余白の単位はポイントです
  • RepeatRows で見出し行を各ページに 1 回ずつ繰り返せます。&P / &N / &A は Excel と同じに動きます
  • 日本語フォントを内蔵しています。日本語は検索・コピーできる文字のまま出力され、禁則処理も効きます。フォントは日本語を含む PDF にだけ埋め込みます
  • ライセンス未適用時は、透かし入りの PDF を出力せずに例外で知らせます。 起動時にキーを設定し、IsLicensed を確認してください

V5 の新機能を見る / 30 日間の評価版を試す


関連記事・ドキュメント

ご自身のプロジェクトで ReoGrid を試す

.NET WinForms / WPF 向けの Excel 互換スプレッドシートコンポーネント。30 日間の無償トライアルをご利用いただけます。

ニュースレター

最新リリースをメールでお届け

ReoGrid の新バージョン・新機能・技術記事のお知らせをお送りします。配信停止はいつでも可能です。

関連する記事

C# でスプレッドシートを印刷する — WinForms / WPF の印刷・プレビュー・PDF出力

.NET デスクトップアプリからワークシートを印刷する方法 — 印刷プレビュー、システム印刷ダイアログ、PDF への直接出力、さらに用紙サイズ・余白・向き・スケーリング・改ページまで。WinForms / WPF で動作する C# コード。

Excel の表示形式コードを読み解く —

#,##0;[Red]-#,##0;"-" は記号の羅列に見えますが、4 つのセクション・桁の記号・色・条件・文字列でできた小さな言語です。読めるようになれば、必要なコードを自分で書けます。この記事では Excel のユーザー定義の表示形式を、千円単位・百万円単位、値を隠す ;;;、単位の付け方、勤務時間の [h]:mm、「33 が 3 つで合計 100」になる理由まで整理し、ReoGrid V5 の C# コードで同じコードを使います。Excel からコピーしたコードがそのまま動き、XLSX に保存しても保たれます。

Excel の「テーブル」を C# で作る — 「テーブルとして書式設定」が実際に保存しているもの

Excel の「テーブルとして書式設定」は、見出しに色が付き 1 行おきに縞が入るので書式に見えます。ところが保存したファイルを開いてみると、塗りを持つセルは 1 つもありません。Excel が保存しているのは「範囲・見出し行・スタイル名」を持つオブジェクトです。xlsx の中のテーブルの正体、組込 60 種のスタイルの中身、そして Excel がファイルを開けるかどうかを分ける 3 つの決まりを整理し、ReoGrid V5.1 の C# コードでフィルター▽と集計行付きのテーブルを作ります。