Technical Article

Áp trang N-up và sắp xếp lại trang với PDFium

Hợp nhất (merge) và chia tách (split) là hai thao tác trang đầu tiên mà mọi người nghĩ đến, và chúng giải quyết được rất nhiều vấn đề. Nhưng chúng không bao gồm tất cả mọi thứ. Có một nhóm công việc riêng biệt giúp sắp xếp lại các trang thay vì di chuyển toàn bộ tệp tin: xếp bốn trang slide lên một tờ giấy để làm tài liệu phát tay, kéo một trang từ cuối tài liệu lên đầu, hoặc trích xuất các trang 3, 7 và 12 thành một đoạn trích ngắn mà không chạm vào phần còn lại. PDFium cung cấp ba phương thức chính xác cho việc này, và mỗi phương thức hoạt động khác với các thao tác hợp nhất và chia tách bạn đã biết. Bài viết này hướng dẫn chi tiết những gì chúng làm, vị trí các điểm đầu ra, và một chi tiết về quyền sở hữu từng gây ra lỗi treo chương trình trong thực tế.

Ba phương thức đó là ImportNPagesToOne cho việc áp trang N-up, MovePages cho việc sắp xếp lại tại chỗ, và ImportPagesByIndex cho việc trích xuất tập con. Hợp nhất xếp chồng các tài liệu từ đầu đến cuối và giữ cho số lượng trang bằng tổng của các đầu vào. Chia tách ghi nhiều tệp đầu ra từ một đầu vào. Ba thao tác ở đây nằm ở khoảng giữa: một thao tác thay đổi số lượng trang nguồn chia sẻ chung một tờ giấy, một thao tác thay đổi thứ tự bên trong một tài liệu đơn lẻ, và một thao tác sao chép một số trang được chọn vào một tài liệu khác. Việc biết rõ từng loại giúp bạn tránh khỏi quy trình hợp nhất và xóa phức tạp khi chỉ cần một lệnh gọi duy nhất là đủ.

Quy trình áp trang N-up thực sự làm gì

Áp trang (imposition) là thuật ngữ trước khi in (prepress) dùng để chỉ việc sắp xếp nhiều trang nguồn lên một tờ giấy lớn hơn để kết quả sau khi in và gấp lại có thể đọc được theo đúng thứ tự. Phiên bản hàng ngày là tài liệu phát tay 2-up, tập sách nhỏ 4-up, hoặc trang chứa một loạt hình thu nhỏ (thumbnails). PDFium xử lý hình học thông qua một lệnh gọi duy nhất:

function ImportNPagesToOne(
  OutputWidth, OutputHeight: Single;
  NumX, NumY               : Cardinal): TPdf;

NumXNumY mô tả lưới ô. Một giá trị 2, 1 đặt hai trang nguồn cạnh nhau; 2, 2 đóng gói bốn trang vào bố cục bốn góc; 4, 3 xây dựng một trang chứa mười hai hình thu nhỏ. PDFium đọc các trang nguồn theo thứ tự, thu nhỏ từng trang để vừa với ô của nó, và điền vào lưới từ trái sang phải, từ trên xuống dưới, bắt đầu một trang đầu ra mới bất cứ khi nào lưới hiện tại đầy. Các trang nguồn không bị thay đổi. Kết quả bạn nhận về là một tài liệu mới có các trang được ghép lại.

Kích thước đầu ra được tính bằng điểm (points), không phải pixel

OutputWidthOutputHeight là các đơn vị người dùng PDF (user units), và một đơn vị người dùng PDF tương đương với một điểm (point), tức là một phần bảy mươi hai inch. Đơn vị này xác định kích thước vật lý của tờ giấy đầu ra, và nó không liên quan gì đến pixel màn hình hay DPI kết xuất. Đây là vị trí phổ biến nhất khiến việc áp trang bị sai lệch, vì một lập trình viên quen dùng bitmap sẽ tìm kiếm một số lượng pixel và nhận về một tờ giấy có kích thước của một con tem thư hoặc một tấm biển quảng cáo.

Những con số đáng ghi nhớ là hai kích thước trang bạn sẽ sử dụng nhiều nhất. US Letter là 612 x 792 điểm, vì 8.5 inch nhân với 72 là 612 và 11 inch nhân với 72 là 792. A4 xấp xỉ 595 x 842 điểm, dựa trên kích thước 210 x 297 milimet của nó. Phần tiêu đề của chính binding nêu rõ quy tắc rằng một đơn vị là một phần bảy mươi hai inch, và đơn vị này đi kèm một hằng số PointsPerInch bằng 72 nếu bạn muốn tính toán kích thước từ inch trong mã nguồn hơn là viết trực tiếp giá trị số.

const
  LetterW = 612.0;   // 8.5 in * 72
  LetterH = 792.0;   // 11  in * 72
var
  Source, Composite: TPdf;
begin
  Source := TPdf.Create(nil);
  Composite := nil;
  try
    Source.FileName := 'slides.pdf';
    Source.Active := True;

    // Four source pages per Letter sheet, 2 by 2 grid.
    Composite := Source.ImportNPagesToOne(LetterW, LetterH, 2, 2);
    if Composite = nil then
      raise Exception.Create('PDFium rejected the imposition arguments');

    Composite.SaveAs('slides-4up.pdf');
  finally
    Composite.Free;   // see the next section: this is mandatory
    Source.Free;
  end;
end;

Tay cầm (handle) trả về thuộc quyền giải phóng của bạn

Hãy xem lại chữ ký hàm một lần nữa. ImportNPagesToOne trả về một đối tượng TPdf, không phải Boolean. Giá trị trả về đó là một tay cầm tài liệu hoàn toàn mới, được cấp phát độc lập với nguồn, và bên gọi sở hữu nó. Đối tượng nguồn TPdf bạn gọi phương thức vẫn không bị ảnh hưởng và vẫn sở hữu tay cầm riêng của nó; tài liệu ghép là đối tượng thứ hai độc lập. Nếu bạn để đối tượng TPdf trả về vượt ra ngoài phạm vi hiệu lực mà không giải phóng nó, bạn sẽ làm rò rỉ toàn bộ tài liệu PDFium.

Sai lầm nguy hiểm hơn diễn ra theo chiều ngược lại. Bên dưới, phương thức yêu cầu PDFium cấp một đối tượng FPDF_DOCUMENT mới thông qua FPDF_ImportNPagesToOne, sau đó bọc tay cầm gốc đó vào bên trong đối tượng TPdf được trả về để vòng đời của lớp bao bọc kiểm soát vòng đời của tay cầm. Từ thời điểm đó, có chính xác một chủ sở hữu đối với tay cầm, và chính xác một vị trí nó cần được đóng lại: khi bạn gọi Free trên đối tượng trả về. Một đường dẫn xử lý lỗi bất cẩn vừa giải phóng lớp bao bọc vừa gọi FPDF_CloseDocument trên tay cầm gốc mà nó đã chụp lại sẽ đóng cùng một tài liệu PDFium hai lần. Đó là lỗi giải phóng bộ nhớ kép, và đó là lỗi cụ thể từng xảy ra với một ứng dụng gọi hàm ở đây. Quy tắc ngăn chặn nó rất ngắn gọn. Chỉ đóng tài liệu trên một đường dẫn duy nhất, bằng cách giải phóng đối tượng TPdf phương thức gia cho bạn, và không bao giờ vượt qua lớp bao bọc để đóng tay cầm mà nó đã tiếp nhận.

Two corollaries fall out of this. First, the method returns nil when PDFium rejects the arguments, such as a zero on either grid axis or an allocation failure, so a nil check belongs before you touch the result. Second, initialise your output variable to nil before the try and free it in finally, as the sample above does, so a failure midway through cannot leave you freeing an undefined reference or skipping the free entirely.

Sắp xếp lại các trang mà không cần ghi lại chúng

Áp trang sẽ xây dựng một tài liệu mới. Sắp xếp lại sẽ thay đổi trực tiếp trên tài liệu hiện có. Phương thức MovePages nhấc một tập hợp các trang ra khỏi vị trí hiện tại của chúng và thả chúng vào một điểm đích, dịch chuyển mọi thứ khác xung quanh khối được di chuyển để số lượng trang giữ nguyên:

function MovePages(
  const PageIndices: array of Integer;
  DestPageIndex    : Integer): Boolean;

Các chỉ mục được tính từ số không. PageIndices liệt kê các trang cần di chuyển, theo thứ tự sắp xếp mong muốn, và DestPageIndex là chỉ mục mà trang được di chuyển đầu tiên hạ cánh sau khi thao tác di chuyển hoàn tất. Bởi vì PDFium định vị lại các trang thay vì sao chép và nén lại nội dung của chúng, thao tác này diễn ra rất nhanh và không hao tổn dữ liệu (lossless): các đối tượng trang giữ nguyên các luồng, tài nguyên và độ chính xác của chúng. Đây là lệnh gọi đứng sau bảng trang kéo-để-sắp-xếp-lại, nơi người dùng kéo một hình thu nhỏ sang một vị trí mới và bạn xác nhận thứ tự mới bằng một lần di chuyển duy nhất. Hàm trả về False when an index is out of range, so validate the result instead of assuming the rearrange took.

var
  Doc: TPdf;
begin
  Doc := TPdf.Create(nil);
  try
    Doc.FileName := 'report.pdf';
    Doc.Active := True;

    // Move the last page (index 4 in a 5-page file) to the very front.
    if not Doc.MovePages([4], 0) then
      raise Exception.Create('MovePages rejected the index');

    Doc.SaveAs('report-reordered.pdf');
  finally
    Doc.Free;
  end;
end;

Trích xuất một phần tài liệu bằng chỉ mục

Thao tác thứ ba sao chép một tập hợp trang cụ thể từ một tài liệu sang một tài liệu khác. ImportPagesByIndex nhận tài liệu nguồn và một mảng chỉ mục bắt đầu từ số không, rồi chèn các trang đó vào tài liệu đích tại một vị trí được chọn:

function ImportPagesByIndex(
  Source           : TPdf;
  const PageIndices: array of Integer;
  InsertAt         : Integer= 0): Boolean;

Bạn gọi nó trên tài liệu đích và truyền nguồn làm đối số đầu tiên. PageIndices đặt tên cho các trang nguồn cần kéo về, theo thứ tự bạn muốn; InsertAt là vị trí bắt đầu từ số không trong tài liệu đích nơi trang đầu tiên được chèn vào, do đó giá trị 0 đặt chúng trước trang đầu tiên hiện tại và ghép thêm vào số lượng trang hiện có của tài liệu đích. Một mảng trống sẽ nhập mọi trang, điều này giúp lệnh gọi thực hiện một thao tác sao chép đầy đủ khi bạn cần. Nó trả về False nếu bất kỳ chỉ mục nào nằm ngoài phạm vi trong tài liệu nguồn.

Đây là nơi sự khác biệt với chia tách thể hiện rõ rệt. Chia tách ghi ra các tệp riêng biệt, một thao tác tạo ra nhiều kết quả đầu ra trên đĩa. ImportPagesByIndex thực hiện cấu trúc công việc ngược lại: nó thu thập một tập hợp các trang được chọn vào một tài liệu đích duy nhất trong bộ nhớ, sau đó bạn chỉ cần lưu lại một lần. Khi yêu cầu là "cho tôi các trang 3, 7 và 12 thành một tệp PDF ngắn duy nhất", đây là con đường trực tiếp nhất, và nó bọc hàm FPDF_ImportPagesByIndex bên dưới.

var
  Source, Excerpt: TPdf;
begin
  Source := TPdf.Create(nil);
  Excerpt := TPdf.Create(nil);
  try
    Source.FileName := 'manual.pdf';
    Source.Active := True;
    Excerpt.CreateDocument;   // start an empty target

    // Pull pages 3, 7 and 12 (zero-based 2, 6, 11) into the excerpt.
    if not Excerpt.ImportPagesByIndex(Source, [2, 6, 11], 0) then
      raise Exception.Create('A requested page index is out of range');

    Excerpt.SaveAs('manual-excerpt.pdf');
  finally
    Excerpt.Free;
    Source.Free;
  end;
end;

Kết hợp mọi thứ một cách sạch sẽ

Cấu trúc tổng thể từ đầu đến cuối là giống nhau trên cả ba phương pháp: mở nguồn bằng cách đặt FileName và chuyển Active sang True, thực hiện thao tác, lưu bằng SaveAs, và giải phóng những gì bạn sở hữu. Nhánh duy nhất cần lưu ý là các lệnh gọi nào cấp phát một tài liệu mới. MovePages thay đổi tài liệu bạn đang nắm giữ, do đó chỉ có một đối tượng cần giải phóng. ImportPagesByIndex ghi vào một tài liệu đích mà bạn tự tạo ra, do đó bạn giải phóng nguồn và tài liệu đích mà bạn đã mở. ImportNPagesToOne là trường hợp ngoại lệ, vì tài liệu mới là giá trị trả về của phương thức chứ không phải thứ bạn xây dựng từ đầu, và việc quên rằng đó là một tay cầm riêng biệt do bên gọi sở hữu là nguyên nhân khiến cả lỗi rò rỉ bộ nhớ lẫn giải phóng kép xảy ra. Khởi tạo kết quả thành nil, kiểm tra nó sau lệnh gọi, và giải phóng nó trên một đường dẫn duy nhất.

Nếu công việc bạn thực sự cần làm là kết hợp toàn bộ các tệp thay vì sắp xếp lại các trang, hãy xem hợp nhất nhiều tệp PDF thành một tài liệu duy nhất. Nếu ngược lại, chia nhỏ một tài liệu thành nhiều tệp khác nhau, hãy xem chia nhỏ tài liệu PDF thành nhiều tệp. Các phương thức áp trang và sắp xếp lại được mô tả ở đây đi kèm như một phần của PDFium Component dành cho Delphi và C++Builder, bên cạnh các API tải, hiển thị và chỉnh sửa được đề cập ở những bài viết khác trên blog này.