はじめに
業務システムでで帳票を扱う場合、ExcelをテンプレートにしてPDFを出力できれば十分というケースも多いかと思います。
そういう用途に使える商用のコンポーネントもありますが、今はそのくらいの内容であればAIに実装してもらえるだろうということで、実際に作ってみたのが今回記事にする内容です。
作ったもの
概要
使用イメージ
テンプレートとして用意するExcelと出力するPDFの例はこんな感じになります。
| Excel | ||
|---|---|---|
![]() |
→ | ![]() |
ライブラリの機能
| カテゴリ | 対応内容 |
|---|---|
| フォント | サイズ、太字/斜体/太字斜体、カラー |
| 塗りつぶし | 背景色 |
| 罫線 | 罫線の太さ、カスタムカラー |
| テキスト配置 | 水平、垂直 |
| セル結合 | 水平、垂直 |
| 画像 | セルアンカー型、フローティング型 |
| ページ設定 | 用紙サイズ、余白 |
| ヘッダー/フッター | ヘッダー・フッターテキスト |
| マルチシート | シート毎の出力 |
| 印刷領域 | 定義済み印刷領域 |
| フォント埋め込み | カスタムフォントリゾルバー |
使用ライブラリ
| ライブラリ | 用途 |
|---|---|
| ClosedXML | Excelファイルの読み込み |
| PDFsharp | PDFの生成 |
| SkiaSharp | フォントメトリクスの計算 |
ライブラリでできること
プレースホルダー
Excelファイルのセルに{{マーカー名}}という書式でプレースホルダーを記述します。
ReplacePlaceholder()を呼ぶことで、プレースホルダーを実際の値に置き換えます。
sheet.ReplacePlaceholder("Subject", "御請求書");
sheet.ReplacePlaceholder("BillingTo", "株式会社サンプル");
sheet.ReplacePlaceholder("InvoiceDate", "2025-04-11");
sheet.ReplacePlaceholder("InvoiceNo", "INV-2025-001");
sheet.ReplacePlaceholder("DeliveryDate", "2025-03-31");
明細行の展開
請求書の品目明細のような繰り返し行を展開するにはTemplateRowを使います。
FindRow()でマーカーを含む行を見つけ、InsertCopyAfter()で行を複製しながらデータを埋め込んでいきます。
最後にテンプレート行自体をDelete()で削除します。
(int No, string Item, int Qty, int Price)[] items =
[
(1, "CPU", 1, 54_800),
(2, "CPUクーラー", 1, 8_980),
(3, "マザーボード", 1, 24_800),
...
];
var templateRow = sheet.FindRow("No");
var row = templateRow;
foreach (var (no, itemName, qty, price) in items)
{
row = templateRow.InsertCopyAfter(row);
row.ReplacePlaceholder("No", no.ToString());
row.ReplacePlaceholder("Item", itemName);
row.ReplacePlaceholder("Qty", qty.ToString("N0"));
row.ReplacePlaceholder("Price", price.ToString("N0"));
row.ReplacePlaceholder("Amount", (qty * price).ToString("N0"));
}
templateRow.Delete();
フォントの解決
PDFsharpはシステムフォントを参照するため、実行環境によって使えるフォントが異なります。
埋め込みフォントを処理したりWindows環境のインストール済みフォントを解決するために、IReportFontResolverインターフェースを用意しています。
PDFsharpのIFontResolverに似たものです。
public interface IReportFontResolver
{
FontResolveInfo? ResolveTypeface(string familyName, bool bold, bool italic);
ReadOnlyMemory<byte>? GetFont(string faceName) => null;
}
Windows環境のフォントを解決するためには以下のような実装を使用します。
public sealed class WindowsJapaneseFontResolver : IReportFontResolver
{
private static readonly Dictionary<string, string> FontMap =
new(StringComparer.OrdinalIgnoreCase)
{
["MS Pゴシック"] = "MS PGothic",
["MS Pゴシック"] = "MS PGothic",
["MS ゴシック"] = "MS Gothic",
["MS P明朝"] = "MS PMincho",
["MS P明朝"] = "MS PMincho",
["MS 明朝"] = "MS Mincho",
["HGP明朝E"] = "HG明朝E",
["HGPMinchoE"] = "HG明朝E",
["HGS明朝E"] = "HG明朝E",
["HGSMinchoE"] = "HG明朝E"
};
public FontResolveInfo? ResolveTypeface(string familyName, bool bold, bool italic) =>
FontMap.TryGetValue(familyName, out var resolvedName) ? new FontResolveInfo(resolvedName) : null;
}
考え方とパイプライン
このライブラリは以下のようにシンプルな考え方で作成しました。
内部的ななフローは、ClosedXMLでExcelファイルを読み取り、レイアウトを計算して、PDFsharpでPDFファイルを作成するという形にしています。
TemplateWorkbook
↓ (ClosedXMLのラッパー、プレースホルダー操作・行の複製等はここで行なう)
ExcelReader
↓ ReportWorkbook (レンダリング情報を収集して内部モデルを構築)
PdfRenderPlanner
↓ PdfRenderSheetPlan[] (PDF上の描画座標に変換)
PdfGenerator
↓ (PDFsharpで描画)
PDF出力
TemplateWorkbook
ClosedXMLのXLWorkbookの薄いラッパーです。
内部でClosedXMLのワークブックをそのまま保持しており、プレースホルダーの置換や行の複製といったテンプレート操作のAPIを追加しています。
ExcelReader → ReportWorkbook
ExcelReaderはClosedXMLのワークブックからレンダリングに必用な情報を収集するためのです。
以下のような情報を収集していますが、この部分の実装がAIさんにがんばって貰ったポイントその1です。
ReportWorkbook
├── ReportMetadata テンプレート名
└── ReportSheet[]
├── ReportRow[] 行の高さ(pt)
├── ReportColumn[] 列の幅(pt)
├── ReportCell[]
│ ├── DisplayText Excelの書式適用後の表示文字列
│ └── ReportCellStyle
│ ├── ReportFont フォント名・サイズ・太字/斜体/色
│ ├── ReportFill 背景色
│ ├── ReportBorders 4辺の罫線スタイル・色
│ └── ReportAlignment 水平/垂直配置
├── ReportMergedRange[] 結合セルの範囲
├── ReportImage[] 埋め込み画像
├── ReportPageSetup 用紙サイズ・余白・センタリング設定
├── ReportHeaderFooter ヘッダー/フッターテキスト
└── ReportPrintArea? 印刷領域
PdfRenderPlanner → PdfRenderSheetPlan
PdfRenderPlannerはReportWorkbookを受け取り、セルごとのPDF上の描画座標を計算してPdfRenderSheetPlan[]を生成します。
この段階で「行・列インデックスの空間」から「ポイント座標の空間」への変換を行いますが、AIさんにがんばって貰ったポイントその2になります。
PdfRenderSheetPlan
├── PdfRenderPagePlan[]
│ ├── PageBounds ページ全体の矩形(pt)
│ ├── PrintableBounds 余白を除いた印刷可能領域の矩形(pt)
│ ├── PdfHeaderFooterRenderInfo ヘッダー/フッターの描画矩形とテキスト
│ └── PdfCellRenderInfo[]
│ ├── CellAddress セルのアドレス
│ ├── OuterBounds セル外形の矩形(pt)
│ ├── ContentBounds 水平パディングを除いたテキスト描画矩形(pt)
│ ├── TextBounds テキストオーバーフローを考慮したクリップ矩形(pt)
│ └── IsMergedOwner 結合セルのオーナーかどうか
└── PdfImageRenderInfo[] 画像の描画矩形(pt)とバイナリ
PdfRenderPlannerを分離している意味は、情報収集、レイアウト計算、描画の責務を分けることにあります。
セルの位置計算、センタリング処理、テキストオーバーフロー判定といった幾何計算を先に済ませておき、PdfGeneratorは計算済みの座標をもとに描画するだけにしています。
PdfGenerator
PdfGeneratorはPdfRenderSheetPlanをもとにPDFsharpで描画します。
簡単なPDFを作成する場合には自前でこの辺りを記述したりしますが、ここもAIさにぶん投げたポイントその3になります。
- セルの背景色をパスでまとめ描画
- セルのテキスト描画(配置・折り返し・テキスト装飾)
- 罫線描画(隣接セルで共有する辺は優先度の高いスタイルで1度だけ描画)
- 画像描画
- ヘッダー/フッター描画
精度まわりの細かい実装、例えば
- Excelの列幅変換式の再現
- 罫線の重複排除と優先度処理
- 結合セルをまたぐ罫線の外周集約
- フォントメトリクスに基づく描画位置の計算
このあたりはAIと詰めていく形で実装しました。
骨格となるパイプラインの設計と全体の方針を自分で決め、各ステップの精度を上げていく細かい仕様対応をAIに任せるという形で実装をしました。
うさコメ
AIを使えば、このくらいのコンポーネントも簡単に実装できる時代になりました( ˙ω˙)
ところで「SaaSは死んだ」という議論を耳にすることがあります。
SaaSはその運用に対して対価を払うモデルなので、そういう意味での死はないと思っていますが(別のSaaSが既存の市場を奪うことは以前より容易になったというはあるでしょうが)。
一方で、プログラム開発におけるコンポーネントの選定については確実に影響が出ていると思います。
コストや機能の点で既製品に課題がある場合、AIの活用によって「自前で必要な機能だけを持つ代替品を作る」という選択肢が現実的になってきています。
以前は、このような自前実装は開発パワーのある組織だけの特権でしたが、AIがそれを民主化した形です。
今回のライブラリ実装もその一例で、パイプラインの設計と方針を決めたら、精度まわりの細かい実装は全てAIに任せるという進め方で作ることができました。
AIを単なるコーディング助手ではなくソフトウェアの調達構造を変えるゲームチェンジャーとして使うというイメージです。
ただし、どの機能を自前で作るべきで、どの部分を既製品に任せるべきかという選別眼(目利き)は依然として重要です。
「作れる」と「作るべき」は別の話で、どちらが適切かを判断する力が重要になってくると思います。

