PORóWNANIE

Darmowe biblioteki PDF dla .NET: Ukryte koszty i lepsze alternatywy w C#

Darmowe biblioteki PDF dla .NET wiążą się z ukrytymi kosztami: pułapkami licencji AGPL, brakiem obsługi HTML, przestarzałymi zależnościami z niezałatanymi lukami CVE, progami przychodów oraz złożonością operacyjną, która często przewyższa koszty licencji komercyjnych.

Zanim zdecydujesz się na którekolwiek z nich, uruchom to w terminalu:


<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

Jeśli Twoja aplikacja .NET korzysta zwkhtmltopdflub którejkolwiek z jego nakładek — DinkToPdf, TuesPechkin, Rotativa, NReco.PdfGenerator — ten kod HTML spowoduje wykonanie ataku typu Server-Side Request Forgery na punkt końcowy metadanych Twojego dostawcy usług w chmurze. Poświadczenia AWS IAM, tokeny tożsamości zarządzanej platformy Azure, klucze kont usług GCP. Wszystko ujawnione. Projekt został zarchiwizowany w styczniu 2023 r. Nie planuje się wydania żadnej poprawki.

Tyle kosztuje "bezpłatność" w produkcji.

Szybki start: Oceń biblioteki PDF dla swojego projektu .NET

// 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

Różnica między tymi dwoma przykładami to różnica między tym, czego potrzebuje większość programistów .NET, a tym, co oferuje większość darmowych bibliotek.

Co właściwie oznacza "bezpłatny" w przypadku bibliotek PDF dla platformy .NET?

Wyszukaj w NuGet hasło "PDF", a znajdziesz biblioteki w pięciu modelach licencyjnych, z których każdy ma inne ograniczenia dotyczące wdrażania w środowisku .NET:

MIT/Apache (naprawdę liberalne): PdfSharp. Używaj go w aplikacjach komercyjnych, kontenerach Docker, Azure Functions, AWS Lambda — bez ograniczeń, bez progów przychodów, bez ujawniania kodu źródłowego. Haczyk: nie można konwertować HTML na PDF.

AGPL (pułapka copyleft):iText Core(dawniej iTextSharp). Wdroż to w dowolnej aplikacji dostępnej w sieci — aplikacjach internetowych, interfejsach API REST, mikrousługach — i musisz udostępnić cały swój kod źródłowy na licencji AGPL. Firmy SaaS nie są wyłączone z tego wymogu.

Ograniczone do wersji komercyjnej: Licencja społecznościowa QuestPDF. Bezpłatnie przy rocznych przychodach brutto poniżej 1 mln USD. Jeśli przekroczysz ten próg, potrzebujesz licencji komercyjnej. Przejście nie jest płynne — to przepaść.

Porzucone:wkhtmltopdfi wszystkie opakowania .NET. Zarchiwizowane z niezałatanymi lukami CVE o ocenie 9,8 (krytyczne). Zero nakładów na utrzymanie bezpieczeństwa. Odpowiedzialność w przypadku każdego audytu zgodności.

Kosztowne w eksploatacji:PuppeteerSharpi Playwright dla .NET. Brak ograniczeń licencyjnych, pełna obsługa nowoczesnego CSS — ale w środowisku produkcyjnym zarządzasz zewnętrznymi procesami przeglądarki, pobieraniem Chromium i cyklem życia pamięci.

Każda kategoria wiąże się z innym ryzykiem dla wdrożeń .NET. W dalszej części artykułu omówiono te zagrożenia, posługując się kodem, liczbami i danymi dotyczącymi ekosystemu NuGet.

Dłączego ceny iText są tak istotne?

Większość artykułów na temat iText skupia się na kwestii egzekwowania licencji AGPL. To zostało omówione gdzie indziej. Bardziej istotnym pytaniem dla zespołów .NET oceniających biblioteki PDF jest to, co się dzieje, gdy potrzebna jest licencja komercyjna.

Ile faktycznie kosztuje komercyjna wersja iText?

W kwietniu 2020 r. firma iText przeszła z modelu Licencji wieczystych na model oparty na subskrypcji. Dane dotyczące cen od podmiotów zewnętrznych pochodzące z bazy danych transakcji Vendr pokazują:

  • Średnia roczna wartość kontraktu: ~45 000 USD
  • Umowy z wyższej półki: do 210 000 USD w zależności od objętości plików PDF
  • Model cenowy: oparty na wolumenie — koszty rosną proporcjonalnie do liczby plików PDF generowanych przez aplikację w ciągu roku

Ten model oparty na objętości powoduje, że budżety dla rozwijających się aplikacji są nieprzewidywalne. Mikrousługa .NET generująca 10 000 plików PDF miesięcznie w pierwszym kwartale, której skala wzrasta do 100 000 w czwartym kwartale, będzie miała rachunek licencyjny proporcjonalny do tego wzrostu — a poziomy cenowe iText nie są podawane do wiadomości publicznej.

Porównaj to z opublikowanymi cenami IronPDF: 749 USD za Licencję wieczystą Lite. Brak rocznej subskrypcji. Bez liczenia objętości. Żadnych niespodzianek podczas skalowania aplikacji.

Jak model subskrypcji wpływa na zespoły .NET?

Przejście z licencji wieczystej na subskrypcyjną zmienia sposób obliczania całkowitego kosztu posiadania (TCO):

Czynnik Subskrypcja iText IronPDF Licencja wieczysta
Koszt w pierwszym roku ~45 000 USD 749–2999 USD
Koszt w trzecim roku ~135 000 USD 749–2999 USD (opłata jednorazowa)
Skalowanie objętości Wzrost kosztów Brak pomiaru objętości
Przewidywalność budżetu Zmienna Fixed
Ryzyko anulowania Utrata dostępu Własność wieczysta

Dla zespołu .NET tworzącego produkt SaaS różnica w ciągu pięciu lat może przekroczyć 200 000 dolarów. To właśnie ta kwestia cenowa ma większe znaczenie niż debaty dotyczące egzekwowania licencji AGPL.

Jakie są ograniczeniaPdfSharpw przypadku wdrożeń .NET?

PdfSharp stanowi wyjątek wśród bezpłatnych bibliotek: licencja MIT, ponad 34 miliony pobrań z NuGet, prawdziwie liberalne warunki użytkowania komercyjnego. Brak progów przychodów. Nie ujawniać kodu źródłowego.

Ograniczenie ma charakter architektoniczny.PdfSharpdziała na poziomie współrzędnych PDF. Nie ma parsera HTML, silnika CSS ani renderowania 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

To 20 wierszy dla faktury z jednym wierszem, bez stylizacji, bez responsywnego układu i bez CSS. Wyobraź sobie teraz tworzenie 15-stronicowego raportu zgodności zawierającego dynamiczne dane, wykresy i elementy identyfikacji wizualnej firmy.

Kwestie związane z wdrażaniem na wielu platformach

PdfSharp działa dobrze w scenariuszach wielopłatformowych .NET 6+ — nie ma żadnych natywnych zależności, plików binarnych Chromium ani procesów zewnętrznych. Rozwiązanie to można łatwo wdrożyć w Dockerze, Azure Functions i AWS Lambda przy minimalnym rozmiarze kontenera.

W przypadku aplikacji, które wymagają jedynie programowego tworzenia plików PDF na podstawie danych strukturalnych — etykiet wysyłkowych, prostych paragonów, wykresów współrzędnych —PdfSharpjest dobrym wyborem. Jego status w NuGet jest dobry: aktywne commit'y, responsywny opiekun, regularne wydania.

W przypadku treści HTML, szablonów internetowych lub nowoczesnego CSS,PdfSharpnie jest odpowiednim narzędziem.

KiedyQuestPDFprzestaje być darmowy?

QuestPDF przyjął inne podejście projektowe niż PdfSharp: płynny interfejs API, który czyta się jak opis układu, a nie jak matematykę współrzędnych. Projekt API jest naprawdę dobry.

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

Jest to bardziej wyraziste niż układ współrzędnych PdfSharp. Ma jednak tę samą podstawową wadę: brak renderowania HTML.

Przełom w przychodach

Licencja społecznościowaQuestPDFjest bezpłatna dla firm, których roczne przychody brutto nie przekraczają 1 000 000 USD. Po przekroczeniu tego progu wymagańa jest licencja Professional (699 USD/rok) lub Licencja Enterprise (1999 USD/rok).

Dla startupu tworzy to scenariusz harmonogramu rozwoju:

  • Rok 1 (przychody w wysokości 200 000 USD): Bezpłatnie. Płynny interfejs APIQuestPDFprzyspiesza początkowy etap rozwoju.
  • Rok 2 (przychody w wysokości 600 000 USD): Nadal bezpłatne. API jest głęboko zintegrowane z kodem źródłowym.
  • Rok 3 (przychody w wysokości 1,1 mln USD): Wymagana licencja. Jesteś teraz przywiązany do API, co wiąże się ze znacznymi kosztami zmiany.

Przejście nie dotyczy kosztów licencji — chodzi o koszty związane z przejściem, które się zgromadziły. W trzecim roku warstwa generująca pliki PDF może obejmować dziesiątki plików z płynnymi wywołaniami API, które nie mają odpowiedników w innych bibliotekach.

Błędne przekonanie dotyczące HTML

Programiści korzystający z frameworków internetowych oczekują, że nowoczesna biblioteka .NET do obsługi plików PDF będzie akceptować dane wejściowe w formacie HTML.QuestPDFwyraźnie nie obsługuje konwersji HTML do PDF. Jego API opiera się wyłącznie na kodzie — każdy element układu jest wywołaniem metody, a nie znacznikiem.

Ta rozbieżność dotyka zespoły, które kupują licencjeQuestPDF(lub opierają się na wersji Community), by w połowie projektu odkryć, że ich istniejące szablony faktur HTML, przepływy pracy typu e-mail-do-PDF lub generatory raportów w ogóle nie mogą korzystać z QuestPDF.

IronPDF akceptuje HTML, CSS i JavaScript jako dane wejściowe, ponieważ zawiera silnik renderujący Chromium. Ten sam kod HTML, który wyświetla się w przeglądarce Chrome, wyświetla się identycznie w pliku PDF.

Dłączego należy unikaćwkhtmltopdfwe wdrożeniach .NET?

Nie bez powodu rozpocząłem ten artykuł od CVE-2022-35583. Ta luka SSRF nie jest teoretyczna — exploity typu proof-of-concept są publicznie dostępne i aktywnie wykorzystywane.

Pełny obraz bezpieczeństwa

wkhtmltopdf zawiera dwa niezałatane luki CVE, które nigdy nie zostaną naprawione:

CVE-2022-35583 (CVSS 9,8 – krytyczne): fałszowanie żądania po stronie serwera poprzez wstrzyknięcie iframe. W środowiskach chmurowych powoduje to ujawnienie punktów końcowych metadanych instancji — poświadczeń AWS IAM, tokenów tożsamości zarządzanych w Azure, kluczy kont usług GCP oraz tokenów kont usług Kubernetes.

CVE-2020-21365 (CVSS 7,5 – wysokie): Przejście katalogowe umożliwiające zdalnym atakującym odczytanie lokalnych plików poprzez spreparowane dane wejściowe HTML.

Dla obu luk w zabezpieczeniach dostępny jest publiczny kod wykorzystujący te luki. Oba są aktywnie wykorzystywane. Żadne z nich nie otrzyma poprawki.

Stan ekosystemu .NET Wrapper

Każda nakładka .NET dlawkhtmltopdfdziedziczy te luki w zabezpieczeniach i zwiększa własne zadłużenie konserwacyjne:

Wrapper Ostatnia znacząca zmiana Otwarte zgłoszenia Wsparcie dla .NET 8
DinkToPdf 2018 Ponad 300 bez odpowiedzi Nie
TuesPechkin 2015 Porzucony Nie
Rotativa 2019 Tylko MVC Nie
NReco.PdfGenerator Aktywne Komercjalne Ograniczone

NReco jest jedyną aktywnie utrzymywaną nakładką, ale nadal opiera się na pliku binarnymwkhtmltopdf— co oznacza, że luki CVE przenoszą się wraz z nim.

Renderowanie zatrzymane w 2013 roku

Poza kwestią bezpieczeństwa, silnik Qt WebKitwkhtmltopdfjest zamrożony na standardach internetowych z 2013 roku. Bez CSS Flexbox. Bez CSS Grid. Brak zmiennych CSS. Brak calc(). JavaScript ES6+ nie działa.

Każda aplikacja .NET Framework korzystająca z Tailwind CSS, Bootstrap 5 lub nowoczesnych frameworków CSS będzie generować nieprawidłowy wynik. W przypadku aplikacji .NET 8 przeznaczonej do wdrożenia w kontenerach, nieaktualizowany plik binarny bez obsługi nowoczesnych standardów internetowych stanowi dług techniczny, który decydujesz się ponosić.

Jaki jest rzeczywisty koszt operacyjny PuppeteerSharp?

To właśnie w tej sekcji większość artykułów na temat "bezpłatnych bibliotek PDF" popełnia błędy.PuppeteerSharpi Playwright dla .NET są doskonałe pod względem technicznym — renderują HTML za pomocą prawdziwego Chromium, obsługując wszystkie funkcje CSS i API JavaScript. Brak ograniczeń licencyjnych. Brak progów przychodów.

Koszt jest operacyjny. Oto jak w rzeczywistości wygląda generowanie plików PDF za pomocą PuppeteerSharp:

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

To ponad 80 wierszy, zanim wygenerujesz choćby jeden plik PDF. Wciąż brakuje funkcji odzyskiwania po błędach, kontroli stanu, wskaźników oraz timera recyklingu pamięci, które są niezbędne w systemach produkcyjnych.

Dłączego ta złożoność ma znaczenie dla wdrożeń .NET

Obciążenie operacyjne rośnie wraz z celem wdrożenia:

Docker: Musisz dołączyć Chromium do obrazu kontenera. Powoduje to zwiększenie rozmiaru obrazu o około 280 MB, co wydłuża czas pobierania, zwiększa koszty przechowywania w rejestrze i opóźnienie przy zimnym starcie. Plik Dockerfile wymaga jawnych polecen apt-get install dla zależności systemówych Chromium — libgbm-dev, libasound2, libatk-bridge2.0-0 i mniej więcej 15 innych, które różnią sie w zależności od obrazu bazowego.

Azure Functions / AWS Lambda: Środowiska bezserwerowe ograniczają pamięć i czas wykonywania. Zimny start Chromium — pobieranie i uruchamianie procesu przeglądarki — może zająć 5–10 sekund i zużyć ponad 500 MB pamięci. Limit pakietu wdrożeniowego Lambda wynoszący 250 MB oznacza, że Chromium ledwo się mieści, a limit pamięci planu Azure Consumption wynoszący 1,5 GB pozostawia niewiele miejsca na faktyczne wygenerowanie pliku PDF.

Kubernetes: Procesy przeglądarki nie współdziałają dobrze z orkiestracją kontenerów. Limity pamięci, które wydają się wystarczające dla kodu aplikacji, stają się niewystarczające, gdy Chromium uruchamia procesy renderowania. Pod OOMKills stają się częstym zjawiskiem, chyba że ustawisz żądania pamięci znacznie wyższe niż faktyczne potrzeby aplikacji.

Odpowiednik kodu 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

Sześć wierszy.IronPDFwykorzystuje wewnętrznie Chromium — cykl życia przeglądarki, zarządzanie pamięcią i puli procesów są obsługiwane przez bibliotekę. Bez SemaphoreSlim. Nie należy używać gotowych tłumaczeń z innych stron. Nie należy wprowadzać żadnych zmian w pliku Dockerfile. Pakiet NuGet zawiera wszystko.

Kompromis jest realny: pakiet NuGetIronPDFjest większy niż pakiet PdfSharp, ponieważ zawiera plik binarny Chromium. Opóźnienie pierwszego pliku PDF wynosi 2–5 sekund podczas inicjalizacji silnika, a następnie 100–500 ms dla kolejnych generacji. W przypadku aplikacji, w których rozmiar wdrożenia jest głównym ograniczeniem, ma to znaczenie. W przypadku zastosowań, w których ważniejszy jest czas programisty i niezawodność działania, lepszym rozwiązaniem jest podejście oparte na wbudowanych komponentach.

Stan ekosystemu .NET: Porównanie pakietów NuGet

Przed wyborem biblioteki sprawdź kondycję jej ekosystemu NuGet. Łańcuchy zależności, częstotliwość wydawania nowych wersji i czas rozwiązywania problemów mówią więcej niż listy funkcji:

Biblioteka Pobieranie z NuGet Ostatnia wersja Otwarte zgłoszenia .NET 8 TFM Zależności natywne
PdfSharp 34 mln+ Aktywne Low None
QuestPDF 8 mln+ Aktywne Low None
iText Core Ponad 30 mln Aktywne Umiarkowany None
IronPDF 10 mln+ Aktywne Low Chromium (w pakiecie)
DinkToPdf 5 mln+ 2018 300+ wkhtmltopdfbinary
PuppeteerSharp Ponad 15 mln Aktywne Umiarkowany Chromium (zewnętrzne)

Duża liczba pobrań porzuconych pakietów, takich jak DinkToPdf, odzwierciedla ich dawną popularność, a nie aktualny stan. Ponad 300 otwartych zgłoszeń bez odpowiedzi mówi same za siebie.

Dla aplikacji .NET 8 celujacych w net8.0 TFM: PdfSharp, QuestPDF,iText CoreorazIronPDFwspierają to natywnie.wkhtmltopdfwrappers tego nie robia — oczekuj błędów niezgodnośći DllNotFoundException i NU1202 dla docelowych platform.

Macierz decyzyjna

Wymagania PdfSharp QuestPDF iText Core wkhtmltopdf PuppeteerSharp IronPDF
Całkowicie bezpłatne (licencja MIT/liberalna) ❌Brama przychodów ❌ AGPL ⚠️ Porzucone ❌Komercyjne
HTML do PDF ⚠️ Ograniczone ⚠️ Uszkodzony CSS
Nowoczesny CSS (Flexbox/Grid)
Wykonanie kodu JavaScript ⚠️ Tylko ES5
Brak zarządzania przeglądarką
Aktywne poprawki zabezpieczeń
Brak progu przychodów Nie dotyczy Nie dotyczy
Przewidywalny koszt licencji Bezpłatne Cliff za 1 mln dolarów ~45 000 USD/rok średnio Nie dotyczy Bezpłatne 749 USD na zawsze
Kompatybilne z Dockerem ✅Małe ✅Małe ✅Małe ⚠️ Pliki binarne ⚠️ +280 MB ✅Samodzielny
Kompatybilność z technologią bezserwerową ⚠️ Rozruch na zimno

Jeśli potrzebujesz jedynie programowego tworzenia plików PDF na podstawie danych ustrukturyzowanych:PdfSharp(MIT, bez ograniczeń) lubQuestPDF(lepsze API, zwróć uwagę na próg przychodów).

Jeśli potrzebujesz konwersji HTML do PDF z nowoczesnym CSS i nie chcesz zajmować się infrastrukturą przeglądarki: IronPDF. Wbudowany Chromium obsługuje cykl życia silnika renderującego. Opublikowane ceny stanowią ułamek kosztów modelu subskrypcyjnego iText.

Jeśli potrzebujesz konwersji HTML do PDF i nie masz problemów z zarządzaniem procesami przeglądarki:PuppeteerSharpzapewnia pełną kontrolę. Budżet na koszty operacyjne.

Jeśli obecnie korzystasz zwkhtmltopdflub którejkolwiek z jego nakładek: Przejdź na nową wersję. Już samo zagrożenie bezpieczeństwa uzasadnia ten wysiłek — a z każdym miesiącem opóźnienia lista CVE pozostaje bez poprawek.