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>
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")
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")
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")
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")
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
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")
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.