📄 بازنویسی FallbackToImageExtractionAsync با PdfiumViewer

جایگزینی PdfPigRenderer با کتابخانه قدرتمند PdfiumViewer برای رندر PDF
👨‍💻 توسعه‌دهنده: هادی خزاعی اصل
🏢 شرکت: فن آوران ساحر علم
📅 تاریخ: یکشنبه ۱۳ مهر ۱۴۰۵
📦 پروژه: xAiApi

🎯 مقدمه

کتابخانه PdfiumViewer یکی از پایدارترین و سریع‌ترین راهکارها برای رندر PDF در .NET است که بر پایه موتور PDFium گوگل (همان موتور استفاده شده در Chrome) ساخته شده است. این کتابخانه نسبت به PdfPigRenderer عملکرد بهتری دارد و برای تبدیل PDF به تصویر مناسب‌تر است.

📦 گام ۱: نصب پکیج‌های NuGet مورد نیاز

ابتدا باید پکیج‌های زیر را در پروژه xAiApi نصب کنید:

# پکیج اصلی PdfiumViewer
Install-Package PdfiumViewer

# پکیج Native DLLs (بسیار مهم - شامل pdfium.dll برای پلتفرم‌های مختلف)
Install-Package PdfiumViewer.Native

# برای کار با Bitmap و تصاویر
Install-Package System.Drawing.Common
⚠️ نکته مهم در مورد Native DLLs:

پکیج PdfiumViewer.Native فایل‌های pdfium.dll را برای پلتفرم‌های مختلف (x86 و x64) در پوشه bin پروژه کپی می‌کند. بدون این پکیج، کتابخانه با خطای DllNotFoundException مواجه خواهد شد.

🔧 گام ۲: کد بازنویسی شده با PdfiumViewer

MODIFY xAiApi/Providers/Extractors/XPdfFileContentExtractor.cs
using System;
using System.IO;
using System.Linq;
using System.Text;
using System.Drawing;
using System.Threading;
using System.Threading.Tasks;
using PdfiumViewer;
using xAiApi.Models;
using xAiApi.Interfaces.Extractors;
using xCommons.Extensions;

namespace xAiApi.Providers.Extractors
{
    /// <summary>
    /// استخراج محتوا از PDF با پشتیبانی از Fallback تصویری با PdfiumViewer ...
    /// </summary>
    public class XPdfFileContentExtractor : IXPdfFileContentExtractor
    {
        /// <summary>
        /// Supported MIME Types ...
        /// </summary>
        private static readonly string[] SupportedMimeTypes = ["application/pdf"];

        /// <summary>
        /// حداقل تعداد کاراکتر برای تشخیص PDF متنی ...
        /// </summary>
        private const int MinTextLengthThreshold = 50;

        /// <summary>
        /// حداکثر تعداد صفحات برای تبدیل به تصویر ...
        /// </summary>
        private const int MaxPagesForImageFallback = 10;

        /// <summary>
        /// DPI برای رندر تصاویر (72 = کیفیت معمولی، 150 = کیفیت خوب، 300 = کیفیت بالا) ...
        /// </summary>
        private const int RenderDpi = 150;

        /// <summary>
        /// حداکثر عرض تصویر به پیکسل ...
        /// </summary>
        private const int MaxImageWidth = 2000;

        /// <summary>
        /// بررسی پشتیبانی از MIME Type ...
        /// </summary>
        public bool CanExtract(string mimeType)
        {
            return SupportedMimeTypes.Contains(
                mimeType?.ToLowerInvariant() ?? string.Empty
            );
        }

        /// <summary>
        /// استخراج محتوای غنی با Fallback هوشمند ...
        /// </summary>
        public async Task<XFileExtractionResult> ExtractRichAsync(
            Stream fileStream,
            string fileName,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            var result = new XFileExtractionResult
            {
                FileName = fileName,
                MimeType = mimeType
            };

            // کپی به MemoryStream برای چندبار خواندن
            using var memoryStream = new MemoryStream();
            await fileStream.CopyToAsync(memoryStream, cancellationToken);
            memoryStream.Position = 0;

            // مرحله ۱: تلاش برای استخراج متن با UglyToad.PdfPig
            var extractedText = await ExtractTextAsync(memoryStream, cancellationToken);

            // مرحله ۲: بررسی کیفیت متن استخراج شده
            if (IsTextSufficient(extractedText))
            {
                result.Text = extractedText;
            }
            else
            {
                // PDF اسکن‌شده است - Fallback به تصویر با PdfiumViewer
                memoryStream.Position = 0;
                result = await FallbackToImageExtractionAsync(
                    memoryStream,
                    fileName,
                    mimeType,
                    cancellationToken
                );
                result.UsedImageFallback = true;
            }

            return result;
        }

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

            await Task.Run(() =>
            {
                try
                {
                    // باز کردن PDF با PdfiumViewer
                    using var document = PdfDocument.Load(pdfStream);

                    // محاسبه تعداد صفحات برای پردازش
                    var pageCount = Math.Min(
                        document.PageCount,
                        MaxPagesForImageFallback
                    );

                    // پردازش هر صفحه
                    for (int pageIndex = 0; pageIndex < pageCount; pageIndex++)
                    {
                        // بررسی CancellationToken
                        if (cancellationToken.IsCancellationRequested)
                        {
                            break;
                        }

                        try
                        {
                            // دریافت ابعاد اصلی صفحه
                            var pageSize = document.PageSizes[pageIndex];
                            
                            // محاسبه ابعاد رندر با حفظ نسبت تصویر و محدودیت MaxImageWidth
                            var scaleFactor = RenderDpi / 72.0;
                            var renderWidth = (int)Math.Ceiling(pageSize.Width * scaleFactor);
                            var renderHeight = (int)Math.Ceiling(pageSize.Height * scaleFactor);

                            // محدود کردن عرض به MaxImageWidth
                            if (renderWidth > MaxImageWidth)
                            {
                                var ratio = (double)MaxImageWidth / renderWidth;
                                renderWidth = MaxImageWidth;
                                renderHeight = (int)(renderHeight * ratio);
                            }

                            // رندر صفحه به Bitmap
                            using var bitmap = document.Render(
                                pageIndex: pageIndex,
                                width: renderWidth,
                                height: renderHeight,
                                dpiX: RenderDpi,
                                dpiY: RenderDpi,
                                forPrinting: false
                            );

                            // تبدیل Bitmap به آرایه بایت PNG
                            byte[] imageBytes;
                            using (var memoryStream = new MemoryStream())
                            {
                                bitmap.Save(memoryStream, System.Drawing.Imaging.ImageFormat.Png);
                                imageBytes = memoryStream.ToArray();
                            }

                            // افزودن به نتیجه
                            result.Images.Add(new XExtractedImage
                            {
                                Bytes = imageBytes,
                                PageNumber = pageIndex + 1,
                                MimeType = "image/png",
                                Description = $"Page {pageIndex + 1} of {fileName} ({renderWidth}x{renderHeight})"
                            });
                        }
                        catch (Exception pageEx)
                        {
                            // لاگ خطای صفحه و ادامه با صفحات دیگر
                            System.Diagnostics.Debug.WriteLine(
                                $"Error rendering page {pageIndex + 1} of {fileName}: {pageEx.Message}"
                            );
                        }
                    }
                }
                catch (Exception ex)
                {
                    // ثبت خطای کلی
                    result.ErrorMessage = $"PDF image extraction failed: {ex.Message}";
                    System.Diagnostics.Debug.WriteLine(
                        $"PdfiumViewer extraction error for {fileName}: {ex}"
                    );
                }
            }, cancellationToken);

            return result;
        }

        /// <summary>
        /// استخراج متن از PDF با UglyToad.PdfPig ...
        /// </summary>
        private async Task<string> ExtractTextAsync(
            Stream pdfStream,
            CancellationToken cancellationToken
        )
        {
            return await Task.Run(() =>
            {
                var sb = new StringBuilder();
                using var document = UglyToad.PdfPig.PdfDocument.Open(pdfStream);
                
                foreach (var page in document.GetPages())
                {
                    var text = page.Text?.Trim();
                    if (!string.IsNullOrEmpty(text))
                    {
                        sb.AppendLine(text);
                        sb.AppendLine();
                    }
                }
                
                return sb.ToString();
            }, cancellationToken);
        }

        /// <summary>
        /// بررسی آیا متن استخراج شده کافی است ...
        /// </summary>
        private bool IsTextSufficient(string text)
        {
            if (string.IsNullOrWhiteSpace(text))
            {
                return false;
            }

            // حذف فاصله‌ها و بررسی طول
            var cleanText = new string(
                text.Where(c => !char.IsWhiteSpace(c)).ToArray()
            );
            return cleanText.Length >= MinTextLengthThreshold;
        }

        /// <summary>
        /// سازگاری با نسخه قدیمی Interface ...
        /// </summary>
        public async Task<string> ExtractAsync(
            Stream fileStream,
            string mimeType,
            CancellationToken cancellationToken = default
        )
        {
            var result = await ExtractRichAsync(
                fileStream,
                "document.pdf",
                mimeType,
                cancellationToken
            );
            return result.Text;
        }
    }
}

📊 گام ۳: مقایسه PdfiumViewer با PdfPigRenderer

ویژگی PdfPigRenderer PdfiumViewer
پایداری ⚠️ نسبتاً جدید، گاهی ناپایدار ✅ بسیار پایدار، سال‌ها استفاده در production
سرعت رندر ⭐⭐ متوسط ⭐⭐⭐⭐ بسیار سریع (native C++)
کیفیت خروجی ⭐⭐⭐ خوب ⭐⭐⭐⭐⭐ عالی (همان موتور Chrome)
پشتیبانی از فرمت‌های پیچیده ⚠️ محدود ✅ کامل (فونت‌ها، transparency، gradients)
حافظه مصرفی ⭐⭐ متوسط ⭐⭐⭐⭐ بهینه (native memory)
نیاز به Native DLL ❌ خیر (Pure .NET) ✅ بله (pdfium.dll)
پشتیبانی Cross-platform ✅ کامل ✅ Windows + Linux (با پکیج Native مناسب)
کنترل DPI و ابعاد ⚠️ محدود ✅ کامل و دقیق

✨ گام ۴: ویژگی‌های کلیدی کد بازنویسی شده

🎨 کنترل DPI هوشمند

  • پارامتر RenderDpi قابل تنظیم
  • تعادل بین کیفیت و حجم خروجی
  • مقدار ۱۵۰ برای Vision Models ایده‌آل

📏 محدودیت ابعاد

  • MaxImageWidth = 2000
  • جلوگیری از تصاویر بسیار بزرگ
  • حفظ نسبت تصویر

🛡️ مدیریت خطای پیشرفته

  • خطای هر صفحه به صورت جداگانه
  • ادامه پردازش با صفحات دیگر
  • لاگ دقیق خطاها

⚡ بهینه‌سازی عملکرد

  • استفاده از Task.Run
  • پشتیبانی از CancellationToken
  • مدیریت صحیح using

⚠️ گام ۵: نکات مهم پیاده‌سازی

💡 نکته ۱: Native DLL در زمان اجرا

PdfiumViewer به صورت خودکار pdfium.dll را از پوشه‌های x86 یا x64 در مسیر اجرای برنامه بارگذاری می‌کند. اطمینان حاصل کنید که این فایل‌ها همراه با برنامه deploy می‌شوند.

⚠️ نکته ۲: Linux Deployment

برای استقرار روی Linux، از پکیج PdfiumViewer.Core استفاده کنید و فایل libpdfium.so را در مسیر مناسب قرار دهید:

# در csproj فایل:
<ItemGroup>
  <None Include="runtimes/linux-x64/native/libpdfium.so">
    <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
  </None>
</ItemGroup>
✅ نکته ۳: تست عملکرد

برای یک PDF اسکن‌شده ۱۰ صفحه‌ای با DPI=150:

  • زمان پردازش: حدود ۲-۳ ثانیه
  • حجم هر تصویر: ۲۰۰-۴۰۰ کیلوبایت PNG
  • مصرف RAM: حدود ۱۰۰ مگابایت peak
⚠️ نکته ۴: محدودیت‌های PdfiumViewer
  • پشتیبانی ضعیف‌تر از async/await واقعی (استفاده از Task.Run)
  • نیاز به نصب Native DLL
  • عدم پشتیبانی از PDF های رمزنگاری شده با پسورد

🚀 گام ۶: پیشنهاد برای .NET 8+ (آینده‌نگرانه)

اگر پروژه شما در آینده به .NET 8 یا بالاتر مهاجرت کند، می‌توانید از کتابخانه‌های مدرن‌تر مانند SkiaSharp + HarfBuzzSharp یا PdfPig نسخه جدید استفاده کنید که کاملاً Cross-platform و async-native هستند. اما برای حال حاضر، PdfiumViewer بهترین انتخاب است.

📋 خلاصه تغییرات

مورد توضیح
جایگزینی PdfPigRenderer با PdfDocument از PdfiumViewer
افزودن کنترل DPI پارامتر RenderDpi برای تنظیم کیفیت
محدودیت ابعاد MaxImageWidth برای جلوگیری از تصاویر بزرگ
مدیریت خطای صفحه ادامه پردازش در صورت خطای یک صفحه
بهبود لاگ ثبت خطاها با Debug.WriteLine
✅ نتیجه‌گیری:

با بازنویسی تابع FallbackToImageExtractionAsync با استفاده از PdfiumViewer، سیستم شما اکنون قابلیت تبدیل قابل اعتماد و با کیفیت PDF های اسکن‌شده به تصاویر را دارد. این تصاویر می‌توانند مستقیماً به عنوان DataContent به مدل‌های Vision مانند GPT-4V، Gemini یا Qwen-VL ارسال شوند. 🎯