The thing that kept eating my Fridays
هر جمعه بعدازظهر، حدود یک سال، یک مراسم کوچک همیشگی داشتم. یک قرارداد به صورت سه فایل میآمد — توافقنامه اصلی در Word، پیوست قیمتگذاری در Excel، و برگه شرایط شریک به صورت PDF — و من باید آنها را به یک PDF تمیز تبدیل میکردم. کاری ساده. Word را باز میکردم، به PDF صادر میکردم. Excel را باز میکردم، به PDF صادر میکردم. یک برنامه رایگان ترکیبگر PDF را باز میکردم، سه فایل را میکشیدم، ترتیب را بررسی میکردم، ذخیره میکردم.
حدود هشت دقیقه طول میکشید. اگر این زمان را در پانزده قرارداد در هفته ضرب کنیم، دو ساعت صرف جابجایی ماوس میشود. بدتر از آن، هر چند هفته یکبار کسی یک بایندر میفرستاد که پیوست در صفحه اول باشد چون نام فایلها به صورت الفبایی در برنامه ترکیبگر مرتب شده بودند.
اگر این برای شما آشناست، بقیه این پست همان بعدازظهر است که در نهایت این مراسم را با کد جایگزین کردم.
هزینه واقعی زمان نیست — بلکه یک قرارداد در هر پنجاهتاست که صفحات به ترتیب اشتباه میآیند و تا زمانی که مشتری نسخهٔ اشتباه را امضا نکند، کسی متوجه نمیشود.
What I actually wanted
نه «یک خط لولهٔ اسناد پیشرفته». فقط سه چیز:
- به یک متد یک لیست از فایلها (هر ترکیبی از DOCX، XLSX، PDF) بدهید و یک PDF دریافت کنید.
- همان منطق را به یک پوشه اعمال کنید تا خود به خود لیست فایلها را پیدا کند.
- یک بازهٔ صفحهای را از بایندر نهایی استخراج کنید بدون اینکه کل ترکیب را دوباره انجام دهید.
این تمام کار است. اگر کتابخانه نتواند این سه مورد را بهصورت تمیز انجام دهد، من نمیخواهم در موردش بشنوم.
Setup
- .NET 6.0 یا بالاتر
- GroupDocs.Merger for .NET 24.10+ (grab a temporary license so you don’t ship the eval watermark)
- یک پوشه با هر ترکیبی از اسنادی که معمولاً بهصورت دستی ترکیب میکنید
dotnet add package GroupDocs.Merger
همین برای وابستگیها کافی است. هیچ مبدل خارجی، هیچ نصب Office بدون سر، هیچ کتابخانهٔ دستکاری PDF در بالای آن نیست.
Step 1 — Let a folder be the input
همیشه از اینجا شروع میکنم چون نقطهٔ ورودی واقعی است. در عمل، چیزی دیگر (یک هندلر بارگذاری، یک کار پردازش ایمیل، یک استخراج شبانه از مالی) مجموعهای از فایلها را در یک دایرکتوری میاندازد و کد من باید با هر چیزی که پیدا میکند کار کند.
// 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)فقط برای اینکه خروجی تعیینپذیر باشد اضافه شده است. بدون آن، دو اجرا روی یک پوشه میتوانند بسته به حالت فایلسیستم متفاوت باشند.
Step 2 — The merge itself
به محض اینکه یک لیست مرتب از مسیرها داشته باشم، ترکیب کوتاهتر از توضیح ترکیب است.
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}");
دو ثانیه کد که بیشتر از هر تستی که نوشتهام، باگ «پیوست مخفیانه حذف شده» را کشف کرده است.
Step 3 — Extract a slice later
درخواست پیگیری که هر بار میگیرم این است: «آیا میتوانید فقط صفحهٔ جلد را بفرستید؟» یا «مشتری فقط امضاها را میخواهد.» بازسازی کل بایندر برای ارسال دو صفحه، کار احمقانهای است — استخراج مستقیم این کار را انجام میدهد.
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 است — بدون دور رفتن تبدیل.
Before vs. after, honestly
| کاری که قبلاً انجام میدادم | با Merger.Join |
|
|---|---|---|
| زمان به ازای هر قرارداد | ۵–۱۰ دقیقه کلیک کردن | زیر ۳۰ ثانیه از ابتدا تا انتها |
| شکست معمولی | صفحات به ترتیب اشتباه، کسی متوجه نمیشود | هر ترتیبی که لیست فایلها میگوید، بهصورت تکراری |
| مقیاسپذیری برای ۱۰۰/روز | امکانپذیر نیست — یک نفر استخدام میکنید | یک کارگر، اکثر زمان بیکاری |
| کدی که نگهداری میکنید | صفحهٔ Confluence با عنوان «Binder Process v4» | یک کلاس، حدود ۷۰ خط |
| خروجی | سه PDF و یک دعا | یک بایندر، با شمارش صفحهای که میتوانید لاگ کنید |
سطر «شکست» برای من مهمترین است. ترکیب دستی بهصورت ساکت شکست میخورد؛ کدی که شمارش صفحه را لاگ میکند، بهصورت بلند شکست میخورد.
A real story from a tiny legal-tech team
یک استارتاپ دو نفره که با آن کار میکردم، یک کارمند پارهوقت داشت که صبحش با ترکیب قراردادها شروع میشد. توافقنامه Word، قیمتگذاری Excel، ضمیمه PDF، در یک برنامه میچسبیدند و به DocuSign آپلود میشد. حدود هشت دقیقه برای هر بسته، که در ۳۰ بسته در روز عملاً تمام صبح او را میگرفت.
آنها روش اسکن پوشه را به سرویس بکاندی که قبلاً ایمیل ورودی را نظارت میکرد، اضافه کردند. بیست ثانیه برای هر بسته، بهعلاوه یک خط لاگ با شمارش صفحه. کارمند پارهوقت به بررسی قراردادها رفت بهجای ترکیب آنها. دیگر هیچکس بایندری با ترتیب اشتباه ارسال نکرد — نه به این دلیل که کتابخانه جادویی است، بلکه چون لیست فایلها بهصورت صریح در کد است و میتوانید آن را مقایسه کنید.
string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";
var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");
این تمام یکپارچهسازی است. همهٔ چیزهای بالادست (شنونده ایمیل، مسیر ذخیره) قبلاً آماده بودند.
Stuff I didn’t need today but will tomorrow
همان کتابخانه کارهای زیادی انجام میدهد که من در این مقاله پوشش ندادم چون مقاله طولانی میشد. بهتقریب به ترتیب استفادهای که تا به حال داشتهام:
- Watermarks on the output برای مهر «DRAFT» روی نسخههای پیشامضا.
- Page rotation برای اسکنهایی که بهصورت افقی میآیند.
- Custom page ordering وقتی ترتیب منبع با ترتیب تحویل متفاوت است.
- PDF encryption برای هر چیزی که به یک طرف مقابل خارجی میرود.
تمام اینها پشت همان API Merger قرار دارند. docs فهرست کامل را دارد — من فقط میخواستم بگویم «merge» گزینهٔ ابتدایی ارزان است و بقیه وقتی نیاز داشته باشید در دسترس هستند.
What I’d tell past-me
اگر میخواهید گام تبدیل DOCX به PDF خودتان را بنویسید چون «فقط یک متد است»، متوقف شوید. تبدیل همان بخشی است که بهصورت ساکت خراب میشود — ویژگیهای جدید Office، پردازش تصویر اسکنشده، فونتهای توکار و غیره. بگذارید چیزی دیگر آن سطح را مدیریت کند و بعدازظهر جمعهتان را صرف کاری کنید که فقط مرتبسازی نام فایل نیست.
کجا برویم بعداً:
- Temporary license — برای خروجی بدون واترمارک ضروری است
- Advanced merging options — JoinOptions، گزینههای ذخیره، فشردهسازی
- Supported formats — خیلی بیشتر از سه فرمی که اینجا نشان دادم
- Sample projects on GitHub — شامل این پروژه