KARşıLAşTıRMA

Ücretsiz PDF Kütüphaneleri için .NET: Gizli Maliyetler ve C#'ta Daha İyi Alternatifler

.NET için ücretsiz PDF kütüphaneleri, gizli maliyetlerle gelir: AGPL lisans tuzakları, eksik HTML desteği, yamalanmamış CVE'lerle birlikte eski bağımlılıklar, gelir eşikleri ve genellikle ticari lisans maliyetlerini aşan operasyonel karmaşıklık.

Onlardan herhangi birine taahhüt etmeden önce, bunu bir terminalde çalıştırın:


<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 uygulamanızwkhtmltopdfveya onun bir sarıcısını — DinkToPdf, TuesPechkin, Rotativa, NReco.PdfGenerator — kullanıyorsa, bu HTML cloud sağlayıcınızın meta veri uç noktasında bir Sunucu Tarafı Talep Sahteciliği gerçekleştirecektir. AWS IAM kimlik bilgileri, Azure yönetilen kimlik belirteçleri, GCP hizmet hesabı anahtarları. Hepsi açığa çıktı. Proje Ocak 2023'te arsivlendi. Hicbir yama gelmiyor.

"Ücretsiz" maliyeti uretimde bu kadar.

Hizli Başlangic: .NET Projeniz icin PDF Kutuphanelerini Degerlendirin

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

Bu iki örnek arasindaki fark, cogunlukla .NET geliştiricilerinin ihtiyaç duydugu sey ile cogunlukla ücretsiz kutuphanelerin sundugu sey arasindaki farktir.

"Ücretsiz" Eger .NET PDF Kutuphanelerinde Gerçekten Ne Anlama Geliyor?

"PDF" icin NuGet aramasi yapin ve .NET dağıtimlari icin farkli kisitlamalari olan bes lisanslama modelinde kutuphaneler bulacaksiniz:

MIT/Apache (gercekten musamaha gösteren): PdfSharp. Ticari uygulamalarda, Docker kapsayicilarinda, Azure Functions, AWS Lambda'da kullanin — hicbir sinirlama yok, hicbir gelir esigi yok, hicbir kaynak kodu aciklamasi yok. Isin yakin: HTML'yi PDF'e dönüştüremez.

AGPL (copyleft tuzagi):iText Core(eski iTextSharp). Herhangi bir agda erisilebilir uygulamada dağıtin — web uygulamalari, REST API'leri, mikrohizmetler — ve tüm kaynak kodunuzu AGPL altinda yayimlamaniz gerekecek. SaaS şirketleri icin muafiyet yok.

Gelir-kapali:QuestPDFTopluluk Lisansi. Yillik $1M brut gelir altinda ücretsiz. Bu esigi gecerseniz ticari bir lisans gerekir. Gecis asamali değil — bir ucurum.

Terk edilmis:wkhtmltopdfve tüm .NET cevreleri. Dokunulmamis CVE'ler 9.8 Kritik ile arsivlenmis. Sifir guvenlik bakimi. Herhangi bir uyumluluk denetiminde bir sorumluluk.

Isletim olarak pahali:PuppeteerSharpve Playwright for .NET. Lisans kisitlamasi yok, tam modern CSS destegi var — ancak uretimde harici tarayici sureclerle, Chromium indirmeleriyle ve bellek omru ile ilgileniyorsunuz.

Her kategori, .NET dağıtimlari icin farkli riskler oluşturur. Bu makalenin geri kalani, bu riskleri kod, sayilar ve NuGet ekosistem verileri ile aciklamaktadir.

Neden iText'in Fiyatlandirilmasi Asil Hikaye?

Cogu iText hakkinda makale, AGPL uygulama acisina odaklanir. Baska yerlerde ele alinmistir. .NET takimlarinin PDF kutuphanelerini degerlendirirken daha alakali olan soru, ticari bir lisansa ihtiyaç duydugunuzda neler oldugu.

Ticari iText Gerçekten Ne Kadara Mal Olur?

Nisan 2020'de, iText süresiz lisanslamadan abone bazli modellere gecti. Vendr'in işlem veritabanindan alinmis ucuncu taraf fiyat verileri su konulari göstermektedir:

  • Ortalama yillik sözleşme: ~$45,000
  • Yuksek ucretli sözleşmeler: PDF hacmine gore $210,000 kadar
  • Fiyat modeli: Hacme dayali — uygulamanizin yilda generate ettigi PDF sayisiyla maliyetler artar

Bu hacme dayali model, buyuyen uygulamalar icin kontrol edilemez butceler yaratir. Q1 doneminde ayda 10,000 PDF oluşturan ve Q4 rominde 100,000'e kadar artan .NET mikrohizmeti, lisans faturasinin buna gore olculduklerini gorecek — ve iText'in fiyatlandirma kademeleri acik degildir.

IronPDF'nin yayimlanmis fiyatlarini karsilastirin: süresiz Lite lisansi icin $749. Yillik abone yok. Hacim olcme yok. Uygulamaniz buyudukce surpriz yok.

Abonelik Modeli .NET Takimlarini Nasıl Etkiliyor?

Süresiz lisanslamadan abone modellere gecis, TCO hesaplamasini değiştiriyor:

FaktöriText AbonelikIronPDFSureli
1. yil maliyeti~$45.000$749 - $2.999
3. yil maliyeti~$135.000$749 - $2.999(bir seferlik)
Hacim olclenmesiMaliyetler artiyorHacim olcme yok
Butce tahmin edilebilirligiDeğişkenFixed
Iptal riskiErişim kaybedilirSüresiz sahiplik

.NET takimi icin bir SaaS urunu geliştirirken, bes yillik delta $200,000'i asabilir. Bu, AGPL uygulama tartismalarindan daha onemli olan fiyat hikayesidir.

PdfSharp'in .NET Dağıtimlari Icin Sinirlamalari Nelerdir?

PdfSharp ücretsiz kutuphaneler arasinda bir istisna: MIT lisansi, 34+ milyon NuGet indirilmesi, ticari kullanim icin gercekten musamaha gösteren. Gelir eşikleri yok. Kaynak kodu aciklamasi yok.

Sinirlama mimari derinliktedir.PdfSharpPDF koordinat seviyesinde calismaktadir. HTML parcayicisi, CSS motoru, DOM görüntülemesi yok.

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

Bu, stil, duyarlı yerleşim, CSS olmadan tek satirlik bir fatura icin 20 satirdir. Simdi dinamik veriler, grafikler ve kurumsal marka ile 15 sayfalik bir uyumluluk raporu oluşturmayi dusunun.

Platformlar Arasi Dağıtim Dusunceleri

PdfSharp, .NET 6+ platformlar arasi senaryolarda iyi calisir — yerli bagimlilik yok, Chromium ikilileri yok, harici surecler yok. Minimal kapsayici boyutuyla Docker, Azure Functions, ve AWS Lambda'ya temizce dağıtilir.

Sadece yapisal verilerden programatik PDF oluşturma gerektiren uygulamalar icin — sevkiyat etiketleri, basit makbuzlar, koordinatla cizilmis diyagramlar —PdfSharpuygun bir secimdir. NuGet sagligi guclu: aktif bagislama, duyarlı bakici, düzenli yayinlar.

HTML icerigi, web semalari veya modern CSS ile ilgili herhangi bir sey icinPdfSharpyanlis aracdir.

QuestPDFNe Zaman Ücretsiz Olmaktan Cikar?

QuestPDF, PdfSharp'tan farkli bir tasarim yaklasimi aldi: koordinat matematigi yerine bir yerlesim tanimi gibi okunan akici bir API. API tasarımı gerçekten iyi.

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'ın koordinat sisteminden daha ifadelidir. Ancak ayni temel sinirlamayi paylasir: HTML'yi görüntüleme yok.

Gelir Ucurumu

QuestPDF'nin Topluluk Lisansi, yillik brut geliri $1,000,000 altinda olan şirketler icin ucretsizdir. Bu esigi gecerseniz, $699/yil icin Profesyonel veya $1,999/yil icin Kurumsal lisans gerekir.

Bir başlar icin, bu bir buyume zaman cizelgesi senaryosu oluşturuyor:

  • 1. Yil ($200K gelir): Ücretsiz. QuestPDF'nin akici API'si başlangic geliştirmeleri hizlandirir.
  • 2. Yil ($600K gelir): Hala ücretsiz. API kod tabaniniza derinlemesine entegre edilmis.
  • 3. Yil ($1.1M gelir): Lisans gerekli. Simdi API'ye kilitlenmis durumdasiniz ve onemli gecis maliyetleri.

Gecis lisans maliyetiyle ilgili değil, bir araya gelmiş gecis maliyetiyle ilgilidir. 3. yil itibariyla, PDF oluşturma katmaniniz, baska kutuphanelerde eslemesi olmayan akici API cagrisiyla onlarca dosyayi kapsayabilir.

HTML yanilgisina

Web cercevelerinden gelen geliştiriciler, modern bir .NET PDF kutuphanesinin HTML girdisini kabul edecegini beklener.QuestPDFkesinlikle HTML'den PDF'e donusumu desteklemez. API tamamen kod - her yerlesim oge bir metod cagrisidir, isaretleme değil.

Bu uyumsuzluk,QuestPDFlisanslari satin alan (veya Topluluk surumune dayali) takimlari, projede ortasında adim attiklarin yerleşik HTML fatura semalari, e-posta-PDF iş akışları, veya rapor generatorlerinin QuestPDF'yi hic kullanamayacaklarını keşfedene kadar tuzağa düser.

IronPDF Chrome'da görüntülen HTML, ayni sekilde bir PDF icinde görüntülenmeniz icin HTML, CSS, ve JavaScript'i girdi olarak kabul eder, cunku bir Chromium render motoru gomuludur.

wkhtmltopdf'yi .NET'de Herhangi Bir Dağıtimda Neden Kullanmayacaksiniz?

Bu makaleyi, bir neden icin CVE-2022-35583 ile actim. SSRF zafiyeti teorik değil — kanit konsepti istismarlar kamusal olarak mevcut ve aktif bir sekilde kullaniliyor.

Tam Guvenlik Tablosu

wkhtmltopdf artik asla duzelmeyecek iki onarilamamis CVE tasir:

CVE-2022-35583 (CVSS 9.8 Kritik): ifame enjeksiyonu yolundan sunucu-taraffni taleplerini sahtekarlik. Uzerinde bulut ortamlari, AWS IAM kimlik bilgileri, Azure yonetilen kimlik belirtecleri, GCP hizmet hesap anahtarlari, Kubernetes hizmet hesap belirtecleri bu sunucu metada verilerini aciklar.

CVE-2020-21365 (CVSS 7.5 Yuksek): ozenli HTML girdisi araciligiyla yerel dosyalari okumak icin uzaktan saldirganlarin dizin gecis izni.

Her iki zafiyet de halka acik exploit kodu içerir. Her ikisi de aktif olarak istismar edilmektedir. Hiçbiri bir yamaya sahip olmayacak.

.NET Sarıcı Ekosistem Sağlığı

wkhtmltopdf için her .NET sarmalayıcısı bu güvenlik açıklarını devralır ve kendi bakım borcunu ekler:

SariciSon Anlamlı CommitAçık Sorunlar.NET 8 Desteği
DinkToPdf2018300'den fazla yanıtsızHayır
TuesPechkin2015Terk edilmişHayır
Rotativa2019Sadece MVCHayır
NReco.PdfGeneratorAktifTicariSınırlı

NReco, halawkhtmltopdfikili dosyasına bağlı olduğu için, seyahat eden CVE'ler ile, aktif olarak bakımı yapılan tek sarmalayıcıdır.

Rendering, 2013'te Dondurulmuştur

Güvenliğin ötesinde, wkhtmltopdf'nin Qt WebKit motoru, 2013 döneminin web standartlarında dondurulmuştur. CSS Flexbox yok. CSS Grid yok. CSS Değişkenleri yok. Hayır calc(). ES6+ JavaScript çalıştırılamıyor.

Tailwind CSS, Bootstrap 5 veya modern CSS çerçeveleri kullanan herhangi bir .NET uygulaması bozuk çıktı üretecektir. Kapsayıcı dağıtımını hedefleyen bir .NET 8 uygulaması için, modern web standartlarını desteklemeyen bakım yapılmayan bir ikili dosya, taşımayı seçtiğiniz teknik borçtur.

PuppeteerSharp'ın Gerçek Operasyonel Maliyeti Nedir?

Bu bölüm, çoğu "ücretsiz PDF kitaplığı" makalesinde yanlış anlaşılmaktadır.PuppeteerSharpve .NET için Playwright teknik olarak mükemmeldir — gerçek Chromium üzerinden HTML'yi işler, her CSS özelliğini ve JavaScript API'sini destekler. Lisanslama kısıtlamaları yok. Gelir eşikleri yok.

Maliyet operasyoneldir. İşte üretimPuppeteerSharpPDF üretiminin aslında nasıl göründüğü:

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

Bir PDF oluşturulmadan önce 80'den fazla satır vardır. Ve hala hata kurtarma, sağlık kontrolleri, metrikler ve üretim sistemlerinin ihtiyaç duyduğu bellek geri dönüşüm zamanlayıcısı eksiktir.

Bu Karmaşıklık, .NET Dağıtımları İçin Neden Önemlidir?

Operasyonel yük, dağıtım hedefi ile ölçeklenmektedir:

Docker: Chromium'u kapsayıcı imajınıza eklemek zorundasınız. Bu, imaja ~280MB ekler, çekim sürelerini, kayıt depolama maliyetlerini ve soğuk başlangıç gecikmesini arttırır. Dockerfile'ınız, Chromium'un sistem bağımlılıkları için açık apt-get install komutlarına ihtiyaç duyar — libgbm-dev, libasound2, libatk-bridge2.0-0 ve temel imaja göre değişen yaklaşık 15 diğerleri.

Azure Functions / AWS Lambda: Sunucusuz ortamlar bellek ve yürütme süresini kısıtlar. Chromium'un soğuk başlangıcı — tarayıcı sürecini indirip başlatmak — 5-10 saniye ve 500MB'den fazla bellek tüketebilir. Lambda'nın 250MB dağıtım paketi sınırı, Chromium'un zar zor sığmasına neden olur ve Azure Tüketim planının 1.5GB bellek sınırı, gerçek PDF üretimi için çok az yer bırakır.

Kubernetes: Tarayıcı süreçleri, kapsayıcı orkestrasyonu ile iyi oynamaz. Uygulama kodunuz için uygun görünen bellek sınırları, Chromium işleyici süreçlerini başlattığında yetersiz hale gelir. Yüksek bellek talepleri ayarlamazsanız, Pod OOMKills sık görülen bir olay haline gelir, uygulamanıza gerçekten ihtiyaç duyduğu bellekten daha fazla bellek talepleriyle birlikte.

EşdeğerIronPDFKodu

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

Altı satır. IronPDF, Chromium'u dahili olarak gömülü olarak kullanır — tarayıcı yaşam döngüsü, bellek yönetimi ve süreç havuzlama, kütüphane tarafından düzenlenir. SemaphoreSlim yok. Tarayıcı geri dönüşümü yok. Dockerfile değişiklikleri yok. NuGet paketi her şeyi içerir.

Karşılığında gerçek bir durum var: IronPDF'nin NuGet paketi, Chromium ikili dosyasını içerdiği için PdfSharp'tan daha büyüktür. Motor başlatıldığında ilk PDF gecikmesi 2-5 saniyedir, sonra sonraki üretimler için 100-500ms'dir. Dağıtım boyutunun birincil sınırlayıcı olduğu uygulamalar için bu önemlidir. Geliştirici zamanı ve operasyonel güvenilirliğin daha önemli olduğu uygulamalar için gömülü yaklaşım kazanır.

.NET Ekosistem Sağlığı: NuGet Paketi Karşılaştırması

Bir kütüphane seçmeden önce NuGet ekosistem sağlığını kontrol edin. Bağımlılık zincirleri, sürüm sıklığı ve sorun çözme süresi, özellik listelerinden daha fazlasını anlatır:

KütüphaneNuGet İndirmeleriSon SürümAçık Sorunlar.NET 8 TFMYerel Bağımlılıklar
PdfSharp34M+AktifLowNone
QuestPDF8M+AktifLowNone
iText Core30M+AktifOrtaNone
IronPDF10M+AktifLowChromium (bütünleşik)
DinkToPdf5M+2018300+wkhtmltopdfbinary
PuppeteerSharp15M+AktifOrtaChromium (harici)

DinkToPdf gibi terkedilmiş paketlerdeki yüksek indirme sayıları, mevcut sağlığı değil, miras olarak benimsenmeyi yansıtır.300+yanıtlanmayan açık problem, gerçek hikayeyi anlatıyor.

.NET 8 uygulamaları net8.0 TFM'i hedefliyorsa: PdfSharp, QuestPDF,iText CoreveIronPDFbunu doğrudan destekler.wkhtmltopdfsarmalayıcıları desteklemez — DllNotFoundException ve NU1202 hedef çerçeve uyum hataları beklenebilir.

Karar Matrisi

GereksinimPdfSharpQuestPDFiText CorewkhtmltopdfPuppeteerSharpIronPDF
Gerçekten ücretsiz (MIT/hoşgörülü)❌ Gelir kapısı❌ AGPL⚠️ Terk edilmiş❌ Ticari
HTML to PDF⚠️ Sınırlı⚠️ Bozuk CSS
Modern CSS (Flexbox/Grid)
JavaScript yürütme⚠️ Yalnızca ES5
Tarayıcı yönetimi yok
Aktif güvenlik yamaları
Gelir eşik yokN/AN/A
Tahmin edilebilir lisans maliyetiÜcretsiz1M $'da uçurumYıllık ortalama ~45K $N/AÜcretsiz749 $ süresiz
Docker dostu✅ Küçük✅ Küçük✅ Küçük⚠️ İkili bağımlılık⚠️ +280MB✅ Kendine yeten
Sunucusuz uyumlu⚠️ Soğuk başlatma

Yalnızca yapılandırılmış verilerden programatik PDF oluşturma ihtiyaçınız varsa:PdfSharp(MIT, kısıtlama yok) veyaQuestPDF(daha iyi API, gelir eşiklerine dikkat edin).

Eğer modern CSS ile HTML'den PDF'ye ihtiyaçınız var ve tarayıcı altyapısını yönetmek istemiyorsanız: IronPDF. Dahili Chromium, işleme motoru yaşam döngüsünü ele alır. Yayınlanmış fiyatlandırma, iText'in abonelik modelinin bir kısmında.

HTML'den PDF'ye ihtiyaçınız varsa ve tarayıcı süreçlerini yönetmekte rahatsanıza:PuppeteerSharpsize tam kontrol sağlar. Operasyonel yük için bütçeler oluşturun.

Şu andawkhtmltopdfveya herhangi bir sarmalayıcı kullanıyorsanız: Geçiş yapın. Sadece güvenlik açığı bile çabayı haklı kılıyor — ve her ay gecikirsiniz, CVE listesi yamalanmadan kalır.