PDFium Component cung cấp cho bạn một phương thức để chia nhỏ tệp PDF: ImportPages. Mọi thứ khác, dù bạn đang tách một trang đơn, cắt theo các ranh giới bất kỳ, hoặc theo cấu trúc dấu trang (bookmark) của chính tài liệu, đều chỉ là những cách khác nhau để quyết định số trang nào sẽ đi vào từng tệp đầu ra. Cơ chế hoạt động vẫn giữ nguyên. Hiểu được điều đó từ sớm sẽ giúp bạn tránh được nhiều hướng đi sai lầm
Vòng lặp chia nhỏ hoạt động như thế nào
Mẫu mã (pattern) đều giống nhau bất kể bạn chia tài liệu nguồn như thế nào. Tạo một bản thể (instance) TPdf mới, gọi CreateDocument trên nó để khởi tạo một PDF trống trong bộ nhớ, nhập các trang bạn muốn bằng ImportPages, lưu kết quả, sau đó đặt lại Active thành False trước lần lặp tiếp theo. Bước cuối cùng đó là bước mà mọi người hay bỏ lỡ: CreateDocument không ngầm đóng tài liệu vẫn còn trong bộ nhớ, vì vậy bạn phải lưu đầu ra của mình và đặt lại Active := False một cách rõ ràng trước khi gọi lại nó; việc đặt lại trước sẽ giữ cho trạng thái sạch sẽ và được xác định rõ ràng. Bản thể TPdf bên ngoài được tái sử dụng trong suốt tất cả các lần lặp, điều này giữ cho áp lực cấp phát (allocation pressure) ở mức thấp đối với các công việc lớn
Đây là những gì việc chia nhỏ từng trang (page-by-page) trông như thế nào khi được rút gọn xuống những phần thiết yếu nhất:
procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 1 to Source.PageCount do
begin
PdfOut.CreateDocument;
// Range is a 1-based page number string; insertion point 1 = first position
if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
raise Exception.CreateFmt('Failed to import page %d', [I]);
OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Tham số Range đối với ImportPages là cùng một định dạng chuỗi (string format) mà PDFium sử dụng bên trong: một danh sách các số trang được phân tách bằng dấu phẩy hoặc các dải được phân cách bằng dấu gạch nối (hyphen-delimited ranges), tất cả đều dựa trên hệ cơ số 1 (1-based). '3' nhập trang 3. '1-5' nhập các trang từ 1 đến 5 theo thứ tự. '2,5,8' nhập ba trang đó. Tham số thứ ba là vị trí chèn dựa trên hệ cơ số 1 trong tài liệu đích; việc truyền 1 luôn đặt các trang được nhập vào phần đầu của một tệp vốn dĩ đang trống, đó chính là điều bạn muốn ở đây
Chia nhỏ theo dải trang (page ranges)
Khi người gọi cung cấp một danh sách như 1-12,13-24,25-36, bạn phân tích cú pháp (parse) nó thành các cặp điểm bắt đầu/kết thúc (start/end pairs) và chạy cùng một vòng lặp đó, xây dựng chuỗi dải (range string) từ mỗi cặp:
procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
const OutputDir: string);
var
I: Integer;
PdfOut: TPdf;
OutFile: string;
begin
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(RangeList) do
begin
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeList[I], 1) then
raise Exception.Create('Invalid page range: ' + RangeList[I]);
OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Việc kiểm tra tính hợp lệ (validation) trước khi bạn tiếp cận ImportPages có ý nghĩa quan trọng ở đây. ImportPages trả về False khi một số trang trong chuỗi dải vượt quá Source.PageCount, nhưng nó không đưa ra một ngoại lệ (exception) nào và nó không tạo ra một tệp đầu ra một phần mà bạn có thể phát hiện chỉ bằng cái tên. Hãy kiểm tra giá trị trả về của SaveAs và ghi nhật ký (log) các lỗi một cách riêng biệt; một dải tạo ra tệp đầu ra trống thì không thấy sai một cách rõ ràng cho đến khi có ai đó mở nó ra
Chia nhỏ tại các ranh giới dấu trang (bookmark boundaries)
Cách tiếp cận thứ ba sử dụng cấu trúc của chính tài liệu thay vì một danh sách được cung cấp từ bên ngoài. Mỗi dấu trang ở cấp cao nhất mang theo một số trang đích; phần mục (section) mà nó xác định sẽ chạy từ trang đó cho đến một trang ngay trước trang của dấu trang tiếp theo, hoặc đến cuối tài liệu đối với mục nhập cuối cùng (last entry)
procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
Bm: TBookmarks;
I, StartPage, EndPage: Integer;
PdfOut: TPdf;
RangeStr, OutFile, SafeTitle: string;
begin
Bm := Source.Bookmarks;
if Length(Bm) = 0 then
Exit;
PdfOut := TPdf.Create(nil);
try
for I := 0 to High(Bm) do
begin
StartPage := Bm[I].PageNumber;
if I < High(Bm) then
EndPage := Bm[I + 1].PageNumber - 1
else
EndPage := Source.PageCount;
if (StartPage < 1) or (EndPage < StartPage) then
Continue;
RangeStr := Format('%d-%d', [StartPage, EndPage]);
PdfOut.CreateDocument;
if not PdfOut.ImportPages(Source, RangeStr, 1) then
begin
PdfOut.Active := False;
Continue; // skip a malformed section instead of writing an empty file
end;
SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
if not PdfOut.SaveAs(OutFile) then
raise Exception.Create('Failed to save ' + OutFile);
PdfOut.Active := False;
end;
finally
PdfOut.Free;
end;
end;
Một tài liệu không có dấu trang không phải là một điều kiện lỗi đáng để báo lên cho người dùng dưới dạng một lỗi; nó chỉ có nghĩa là chế độ chia nhỏ này không có gì để thực hiện. Việc chặn bằng Length(Bm) = 0 sẽ xử lý điều đó một cách âm thầm. Điều đáng được đưa lên (surfacing) là khi số trang của dấu trang nằm ngoài phạm vi (range) của tài liệu, điều này xảy ra trong các tệp bị lỗi định dạng (malformed files) nơi outline không bao giờ được cập nhật sau khi các trang đã bị xóa đi. Bước kiểm tra các giới hạn (bounds check) trên StartPage và EndPage sẽ bỏ qua các mục đó thay vì chuyển một dải chứa lỗi (garbage range) đến ImportPages
Việc đặt tên tệp đầu ra và khôi phục trạng thái (Active reset)
Sự an toàn của tên tệp (filename safety) đối với các tên được suy ra từ dấu trang cần được chú ý một cách rõ ràng. Tiêu đề dấu trang có thể chứa các ký tự hợp lệ trong một chuỗi PDF nhưng không hợp lệ trong một đường dẫn (path) hệ thống tệp. Ở mức tối thiểu, hãy thay thế dấu gạch chéo xuôi (forward slash), dấu gạch chéo ngược (backslash), và dấu hai chấm (colon) trước khi xây dựng đường dẫn đầu ra. Trên Windows, các ký hiệu *, ?, ", <, >, và | cũng bị cấm; một vòng lặp đơn giản chạy trên một tập hợp cố định (fixed set) sẽ bao gồm chúng mà không cần phải gọi một regex vào
Dòng Active := False ở cuối mỗi vòng lặp đáng được nhấn mạnh bởi vì đây là yêu cầu duy nhất không rõ ràng trong cái mẫu này. CreateDocument không ngầm đóng lại bất cứ thứ gì đang mở. Nếu Active vẫn là True khi CreateDocument chạy lại, tài liệu vẫn còn ở trong bộ nhớ thì chưa bao giờ được đóng hoặc lưu lại một cách thích hợp, và bạn không thể dựa vào một hành vi được xác định rõ (well-defined behavior) ở trong trạng thái đó, vì vậy hãy lưu và đặt lại một cách rõ ràng trước khi bắt đầu tài liệu tiếp theo. Hãy nghĩ về nó giống như một cặp đối với try/finally: khối finally sẽ giải phóng (frees) đối tượng bên ngoài; phần Active := False thiết lập lại trạng thái tài liệu bên trong giữa các lần lặp của vòng lặp
Mức sử dụng bộ nhớ (memory use) trên một công việc chia tách lớn sẽ duy trì mức ổn định với cách tiếp cận này bởi vì bạn không bao giờ giữ nhiều hơn một tài liệu đầu ra trong bộ nhớ cùng một lúc. Tài liệu nguồn sẽ luôn ở trạng thái mở và chỉ-đọc (read-only) trong suốt quá trình; ImportPages chỉ sao chép dữ liệu trang vào tài liệu mới mà không cần phải thay đổi nguồn. Nếu nguồn đã bị mã hóa, hãy mở nó bằng mật khẩu của nó ngay từ trước vòng lặp và các trang được sao chép trong mỗi tệp đầu ra sẽ không bị mã hóa nữa, thường thì đây là hành vi phù hợp với việc đầu ra bị phân tách sau đó phân phối lại cho các người nhận khác nhau
Thêm một điều nữa về SaveAs: nó trả về một kiểu Boolean. Một thư mục đầu ra không tồn tại, một đường dẫn chứa các ký tự mà hệ điều hành từ chối, hay một tình trạng đầy ổ đĩa (disk-full condition) sẽ đều làm cho SaveAs trả lại False mà không tung ra một ngoại lệ nào. Trong một tác vụ hàng loạt (batch job) đi chia nhỏ một tài liệu 200 trang thành 200 tệp một-trang, một lỗi âm thầm (silent failure) xảy ra ở trang 147 rất dễ bị bỏ qua. Hãy kiểm tra giá trị trả về của mỗi lệnh gọi và đối chiếu số lượng thành công so với tổng số đã dự kiến mỗi khi vòng lặp kết thúc
Các phương thức ImportPages và CreateDocument được hiển thị ở đây là một phần của PDFium Component dành cho Delphi và C++Builder