🎯 معرفی PdfiumViewer
PdfiumViewer یک کتابخانه قدرتمند و سریع برای کار با فایلهای PDF در .NET است که بر پایه موتور PDFium گوگل (همان موتوری که Chrome استفاده میکند) ساخته شده است.
⚡ مزایای PdfiumViewer
- سرعت بالا در رندر صفحات
- پشتیبانی کامل از PDF های پیچیده
- کیفیت رندر عالی
- کنترل دقیق DPI و اندازه خروجی
- پشتیبانی از فرمتهای مختلف تصویر
⚠️ ملاحظات مهم
- نیاز به Native DLL های PDFium
- حجم فایل DLL ها حدود 10-20 مگابایت
- نیاز به کپی DLL ها در output directory
- Thread-safe نیست (نیاز به قفل)
📦 گام ۱: نصب پکیجهای NuGet
# نصب پکیج اصلی PdfiumViewer Install-Package PdfiumViewer # نصب Native DLL های PDFium (برای پلتفرم x64) Install-Package PdfiumViewer.Native # یا برای هر دو پلتفرم x86 و x64 Install-Package PdfiumViewer.Native.x86 Install-Package PdfiumViewer.Native.x64
💡 نکته: پکیج
PdfiumViewer.Native به صورت خودکار DLL های pdfium.dll
را در پوشه x86 و x64 پروژه کپی میکند و در runtime بر اساس معماری سیستم،
DLL مناسب را لود میکند.
🔧 گام ۲: بازنویسی تابع FallbackToImageExtractionAsync
MODIFY
xAiApi/Providers/Extractors/XPdfFileContentExtractor.cs
using System;
using System.IO;
using System.Drawing;
using System.Drawing.Imaging;
using System.Threading;
using System.Threading.Tasks;
using PdfiumViewer;
using xAiApi.Models;
namespace xAiApi.Providers.Extractors
{
public class XPdfFileContentExtractor : IXPdfFileContentExtractor
{
private const int MaxPagesForImageFallback = 10;
private const int MinTextLengthThreshold = 50;
/// <summary>
/// DPI پیشفرض برای رندر تصاویر (72 = استاندارد، 150 = کیفیت بالا)
/// </summary>
private const int DefaultDpi = 150;
// ... (سایر متدها)
/// <summary>
/// Fallback: تبدیل صفحات PDF به تصویر با استفاده از PdfiumViewer ...
/// </summary>
private async Task<XFileExtractionResult> FallbackToImageExtractionAsync(
Stream pdfStream,
string fileName,
string mimeType,
CancellationToken cancellationToken
)
{
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType,
UsedImageFallback = true
};
// کپی Stream به MemoryStream برای PdfiumViewer
// (PdfiumViewer به Stream قابل Seek نیاز دارد)
using var memoryStream = new MemoryStream();
await pdfStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
await Task.Run(() =>
{
try
{
// بارگذاری PDF با PdfiumViewer
using var pdfDocument = PdfDocument.Load(memoryStream);
// محاسبه تعداد صفحات برای پردازش
var pageCount = Math.Min(
pdfDocument.PageCount,
MaxPagesForImageFallback
);
// حلقه روی صفحات و تبدیل به تصویر
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
// بررسی CancellationToken
cancellationToken.ThrowIfCancellationRequested();
// رندر صفحه به Bitmap
// متد RenderPage پارامترهای زیر را میپذیرد:
// - pageIndex: شماره صفحه
// - width: عرض تصویر خروجی (0 = اندازه اصلی)
// - height: ارتفاع تصویر خروجی (0 = اندازه اصلی)
// - dpi: کیفیت رندر
using var bitmap = pdfDocument.RenderPage(
pageIndex: pageIndex,
width: 0, // 0 = استفاده از اندازه اصلی
height: 0, // 0 = استفاده از اندازه اصلی
dpiX: DefaultDpi,
dpiY: DefaultDpi
);
if (bitmap != null)
{
// تبدیل Bitmap به PNG
using var imageStream = new MemoryStream();
bitmap.Save(imageStream, ImageFormat.Png);
var imageBytes = imageStream.ToArray();
// افزودن به لیست تصاویر
result.Images.Add(new XExtractedImage
{
Bytes = imageBytes,
PageNumber = pageIndex + 1,
MimeType = "image/png",
Description = $"Page {pageIndex + 1} of {fileName}"
});
}
}
}
catch (OperationCanceledException)
{
// عملیات لغو شد
throw;
}
catch (Exception ex)
{
// خطا در پردازش PDF
result.ErrorMessage = $"Failed to render PDF pages to images: {ex.Message}";
}
}, cancellationToken);
return result;
}
// ... (سایر متدها)
}
}
🎨 گام ۳: نسخه پیشرفته با کنترل دقیق DPI
اگر نیاز به کنترل دقیقتر روی کیفیت و اندازه تصاویر دارید، از این نسخه استفاده کنید:
ADVANCED
نسخه پیشرفته با کنترل DPI
/// <summary>
/// Fallback پیشرفته با کنترل دقیق DPI و کیفیت ...
/// </summary>
private async Task<XFileExtractionResult> FallbackToImageExtractionAdvancedAsync(
Stream pdfStream,
string fileName,
string mimeType,
int targetDpi = 150,
ImageFormat outputFormat = null,
CancellationToken cancellationToken = default
)
{
outputFormat ??= ImageFormat.Png;
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType,
UsedImageFallback = true
};
using var memoryStream = new MemoryStream();
await pdfStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
await Task.Run(() =>
{
using var pdfDocument = PdfDocument.Load(memoryStream);
var pageCount = Math.Min(pdfDocument.PageCount, MaxPagesForImageFallback);
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
// دریافت اندازه اصلی صفحه
var pageSize = pdfDocument.PageSizes[pageIndex];
// محاسبه اندازه تصویر بر اساس DPI
var width = (int)(pageSize.Width * targetDpi / 72.0);
var height = (int)(pageSize.Height * targetDpi / 72.0);
// رندر با اندازه مشخص
using var bitmap = pdfDocument.RenderPage(
pageIndex: pageIndex,
width: width,
height: height,
dpiX: targetDpi,
dpiY: targetDpi
);
if (bitmap != null)
{
using var imageStream = new MemoryStream();
bitmap.Save(imageStream, outputFormat);
var imageBytes = imageStream.ToArray();
result.Images.Add(new XExtractedImage
{
Bytes = imageBytes,
PageNumber = pageIndex + 1,
MimeType = outputFormat == ImageFormat.Jpeg ? "image/jpeg" : "image/png",
Description = $"Page {pageIndex + 1} of {fileName} ({width}x{height}px @ {targetDpi}dpi)"
});
}
}
}, cancellationToken);
return result;
}
⚙️ گام ۴: پیکربندی DPI در appsettings.json
CONFIG
appsettings.json
{
"AiApiConfiguration": {
"FileExtraction": {
"PdfImageFallback": {
"Enabled": true,
"MaxPages": 10,
"Dpi": 150,
"OutputFormat": "png",
"MinTextLengthThreshold": 50
}
}
}
}
CLASS
xAiApi/Configurations/XPdfImageFallbackConfiguration.cs
namespace xAiApi.Configurations
{
public class XPdfImageFallbackConfiguration
{
public bool Enabled { get; set; } = true;
public int MaxPages { get; set; } = 10;
public int Dpi { get; set; } = 150;
public string OutputFormat { get; set; } = "png";
public int MinTextLengthThreshold { get; set; } = 50;
}
}
🔗 گام ۵: استفاده از پیکربندی در Extractor
MODIFY
XPdfFileContentExtractor.cs
public class XPdfFileContentExtractor : IXPdfFileContentExtractor
{
private readonly XPdfImageFallbackConfiguration _config;
public XPdfFileContentExtractor(XPdfImageFallbackConfiguration config)
{
_config = config ?? new XPdfImageFallbackConfiguration();
}
private async Task<XFileExtractionResult> FallbackToImageExtractionAsync(
Stream pdfStream,
string fileName,
string mimeType,
CancellationToken cancellationToken
)
{
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType,
UsedImageFallback = true
};
using var memoryStream = new MemoryStream();
await pdfStream.CopyToAsync(memoryStream, cancellationToken);
memoryStream.Position = 0;
await Task.Run(() =>
{
using var pdfDocument = PdfDocument.Load(memoryStream);
var pageCount = Math.Min(pdfDocument.PageCount, _config.MaxPages);
for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
using var bitmap = pdfDocument.RenderPage(
pageIndex: pageIndex,
width: 0,
height: 0,
dpiX: _config.Dpi,
dpiY: _config.Dpi
);
if (bitmap != null)
{
using var imageStream = new MemoryStream();
// انتخاب فرمت خروجی بر اساس پیکربندی
var format = _config.OutputFormat?.ToLowerInvariant() switch
{
"jpeg" or "jpg" => ImageFormat.Jpeg,
"bmp" => ImageFormat.Bmp,
_ => ImageFormat.Png
};
bitmap.Save(imageStream, format);
var imageBytes = imageStream.ToArray();
result.Images.Add(new XExtractedImage
{
Bytes = imageBytes,
PageNumber = pageIndex + 1,
MimeType = format == ImageFormat.Jpeg ? "image/jpeg" : "image/png",
Description = $"Page {pageIndex + 1} of {fileName}"
});
}
}
}, cancellationToken);
return result;
}
}
🔌 گام ۶: ثبت در Dependency Injection
MODIFY
xAiApi/Startup.cs
public void ConfigureServices(IServiceCollection services)
{
// ... (سایر ثبتها)
// ✅ ثبت پیکربندی PDF Image Fallback
services.Configure<XPdfImageFallbackConfiguration>(
Configuration.GetSection("AiApiConfiguration:FileExtraction:PdfImageFallback")
);
// ✅ ثبت PDF Extractor با پیکربندی
services.AddSingleton<IXFileContentExtractor>(sp =>
{
var config = sp.GetRequiredService<IOptions<XPdfImageFallbackConfiguration>>().Value;
return new XPdfFileContentExtractor(config);
});
// ... (بقیه کدها)
}
🐛 گام ۷: عیبیابی و نکات مهم
| مشکل | علت | راهحل |
|---|---|---|
DllNotFoundException |
pdfium.dll یافت نشد | نصب PdfiumViewer.Native و rebuild پروژه |
BadImageFormatException |
عدم تطابق معماری (x86/x64) | استفاده از پکیج Native مناسب یا تنظیم Platform Target |
خطا در PdfDocument.Load |
Stream غیرقابل Seek | کپی به MemoryStream قبل از Load |
| کیفیت پایین تصاویر | DPI خیلی کم | افزایش DPI به 200 یا 300 |
OutOfMemoryException |
تعداد صفحات زیاد یا DPI بالا | کاهش MaxPages یا Dpi |
⚠️ Thread Safety:
PdfiumViewer Thread-safe نیست. اگر چندین درخواست همزمان PDF را پردازش میکنند،
باید از lock یا SemaphoreSlim استفاده کنید:
private static readonly SemaphoreSlim _pdfLock = new SemaphoreSlim(2, 2); // حداکثر 2 همزمان
await _pdfLock.WaitAsync(cancellationToken);
try
{
// پردازش PDF
}
finally
{
_pdfLock.Release();
}
📊 گام ۸: مقایسه PdfPigRenderer و PdfiumViewer
| ویژگی | PdfPigRenderer | PdfiumViewer |
|---|---|---|
| سرعت رندر | ⭐⭐⭐ متوسط | ⭐⭐⭐⭐⭐ بسیار سریع |
| کیفیت خروجی | ⭐⭐⭐ خوب | ⭐⭐⭐⭐⭐ عالی |
| حجم DLL | کوچک (فقط .NET) | بزرگ (10-20 MB Native) |
| پشتیبانی از PDF های پیچیده | ⭐⭐⭐ محدود | ⭐⭐⭐⭐⭐ کامل |
| کنترل DPI | ⭐⭐ محدود | ⭐⭐⭐⭐⭐ دقیق |
| Thread Safety | ✅ Thread-safe | ❌ نیاز به قفل |
| نیاز به Native DLL | ❌ خیر | ✅ بله |
✅ نتیجهگیری:
PdfiumViewer انتخاب بهتری برای سناریوهای Production است، به ویژه اگر:
- کیفیت رندر برایتان مهم است
- با PDF های پیچیده (فونتهای خاص، تصاویر، فرمها) کار میکنید
- سرعت پردازش اولویت دارد
- حجم DLL ها مسئلهای نیست
📋 گام ۹: خلاصه تغییرات
| فایل | تغییر |
|---|---|
XPdfFileContentExtractor.cs |
بازنویسی FallbackToImageExtractionAsync با PdfiumViewer |
XPdfImageFallbackConfiguration.cs |
کلاس جدید برای پیکربندی |
appsettings.json |
افزودن بخش PdfImageFallback |
Startup.cs |
ثبت پیکربندی و Extractor در DI |
| NuGet Packages | نصب PdfiumViewer و PdfiumViewer.Native |
💡 نکات کلیدی:
- ✅ PdfiumViewer سریعتر و باکیفیتتر از PdfPigRenderer است
- ✅ کنترل دقیق DPI و اندازه خروجی
- ⚠️ نیاز به Native DLL ها (حدود 10-20 MB)
- ⚠️ Thread-safe نیست و نیاز به قفل دارد
- ✅ پیکربندیپذیر از طریق appsettings.json