چیزی که جمعههای من را میخورد
هر بعدازظهر جمعه، حدود یک سال، یک مراسم کوچک داشتم. یک قرارداد به صورت سه فایل میآمد — توافقنامه اصلی در Word، پیوست قیمتگذاری در Excel، و برگه شرایط شریک به صورت PDF — و من باید آنها را به یک PDF تمیز تبدیل میکردم. کاری سخت نبود. Word را باز میکردم، به PDF صادر میکردم. Excel را باز میکردم، به PDF صادر میکردم. یک برنامه رایگان ادغام PDF را باز میکردم، سه فایل را میکشیدم، ترتیب را بررسی میکردم، ذخیره میکردم.
حدود هشت دقیقه طول میکشید. اگر این زمان را در پانزده قرارداد در هفته ضرب کنیم، دو ساعت صرف حرکت ماوس میشود. بدتر از آن، هر چند هفته یکبار کسی یک بایندر میفرستاد که پیوست در صفحه اول باشد چون نام فایلها به صورت الفبایی در برنامه ادغام مرتب میشدند.
اگر این برای شما آشناست، بقیه این پست همان بعدازظهر است که در نهایت این مراسم را با کد جایگزین کردم.
هزینه واقعی زمان نیست — بلکه یک قرارداد از هر پنجاه است که صفحات به ترتیب اشتباه میآیند و تا زمانی که مشتری نسخهٔ اشتباه را امضا کند، کسی متوجه نمیشود.
دقیقاً چه میخواستم
نه «یک خط لولهٔ اسناد پیشرفته». فقط سه چیز:
- به یک متد لیستی از فایلها (هر ترکیبی از DOCX، XLSX، PDF) بدهید و یک PDF دریافت کنید.
- همان منطق را به یک پوشه اعمال کنید تا خود بهطور خودکار لیست فایلها را پیدا کند.
- بازهٔ صفحهای را از بایندر نهایی استخراج کنید بدون اینکه کل ادغام را دوباره انجام دهید.
این تمام کار است. اگر کتابخانه نتواند این سه کار را بهصورت تمیز انجام دهد، نمیخواهم در موردش بدانم.
راهاندازی
- .NET 6.0 یا بالاتر
- GroupDocs.Merger برای .NET 24.10+ (یک مجوز موقت بگیرید تا واترمارک ارزیابی را نداشته باشید)
- یک پوشه با هر ترکیبی از اسنادی که معمولاً بهصورت دستی ادغام میکنید
dotnet add package GroupDocs.Merger
همین برای وابستگیها کافی است. هیچ مبدل خارجی، هیچ نصب Office بدون سر، هیچ کتابخانهٔ دستکاری PDF در بالای آن نیست.
گام ۱ — اجازه دهید یک پوشه ورودی باشد
من همیشه از اینجا شروع میکنم چون نقطهٔ ورودی واقعی است. در عمل، چیز دیگری (یک هندلر بارگذاری، یک کار پردازش ایمیل، یک استخراج شبانه از مالی) مجموعهای از فایلها را در یک دایرکتوری میاندازد و کد من باید با هر چیزی که پیدا میکند کار کند.
// Pick up every supported file in the drop folder; the PDF wins
// the tie-break for position 0 so the merger keeps the output
// as a PDF regardless of how files are named.
string[] extensions = { ".pdf", ".docx", ".xlsx" };
var files = Directory.EnumerateFiles(folderPath)
.Where(f => extensions.Contains(Path.GetExtension(f).ToLowerInvariant()))
.OrderBy(f => Path.GetExtension(f).ToLowerInvariant() == ".pdf" ? 0 : 1)
.ThenBy(f => f)
.ToArray();
if (files.Length == 0)
throw new InvalidOperationException(
$"No supported documents found in '{folderPath}'.");
ترفند OrderBy بخش جالب است. GroupDocs.Merger فرمت خروجی خود را از فایلی که اولین بار باز میکنید انتخاب میکند — اگر من یک DOCX را بهعنوان سند اصلی بدهیم، خروجی DOCX میشود. چون خط لولهٔ من همیشه میخواهد خروجی PDF باشد، مطمئن میشوم هر PDF موجود در پوشه موقعیت ۰ را بگیرد.
دو نکته قابل ذکر:
ToLowerInvariant()چون ممکن است یک شریک روزی فایلREPORT.PDFبفرستد و فیلتر فقط حروف کوچک شما بهصورت ساکت آن را حذف کند.ThenBy(f)فقط برای اینکه خروجی تعیینپذیر باشد اضافه شده است. بدون آن، دو اجرا روی یک پوشه میتوانند بسته به حالت فایلسیستم متفاوت باشند.
گام ۲ — خود ادغام
بهمحض اینکه یک لیست مرتب از مسیرها داشته باشم، ادغام کوتاهتر از توصیف خود ادغام است.
Console.WriteLine($"Primary source: {sourcePaths[0]}");
using var merger = new Merger(sourcePaths[0]);
var joinOptions = new JoinOptions();
for (int i = 1; i < sourcePaths.Length; i++)
{
Console.WriteLine($"Joining: {sourcePaths[i]}");
merger.Join(sourcePaths[i], joinOptions);
}
merger.Save(outputPath);
Console.WriteLine($"Unified PDF binder saved to: {Path.GetFullPath(outputPath)}");
چند نکته از استفادهٔ این کتابخانه در شرایط سخت:
usingمهم است.Mergerدستگیرههای فایل را روی منابع نگه میدارد؛ اگر آن را نادیده بگیرید، کارگر پوشهٔ ورودی در نهایت نمیتواند ورودیهای خود را حذف کند.JoinOptionsاینجا خالی است چون پیشفرضها ۹۵٪ زمان مورد نیاز من هستند. وقتی به آن نیاز داشته باشید، همانجا بازههای صفحه، چرخش و موقعیتهای درج قرار میگیرند.- وقتی Excel به بایندر میپیوندد، چیدمان شیت‑به‑صفحه توسط ناحیهٔ چاپ کتابکار منبع تعیین میشود. اگر XLSX شما به ۳۸ صفحه تبدیل شود و شما فقط سه صفحه میخواهید، اصلاح در خود صفحهگسترده انجام میشود، نه در
JoinOptions.
یک بررسی صحت که همیشه بلافاصله پس از ذخیره اضافه میکنم:
using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");
دو ثانیه کد که بیشتر از هر تستی که نوشتهام، باگ «پیوست بهصورت ساکت حذف شد» را کشف کرده است.
گام ۳ — استخراج یک برش بعداً
درخواست پیگیری که هر بار دریافت میکنم این است: «میتوانید فقط صفحهٔ رویه را بفرستید؟» یا «مشتری فقط امضاها را میخواهد». بازسازی کل بایندر برای تحویل دو صفحه احمقانه است — استخراج مستقیماً این کار را انجام میدهد.
using var merger = new Merger(binderPath);
merger.ExtractPages(new ExtractOptions(pages));
merger.Save(outputPath);
Console.WriteLine($"Extracted pages [{string.Join(",", pages)}] to " +
Path.GetFullPath(outputPath));
pages یک int[] از شمارههای صفحهٔ ۱‑پایه است که میخواهید نگه دارید. بقیه حذف میشوند. این سریع است چون نتیجه از پیش یک PDF است — بدون دور رفتن تبدیل.
قبل و بعد، صادقانه
| کاری که قبلاً انجام میدادم | با Merger.Join |
|
|---|---|---|
| زمان بهازای هر قرارداد | ۵–۱۰ دقیقه کلیک کردن | زیر ۳۰ ثانیه از ابتدا تا انتها |
| شکست معمولی | صفحات به ترتیب اشتباه، کسی متوجه نمیشود | هر ترتیبی که لیست فایلها میگوید، بهصورت تکرارپذیر |
| مقیاس به ۱۰۰/روز | نمیشود — یک نفر استخدام میکنید | یک کارگر، بیشتر زمان خسته میشود |
| کدی که نگهداری میکنید | صفحهٔ Confluence با عنوان «Binder Process v4» | یک کلاس، حدود ۷۰ خط |
| خروجی | سه PDF و یک دعا | یک بایندر، با شمارش صفحهای که میتوانید لاگ کنید |
سطر «شکست» برای من مهمترین است. ادغام دستی بهصورت ساکت شکست میخورد؛ کدی که شمارش صفحه را لاگ میکند، بهصورت بلند شکست میخورد.
داستان واقعی از یک تیم کوچک تکنولوژی حقوقی
استارتاپ دو نفرهای که با آن کار میکردم، یک کارمند پارالیگل داشت که صبحش با مونتاژ قرارداد شروع میشد. توافقنامه Word، قیمتگذاری Excel، ضمیمه PDF، در یک برنامه ترکیب میشد، سپس به DocuSign بارگذاری میشد. حدود هشت دقیقه برای هر بسته، که با ۳۰ بسته در روز تقریباً تمام صبح او را میگرفت.
آنها روش اسکن پوشه را به سرویس بکاندی که قبلاً ایمیلهای ورودی را نظارت میکرد، اضافه کردند. بیست ثانیه برای هر بسته، بهعلاوه یک خط لاگ با شمارش صفحه. کارمند پارالیگل به بررسی قراردادها پرداخت بهجای مونتاژ آنها. دیگر هیچکس بایندری با ترتیب اشتباه ارسال نکرد — نه به این دلیل که کتابخانه جادویی است، بلکه چون لیست فایلها بهصورت صریح در کد است و میتوانید آن را مقایسه کنید.
string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";
var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");
این تمام یکپارچهسازی است. همهٔ چیزهای بالادست (شنونده ایمیل، مسیر ذخیرهسازی) قبلاً آماده بودند.
چیزهایی که امروز نیاز نداشتم اما فردا خواهم داشت
همان کتابخانه کارهای زیادی انجام میدهد که من به آنها نپرداختم چون مقاله طولانی میشد. بهتقریب به ترتیب استفادهای که تا بهحال داشتهام:
- Watermarks on the output برای مهرهای «پیشنویس» روی نسخههای پیش از امضا.
- Page rotation برای اسکنهایی که بهصورت افقی آمدهاند.
- Custom page ordering وقتی ترتیب منبع با ترتیب تحویل متفاوت است.
- PDF encryption برای هر چیزی که به طرف خارجی میرود.
همه اینها پشت همان API Merger قرار دارند. مستندات فهرست کامل را دارند — فقط میخواستم بگویم «ادغام» شروعکنندهٔ ارزان است و بقیه وقتی نیاز داشته باشید در دسترس هستند.
چیزی که به خودم در گذشته میگفتم
اگر میخواهید گام DOCX‑به‑PDF خود را بنویسید چون «فقط یک متد است»، متوقف شوید. تبدیل همان بخشی است که بهصورت ساکت خراب میشود — ویژگیهای جدید Office، پردازش تصویر اسکنشده، فونتهای توکار و غیره. بگذارید چیزی دیگر آن سطح را مدیریت کند و بعدازظهر جمعهتان را صرف کاری کنید که فقط مرتبسازی نام فایل نیست.
کجا بروید بعدی:
- مجوز موقت — برای خروجی بدون واترمارک لازم است
- گزینههای پیشرفتهٔ ادغام — JoinOptions، گزینههای ذخیره، فشردهسازی
- فرمتهای پشتیبانیشده — خیلی بیشتر از سه فرمی که اینجا نشان دادم
- پروژههای نمونه در GitHub — شامل این مثال