The thing that kept eating my Fridays
Mỗi chiều thứ Sáu, trong khoảng một năm, tôi có cùng một nghi lễ nhỏ. Một hợp đồng sẽ đến dưới dạng ba tệp — thỏa thuận chính ở Word, phụ lục giá ở Excel, và bảng điều khoản của đối tác dưới dạng PDF — và tôi phải gộp chúng lại thành một PDF sạch sẽ. Không khó gì. Mở Word, xuất ra PDF. Mở Excel, xuất ra PDF. Mở một ứng dụng gộp PDF miễn phí, kéo ba tệp vào, kiểm tra thứ tự, lưu.
Quá trình này mất khoảng tám phút. Nhân với mười lăm hợp đồng mỗi tuần và bạn đã mất hai giờ chỉ để di chuyển chuột. Tệ hơn, mỗi vài tuần có người lại gửi một binder có phụ lục ở trang một vì tên tệp được sắp xếp alphabet trong ứng dụng gộp.
Nếu bạn thấy điều này quen thuộc, phần còn lại của bài viết là buổi chiều tôi cuối cùng thay thế nghi lễ bằng mã.
Chi phí thực sự không phải là thời gian — mà là một hợp đồng trong năm mươi mà các trang bị sắp xếp sai và không ai nhận ra cho đến khi khách hàng ký phiên bản sai.
What I actually wanted
Không phải “một pipeline tài liệu sang trọng”. Chỉ ba điều:
- Cung cấp cho một phương thức một danh sách các tệp (bất kỳ sự kết hợp nào của DOCX, XLSX, PDF) và nhận lại một PDF.
- Đưa cùng một logic vào một thư mục và để nó tự xác định danh sách tệp.
- Lấy ra một phạm vi trang từ binder đã hoàn thành mà không phải thực hiện lại toàn bộ quá trình gộp.
Đó là toàn bộ công việc. Nếu thư viện không thực hiện được ba điều trên một cách sạch sẽ, tôi không muốn biết.
Setup
- .NET 6.0 trở lên
- GroupDocs.Merger for .NET 24.10+ (grab a temporary license để bạn không phải phát hành watermark đánh giá)
- Một thư mục chứa bất kỳ sự kết hợp tài liệu nào mà bạn thường gộp thủ công
dotnet add package GroupDocs.Merger
Chỉ có vậy là đủ các phụ thuộc. Không cần bộ chuyển đổi bên ngoài, không cần cài đặt Office không giao diện, không cần thư viện xử lý PDF bổ sung.
Step 1 — Let a folder be the input
Tôi luôn bắt đầu ở đây vì đây là điểm vào thực tế. Trong thực tiễn, một thứ khác (bộ xử lý tải lên, công việc nhập email, dump đêm từ bộ phận tài chính) sẽ đưa một loạt tệp vào một thư mục, và mã của tôi phải xử lý bất kỳ gì nó tìm thấy.
// 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}'.");
Mánh khóe OrderBy là phần thú vị. GroupDocs.Merger chọn định dạng đầu ra dựa trên tệp đầu tiên bạn mở — nếu tôi đưa cho nó một DOCX làm tài liệu chính, tôi sẽ nhận được DOCX ra. Vì pipeline của tôi luôn muốn đầu ra là PDF, tôi đảm bảo bất kỳ PDF nào có trong thư mục cũng nhận vị trí 0.
Hai điểm đáng lưu ý:
ToLowerInvariant()vì một đối tác có thể một ngày nào đó gửi cho bạnREPORT.PDFvà bộ lọc chỉ nhận chữ thường sẽ lặng lẽ bỏ qua nó.ThenBy(f)chỉ để làm cho kết quả đầu ra có tính quyết định. Nếu không có nó, hai lần chạy trên cùng một thư mục có thể cho kết quả khác nhau tùy vào trạng thái hệ thống tập tin.
Step 2 — The merge itself
Khi tôi đã có danh sách các đường dẫn đã được sắp xếp, quá trình gộp ngắn hơn mô tả của nó.
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)}");
Một vài lưu ý khi dùng nó trong thực tế:
usingquan trọng.Mergergiữ các handle file trên các nguồn; nếu quên giải phóng, worker quét thư mục sẽ cuối cùng không thể xóa các tệp đầu vào của mình.JoinOptionsở đây để trống vì các giá trị mặc định là những gì tôi muốn 95 % thời gian. Khi bạn cần tùy chỉnh, đó là nơi các phạm vi trang, xoay, và vị trí chèn được đặt.- Khi Excel vào binder, bố cục sheet‑to‑page được quyết định bởi vùng in của workbook nguồn. Nếu XLSX của bạn ra 38 trang mà bạn chỉ muốn ba, việc sửa phải thực hiện trong bảng tính, không phải trong
JoinOptions.
Một kiểm tra hợp lý tôi luôn thêm ngay sau khi lưu:
using var verify = new Merger(outputPath);
Console.WriteLine($"Result pages: {verify.GetDocumentInfo().PageCount}");
Hai giây mã đã bắt được nhiều lỗi “phụ lục bị bỏ lỡ” hơn bất kỳ bài kiểm tra nào tôi viết.
Step 3 — Extract a slice later
Yêu cầu tiếp theo tôi nhận được mỗi lần: “Bạn có thể gửi cho tôi trang bìa không?” hoặc “Khách hàng chỉ muốn các chữ ký.” Việc xây dựng lại toàn bộ binder chỉ để đưa hai trang là vô lý — Extract làm việc này trực tiếp.
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 là một int[] chứa các số trang (đánh số từ 1) bạn muốn giữ lại. Mọi thứ khác sẽ bị loại bỏ. Nó nhanh vì kết quả đã là PDF — không có vòng quay chuyển đổi.
Before vs. after, honestly
| What I used to do | With Merger.Join |
|
|---|---|---|
| Per-contract time | 5–10 minutes of clicking | under 30 seconds end-to-end |
| Typical failure | Pages in the wrong order, nobody notices | Whatever order the file list says, repeatably |
| Scaling to 100/day | Doesn’t — you hire a person | One worker, bored most of the time |
| Code you maintain | A Confluence page titled “Binder Process v4” | One class, ~70 lines |
| Output | Three PDFs and a prayer | One binder, with page count you can log |
Hàng tôi quan tâm nhất là hàng “failure”. Gộp thủ công thất bại một cách âm thầm; mã ghi lại số trang sẽ thất bại một cách rõ ràng.
A real story from a tiny legal-tech team
Một startup hai người mà tôi đã làm việc có một trợ lý pháp lý, người bắt đầu buổi sáng bằng việc lắp ráp hợp đồng. Thỏa thuận Word, bảng giá Excel, phụ lục PDF, ghép lại trong một app, tải lên DocuSign. Khoảng tám phút cho một gói, và với 30 gói mỗi ngày, đó gần như toàn bộ buổi sáng của cô ấy.
Họ đưa phương pháp quét thư mục vào dịch vụ backend đã theo dõi email đầu vào. Hai mươi giây cho mỗi gói, cộng một dòng log với số trang. Trợ lý pháp lý chuyển sang việc xem xét hợp đồng thay vì lắp ráp chúng. Không ai còn gửi binder sai thứ tự nữa — không phải vì thư viện có phép màu, mà vì danh sách tệp được xác định rõ trong mã và bạn có thể diff nó.
string folder = @"C:\IncomingContracts";
string output = @"C:\Processed\ContractPackage.pdf";
var files = CreatePdfBinderFromFolder(folder, output);
Console.WriteLine($"Package created: {files}");
Đó là toàn bộ tích hợp. Mọi thứ phía trên (listener email, đường dẫn lưu trữ) đã có sẵn.
Stuff I didn’t need today but will tomorrow
Thư viện này còn làm được rất nhiều thứ mà tôi chưa đề cập vì bài viết sẽ kéo dài. Theo thứ tự tôi đã dùng chúng:
- Watermarks on the output cho các dấu “DRAFT” trên bản sao trước khi ký.
- Page rotation cho các bản scan bị quay ngang.
- Custom page ordering khi thứ tự nguồn không phải là thứ tự giao hàng.
- PDF encryption cho bất kỳ tài liệu nào gửi tới bên đối tác bên ngoài.
Tất cả đều nằm sau cùng một API Merger. docs có danh sách đầy đủ — tôi chỉ muốn nhấn mạnh rằng “merge” là tính năng khởi đầu rẻ tiền và các tính năng còn lại có sẵn khi bạn cần.
What I’d tell past-me
Nếu bạn đang định tự viết bước chuyển DOCX‑to‑PDF vì “chỉ một phương thức”, hãy dừng lại. Việc chuyển đổi là phần dễ bị hỏng — các tính năng mới của Office, xử lý ảnh scan, font nhúng, v.v. Hãy để một thứ khác chịu trách nhiệm phần này, và dành buổi chiều thứ Sáu cho việc không phải sắp xếp tên tệp.
Where to go next:
- Temporary license — cần thiết để xuất ra không có watermark
- Advanced merging options — JoinOptions, tùy chọn lưu, nén
- Supported formats — vượt xa ba định dạng tôi đã trình bày
- Sample projects on GitHub — bao gồm cả dự án này