skiasharp + pdfiumsharp 是 .net 6+ 下最稳的 pdf 页面解码方案,因完全托管、跨平台、无系统依赖;需启用多线程、用 skimage.encode 避免 gdi+ 异常,并注意 stream 生命周期。

用 SkiaSharp + PDFiumSharp 提前解码 PDF 页面最稳
直接调第三方 PDF 渲染库(比如 Ghostscript 或老旧的 PDFBox 绑定)容易在 Windows 服务、Docker 容器里崩,根本原因是它们依赖本地系统级渲染上下文或全局字体缓存。SkiaSharp 搭配 PDFiumSharp 是目前 .NET 6+ 下最轻量、跨平台、无外部依赖的组合——它把 PDF 解析和光栅化全放在托管内存里跑。
实操建议:
- 安装
PDFiumSharp(注意不是PDFium.NET)和SkiaSharp:两个包都要装,缺一不可 -
PDFiumSharp默认不启用多线程解码,高并发时得手动调PdfDocument.Load(..., new PdfLoadOptions { EnableMultiThreading = true }) - 别用
Bitmap.Save()直出 PNG——它默认走 GDI+,Linux 容器里会抛System.PlatformNotSupportedException;必须用SKImage.Encode(SKEncodedImageFormat.Png, 100)+ToArray()写文件
ImageMagick 在 .NET 中调用失败的三个常见原因
很多人试过 ImageMagick 的 Magick.NET 包,结果一读 PDF 就报 Unable to open image '/tmp/xxx.pdf': No decode delegate for this image format,本质是没装对“解码器”。
关键点:
- Windows 下装
Magick.NET-Q16-AnyCPU不够,必须额外下载并注册 Ghostscript(gswin64c.exe),且路径要加进Environment.SetEnvironmentVariable("MAGICK_CODER_MODULE_PATH", "...") - Linux Docker 镜像得带
ghostscript和libpng-dev,否则即使 NuGet 包装了也加载不了 PDF 模块 -
MagickReadSettings.Density设太低(比如 72)会导致文字发虚,设太高(300+)又可能 OOM;建议按用途分档:96(预览)、150(文档归档)、200(OCR 前处理)
为什么不用 iTextSharp 直接转图?
iTextSharp(包括新版 iText7)根本不是渲染引擎,它只负责生成/解析 PDF 结构。你不能靠它“画出页面”,它连 Page.GetPageSize() 返回的都是逻辑尺寸,没有像素坐标系。有人硬套 ColumnText.ShowTextAligned() 模拟截图,结果发现线条错位、中文字体全成方块——因为缺失字体度量与光栅化管线。
真实场景下该怎么做:
- 如果只是提取 PDF 里的嵌入图片(比如扫描件中的 JPG),用
iText7的PdfCanvasProcessor+ImageRenderInfo回调,能拿到原始字节流,比转图快十倍 - 如果目标是保留矢量精度(比如 CAD 图纸导出),别转图,改用
SkiaSharp渲染到SKSurface后导出 SVG 字符串(需自行映射 PDF 路径指令) - 想批量加水印?先用
iText7修改 PDF 内容,再用PDFiumSharp渲染——别在一个流程里混用多个渲染栈
异步转图时 Stream 生命周期最容易漏掉的坑
常见写法是 using var stream = File.OpenRead(path); var doc = PdfDocument.Load(stream);,然后开 Task.Run 去渲染。结果偶尔报 ObjectDisposedException,但不是每次复现——因为 PdfDocument 默认延迟加载页对象,真正解码发生在 page.Render() 那一刻,而此时 stream 早被 using 释放了。
正确姿势只有两种:
- 把 PDF 文件整个读进
byte[],传给PdfDocument.Load(byte[], ...)——这是最省心的做法 - 如果文件超大(>200MB),必须流式处理,就得自己维护
Stream生命周期:用new FileStream(..., FileShare.Read)创建,并确保所有Render()完成后再Dispose() - 千万别在 ASP.NET Core API 里直接传
HttpContext.Request.Body给 PDF 加载器——它不支持重复读取,第一次Load()成功后第二次就空了










