📄

C# PDF生成ライブラリ比較 — PDFSharp vs QuestPDFを検査成績書で実装して選び方を考えた

に公開

「検査成績書のPDF、手作業で作ってませんか?」

製造業の現場で避けて通れないのが 検査成績書(検査報告書・試験成績書)のPDF出力。

よくあるフローはこうだ。

  1. Excelテンプレートに測定値を手入力する
  2. 印刷プレビューで体裁を確認する
  3. 「Microsoft Print to PDF」で保存する
  4. ファイル名を手動でリネームして、共有フォルダに置く

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の考え方はすぐ馴染む。ColumnRowTable の入れ子構造は、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