PDF-to-JPG in .NET means choosing a rendering engine. PDFium.Net ships PDFium binaries as a NuGet package and produces browser-quality output. Magick.NET delegates to Ghostscript and needs GS installed. The ChangeThisFile API runs Poppler server-side — zero native deps in your app, free up to 1,000 conversions per month.

Method 1: PDFium.Net (NuGet, no system deps)

PDFium.Net embeds the PDFium renderer used by Chrome. Best quality, ships as a NuGet package, no system dependencies.

dotnet add package PdfiumViewer
# On Linux also add: dotnet add package PdfiumViewer.Native.x86_64.v8-xfa
using PdfiumViewer;
using System.Drawing;
using System.Drawing.Imaging;

public static class PdfToJpg
{
    /// <summary>Renders each page of a PDF to a JPEG file.</summary>
    public static IReadOnlyList<string> Convert(
        string inputPath, string outputDir, int dpi = 150)
    {
        Directory.CreateDirectory(outputDir);
        var paths = new List<string>();

        using var document = PdfDocument.Load(inputPath);
        for (int i = 0; i < document.PageCount; i++)
        {
            using var image = document.Render(i, dpi, dpi, PdfRenderFlags.Annotations);
            var outPath = Path.Combine(outputDir, $"page-{i + 1:D3}.jpg");
            image.Save(outPath, ImageFormat.Jpeg);
            paths.Add(outPath);
        }
        return paths;
    }
}

// Usage
var pages = PdfToJpg.Convert("document.pdf", "./pages", dpi: 200);
Console.WriteLine($"Wrote {pages.Count} pages");

DPI guide: 72 = screen thumbnail, 150 = web quality (good default), 200–300 = print quality, 600+ = archival (large files).

PdfiumViewer uses System.Drawing which requires EnableWindowsFormsCompatibility on .NET 6+ or an alternative save path via SkiaSharp. On Linux, add the native NuGet package for your architecture.

Method 2: Magick.NET (Ghostscript backend)

Magick.NET wraps ImageMagick, which delegates PDF rendering to Ghostscript. Pure NuGet install on Windows; Linux/macOS need Ghostscript installed separately.

dotnet add package Magick.NET-Q16-AnyCPU
# Linux: apt install ghostscript
# macOS: brew install ghostscript
using ImageMagick;

public static class PdfToJpgMagick
{
    public static IReadOnlyList<string> Convert(
        string inputPath, string outputDir, int dpi = 150)
    {
        Directory.CreateDirectory(outputDir);
        var paths = new List<string>();

        var settings = new MagickReadSettings
        {
            Density = new Density(dpi, dpi)
        };

        using var images = new MagickImageCollection();
        images.Read(inputPath, settings);

        for (int i = 0; i < images.Count; i++)
        {
            var img = images[i];
            img.Format = MagickFormat.Jpg;
            img.Quality = 90;
            // Flatten transparent pages to white background
            img.BackgroundColor = MagickColors.White;
            img.Alpha(AlphaOption.Remove);

            var outPath = Path.Combine(outputDir, $"page-{i + 1:D3}.jpg");
            img.Write(outPath);
            paths.Add(outPath);
        }
        return paths;
    }
}

Magick.NET loads all pages into memory at once — watch RAM on large PDFs. For 100+ page documents, consider processing in batches or use PDFium's page-by-page approach instead.

Method 3: ChangeThisFile API (HttpClient, no installs)

POST the PDF with MultipartFormDataContent. The API runs Poppler server-side. Free tier: 1,000 conversions/month, no card required.

# Verify with curl first
curl -X POST https://changethisfile.com/v1/convert \
  -H "Authorization: Bearer ctf_sk_your_key" \
  -F "file=@document.pdf" \
  -F "target=jpg" \
  --output result.jpg
using System.IO.Compression;
using System.Net.Http;
using System.Net.Http.Headers;

// Register IHttpClientFactory in Program.cs:
// builder.Services.AddHttpClient("ctf", c =>
//     c.BaseAddress = new Uri("https://changethisfile.com"));

public class PdfToJpgService
{
    private readonly HttpClient _http;
    private const string ApiKey = "ctf_sk_your_key_here";

    public PdfToJpgService(IHttpClientFactory factory)
        => _http = factory.CreateClient("ctf");

    public async Task<IReadOnlyList<string>> ConvertAsync(
        string inputPath,
        string outputDir,
        CancellationToken ct = default)
    {
        Directory.CreateDirectory(outputDir);

        await using var fileStream = File.OpenRead(inputPath);
        using var form = new MultipartFormDataContent();

        var fileContent = new StreamContent(fileStream);
        fileContent.Headers.ContentType =
            new MediaTypeHeaderValue("application/pdf");
        form.Add(fileContent, "file", Path.GetFileName(inputPath));
        form.Add(new StringContent("jpg"), "target");

        using var request = new HttpRequestMessage(HttpMethod.Post, "/v1/convert")
        {
            Content = form,
            Headers = { Authorization =
                new AuthenticationHeaderValue("Bearer", ApiKey) }
        };

        using var response = await _http.SendAsync(request, ct);
        response.EnsureSuccessStatusCode();

        var contentType = response.Content.Headers.ContentType?.MediaType ?? "";
        var paths = new List<string>();

        if (contentType.Contains("zip"))
        {
            // Multi-page PDF returns a zip with one JPG per page
            var zipBytes = await response.Content.ReadAsByteArrayAsync(ct);
            var zipPath = Path.Combine(outputDir, "pages.zip");
            await File.WriteAllBytesAsync(zipPath, zipBytes, ct);
            ZipFile.ExtractToDirectory(zipPath, outputDir, overwriteFiles: true);
            File.Delete(zipPath);
            paths.AddRange(Directory.GetFiles(outputDir, "*.jpg").OrderBy(x => x));
        }
        else
        {
            var outPath = Path.Combine(outputDir, "page-001.jpg");
            await using var outStream = File.Create(outPath);
            await response.Content.CopyToAsync(outStream, ct);
            paths.Add(outPath);
        }

        return paths;
    }
}

No NuGet package for the API — just the standard System.Net.Http that ships with .NET.

When to use each

ApproachBest forTradeoff
PDFium.NetChrome-quality rendering, no system deps, page-by-page controlLarge NuGet package; System.Drawing quirks on Linux
Magick.NETExisting ImageMagick pipelines, rich format manipulationGhostscript required on Linux/macOS; loads all pages in RAM
ChangeThisFile APINo native deps, serverless/container deploys, free tierNetwork latency, 25MB file limit on free tier

Production tips

  • Always use IHttpClientFactory. Never new HttpClient() in a service — socket exhaustion under load. Register a named client in Program.cs and inject IHttpClientFactory.
  • Pass CancellationToken everywhere. Both the API call and file I/O accept a token. Wire it from your controller's HttpContext.RequestAborted so cancelled requests don't leave orphan conversions.
  • Use async file I/O. File.OpenRead is synchronous. Prefer new FileStream(..., FileOptions.Asynchronous) or File.OpenRead wrapped in a Task for truly async pipelines.
  • Stream, don't buffer large PDFs. With the API, stream the upload rather than loading the whole file into a byte array: pass the FileStream directly to StreamContent as shown above.
  • Set a timeout on HttpClient. Default is 100 seconds. Large PDFs can take longer. Set client.Timeout = TimeSpan.FromMinutes(3) in your named client factory config.
  • 150 DPI is the right web default. 300 DPI is for print-bound output and quadruples file size. Benchmark your target display size before bumping DPI.

For most .NET applications, PDFium.Net gives the cleanest install story. For teams already using ImageMagick, Magick.NET keeps the pipeline consistent. For containerized or serverless deployments where you want zero native deps, the ChangeThisFile API is one HttpClient call. Free tier: 1,000 conversions/month.