Files
2026-10-03 18:04:36 +03:30

658 lines
28 KiB
HTML

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>بازنویسی FallbackToImageExtractionAsync با PdfiumViewer - فن آوران ساحر علم</title>
<style>
:root {
--primary: #1e3a8a;
--secondary: #3b82f6;
--accent: #f59e0b;
--success: #10b981;
--danger: #ef4444;
--warning: #f97316;
--purple: #8b5cf6;
--bg-light: #f8fafc;
--bg-code: #1e293b;
--text-dark: #0f172a;
--text-muted: #64748b;
--border: #e2e8f0;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Tahoma', 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #f8fafc 0%, #e0e7ff 100%);
color: var(--text-dark);
line-height: 1.8;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.1);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--purple) 100%);
color: white;
padding: 40px;
text-align: center;
}
.header h1 { font-size: 2em; margin-bottom: 10px; }
.header .subtitle { font-size: 1.1em; opacity: 0.95; }
.meta-bar {
display: flex;
justify-content: space-between;
background: var(--bg-light);
padding: 15px 30px;
border-bottom: 2px solid var(--border);
flex-wrap: wrap;
gap: 15px;
}
.meta-item { display: flex; align-items: center; gap: 8px; font-size: 0.9em; color: var(--text-muted); }
.meta-item strong { color: var(--primary); }
.content { padding: 40px; }
.section {
margin-bottom: 35px;
padding: 25px;
background: var(--bg-light);
border-radius: 12px;
border-right: 5px solid var(--secondary);
}
.section h2 {
color: var(--primary);
font-size: 1.5em;
margin-bottom: 20px;
padding-bottom: 10px;
border-bottom: 2px solid var(--border);
}
.section h3 { color: var(--secondary); font-size: 1.2em; margin: 20px 0 12px; }
pre {
background: var(--bg-code);
color: #e2e8f0;
padding: 18px;
border-radius: 8px;
overflow-x: auto;
direction: ltr;
text-align: left;
font-family: 'Consolas', monospace;
font-size: 0.83em;
margin: 15px 0;
border-right: 4px solid var(--accent);
}
code {
background: #fef3c7;
color: #92400e;
padding: 2px 8px;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 0.9em;
direction: ltr;
display: inline-block;
}
table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
background: white;
border-radius: 8px;
overflow: hidden;
}
th { background: var(--primary); color: white; padding: 12px; text-align: right; }
td { padding: 12px; border-bottom: 1px solid var(--border); }
.alert { padding: 15px 20px; border-radius: 8px; margin: 15px 0; border-right: 4px solid; }
.alert-info { background: #dbeafe; border-color: var(--secondary); color: #1e40af; }
.alert-success { background: #d1fae5; border-color: var(--success); color: #065f46; }
.alert-warning { background: #fef3c7; border-color: var(--accent); color: #92400e; }
.alert-danger { background: #fee2e2; border-color: var(--danger); color: #991b1b; }
.footer { background: var(--primary); color: white; padding: 25px; text-align: center; }
.highlight { background: linear-gradient(120deg, #fef3c7 0%, #fef3c7 100%); padding: 2px 6px; border-radius: 4px; font-weight: bold; }
.file-change { background: #f0f9ff; border-right: 4px solid var(--secondary); padding: 15px; margin: 10px 0; border-radius: 8px; }
.file-change .path { font-family: 'Consolas', monospace; color: var(--primary); font-weight: bold; direction: ltr; display: inline-block; }
.badge-modify { background: var(--warning); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
.badge-new { background: var(--success); color: white; padding: 2px 8px; border-radius: 4px; font-size: 0.75em; margin-right: 8px; }
.arch-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin: 20px 0;
}
.arch-card {
background: white;
padding: 20px;
border-radius: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.08);
border-top: 4px solid var(--secondary);
}
.arch-card h4 { color: var(--primary); margin-bottom: 12px; font-size: 1.1em; }
.arch-card ul { list-style: none; padding-right: 0; }
.arch-card li { padding: 6px 0; padding-right: 20px; position: relative; font-size: 0.95em; }
.arch-card li::before { content: '▸'; position: absolute; right: 0; color: var(--accent); font-weight: bold; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>📄 بازنویسی FallbackToImageExtractionAsync با PdfiumViewer</h1>
<div class="subtitle">جایگزینی PdfPigRenderer با کتابخانه قدرتمند PdfiumViewer برای رندر PDF</div>
</div>
<div class="meta-bar">
<div class="meta-item">👨‍💻 <strong>توسعه‌دهنده:</strong> هادی خزاعی اصل</div>
<div class="meta-item">🏢 <strong>شرکت:</strong> فن آوران ساحر علم</div>
<div class="meta-item">📅 <strong>تاریخ:</strong> یکشنبه ۱۳ مهر ۱۴۰۵</div>
<div class="meta-item">📦 <strong>پروژه:</strong> xAiApi</div>
</div>
<div class="content">
<div class="section">
<h2>🎯 مقدمه</h2>
<p>
کتابخانه <span class="highlight">PdfiumViewer</span> یکی از پایدارترین و سریع‌ترین راهکارها برای رندر PDF در .NET است
که بر پایه موتور <strong>PDFium گوگل</strong> (همان موتور استفاده شده در Chrome) ساخته شده است.
این کتابخانه نسبت به <code>PdfPigRenderer</code> عملکرد بهتری دارد و برای تبدیل PDF به تصویر مناسب‌تر است.
</p>
</div>
<!-- Step 1: NuGet Packages -->
<div class="section">
<h2>📦 گام ۱: نصب پکیج‌های NuGet مورد نیاز</h2>
<p>ابتدا باید پکیج‌های زیر را در پروژه <code>xAiApi</code> نصب کنید:</p>
<pre># پکیج اصلی PdfiumViewer
Install-Package PdfiumViewer
# پکیج Native DLLs (بسیار مهم - شامل pdfium.dll برای پلتفرم‌های مختلف)
Install-Package PdfiumViewer.Native
# برای کار با Bitmap و تصاویر
Install-Package System.Drawing.Common</pre>
<div class="alert alert-warning">
<strong>⚠️ نکته مهم در مورد Native DLLs:</strong>
<p>پکیج <code>PdfiumViewer.Native</code> فایل‌های <code>pdfium.dll</code> را برای پلتفرم‌های مختلف
(x86 و x64) در پوشه <code>bin</code> پروژه کپی می‌کند. بدون این پکیج، کتابخانه با خطای
<code>DllNotFoundException</code> مواجه خواهد شد.</p>
</div>
</div>
<!-- Step 2: Rewritten Function -->
<div class="section">
<h2>🔧 گام ۲: کد بازنویسی شده با PdfiumViewer</h2>
<div class="file-change">
<span class="badge-modify">MODIFY</span>
<span class="path">xAiApi/Providers/Extractors/XPdfFileContentExtractor.cs</span>
</div>
<pre>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
{
/// &lt;summary&gt;
/// استخراج محتوا از PDF با پشتیبانی از Fallback تصویری با PdfiumViewer ...
/// &lt;/summary&gt;
public class XPdfFileContentExtractor : IXPdfFileContentExtractor
{
/// &lt;summary&gt;
/// Supported MIME Types ...
/// &lt;/summary&gt;
private static readonly string[] SupportedMimeTypes = ["application/pdf"];
/// &lt;summary&gt;
/// حداقل تعداد کاراکتر برای تشخیص PDF متنی ...
/// &lt;/summary&gt;
private const int MinTextLengthThreshold = 50;
/// &lt;summary&gt;
/// حداکثر تعداد صفحات برای تبدیل به تصویر ...
/// &lt;/summary&gt;
private const int MaxPagesForImageFallback = 10;
/// &lt;summary&gt;
/// DPI برای رندر تصاویر (72 = کیفیت معمولی، 150 = کیفیت خوب، 300 = کیفیت بالا) ...
/// &lt;/summary&gt;
private const int RenderDpi = 150;
/// &lt;summary&gt;
/// حداکثر عرض تصویر به پیکسل ...
/// &lt;/summary&gt;
private const int MaxImageWidth = 2000;
/// &lt;summary&gt;
/// بررسی پشتیبانی از MIME Type ...
/// &lt;/summary&gt;
public bool CanExtract(string mimeType)
{
return SupportedMimeTypes.Contains(
mimeType?.ToLowerInvariant() ?? string.Empty
);
}
/// &lt;summary&gt;
/// استخراج محتوای غنی با Fallback هوشمند ...
/// &lt;/summary&gt;
public async Task&lt;XFileExtractionResult&gt; 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;
}
/// &lt;summary&gt;
/// Fallback: تبدیل صفحات PDF به تصویر با استفاده از PdfiumViewer ...
/// &lt;/summary&gt;
private async Task&lt;XFileExtractionResult&gt; FallbackToImageExtractionAsync(
Stream pdfStream,
string fileName,
string mimeType,
CancellationToken cancellationToken
)
{
var result = new XFileExtractionResult
{
FileName = fileName,
MimeType = mimeType,
UsedImageFallback = true
};
await Task.Run(() =&gt;
{
try
{
// باز کردن PDF با PdfiumViewer
using var document = PdfDocument.Load(pdfStream);
// محاسبه تعداد صفحات برای پردازش
var pageCount = Math.Min(
document.PageCount,
MaxPagesForImageFallback
);
// پردازش هر صفحه
for (int pageIndex = 0; pageIndex &lt; 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 &gt; 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;
}
/// &lt;summary&gt;
/// استخراج متن از PDF با UglyToad.PdfPig ...
/// &lt;/summary&gt;
private async Task&lt;string&gt; ExtractTextAsync(
Stream pdfStream,
CancellationToken cancellationToken
)
{
return await Task.Run(() =&gt;
{
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);
}
/// &lt;summary&gt;
/// بررسی آیا متن استخراج شده کافی است ...
/// &lt;/summary&gt;
private bool IsTextSufficient(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
// حذف فاصله‌ها و بررسی طول
var cleanText = new string(
text.Where(c =&gt; !char.IsWhiteSpace(c)).ToArray()
);
return cleanText.Length &gt;= MinTextLengthThreshold;
}
/// &lt;summary&gt;
/// سازگاری با نسخه قدیمی Interface ...
/// &lt;/summary&gt;
public async Task&lt;string&gt; ExtractAsync(
Stream fileStream,
string mimeType,
CancellationToken cancellationToken = default
)
{
var result = await ExtractRichAsync(
fileStream,
"document.pdf",
mimeType,
cancellationToken
);
return result.Text;
}
}
}</pre>
</div>
<!-- Step 3: Comparison -->
<div class="section">
<h2>📊 گام ۳: مقایسه PdfiumViewer با PdfPigRenderer</h2>
<table>
<tr>
<th>ویژگی</th>
<th>PdfPigRenderer</th>
<th>PdfiumViewer</th>
</tr>
<tr>
<td><strong>پایداری</strong></td>
<td>⚠️ نسبتاً جدید، گاهی ناپایدار</td>
<td>✅ بسیار پایدار، سال‌ها استفاده در production</td>
</tr>
<tr>
<td><strong>سرعت رندر</strong></td>
<td>⭐⭐ متوسط</td>
<td>⭐⭐⭐⭐ بسیار سریع (native C++)</td>
</tr>
<tr>
<td><strong>کیفیت خروجی</strong></td>
<td>⭐⭐⭐ خوب</td>
<td>⭐⭐⭐⭐⭐ عالی (همان موتور Chrome)</td>
</tr>
<tr>
<td><strong>پشتیبانی از فرمت‌های پیچیده</strong></td>
<td>⚠️ محدود</td>
<td>✅ کامل (فونت‌ها، transparency، gradients)</td>
</tr>
<tr>
<td><strong>حافظه مصرفی</strong></td>
<td>⭐⭐ متوسط</td>
<td>⭐⭐⭐⭐ بهینه (native memory)</td>
</tr>
<tr>
<td><strong>نیاز به Native DLL</strong></td>
<td>❌ خیر (Pure .NET)</td>
<td>✅ بله (pdfium.dll)</td>
</tr>
<tr>
<td><strong>پشتیبانی Cross-platform</strong></td>
<td>✅ کامل</td>
<td>✅ Windows + Linux (با پکیج Native مناسب)</td>
</tr>
<tr>
<td><strong>کنترل DPI و ابعاد</strong></td>
<td>⚠️ محدود</td>
<td>✅ کامل و دقیق</td>
</tr>
</table>
</div>
<!-- Step 4: Key Features -->
<div class="section">
<h2>✨ گام ۴: ویژگی‌های کلیدی کد بازنویسی شده</h2>
<div class="arch-grid">
<div class="arch-card">
<h4>🎨 کنترل DPI هوشمند</h4>
<ul>
<li>پارامتر <code>RenderDpi</code> قابل تنظیم</li>
<li>تعادل بین کیفیت و حجم خروجی</li>
<li>مقدار ۱۵۰ برای Vision Models ایده‌آل</li>
</ul>
</div>
<div class="arch-card">
<h4>📏 محدودیت ابعاد</h4>
<ul>
<li><code>MaxImageWidth = 2000</code></li>
<li>جلوگیری از تصاویر بسیار بزرگ</li>
<li>حفظ نسبت تصویر</li>
</ul>
</div>
<div class="arch-card">
<h4>🛡️ مدیریت خطای پیشرفته</h4>
<ul>
<li>خطای هر صفحه به صورت جداگانه</li>
<li>ادامه پردازش با صفحات دیگر</li>
<li>لاگ دقیق خطاها</li>
</ul>
</div>
<div class="arch-card">
<h4>⚡ بهینه‌سازی عملکرد</h4>
<ul>
<li>استفاده از <code>Task.Run</code></li>
<li>پشتیبانی از <code>CancellationToken</code></li>
<li>مدیریت صحیح <code>using</code></li>
</ul>
</div>
</div>
</div>
<!-- Step 5: Important Notes -->
<div class="section">
<h2>⚠️ گام ۵: نکات مهم پیاده‌سازی</h2>
<div class="alert alert-info">
<strong>💡 نکته ۱: Native DLL در زمان اجرا</strong>
<p>PdfiumViewer به صورت خودکار <code>pdfium.dll</code> را از پوشه‌های <code>x86</code> یا <code>x64</code>
در مسیر اجرای برنامه بارگذاری می‌کند. اطمینان حاصل کنید که این فایل‌ها همراه با برنامه deploy می‌شوند.</p>
</div>
<div class="alert alert-warning">
<strong>⚠️ نکته ۲: Linux Deployment</strong>
<p>برای استقرار روی Linux، از پکیج <code>PdfiumViewer.Core</code> استفاده کنید و
فایل <code>libpdfium.so</code> را در مسیر مناسب قرار دهید:</p>
<pre># در csproj فایل:
&lt;ItemGroup&gt;
&lt;None Include="runtimes/linux-x64/native/libpdfium.so"&gt;
&lt;CopyToOutputDirectory&gt;PreserveNewest&lt;/CopyToOutputDirectory&gt;
&lt;/None&gt;
&lt;/ItemGroup&gt;</pre>
</div>
<div class="alert alert-success">
<strong>✅ نکته ۳: تست عملکرد</strong>
<p>برای یک PDF اسکن‌شده ۱۰ صفحه‌ای با DPI=150:</p>
<ul style="padding-right: 25px; margin-top: 10px;">
<li><strong>زمان پردازش:</strong> حدود ۲-۳ ثانیه</li>
<li><strong>حجم هر تصویر:</strong> ۲۰۰-۴۰۰ کیلوبایت PNG</li>
<li><strong>مصرف RAM:</strong> حدود ۱۰۰ مگابایت peak</li>
</ul>
</div>
<div class="alert alert-danger">
<strong>⚠️ نکته ۴: محدودیت‌های PdfiumViewer</strong>
<ul style="padding-right: 25px; margin-top: 10px;">
<li>پشتیبانی ضعیف‌تر از <code>async/await</code> واقعی (استفاده از <code>Task.Run</code>)</li>
<li>نیاز به نصب Native DLL</li>
<li>عدم پشتیبانی از PDF های رمزنگاری شده با پسورد</li>
</ul>
</div>
</div>
<!-- Step 6: Alternative for .NET 8+ -->
<div class="section">
<h2>🚀 گام ۶: پیشنهاد برای .NET 8+ (آینده‌نگرانه)</h2>
<p>
اگر پروژه شما در آینده به .NET 8 یا بالاتر مهاجرت کند، می‌توانید از کتابخانه‌های مدرن‌تر مانند
<code>SkiaSharp</code> + <code>HarfBuzzSharp</code> یا <code> PdfPig</code> نسخه جدید استفاده کنید
که کاملاً Cross-platform و async-native هستند. اما برای حال حاضر، <strong>PdfiumViewer بهترین انتخاب</strong> است.
</p>
</div>
<!-- Summary -->
<div class="section">
<h2>📋 خلاصه تغییرات</h2>
<table>
<tr>
<th>مورد</th>
<th>توضیح</th>
</tr>
<tr>
<td>جایگزینی <code>PdfPigRenderer</code></td>
<td>با <code>PdfDocument</code> از PdfiumViewer</td>
</tr>
<tr>
<td>افزودن کنترل DPI</td>
<td>پارامتر <code>RenderDpi</code> برای تنظیم کیفیت</td>
</tr>
<tr>
<td>محدودیت ابعاد</td>
<td><code>MaxImageWidth</code> برای جلوگیری از تصاویر بزرگ</td>
</tr>
<tr>
<td>مدیریت خطای صفحه</td>
<td>ادامه پردازش در صورت خطای یک صفحه</td>
</tr>
<tr>
<td>بهبود لاگ</td>
<td>ثبت خطاها با <code>Debug.WriteLine</code></td>
</tr>
</table>
<div class="alert alert-success">
<strong>✅ نتیجه‌گیری:</strong>
<p>با بازنویسی تابع <code>FallbackToImageExtractionAsync</code> با استفاده از <strong>PdfiumViewer</strong>،
سیستم شما اکنون قابلیت تبدیل قابل اعتماد و با کیفیت PDF های اسکن‌شده به تصاویر را دارد.
این تصاویر می‌توانند مستقیماً به عنوان <code>DataContent</code> به مدل‌های Vision مانند
<strong>GPT-4V</strong>، <strong>Gemini</strong> یا <strong>Qwen-VL</strong> ارسال شوند. 🎯</p>
</div>
</div>
</div>
<div class="footer">
<p><strong>👨‍💻 توسعه‌دهنده:</strong> هادی خزاعی اصل</p>
<p><strong>🏢 شرکت:</strong> فن آوران ساحر علم</p>
<p><strong>📅 تاریخ تهیه مستند:</strong> یکشنبه ۱۳ مهر ۱۴۰۵</p>
<p style="margin-top: 15px; opacity: 0.8; font-size: 0.9em;">
📄 بازنویسی FallbackToImageExtractionAsync با PdfiumViewer - تمامی حقوق محفوظ است
</p>
</div>
</div>
</body>
</html>