PDFium Component hỗ trợ việc ghép PDF thông qua một phương thức duy nhất: ImportPages. Mô hình luôn là giống nhau: tạo một tài liệu đích trống, mở từng tệp nguồn, gọi ImportPages để sao chép các trang qua, đóng nguồn, và lặp lại. Khi vòng lặp hoàn thành, SaveAs ghi kết quả xuống đĩa. Không có chế độ hợp nhất đặc biệt nào, không có cấu hình nào để chuyển đổi. Sự phức tạp nằm ở các trường hợp cạnh (edge cases), và có một vài trường hợp sẽ gây rắc rối nếu không có cảnh báo trước
Vòng lặp cốt lõi
Hai đối tượng TPdf là tất cả những gì bạn cần. Một đối tượng giữ tài liệu đích, được tạo trống bằng CreateDocument. Đối tượng kia lần lượt mở từng tệp nguồn. Dưới đây là một thủ tục nhận danh sách các đường dẫn tệp và ghi đầu ra đã ghép vào một đường dẫn duy nhất:
procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
PdfDest, PdfSrc: TPdf;
InsertAt, I: Integer;
begin
PdfDest := TPdf.Create(nil);
PdfSrc := TPdf.Create(nil);
try
PdfDest.CreateDocument;
InsertAt := 1; // ImportPages uses 1-based destination position
for I := 0 to FileList.Count - 1 do
begin
PdfSrc.FileName := FileList[I];
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);
PdfDest.ImportPages(
PdfSrc,
'1-' + IntToStr(PdfSrc.PageCount), // full document range
InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
end;
PdfDest.SaveAs(OutputPath);
finally
PdfSrc.Free;
PdfDest.Free;
end;
end;
Có hai điều trong đoạn mã đó dễ bị bỏ qua ở lần đọc đầu tiên. Thứ nhất là cách PDFium báo cáo lỗi tải. Active := True không bao giờ ném ra một ngoại lệ: nếu tệp bị thiếu, bị hỏng hoặc được bảo vệ bằng mật khẩu, PDFium bắt lỗi ở bên trong và để Active là False. Nếu không có kiểm tra tường minh ở dòng 10, một tệp hỏng sẽ âm thầm rơi khỏi quá trình hợp nhất mà không có dấu hiệu nào trong kết quả. Tệp PDF cuối cùng sẽ có ít trang hơn dự kiến và bạn sẽ không biết tệp nào là thủ phạm
Điều thứ hai là bộ đếm InsertAt. Đối số thứ ba của ImportPages là vị trí dựa trên chỉ số 1 ở đích nơi trang nguồn đầu tiên được nhập sẽ được chèn vào. Bắt đầu ở vị trí 1 sẽ đặt tài liệu nguồn đầu tiên ở đầu một tệp trống. Sau mỗi nguồn, bộ đếm sẽ tiến thêm PdfSrc.PageCount, do đó lô trang tiếp theo sẽ nối tiếp sau trang cuối cùng. Nếu quên tăng nó, mọi nguồn tiếp theo sẽ ghi đè lên các trang ở vị trí 1, cho bạn tài liệu cuối cùng trong danh sách và không có gì khác
Phạm vi trang được chọn lọc
Bạn không phải lấy mọi trang từ một nguồn. Chuỗi phạm vi được truyền làm đối số thứ hai tuân theo định dạng dấu phẩy và dấu gạch nối đơn giản: "1-3" lấy trang 1 đến trang 3, "2,4,6" lấy ba trang cụ thể, và "1-" có nghĩa là trang 1 đến cuối tài liệu. Các phạm vi có thể được kết hợp trong một chuỗi duy nhất, do đó "1-3,5,7-" bỏ qua các trang 4 và 6. Một sự tinh tế quan trọng ở đây: các con số luôn luôn tham chiếu đến các trang trong tài liệu nguồn, bắt đầu từ 1, bất kể những trang đó sẽ kết thúc ở đâu trong đích. Nếu bạn muốn các trang từ 40 đến 50 ra khỏi một danh mục 200 trang, chuỗi phạm vi là "40-50", không phải là vị trí tương đối với những gì đã có trong đích
// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active := True;
if PdfSrc.Active then
begin
// Page 1 is the cover; pages 3-5 are the summary
PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
Inc(InsertAt, 4); // 1 cover + 3 summary pages = 4 pages added
PdfSrc.Active := False;
end;
Khi tính toán số lượng tăng thêm cho InsertAt, hãy đếm các trang bạn thực sự đã nhập, không phải số lượng trang của nguồn. Nếu bạn truyền '1,3-5', bạn đã nhập 4 trang, vì vậy hãy tiến lên 4. Nếu tiến lên theo PdfSrc.PageCount sẽ để lại một khoảng trống các vị trí đích và đặt tài liệu nguồn tiếp theo sâu hơn vào tệp so với ý định
Những gì ImportPages bảo tồn và không bảo tồn
Các trang được sao chép bởi ImportPages mang nội dung trực quan của chúng nguyên vẹn. Văn bản, đồ họa vector, hình ảnh raster, phông chữ nhúng, và biểu mẫu XObjects tất cả đều được chuyển thành một phần của luồng nội dung trang. Các chú thích ở cấp trang, bao gồm bình luận, phần tô sáng và nét mực, cũng đi qua, vì chúng được lưu trữ bên trong từ điển trang thay vì ở cấp độ tài liệu
Siêu dữ liệu (metadata) ở cấp tài liệu lại là một câu chuyện khác. Tiêu đề, tác giả, chủ đề và từ khóa trong từ điển Info của nguồn sẽ ở lại. Tài liệu đích bắt đầu với siêu dữ liệu trống sau CreateDocument, do đó, nếu kết quả đã hợp nhất cần được điền vào các trường đó, bạn phải gán trực tiếp cho PdfDest trước khi gọi SaveAs. Thuộc tính Title, Author, Subject, Keywords và Creator trên TPdf nhận các chuỗi thông thường và ghi vào từ điển Info khi lưu
Các trường biểu mẫu tương tác (interactive form fields) phức tạp hơn. Các định nghĩa trường AcroForm nằm trong từ điển ở cấp tài liệu thay vì bên trong các luồng trang riêng lẻ. Khi ImportPages sao chép một trang chứa các trường biểu mẫu, hình thức bên ngoài của các trường đó sẽ chuyển đi vì nó được hiển thị vào luồng nội dung trang, nhưng các tiện ích trường (field widgets) làm cho chúng có thể tương tác lại là một phần của cấu trúc AcroForm và không đi theo. Trong một quá trình hợp nhất thông thường, một trường văn bản từ tài liệu nguồn sẽ hiển thị giá trị mà nó có tại thời điểm nhập, nhưng nó sẽ không thể chỉnh sửa trong tệp đã hợp nhất. Nếu bạn cần các trường vẫn có thể điền, hãy làm phẳng (flatten) chúng trong mỗi tài liệu nguồn trước khi nhập: điều đó sẽ nung các giá trị hiện tại vào luồng nội dung và loại bỏ lớp tương tác bên trên, cho bạn một kết quả hình ảnh sạch mà không có các widget bị hỏng ở đầu ra
Tệp nguồn được mã hóa
Tài liệu nguồn được bảo vệ bằng mật khẩu mở theo cách giống như tài liệu không được mã hóa, chỉ với một thuộc tính bổ sung cần được thiết lập đầu tiên. Gán mật khẩu cho PdfSrc.Password trước khi chuyển đổi Active := True, và PDFium sẽ sử dụng nó trong quá trình mở:
PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active := True;
if not PdfSrc.Active then
raise Exception.Create('Wrong password or file cannot be opened');
PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;
Sai mật khẩu gây ra kết quả im lặng Active = False tương tự như khi bị thiếu tệp, do đó việc kiểm tra rõ ràng ở đây là cần thiết. Khóa mã hóa không chuyển sang đích: các trang được nhập từ nguồn được bảo vệ sẽ vào đích dưới dạng nội dung không được bảo vệ. Nếu đầu ra ghép cần mã hóa, hãy cấu hình nó trên PdfDest trước khi gọi SaveAs
Lưu kết quả
SaveAs trên TPdf chấp nhận một đường dẫn tệp hoặc một TStream. Đối với hầu hết các việc hợp nhất, phần nạp chồng (overload) tệp là những gì bạn muốn:
PdfDest.SaveAs('merged-output.pdf');
Đối số thứ hai tùy chọn là một TSaveOption dùng để điều khiển chế độ lưu. Mặc định, saNone, viết một bản cập nhật gia tăng (incremental update) nếu tài liệu được tải từ một tệp, hoặc viết lại hoàn toàn nếu nó được tạo mới. Vì một tài liệu đích được xây dựng với CreateDocument luôn là mới, đầu ra sẽ là một tệp bản sửa đổi duy nhất được thu gọn. Đối số thứ ba, TPdfVersion, cho phép bạn ghim tiêu đề phiên bản PDF khi bạn có những ứng dụng xử lý cần một phiên bản cụ thể; để ở mức pvUnknown để cho phép PDFium tự chọn dựa trên nội dung
Các phương thức ImportPages và SaveAs hiển thị ở đây là một phần của PDFium Component dành cho Delphi và C++Builder