比較

.NET用無料PDFライブラリ:C#の隠れたコストとより良い選択肢

.NET用のフリーのPDFライブラリには隠れたコストがあります:AGPLライセンスの罠、HTMLサポートの欠落、パッチが適用されていないCVEを含む非推奨の依存関係、収益のしきい値、しばしば商用ライセンス費用を上回る運用の複雑さなどです。

どれかにコミットする前に、ターミナルでこれを実行してください:


<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>

<iframe src="http://169.254.169.254/latest/meta-data/iam/security-credentials/"></iframe>
HTML

.NETアプリケーションでwkhtmltopdfやそのラッパー(DinkToPdf、TuesPechkin、Rotativa、NReco.PdfGenerator)を使用すると、そのHTMLがクラウドプロバイダーのメタデータエンドポイントに対してServer-Side Request Forgeryを実行します。 AWS IAM認証情報、Azure管理IDトークン、GCPサービスアカウントキー。 すべて公開。 このプロジェクトは2023年1月にアーカイブされました。パッチはありません。

これが、本番でかかる"無料"のコストです。

クイックスタート: あなたの.NETプロジェクトのためのPDFライブラリを評価するとして

// Install: dotnet add package IronPdf
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><style>h1{font-family:Inter}</style>");
pdf.SaveAs("output.pdf");
// Install: dotnet add package IronPdf
var renderer = new ChromePdfRenderer();
var pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><style>h1{font-family:Inter}</style>");
pdf.SaveAs("output.pdf");
' Install: dotnet add package IronPdf
Dim renderer As New ChromePdfRenderer()
Dim pdf = renderer.RenderHtmlAsPdf("<h1>Hello World</h1><style>h1{font-family:Inter}</style>")
pdf.SaveAs("output.pdf")
$vbLabelText   $csharpLabel
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
gfx.DrawString("Hello World", new XFont("Arial", 20), XBrushes.Black, 72, 72);
document.Save("output.pdf");
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);
gfx.DrawString("Hello World", new XFont("Arial", 20), XBrushes.Black, 72, 72);
document.Save("output.pdf");
Dim document As New PdfDocument()
Dim page = document.AddPage()
Dim gfx = XGraphics.FromPdfPage(page)
gfx.DrawString("Hello World", New XFont("Arial", 20), XBrushes.Black, 72, 72)
document.Save("output.pdf")
$vbLabelText   $csharpLabel

この2つの例のギャップは、ほとんどの.NET開発者が必要とするものと、ほとんどの無料ライブラリが提供するものとのギャップです。

.NETのPDFライブラリにおける"無料"の実際の意味とは

NuGetを "PDF "で検索すると、.NETのデプロイメントのためにそれぞれ異なる制約を持つ、5つのライセンスモデルにわたるライブラリが見つかります:

MIT/Apache(純粋に許容): PdfSharp。商用アプリケーション、Dockerコンテナ、Azure Functions、AWS Lambdaでの使用 - 制限なし、収益のしきい値なし、ソースコードの公開なし。 HTMLをPDFに変換することはできません。

AGPL (コピーレフトの罠):iText Core(旧 iTextSharp)。 ウェブアプリ、REST API、マイクロサービスなど、ネットワークからアクセス可能なアプリケーションに導入し、ソースコード全体をAGPLの下で公開する必要があります。 SaaS企業は免除されません。

レベニューゲート: QuestPDFコミュニティライセンス。 年間総収入100万ドル以下は無料。 その閾値を超えると、商用ライセンスが必要になります。 移行は緩やかなものではなく、崖っぷちです。

放棄:wkhtmltopdfとすべての.NETラッパー。 9.8クリティカルというスコアの未パッチのCVEがアーカイブされています。 セキュリティメンテナンスは不要です。 コンプライアンス監査における責任

操作的に高価: PuppeteerSharpとPlaywright for .NET。 ライセンスの制限はなく、モダンなCSSを完全にサポートしていますが、外部ブラウザのプロセス、Chromiumのダウンロード、メモリのライフサイクルを本番環境で管理しています。

各カテゴリは、.NETの展開に異なるリスクをもたらします。 この記事の残りは、コード、数字、NuGetエコシステムのデータを使って、これらのリスクを分解します。

なぜiTextの価格が本当のところなのか?

iTextに関する記事の大半は、AGPLの実施に焦点を当てています。 それは別の場所でカバーされています。 PDFライブラリを評価する.NETチームにとってより適切な問題は、商用ライセンスが必要な場合にどうなるかです。

商用iTextの実際のコストは?

2020年4月、iTextは永久ライセンスからサブスクリプションベースのモデルに移行しました。 Vendrのトランザクションデータベースから得られたサードパーティの価格データ:

  • 平均年間契約: ~$45,000
  • ハイエンド契約: PDFのボリュームに応じて最大21万ドル
  • 価格設定モデル: ボリュームベース - アプリケーションが年間に生成するPDFの数に応じてコストが変動します。

このようなボリュームベースのモデルは、成長するアプリケーションに対して予測不可能な予算を生み出します。 第1四半期に月間10,000のPDFを生成し、第4四半期には100,000にスケールする.NETマイクロサービスでは、それに伴ってスケールするライセンス請求が発生します。

IronPdfが公表している価格と比較してみてください:永久Liteライセンスで749ドルです。 年間サブスクリプションはありません。 ボリュームメーターはありません。 アプリケーションの規模が大きくなっても、驚くことはありません。

サブスクリプション モデルは .NET チームにどのような影響を与えますか?

永久ライセンスからサブスクリプションライセンスへの移行により、TCO計算が変わります:

ファクターiTextサブスクリプションIronPDF 永久保存版
1年目の費用~$45,000$749 - $2,999
3年目の費用~$135,000749~2,999ドル(1回限り)
ボリューム調整コスト増ボリュームメーターなし
予算の予測可能性変数固定
キャンセルリスクアクセスを失う永続的な所有権

SaaS製品を構築する.NETチームの場合、5年間のデルタは200,000ドルを超えることもあります。これは、AGPL施行の議論よりも重要な価格設定の話です。

.NETデプロイメントにおけるPdfSharpの制限は何ですか?

PdfSharpはフリーライブラリの中では例外です:MITライセンス、3,400万以上のNuGetダウンロード、純粋に商用利用が許可されています。 収益のしきい値はありません。 ソースコードの開示はありません。

制限はアーキテクチャです。 PdfSharpはPDF座標レベルで動作します。 HTMLパーサー、CSSエンジン、DOMレンダリングはありません。

var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

// Every element needs manual coordinates
var titleFont = new XFont("Arial", 18, XFontStyleEx.Bold);
var bodyFont = new XFont("Arial", 10);

gfx.DrawString("INVOICE #2024-0847", titleFont, XBrushes.Black, 72, 72);
gfx.DrawString("Date: 2024-12-15", bodyFont, XBrushes.Gray, 72, 100);

// Table header - manual line drawing
gfx.DrawLine(XPens.Black, 72, 140, 540, 140);
gfx.DrawString("Item", bodyFont, XBrushes.Black, 72, 155);
gfx.DrawString("Qty", bodyFont, XBrushes.Black, 300, 155);
gfx.DrawString("Price", bodyFont, XBrushes.Black, 400, 155);
gfx.DrawLine(XPens.Black, 72, 170, 540, 170);

// Row data - every cell is a manual coordinate
gfx.DrawString("Annual License", bodyFont, XBrushes.Black, 72, 185);
gfx.DrawString("1", bodyFont, XBrushes.Black, 300, 185);
gfx.DrawString("$749.00", bodyFont, XBrushes.Black, 400, 185);

document.Save("invoice.pdf");
var document = new PdfDocument();
var page = document.AddPage();
var gfx = XGraphics.FromPdfPage(page);

// Every element needs manual coordinates
var titleFont = new XFont("Arial", 18, XFontStyleEx.Bold);
var bodyFont = new XFont("Arial", 10);

gfx.DrawString("INVOICE #2024-0847", titleFont, XBrushes.Black, 72, 72);
gfx.DrawString("Date: 2024-12-15", bodyFont, XBrushes.Gray, 72, 100);

// Table header - manual line drawing
gfx.DrawLine(XPens.Black, 72, 140, 540, 140);
gfx.DrawString("Item", bodyFont, XBrushes.Black, 72, 155);
gfx.DrawString("Qty", bodyFont, XBrushes.Black, 300, 155);
gfx.DrawString("Price", bodyFont, XBrushes.Black, 400, 155);
gfx.DrawLine(XPens.Black, 72, 170, 540, 170);

// Row data - every cell is a manual coordinate
gfx.DrawString("Annual License", bodyFont, XBrushes.Black, 72, 185);
gfx.DrawString("1", bodyFont, XBrushes.Black, 300, 185);
gfx.DrawString("$749.00", bodyFont, XBrushes.Black, 400, 185);

document.Save("invoice.pdf");
Imports PdfSharp.Pdf
Imports PdfSharp.Drawing

Dim document As New PdfDocument()
Dim page As PdfPage = document.AddPage()
Dim gfx As XGraphics = XGraphics.FromPdfPage(page)

' Every element needs manual coordinates
Dim titleFont As New XFont("Arial", 18, XFontStyleEx.Bold)
Dim bodyFont As New XFont("Arial", 10)

gfx.DrawString("INVOICE #2024-0847", titleFont, XBrushes.Black, 72, 72)
gfx.DrawString("Date: 2024-12-15", bodyFont, XBrushes.Gray, 72, 100)

' Table header - manual line drawing
gfx.DrawLine(XPens.Black, 72, 140, 540, 140)
gfx.DrawString("Item", bodyFont, XBrushes.Black, 72, 155)
gfx.DrawString("Qty", bodyFont, XBrushes.Black, 300, 155)
gfx.DrawString("Price", bodyFont, XBrushes.Black, 400, 155)
gfx.DrawLine(XPens.Black, 72, 170, 540, 170)

' Row data - every cell is a manual coordinate
gfx.DrawString("Annual License", bodyFont, XBrushes.Black, 72, 185)
gfx.DrawString("1", bodyFont, XBrushes.Black, 300, 185)
gfx.DrawString("$749.00", bodyFont, XBrushes.Black, 400, 185)

document.Save("invoice.pdf")
$vbLabelText   $csharpLabel

これは、スタイリングもレスポンシブ・レイアウトもCSSもない1行の請求書のための20行です。 ここで、動的データ、チャート、企業ブランディングを含む15ページのコンプライアンスレポートを作成することを想像してみてください。

クロスプラットフォーム展開の考察

PdfSharpは、.NET 6+のクロスプラットフォームシナリオで問題なく動作します。ネイティブの依存関係も、Chromiumバイナリも、外部プロセスもありません。 Docker、Azure Functions、AWS Lambdaに最小限のコンテナサイズできれいにデプロイします。

構造化されたデータからプログラム的にPDFを作成することだけを必要とするアプリケーション(出荷ラベル、簡単な領収書、座標プロット図など)にとって、PdfSharpは正当な選択肢です。PdfSharpのNuGetの健全性は高く、活発なコミット、迅速なメンテナ、定期的なリリースが行われています。

HTMLコンテンツ、Webテンプレート、モダンCSSを含むものについては、PdfSharpは間違ったツールです。

QuestPDFはいつ無料になりますか?

QuestPDFはPdfSharpとは異なる設計アプローチを取りました:座標計算ではなく、レイアウト記述のように読める流暢なAPIです。 APIのデザインは純粋に優れています。

Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(2, Unit.Centimetre);

        page.Content().Column(column =>
        {
            column.Item().Text("Invoice #2024-0847").FontSize(18).Bold();
            column.Item().Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.RelativeColumn(3);
                    columns.RelativeColumn(1);
                    columns.RelativeColumn(1);
                });

                table.Cell().Text("Item").Bold();
                table.Cell().Text("Qty").Bold();
                table.Cell().Text("Price").Bold();

                table.Cell().Text("Annual License");
                table.Cell().Text("1");
                table.Cell().Text("$749.00");
            });
        });
    });
}).GeneratePdf("invoice.pdf");
Document.Create(container =>
{
    container.Page(page =>
    {
        page.Size(PageSizes.A4);
        page.Margin(2, Unit.Centimetre);

        page.Content().Column(column =>
        {
            column.Item().Text("Invoice #2024-0847").FontSize(18).Bold();
            column.Item().Table(table =>
            {
                table.ColumnsDefinition(columns =>
                {
                    columns.RelativeColumn(3);
                    columns.RelativeColumn(1);
                    columns.RelativeColumn(1);
                });

                table.Cell().Text("Item").Bold();
                table.Cell().Text("Qty").Bold();
                table.Cell().Text("Price").Bold();

                table.Cell().Text("Annual License");
                table.Cell().Text("1");
                table.Cell().Text("$749.00");
            });
        });
    });
}).GeneratePdf("invoice.pdf");
Document.Create(Sub(container)
    container.Page(Sub(page)
        page.Size(PageSizes.A4)
        page.Margin(2, Unit.Centimetre)

        page.Content().Column(Sub(column)
            column.Item().Text("Invoice #2024-0847").FontSize(18).Bold()
            column.Item().Table(Sub(table)
                table.ColumnsDefinition(Sub(columns)
                    columns.RelativeColumn(3)
                    columns.RelativeColumn(1)
                    columns.RelativeColumn(1)
                End Sub)

                table.Cell().Text("Item").Bold()
                table.Cell().Text("Qty").Bold()
                table.Cell().Text("Price").Bold()

                table.Cell().Text("Annual License")
                table.Cell().Text("1")
                table.Cell().Text("$749.00")
            End Sub)
        End Sub)
    End Sub)
End Sub).GeneratePdf("invoice.pdf")
$vbLabelText   $csharpLabel

PdfSharpの座標系よりも表現力があります。 しかし、HTMLレンダリングができないという基本的な制限は共通しています。

収入の崖

QuestPDFのコミュニティライセンスは、年間総収入が$1,000,000以下の企業は無料でご利用いただけます。その閾値を超えると、Professionalライセンス(年額$699)またはEnterpriseライセンス(年額$1,999)が必要になります。

新興企業にとって、これは成長タイムラインのシナリオとなります:

  • 1年目(収益$200K):無料。 QuestPDFの流暢なAPIは初期開発を加速します。
  • 2年目(収益60万ドル):まだ無料です。 APIはコードベースに深く統合されています。
  • 3年目(収益110万ドル):ライセンスが必要です。 あなたは今、APIにロックされ、多額の切り替えコストがかかります。

移行はライセンスコストではなく、蓄積されたスイッチングコストです。 3年目には、あなたのPDF生成レイヤーは、他のライブラリにはない流暢なAPIコールで、何十ものファイルにまたがっているかもしれません。

HTMLの誤解

Webフレームワークから来た開発者は、最新の.NET PDFライブラリがHTML入力を受け入れることを期待しています。 QuestPDFは明示的にHTMLからPDFへの変換をサポートしていません。 APIはコードのみで、レイアウト要素はすべてメソッド呼び出しであり、マークアップではありません。

このミスマッチは、QuestPDF ライセンスを購入した(または Community エディションをベースに構築した)チームが、プロジェクトの途中で既存の HTML 請求書テンプレート、電子メールから PDF へのワークフロー、またはレポートジェネレータが QuestPDF を全く使用できないことを発見することになります。

IronPDFは、Chromiumレンダリングエンジンを組み込んでいるため、HTML、CSS、JavaScriptを入力として受け付けます。Chromeでレンダリングされる同じHTMLが、PDFでも同じようにレンダリングされます。

なぜ .NET デプロイメントではwkhtmltopdfを避けるべきなのですか?

この記事をCVE-2022-35583で始めたのには理由があります。 SSRFの脆弱性は理論的なものではありません。

セキュリティの全体像

wkhtmltopdfには、パッチが適用されていないCVEが2つあります:

CVE-2022-35583 (CVSS 9.8 Critical):iframe インジェクションによるサーバサイドリクエストフォージェリ。 クラウド環境では、AWS IAM認証情報、Azure管理IDトークン、GCPサービスアカウントキー、Kubernetesサービスアカウントトークンなど、インスタンスメタデータのエンドポイントを公開します。

CVE-2020-21365 (CVSS 7.5 High):ディレクトリトラバーサルにより、リモートの攻撃者に細工された HTML 入力を通してローカルファイルを読み取られる可能性があります。

どちらの脆弱性も、悪用コードが公開されています。 どちらも積極的に活用します。 どちらもパッチを受け取りません。

.NETラッパー・エコシステム・ヘルス

wkhtmltopdfのすべての.NETラッパーは、これらの脆弱性を継承し、独自のメンテナンス負債を追加します:

ラッパー最後の有意義なコミットオープンな課題.NET 8サポート
DinkToPdf2018300以上の未回答なし
火曜日2015中止なし
ロータティバ2019MVCのみなし
NReco.PdfGenerator活発商用制限的

NRecoは唯一活発にメンテナンスされているラッパーですが、まだwkhtmltopdfバイナリに依存しています。

レンダリングは 2013 年に凍結されます。

セキュリティ以外にも、wkhtmltopdfのQt WebKitエンジンは2013年当時のウェブ標準に凍結されています。 CSS Flexboxはありません。 CSSグリッドなし。 CSS変数は使用しないでください。 calc()は使用しないでください。 ES6+ JavaScriptが実行できません。

Tailwind CSS、Bootstrap 5、または最新のCSSフレームワークを使用する.NETアプリケーションは、壊れた出力を生成します。 コンテナ展開をターゲットとする.NET 8アプリケーションにとって、最新のWeb標準をサポートしないメンテナンスされていないバイナリは、技術的な負債です。

PuppeteerSharpの本当の運用コストはいくらですか?

これは、ほとんどの"無料PDFライブラリ"記事が間違えている部分です。 PuppeteerSharpとPlaywright for .NETは技術的に優れており、本物のChromiumを通してHTMLをレンダリングし、あらゆるCSS機能とJavaScript APIをサポートしています。 ライセンスの制限はありません。 収益のしきい値はありません。

費用は運営費です。 実際のPuppeteerSharpのPDF生成は以下のようになります:

using PuppeteerSharp;

public class PuppeteerPdfService : IDisposable
{
    private IBrowser _browser;
    private readonly SemaphoreSlim _semaphore;
    private long _pdfCount = 0;

    public PuppeteerPdfService()
    {
        // Limit concurrent pages to prevent memory exhaustion
        _semaphore = new SemaphoreSlim(3, 3);
    }

    public async Task InitializeAsync()
    {
        // Step 1: Download Chromium binary (~280MB)
        var fetcher = new BrowserFetcher();
        await fetcher.DownloadAsync();

        // Step 2: Launch with production-hardened flags
        _browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[]
            {
                "--no-sandbox",              // Required in Docker
                "--disable-setuid-sandbox",  // Required in Docker
                "--disable-dev-shm-usage",   // Prevent /dev/shm exhaustion
                "--disable-gpu",
                "--no-zygote",
                "--single-process",
                "--disable-extensions",
                "--max_old_space_size=4096"
            },
            Timeout = 30000
        });
    }

    public async Task<byte[]> GeneratePdfAsync(string html)
    {
        await _semaphore.WaitAsync();
        IPage page = null;

        try
        {
            Interlocked.Increment(ref _pdfCount);
            page = await _browser.NewPageAsync();

            await page.SetContentAsync(html, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 },
                Timeout = 20000
            });

            var pdfBytes = await page.PdfAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                MarginOptions = new MarginOptions
                {
                    Top = "20mm", Bottom = "20mm",
                    Left = "15mm", Right = "15mm"
                }
            });

            return pdfBytes;
        }
        finally
        {
            if (page != null)
            {
                try { await page.CloseAsync(); }
                catch { /* Disposal can hang — GitHub issue #1489 */ }
            }
            _semaphore.Release();
        }
    }

    // Restart browser periodically to reclaim leaked memory
    public async Task RecycleBrowserAsync()
    {
        var oldBrowser = _browser;
        await InitializeAsync();
        try { oldBrowser?.Dispose(); } catch { }
    }

    public void Dispose()
    {
        _browser?.Dispose();
        _semaphore?.Dispose();
    }
}
using PuppeteerSharp;

public class PuppeteerPdfService : IDisposable
{
    private IBrowser _browser;
    private readonly SemaphoreSlim _semaphore;
    private long _pdfCount = 0;

    public PuppeteerPdfService()
    {
        // Limit concurrent pages to prevent memory exhaustion
        _semaphore = new SemaphoreSlim(3, 3);
    }

    public async Task InitializeAsync()
    {
        // Step 1: Download Chromium binary (~280MB)
        var fetcher = new BrowserFetcher();
        await fetcher.DownloadAsync();

        // Step 2: Launch with production-hardened flags
        _browser = await Puppeteer.LaunchAsync(new LaunchOptions
        {
            Headless = true,
            Args = new[]
            {
                "--no-sandbox",              // Required in Docker
                "--disable-setuid-sandbox",  // Required in Docker
                "--disable-dev-shm-usage",   // Prevent /dev/shm exhaustion
                "--disable-gpu",
                "--no-zygote",
                "--single-process",
                "--disable-extensions",
                "--max_old_space_size=4096"
            },
            Timeout = 30000
        });
    }

    public async Task<byte[]> GeneratePdfAsync(string html)
    {
        await _semaphore.WaitAsync();
        IPage page = null;

        try
        {
            Interlocked.Increment(ref _pdfCount);
            page = await _browser.NewPageAsync();

            await page.SetContentAsync(html, new NavigationOptions
            {
                WaitUntil = new[] { WaitUntilNavigation.Networkidle0 },
                Timeout = 20000
            });

            var pdfBytes = await page.PdfAsync(new PdfOptions
            {
                Format = PaperFormat.A4,
                PrintBackground = true,
                MarginOptions = new MarginOptions
                {
                    Top = "20mm", Bottom = "20mm",
                    Left = "15mm", Right = "15mm"
                }
            });

            return pdfBytes;
        }
        finally
        {
            if (page != null)
            {
                try { await page.CloseAsync(); }
                catch { /* Disposal can hang — GitHub issue #1489 */ }
            }
            _semaphore.Release();
        }
    }

    // Restart browser periodically to reclaim leaked memory
    public async Task RecycleBrowserAsync()
    {
        var oldBrowser = _browser;
        await InitializeAsync();
        try { oldBrowser?.Dispose(); } catch { }
    }

    public void Dispose()
    {
        _browser?.Dispose();
        _semaphore?.Dispose();
    }
}
Imports PuppeteerSharp
Imports System.Threading

Public Class PuppeteerPdfService
    Implements IDisposable

    Private _browser As IBrowser
    Private ReadOnly _semaphore As SemaphoreSlim
    Private _pdfCount As Long = 0

    Public Sub New()
        ' Limit concurrent pages to prevent memory exhaustion
        _semaphore = New SemaphoreSlim(3, 3)
    End Sub

    Public Async Function InitializeAsync() As Task
        ' Step 1: Download Chromium binary (~280MB)
        Dim fetcher = New BrowserFetcher()
        Await fetcher.DownloadAsync()

        ' Step 2: Launch with production-hardened flags
        _browser = Await Puppeteer.LaunchAsync(New LaunchOptions With {
            .Headless = True,
            .Args = {
                "--no-sandbox",              ' Required in Docker
                "--disable-setuid-sandbox",  ' Required in Docker
                "--disable-dev-shm-usage",   ' Prevent /dev/shm exhaustion
                "--disable-gpu",
                "--no-zygote",
                "--single-process",
                "--disable-extensions",
                "--max_old_space_size=4096"
            },
            .Timeout = 30000
        })
    End Function

    Public Async Function GeneratePdfAsync(html As String) As Task(Of Byte())
        Await _semaphore.WaitAsync()
        Dim page As IPage = Nothing

        Try
            Interlocked.Increment(_pdfCount)
            page = Await _browser.NewPageAsync()

            Await page.SetContentAsync(html, New NavigationOptions With {
                .WaitUntil = {WaitUntilNavigation.Networkidle0},
                .Timeout = 20000
            })

            Dim pdfBytes = Await page.PdfAsync(New PdfOptions With {
                .Format = PaperFormat.A4,
                .PrintBackground = True,
                .MarginOptions = New MarginOptions With {
                    .Top = "20mm", .Bottom = "20mm",
                    .Left = "15mm", .Right = "15mm"
                }
            })

            Return pdfBytes
        Finally
            If page IsNot Nothing Then
                Try
                    Await page.CloseAsync()
                Catch
                    ' Disposal can hang — GitHub issue #1489
                End Try
            End If
            _semaphore.Release()
        End Try
    End Function

    ' Restart browser periodically to reclaim leaked memory
    Public Async Function RecycleBrowserAsync() As Task
        Dim oldBrowser = _browser
        Await InitializeAsync()
        Try
            oldBrowser?.Dispose()
        Catch
        End Try
    End Function

    Public Sub Dispose() Implements IDisposable.Dispose
        _browser?.Dispose()
        _semaphore?.Dispose()
    End Sub
End Class
$vbLabelText   $csharpLabel

PDFを1つ作成するまでに80行以上あります。 また、エラーリカバリー、ヘルスチェック、メトリクス、本番システムが必要とするメモリリサイクルタイマーがまだ欠けています。

なぜこの複雑さが .NET デプロイメントにとって重要なのか

運用の負担は、デプロイメントの対象によって変化します:

Docker:コンテナイメージにChromiumを含める必要があります。 そのため、イメージに~280MBが追加され、プル時間、レジストリのストレージコスト、コールドスタートの待ち時間が増加します。 あなたのDockerfileには、Chromiumのシステム依存のための明示的なapt-get installコマンドが必要です - libgbm-devlibasound2libatk-bridge2.0-0、その他ベースイメージによって異なる約15個。

Azure Functions / AWS Lambda:サーバーレス環境では、メモリと実行時間が制約されます。Chromium のコールド スタート(ブラウザ プロセスのダウンロードと起動)は、5~10 秒と 500MB 以上のメモリを消費します。 Lambdaの250MBのデプロイメントパッケージの制限は、Chromiumがほとんど入らないことを意味し、Azure Consumptionプランの1.5GBのメモリ上限は、実際のPDF生成のためのほとんどスペースを残さない。

Kubernetes:ブラウザプロセスは、コンテナオーケストレーションと相性がよくありません。 Chromiumがレンダラー・プロセスを生成するとき、アプリケーション・コードでは問題ないように見えるメモリ制限が、不十分なものになります。 ポッドOOMKillは、アプリケーションが実際に必要とするメモリ要求よりもかなり高く設定しない限り、定期的に発生します。

同等のIronPDFコード

var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
var renderer = new ChromePdfRenderer();
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4;
renderer.RenderingOptions.MarginTop = 20;
renderer.RenderingOptions.MarginBottom = 20;

var pdf = renderer.RenderHtmlAsPdf(html);
pdf.SaveAs("output.pdf");
Dim renderer As New ChromePdfRenderer()
renderer.RenderingOptions.PaperSize = IronPdf.Rendering.PdfPaperSize.A4
renderer.RenderingOptions.MarginTop = 20
renderer.RenderingOptions.MarginBottom = 20

Dim pdf = renderer.RenderHtmlAsPdf(html)
pdf.SaveAs("output.pdf")
$vbLabelText   $csharpLabel

6行。 IronPdfはChromiumを内部に組み込みます - ブラウザのライフサイクル、メモリ管理、プロセスプールはライブラリによって処理されます。 SemaphoreSlimはありません。 ブラウザリサイクルはありません。 Dockerfileの修正はありません。 NuGetパッケージにはすべてが含まれています。

IronPdfのNuGetパッケージはChromiumバイナリを含むため、PdfSharpのパッケージよりも大きくなります。 最初のPDFの待ち時間は、エンジンの初期化中に2~5秒、その後の世代では100~500msです。 デプロイメント・サイズが主な制約となるアプリケーションでは、それが重要です。 開発者の時間と操作の信頼性がより重要なアプリケーションでは、組み込み型アプローチの勝利です。

.NETエコシステムの健全性:NuGetパッケージの比較。

ライブラリを選択する前に、NuGetエコシステムの健全性を確認してください。 依存関係の連鎖、リリース頻度、問題解決時間は、機能リストよりも重要です:

ライブラリNuGetダウンロード最終リリースオープンな課題.NET 8 TFMネイティブの依存関係
PdfSharp34M+活発低レベルなし
QuestPDF8M+活発低レベルなし
iText Core30M+活発適度なし
IronPDF10M+活発低レベルChromium (バンドル)
DinkToPdf5M+2018300+wkhtmltopdfバイナリ
PuppeteerSharp15M+活発適度クロミウム(外部)

DinkToPdf のような放棄されたパッケージのダウンロード数の多さは、現在の健全性ではなく、レガシーな採用状況を反映しています。 回答がない300件以上の未解決の問題が、本当のことを物語っています。

net8.0<//code> TFMをターゲットとする.NET 8アプリケーションの場合: PdfSharp、QuestPDF、iText Core、IronPDFはすべてネイティブにサポートしています。 wkhtmltopdfラッパーは、DllNotFoundExceptionNU1202ターゲットフレームワークの互換性エラーは発生しません。

意思決定マトリックス

要件PdfSharpQuestPDFiText CorewkhtmltopdfPuppeteerSharpIronPDF
本当に無料です(MIT/パーミッシブ)❌ 収益ゲート❌AGPL⚠️ 断念商用
HTMLからPDFへ⚠️ 有限会社⚠️ 壊れた CSS
モダンCSS(フレックスボックス/グリッド)
JavaScriptの実行⚠️ ES5 のみ
ブラウザ管理なし
アクティブなセキュリティパッチ
収益のしきい値はありません該当なし該当なし
予測可能なライセンスコスト無料100万ドルのクリフ~平均 45,000 ドル/年該当なし無料永久749ドル
Dockerフレンドリー⚠️ バイナリ⚠️ +280MB✅ 自己完結型であること。
サーバーレス対応⚠️ コールドスタート

構造化データからのプログラムによるPDF作成のみが必要な場合: PdfSharp (MIT、制限なし) または QuestPDF (より優れたAPI、収益のしきい値に注意)。

モダンなCSSによるHTMLからPDFへの変換が必要で、ブラウザのインフラを管理したくない場合: IronPDF。 Embedded Chromiumは、レンダリングエンジンのライフサイクルを処理します。 iTextのサブスクリプションモデルの数分の一の価格で公開。

HTMLからPDFへの変換が必要で、ブラウザプロセスの管理に慣れている場合:PuppeteerSharpは完全なコントロールを提供します。 運用経費の予算

現在wkhtmltopdfまたはそのラッパーを使用している場合: 移行してください。 また、毎月のようにCVEリストにパッチが適用されないままになってしまいます。