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
| Approach | Best for | Tradeoff |
|---|---|---|
| PDFium.Net | Chrome-quality rendering, no system deps, page-by-page control | Large NuGet package; System.Drawing quirks on Linux |
| Magick.NET | Existing ImageMagick pipelines, rich format manipulation | Ghostscript required on Linux/macOS; loads all pages in RAM |
| ChangeThisFile API | No native deps, serverless/container deploys, free tier | Network 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 inProgram.csand injectIHttpClientFactory. - Pass CancellationToken everywhere. Both the API call and file I/O accept a token. Wire it from your controller's
HttpContext.RequestAbortedso cancelled requests don't leave orphan conversions. - Use async file I/O.
File.OpenReadis synchronous. Prefernew FileStream(..., FileOptions.Asynchronous)orFile.OpenReadwrapped 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
FileStreamdirectly toStreamContentas 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.