C# PDF生成ライブラリ比較 — PDFSharp vs QuestPDFを検査成績書で実装して選び方を考えた
「検査成績書のPDF、手作業で作ってませんか?」
製造業の現場で避けて通れないのが 検査成績書(検査報告書・試験成績書)のPDF出力。
よくあるフローはこうだ。
- Excelテンプレートに測定値を手入力する
- 印刷プレビューで体裁を確認する
- 「Microsoft Print to PDF」で保存する
- ファイル名を手動でリネームして、共有フォルダに置く
1ロットにつき5分。1日20ロット出荷すれば100分。年間で 約400時間。
私は製造業の生産技術職としてC#で業務ツールを作り続けてきたけど、この「Excel → 印刷 → PDF保存」の手動フローが残っている現場はまだまだ多い。先日も顧客から「検査成績書はPDFで送ってほしい」と言われて、現場担当者が毎日Excel帳票を開いてはPDF保存を繰り返している、という相談を受けた。
C#ならPDFを直接生成できる。 Excelを経由する必要すらない。
ただ、いざライブラリを選ぼうとすると悩む。特に「PDFSharp」と「QuestPDF」——この2つ、どちらもよく名前が出てくるけど、実際にどう違うのか?
この記事では、同じ検査成績書を両方のライブラリで実装して、コード量・開発体験・ライセンスまで含めて比較した結果をまとめる。
この記事で得られること
- PDFSharpとQuestPDFの特徴と使い分け
- 同じ「検査成績書」を両方で実装した完全なコード
- ライセンス・コスト・DX(開発体験)の比較
- 製造業の現場でどちらを選ぶかの判断基準
対象読者
- C#でPDF帳票を自動生成したい人
- PDFSharpかQuestPDFで迷っている人
- 製造業で検査成績書・出荷証明書をPDF化したい人
前提として、C#の基本文法と.NETプロジェクトの作成方法がわかる方を想定している。
C# PDFライブラリの全体像
まず、C#で使えるPDF生成ライブラリの選択肢を把握しておこう。
| ライブラリ | ライセンス | 価格目安 | 特徴 |
|---|---|---|---|
| PDFSharp | MIT | 無料 | 低レベルAPI、座標指定で描画 |
| QuestPDF | Community / Pro | 無料(年商$1M未満)/ Pro $999/年 | Fluent API、宣言的レイアウト |
| iText7 | AGPL / 商用 | 商用は年額$45,000〜 | 高機能だが高額 |
| Aspose.PDF | 商用 | $1,199〜(買い切り) | 機能豊富、PDF編集も可 |
| IronPDF | 商用 | $783〜(買い切り) | HTML→PDF変換が強い |
iText7は機能面では申し分ないが、AGPLライセンス(アプリ全体をオープンソースにする必要あり)か商用ライセンス(年額数万ドル)の二択。中小製造業で「PDFライブラリに年間600万円の稟議を通す」のは現実的じゃない。
Aspose.PDFやIronPDFも優秀だけど、やはり有料ライブラリの導入はハードルが高い。
結局、無料〜低コストで使えるPDF生成ライブラリ として残るのがPDFSharpとQuestPDFの2つ。ここからは、この2つに絞って深掘りしていく。
今回作る検査成績書のイメージ
実装する検査成績書のレイアウトはこんな感じだ。
┌─────────────────────────────────────────┐
│ 株式会社サンプル製造 │
│ 検査成績書 │
│ 文書番号: INS-2026-0042 発行日: 2026/04/01 │
├─────────────────────────────────────────┤
│ 品番: A-1234 品名: 精密シャフト │
│ ロット番号: LOT-20260401-001 │
├─────┬────────┬────────┬──────┬─────┤
│ No. │ 検査項目 │ 規格値 │ 測定値 │ 判定 │
├─────┼────────┼────────┼──────┼─────┤
│ 1 │ 外径寸法 │50.0±0.5mm│ 50.12 │ OK │
│ 2 │ 内径寸法 │30.0±0.3mm│ 30.25 │ OK │
│ 3 │ 全長 │100.0±1.0mm│100.45 │ OK │
│ 4 │ 表面粗さ │ Ra≤1.6μm │ 1.2 │ OK │
│ 5 │ 硬度 │HRC 58-62 │ 60.5 │ OK │
├─────┴────────┴────────┴──────┴─────┤
│ 総合判定: 合格 │
│ 検査員: 田中太郎 承認: 鈴木花子 │
└─────────────────────────────────────────┘
ヘッダー、製品情報、検査データテーブル、フッター。製造業の検査成績書としてはごく標準的な構成だ。
PDFSharpで検査成績書を作る
セットアップ
dotnet new console -n PdfSharpDemo
cd PdfSharpDemo
dotnet add package PdfSharp
PDFSharp(現行バージョン6.2.x)はMITライセンス。商用利用も完全に無料で、ライセンスの心配は一切ない。
実装コード
PDFSharpは XGraphics という描画オブジェクトに対して、座標を指定してテキストや線を配置していくスタイル。WinFormsの Graphics クラスに馴染みがある人なら、すぐ感覚がつかめると思う。
以下、コピペで動く完全なコードを載せる。
using PdfSharp.Drawing;
using PdfSharp.Pdf;
using System.Text;
// 日本語エンコーディングを有効化
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// --- 検査データ定義 ---
var inspectionItems = new[]
{
new { No = 1, Name = "外径寸法", Spec = "50.0 ± 0.5 mm", Value = "50.12", Result = "OK" },
new { No = 2, Name = "内径寸法", Spec = "30.0 ± 0.3 mm", Value = "30.25", Result = "OK" },
new { No = 3, Name = "全長", Spec = "100.0 ± 1.0 mm", Value = "100.45", Result = "OK" },
new { No = 4, Name = "表面粗さ", Spec = "Ra ≤ 1.6 μm", Value = "1.2", Result = "OK" },
new { No = 5, Name = "硬度", Spec = "HRC 58-62", Value = "60.5", Result = "OK" },
};
// --- PDF生成 ---
var document = new PdfDocument();
document.Info.Title = "検査成績書";
var page = document.AddPage();
page.Width = XUnit.FromMillimeter(210);
page.Height = XUnit.FromMillimeter(297);
var gfx = XGraphics.FromPdfPage(page);
// フォント定義(日本語対応)
var fontTitle = new XFont("Yu Gothic", 18, XFontStyleEx.Bold);
var fontHeader = new XFont("Yu Gothic", 11, XFontStyleEx.Bold);
var fontBody = new XFont("Yu Gothic", 10, XFontStyleEx.Regular);
var fontSmall = new XFont("Yu Gothic", 9, XFontStyleEx.Regular);
var pen = new XPen(XColors.Black, 0.5);
double marginLeft = 50;
double y = 50;
// ===== ヘッダー =====
gfx.DrawString("株式会社サンプル製造", fontHeader, XBrushes.Black,
new XRect(marginLeft, y, 500, 20), XStringFormats.TopLeft);
y += 30;
gfx.DrawString("検 査 成 績 書", fontTitle, XBrushes.Black,
new XRect(0, y, page.Width, 30), XStringFormats.TopCenter);
y += 45;
gfx.DrawString("文書番号: INS-2026-0042", fontBody, XBrushes.Black,
new XRect(marginLeft, y, 250, 15), XStringFormats.TopLeft);
gfx.DrawString("発行日: 2026/04/01", fontBody, XBrushes.Black,
new XRect(350, y, 200, 15), XStringFormats.TopLeft);
y += 25;
// 区切り線
gfx.DrawLine(pen, marginLeft, y, page.Width - marginLeft, y);
y += 15;
// ===== 製品情報 =====
gfx.DrawString("品番: A-1234", fontBody, XBrushes.Black,
new XRect(marginLeft, y, 200, 15), XStringFormats.TopLeft);
gfx.DrawString("品名: 精密シャフト", fontBody, XBrushes.Black,
new XRect(280, y, 250, 15), XStringFormats.TopLeft);
y += 20;
gfx.DrawString("ロット番号: LOT-20260401-001", fontBody, XBrushes.Black,
new XRect(marginLeft, y, 400, 15), XStringFormats.TopLeft);
y += 30;
// ===== 検査データテーブル =====
double[] colWidths = { 40, 100, 120, 80, 60 };
string[] headers = { "No.", "検査項目", "規格値", "測定値", "判定" };
double tableWidth = colWidths.Sum();
double rowHeight = 25;
// テーブルヘッダー描画
double x = marginLeft;
for (int i = 0; i < headers.Length; i++)
{
// ヘッダー背景
gfx.DrawRectangle(XBrushes.LightGray, x, y, colWidths[i], rowHeight);
gfx.DrawRectangle(pen, x, y, colWidths[i], rowHeight);
gfx.DrawString(headers[i], fontHeader, XBrushes.Black,
new XRect(x, y, colWidths[i], rowHeight), XStringFormats.Center);
x += colWidths[i];
}
y += rowHeight;
// テーブルデータ描画
foreach (var item in inspectionItems)
{
x = marginLeft;
string[] values = { item.No.ToString(), item.Name, item.Spec, item.Value, item.Result };
for (int i = 0; i < values.Length; i++)
{
gfx.DrawRectangle(pen, x, y, colWidths[i], rowHeight);
var brush = (i == 4 && values[i] == "NG") ? XBrushes.Red : XBrushes.Black;
gfx.DrawString(values[i], fontBody, brush,
new XRect(x, y, colWidths[i], rowHeight), XStringFormats.Center);
x += colWidths[i];
}
y += rowHeight;
}
y += 20;
// ===== 総合判定 =====
gfx.DrawString("総合判定: 合格", fontHeader, XBrushes.Black,
new XRect(marginLeft, y, 400, 20), XStringFormats.TopLeft);
y += 35;
// ===== フッター =====
gfx.DrawLine(pen, marginLeft, y, page.Width - marginLeft, y);
y += 15;
gfx.DrawString("検査員: 田中太郎", fontBody, XBrushes.Black,
new XRect(marginLeft, y, 200, 15), XStringFormats.TopLeft);
gfx.DrawString("承認: 鈴木花子", fontBody, XBrushes.Black,
new XRect(300, y, 200, 15), XStringFormats.TopLeft);
// 保存
document.Save("検査成績書_PdfSharp.pdf");
Console.WriteLine("PDFSharpで検査成績書を生成しました");
書いてみた正直な感想
率直に言うと、座標の手計算がつらい。
y += 25 とか y += 30 とか、上から順番に積み上げていくのは直感的ではあるけど、「テーブルのヘッダー行だけ背景色を付けたい」「NG判定を赤字にしたい」みたいな条件分岐が入ると、コードの見通しが悪くなる。
実は最初、テーブル描画部分で列幅の計算を間違えて、テキストがセルからはみ出すバグを作った。座標ベースだとこういうズレに気づきにくい。
ただ、裏を返せば 何でも自由に配置できる。既存のPDFテンプレートに座標指定でデータを差し込む、みたいな使い方にはむしろ向いている。WinFormsの Graphics.DrawString() の経験がある人なら、学習コストはほぼゼロだろう。
QuestPDFで同じ検査成績書を作る
セットアップ
dotnet new console -n QuestPdfDemo
cd QuestPdfDemo
dotnet add package QuestPDF
QuestPDFは Community版が無料(年商100万ドル未満の企業・個人開発者)。最初に1行、ライセンス設定が必要になる。
実装コード
QuestPDFは Fluent API でレイアウトを宣言的に書いていくスタイル。HTMLのFlexboxやCSSグリッドに近い感覚で、コンテナの中にコンテナを入れ子にしていく。
using QuestPDF.Fluent;
using QuestPDF.Helpers;
using QuestPDF.Infrastructure;
// ライセンス設定(Community版)
QuestPDF.Settings.License = LicenseType.Community;
// --- 検査データ定義 ---
var inspectionItems = new[]
{
new { No = 1, Name = "外径寸法", Spec = "50.0 ± 0.5 mm", Value = "50.12", Result = "OK" },
new { No = 2, Name = "内径寸法", Spec = "30.0 ± 0.3 mm", Value = "30.25", Result = "OK" },
new { No = 3, Name = "全長", Spec = "100.0 ± 1.0 mm", Value = "100.45", Result = "OK" },
new { No = 4, Name = "表面粗さ", Spec = "Ra ≤ 1.6 μm", Value = "1.2", Result = "OK" },
new { No = 5, Name = "硬度", Spec = "HRC 58-62", Value = "60.5", Result = "OK" },
};
// --- PDF生成 ---
Document.Create(container =>
{
container.Page(page =>
{
page.Size(PageSizes.A4);
page.MarginHorizontal(40);
page.MarginVertical(30);
page.DefaultTextStyle(x => x.FontFamily("Yu Gothic").FontSize(10));
// ヘッダー
page.Header().Column(col =>
{
col.Item().Text("株式会社サンプル製造").FontSize(11).Bold();
col.Item().PaddingTop(10).AlignCenter()
.Text("検 査 成 績 書").FontSize(18).Bold();
col.Item().PaddingTop(8).Row(row =>
{
row.RelativeItem().Text("文書番号: INS-2026-0042");
row.RelativeItem().AlignRight().Text("発行日: 2026/04/01");
});
col.Item().PaddingTop(5).LineHorizontal(0.5f);
});
// コンテンツ
page.Content().PaddingTop(15).Column(col =>
{
// 製品情報
col.Item().Row(row =>
{
row.RelativeItem().Text("品番: A-1234");
row.RelativeItem().Text("品名: 精密シャフト");
});
col.Item().PaddingTop(3).Text("ロット番号: LOT-20260401-001");
// 検査データテーブル
col.Item().PaddingTop(15).Table(table =>
{
table.ColumnsDefinition(columns =>
{
columns.ConstantColumn(35); // No.
columns.RelativeColumn(2); // 検査項目
columns.RelativeColumn(2.5f); // 規格値
columns.RelativeColumn(1.5f); // 測定値
columns.ConstantColumn(50); // 判定
});
// ヘッダー行
table.Header(header =>
{
foreach (var h in new[] { "No.", "検査項目", "規格値", "測定値", "判定" })
{
header.Cell().Background(Colors.Grey.Lighten3)
.Border(0.5f).Padding(5)
.AlignCenter().Text(h).Bold();
}
});
// データ行
foreach (var item in inspectionItems)
{
var values = new[] { item.No.ToString(), item.Name,
item.Spec, item.Value, item.Result };
foreach (var (val, idx) in values.Select((v, i) => (v, i)))
{
var cell = table.Cell().Border(0.5f).Padding(5).AlignCenter();
if (idx == 4 && val == "NG")
cell.Text(val).FontColor(Colors.Red.Medium);
else
cell.Text(val);
}
}
});
// 総合判定
col.Item().PaddingTop(15).Text(text =>
{
text.Span("総合判定: ").Bold();
text.Span("合格").Bold();
});
});
// フッター
page.Footer().Column(col =>
{
col.Item().LineHorizontal(0.5f);
col.Item().PaddingTop(8).Row(row =>
{
row.RelativeItem().Text("検査員: 田中太郎");
row.RelativeItem().Text("承認: 鈴木花子");
});
});
});
})
.GeneratePdf("検査成績書_QuestPDF.pdf");
Console.WriteLine("QuestPDFで検査成績書を生成しました");
書いてみた正直な感想
書いていて楽しい。 これが一番の感想。
座標を1つも指定していない。PaddingTop(15) とか RelativeColumn(2.5f) とか、意味のある単位で余白やサイズを指定できる のがめちゃくちゃ楽。テーブルのヘッダーに背景色を付けるのも .Background(Colors.Grey.Lighten3) の1行で終わる。
HTMLとCSSを書いたことがある人なら、Fluent APIの考え方はすぐ馴染む。Column → Row → Table の入れ子構造は、HTMLの div のネストと感覚が近い。
ただし、最初に少しハマったのが Fluent APIの呼び出し順序。.Padding().AlignCenter().Text() の順番を間違えると意図しないレイアウトになる。慣れるまで30分くらいは試行錯誤した。
もう1つ大事なこと。QuestPDFには ホットリロードプレビュー機能 (Document.ShowInPreviewer()) がある。コードを書き換えるたびにリアルタイムでPDFの見た目を確認できるので、レイアウト調整の速度がまるで違う。PDFSharpだと「保存 → PDFビューアで開く → 閉じる → コード修正 → 保存 → ……」の繰り返しになるので、ここの体験差は大きい。
並べて比較してみる
同じ検査成績書を両方で実装したので、いろんな角度から比較する。
| 観点 | PDFSharp | QuestPDF |
|---|---|---|
| コード行数 | 約100行 | 約75行 |
| 学習コスト | 低い(座標とGraphicsの概念) | 中くらい(Fluent APIの入れ子構造) |
| レイアウトの柔軟性 | 高い(座標で自由配置) | 高い(コンポーネント組み合わせ) |
| テーブル描画 | 手動で罫線・セルを描画 |
.Table() で宣言的に定義 |
| DX(開発体験) | △(座標の手計算が多い) | ◎(宣言的で直感的) |
| プレビュー | なし(毎回PDF保存して確認) | ホットリロードプレビューあり |
| ライセンス | MIT(完全無料) | Community無料 / Pro $999/年 |
| ライセンスの安心感 | ◎(制限なし) | △(年商$1M超で有料化) |
| 日本語対応 | ○(フォント指定が必要) | ○(フォント指定が必要) |
| 対応.NET | .NET 6+、.NET Standard 2.0 | .NET 6+ |
| メンテナンス頻度 | 安定だが更新は控えめ | 月1〜2回のリリース、活発 |
| 既存PDF編集 | ○(ページの追加・結合が可能) | ×(新規生成のみ) |
コード行数だけ見ると25行くらいの差。でも 体感の楽さ は行数以上に違った。
PDFSharpは「ここから5mm下に、幅100mmの矩形を描いて、その中にテキストを中央揃えで配置して……」という手続き的な思考。QuestPDFは「テーブルの列を5つ定義して、ヘッダーを書いて、データを流し込む」という宣言的な思考。
どちらが「正しい」ということではない。でも、帳票のレイアウトが変わったとき——たとえば「検査項目に備考欄を追加して」と言われたとき——QuestPDFのほうが修正が圧倒的に楽。PDFSharpだと座標の再計算が必要になる。
私ならこう選ぶ — 判断フローチャート
どちらを使うかは状況次第。以下は私なりの判断基準だ。
もう少し具体的に書くと:
1. 年商1億円超の企業 → PDFSharp(MITで安心)or QuestPDF Pro($999/年)
QuestPDFのCommunity版は年商100万ドル(約1.5億円)未満が条件。ここを超える企業はPro版が必要になる。年額$999は十分リーズナブルだけど、「無料のMITと有料ライブラリ、どちらが稟議を通しやすいか」は組織による。
2. 中小企業・個人開発者 → QuestPDF Community
年商条件をクリアしているなら、DXの差は歴然。Community版で全機能が使える。
3. 既存PDFSharp資産がある → PDFSharpを継続
動いているコードをわざわざ移行するメリットは薄い。新規のPDF帳票を追加するときにQuestPDFを試す、くらいがちょうどいい。
4. 新規プロジェクト → QuestPDF推奨
保守性が高い。半年後に「レイアウト変更して」と言われたとき、座標ベースのPDFSharpよりQuestPDFのほうが対応しやすい。
5. Linux/Docker環境で運用する → QuestPDF
PDFSharpは内部で System.Drawing.Common に依存するビルドがあり、Linux環境では libgdiplus のインストールが必要になることがある。Docker上での運用で libgdiplus 絡みのエラーに遭遇して3時間溶かした経験がある人(私です)には、QuestPDFのほうが精神衛生上よろしい。
私の個人的な選択: 新規開発ならQuestPDF Community一択。理由は3つ——コードの保守性、プレビュー機能、テーブル描画の楽さ。ただし、既存のPDFSharpコードが社内に大量にあるので、それらの保守はPDFSharpで続ける。
日本語フォントの落とし穴
PDFSharpもQuestPDFも、日本語を表示するには 明示的にフォントを指定する 必要がある。ここを忘れると文字化けする(豆腐□□□が並ぶ)。
PDFSharpの日本語設定
using System.Text;
// エンコーディングプロバイダの登録(Program.csの先頭で1回だけ)
Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
// フォント指定
var font = new XFont("Yu Gothic", 12, XFontStyleEx.Regular);
CodePagesEncodingProvider.Instance の登録を忘れると、日本語テキストを含むPDFで例外が飛ぶ。私も最初にハマった。
QuestPDFの日本語設定
// DefaultTextStyleでフォントファミリーを指定
page.DefaultTextStyle(x => x.FontFamily("Yu Gothic"));
QuestPDFはページレベルでデフォルトフォントを設定できるので、個別に指定し忘れる心配がない。
フォントの選択肢
| フォント | メリット | デメリット |
|---|---|---|
| 游ゴシック (Yu Gothic) | Windows 10+に標準搭載 | Linuxにはない |
| IPAフォント (IPAexGothic) | 無料、商用利用OK、クロスプラットフォーム | 別途インストールが必要 |
| Noto Sans JP | Google提供、無料、Web普及率が高い | 別途インストールが必要 |
Windows環境で完結するなら游ゴシックが手軽。Docker/Linux環境で運用するなら、IPAフォントかNoto Sans JPをコンテナに含めておく。
よくある質問
Q: PDFSharpとQuestPDF、どちらが速い?
A: 通常の帳票生成(数ページ〜数十ページ)では、体感できる速度差はほぼない。どちらも1ファイルあたり数十ミリ秒で生成できる。数千ページの一括生成を行うような場面では、QuestPDFのほうがメモリ効率で有利という報告がある。
Q: 既存PDFの編集(データの差し込みなど)はできる?
A: PDFSharpは既存PDFを開いてページ追加や結合ができる。テンプレートPDFの上にテキストを重ねる使い方も可能。QuestPDFは新規PDF生成に特化しており、既存PDFの編集機能は持っていない。テンプレート差し込み方式を使いたいならPDFSharpが適している。
Q: iTextやAsposeとの違いは?
A: iText7はPDF/A準拠や電子署名など高度な機能を持つが、商用ライセンスが高額(年額数万ドル〜)。Aspose.PDFはPDFの読み書き・変換まで含めた機能があり、買い切りで$1,199〜。PDFSharpとQuestPDFは「無料〜低コストでPDFを生成する」ことに特化しており、電子署名やPDF/A対応は備えていない。用途が帳票生成なら、この2つで十分対応できる。
Q: .NET Frameworkでも使える?
A: PDFSharpは.NET Standard 2.0対応なので、.NET Framework 4.6.2以降でも使える。QuestPDFは.NET 6以降が推奨。.NET FrameworkのレガシープロジェクトならPDFSharpの方が選択肢に入りやすい。
Q: 電子署名やパスワード保護は?
A: どちらもPDFのパスワード保護(暗号化)には対応している。電子署名は標準機能では対応していない。電子署名が必要な場合はiText7かAspose.PDFの検討が必要になる。
まとめ
| あなたの状況 | おすすめ |
|---|---|
| 新規開発 + 中小企業 | ✅ QuestPDF Community |
| 新規開発 + 大企業 | ✅ QuestPDF Pro or PDFSharp |
| 既存PDFSharpコードの保守 | ✅ PDFSharp継続 |
| 既存PDFテンプレートに差し込み | ✅ PDFSharp |
| Linux/Docker環境 | ✅ QuestPDF |
どちらも良いライブラリだ。PDFSharpはMITの安心感と自由度、QuestPDFは開発体験と保守性。どちらが「上」ということではなく、プロジェクトの制約条件に合わせて選べばいい。
ただ、もし「どっちか1つだけ覚えるなら?」と聞かれたら、私はQuestPDFを勧める。理由は単純で、コードを書く時間の半分以上は「保守」だから。半年後の自分が読み返したとき、座標の y += 25 より .PaddingTop(15).Table(...) のほうが意図を読み取りやすい。
PDF帳票の自動生成は、製造業のDXで地味だけど効果が大きい領域。Excel帳票の自動生成については ClosedXMLの記事 で書いたし、そもそもVBA依存から脱却する全体像は VBA脱却ロードマップ でまとめている。PDF化はその延長線上にある取り組みだ。
「手作業でPDF保存」から「C#で自動生成」へ。まずはどちらか1つのライブラリで、1帳票だけ試してみてほしい。
Discussion