Bài viết Kỹ thuật

Ghép trang và Template Watermark trong Delphi PDF

Ghép trang (page stitching) là việc kết hợp nội dung của các trang hiện có lên một canvas chung, đây là một thao tác khác hoàn toàn so với việc gộp file (merging files). losLab PDF Library triển khai tính năng ghép trang Form XObject và các template watermark có thể kết hợp (composable watermark templates) cho Delphi: StitchPageOverlay, StitchPagesSideBySide, và StitchPagesVertically giúp đặt các trang đã chụp ở bất kỳ tỷ lệ nào với độ sắc nét vector đầy đủ, và TPDFlibCompositeWatermark giúp xếp chồng các watermark văn bản và hình ảnh trên một phạm vi trang chỉ trong một lệnh gọi. Nếu những gì bạn thực sự cần là nối toàn bộ các trang từ nhiều tài liệu thành một file duy nhất, đó là nhóm hàm gộp (merge), được đề cập trong bài viết về gộp PDF nhanh ở cấp độ byte; còn bài viết này nói về việc kết hợp nội dung trang trong cùng một không gian tọa độ

Các yêu cầu dẫn đến tính năng này rất phổ biến và thực tế. Một trường học muốn dàn hai trang đề thi lên một tờ giấy nằm ngang để tiết kiệm một nửa lượng giấy. Một người đánh giá bản địa hóa muốn đặt trang tiếng Anh và bản dịch của nó cạnh nhau trên một trang duy nhất để cuộn cùng nhau. Một đội ngũ pháp lý muốn mỗi bản nháp được đóng dấu ba lớp cùng lúc: một logo ở góc, một chữ CONFIDENTIAL nằm chéo ở giữa và một dòng footer nhỏ, chỉ trên các trang từ 1 đến 3. Cả ba đều là cùng một vấn đề cốt lõi, cụ thể là đặt nội dung trang đã được dàn trang sẵn vào một nơi khác với kích thước khác mà không làm giảm chất lượng thành ảnh bitmap

Tại sao việc nối trực tiếp các content stream của PDF lại bị lỗi?

Phương pháp ghép trang đơn giản là sao chép stream /Contents của trang nguồn vào trang đích sẽ thất bại vì ba lý do cấu trúc. Thứ nhất, xung đột tọa độ: cả hai content stream đều giả định gốc tọa độ của chúng ở góc dưới bên trái của MediaBox của chính chúng, do đó nội dung được dán sẽ đè lên nội dung hiện có ở sai vị trí. Thứ hai, xung đột tên tài nguyên: mỗi trang giải quyết các operator như /F1 Tf/Im0 Do dựa trên dictionary /Resources của riêng nó, và hai trang thường liên kết cùng một tên với các font hoặc hình ảnh khác nhau, do đó một trong hai trang sẽ tự động được dựng hình với tài nguyên sai. Thứ ba, việc sao chép stream thô không mang lại cho bạn khả năng thu phóng; điều tốt nhất bạn có thể làm chỉ là dịch chuyển tọa độ

Form XObject giải quyết cả ba vấn đề cùng lúc, và đó là lý do tại sao losLab PDF Library xây dựng tính năng ghép trang dựa trên chúng. ISO 32000-1 §8.10 định nghĩa một form XObject là một content stream độc lập với dictionary /Resources và không gian tọa độ riêng của nó, được đặt vào một trang bằng cách gọi tên của nó dưới bất kỳ ma trận biến đổi hiện tại (current transformation matrix) nào đang hoạt động. CapturePage đóng gói toàn bộ một trang hiện có, nội dung cộng với tài nguyên, thành một XObject /Subtype /Form, và các hàm ghép trang sau đó định vị nó bằng một ma trận cm: thu phóng, dịch chuyển, xoay tùy ý. Vì nguồn vẫn là vector, văn bản vẫn là văn bản và các đường dẫn (path) vẫn là đường dẫn ở bất kỳ mức thu phóng nào, cùng một đặc tính giúp cho đồ họa vector và shading pattern không phụ thuộc vào độ phân giải. Không có gì bị raster hóa, và dictionary tài nguyên biệt lập giúp trang được chụp không bị xung đột với trang đích về tên /F1

Làm thế nào để kết hợp hai trang PDF vào một trang?

StitchPagesSideBySide(Page1, Page2, Gap, TargetHeight) là câu trả lời trực tiếp: nó chụp cả hai trang nguồn, tạo ra một trang mới rộng hơn, và đặt hai trang đã chụp ở bên trái và bên phải với một khoảng trống giữa chúng, mỗi trang được thu phóng để khớp với TargetHeight. Hàm này trả về số trang mới (bắt đầu từ 1) hoặc 0 nếu thất bại, và cả hai trang nguồn đều được chụp và ẩn đi như một phần của thao tác. Trường hợp đề thi hai trang trên một tờ giấy là một công việc chỉ với ba dòng code

var
  Lib: TPDFlib;
  NewPage: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('exam.pdf', '');
    // Pages 1 and 2 on one new wider page with an 18pt gutter,
    // both scaled to a 595pt output height (A4 landscape)
    NewPage := Lib.StitchPagesSideBySide(1, 2, 18, 595);
    if NewPage > 0 then
      Lib.SaveToFile('exam-2up.pdf');
  finally
    Lib.Free;
  end;
end;

Đối với bố cục dọc, StitchPagesVertically(PageRanges, Gap, TargetWidth) xếp chồng toàn bộ một phạm vi trang thành một trang dài kiểu poster. PageRanges nhận cú pháp phạm vi tiêu chuẩn như '1-3,5'. Mỗi trang nguồn được thu phóng sao cho chiều rộng của nó khớp với TargetWidth, do đó phần đóng góp chiều cao của mỗi trang là chiều cao của chính nó nhân với hệ số thu phóng của chính nó, và chiều cao trang đầu ra là tổng của các chiều cao đó cộng với một khoảng Gap giữa mỗi cặp. Đây là định dạng bạn muốn cho các tài liệu xem dạng cuộn liên tục hoặc các hóa đơn dài được lắp ráp từ các nguồn kích thước letter

Làm thế nào để chồng một trang lên một trang khác?

StitchPageOverlay(SourcePage, TargetPage, Left, Top, Width, Height, Opacity) vẽ một trang hiện có lên một trang hiện có khác, thay vì tạo ra một trang mới. Trang nguồn được chụp dưới dạng một form XObject và được vẽ lên trang đích tại hình chữ nhật mà bạn cung cấp, với Opacity từ 0 đến 1 (1 là hoàn toàn mờ đục). Chiều rộng và chiều cao là các tham số tự do, vì vậy việc đặt hình ảnh sẽ bị co giãn nếu tỷ lệ của bạn khác với tỷ lệ khung hình của chính trang nguồn; hãy truyền một cặp tỷ lệ thuận khi bạn muốn có một hình thu nhỏ không bị biến dạng của trang nguồn. Một trường hợp sử dụng điển hình là một tài liệu có khối phê duyệt (approval block) được duy trì như một trang riêng của nó và được đóng dấu lên trang bìa

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('report.pdf', '');
    // Page 5 holds the approval block; place it on page 1
    // in a 260 x 180 box at 45% opacity. Page 5 is consumed:
    // it is captured and hidden, so pages 6..n become 5..n-1
    if Lib.StitchPageOverlay(5, 1, 300, 500, 260, 180, 0.45) = 1 then
      Lib.SaveToFile('report-approved.pdf');
  finally
    Lib.Free;
  end;
end;

Điều gì xảy ra với số trang sau khi chụp?

CapturePage tiêu thụ trang nguồn: nội dung của nó được chuyển vào XObject và trang hiện tại trống sẽ bị ẩn đi, do đó mọi trang có số thứ tự cao hơn trang nguồn sẽ ngay lập tức dịch xuống một vị trí. Đây là ngữ nghĩa quan trọng nhất trong toàn bộ API ghép trang. losLab PDF Library xử lý các phép toán số học này bên trong mỗi lượt gọi ghép trang, StitchPageOverlay điều chỉnh chỉ mục đích khi trang đích nằm trên trang nguồn, và StitchPagesSideBySide chụp trang có số thứ tự cao hơn trước để chỉ mục của lượt chụp thứ hai vẫn hợp lệ, nhưng code xung quanh của chính bạn phải tự tính toán cho sự dịch chuyển này giữa các lượt gọi. Có hai quy tắc giúp cho code ghép nhiều trang luôn chính xác. Hãy ghi lại bất kỳ kích thước trang hoặc số trang nào bạn sẽ cần tước lượt chụp đầu tiên, bởi vì chúng sẽ thay đổi sau đó. Và khi bạn tự chụp nhiều trang trong một vòng lặp, hãy lặp theo thứ tự trang giảm dần, để việc ẩn một trang sau không bao giờ làm ảnh hưởng đến chỉ mục của một trang trước mà bạn chưa chụp

Làm thế nào để áp dụng watermark nhiều lớp cho một phạm vi trang?

Lớp template watermark trong PDFlibWatermark.pas biến việc "định nghĩa một lần, áp dụng cho nhiều trang" thành các đối tượng. TPDFlibWatermarkBase mang những gì mọi watermark đều chia sẻ: một thuộc tính Position chín điểm (wpTopLeft đến wpBottomRight, cộng với wpCustom sử dụng tọa độ X/Y thô), một góc Angle tính bằng độ, một độ mờ Opacity từ 0 đến 1, và một chuỗi PageRange với giá trị trống nghĩa là tất cả các trang. TPDFlibTextWatermark thêm văn bản, một ID font (mặc định là Helvetica), kích thước và màu RGB; TPDFlibImageWatermark thêm một ID hình ảnh từ một hình ảnh đã thêm trước đó cộng với chiều rộng và chiều cao định vị. TPDFlibCompositeWatermark chứa một danh sách các watermark con và áp dụng chúng một cách đệ quy, và TPDFlib.ApplyWatermark là điểm đầu vào một lần gọi tuân theo phạm vi trang của chính template và trả về số lượng trang được đóng dấu. Dấu pháp lý ba lớp trông như thế này

var
  Lib: TPDFlib;
  Logo: TPDFlibImageWatermark;
  Diagonal, Footer: TPDFlibTextWatermark;
  Stack: TPDFlibCompositeWatermark;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('draft.pdf', '');
    Stack := TPDFlibCompositeWatermark.Create;
    try
      Logo := TPDFlibImageWatermark.Create;
      Logo.ImageID := Lib.AddImageFromFile('logo.png', 0);
      Logo.Position := wpTopRight;
      Logo.Width := 90;
      Logo.Height := 32;
      Stack.Add(Logo);

      Diagonal := TPDFlibTextWatermark.Create;
      Diagonal.Text := 'CONFIDENTIAL';
      Diagonal.Position := wpCenter;
      Diagonal.Angle := 45;
      Diagonal.FontSize := 60;
      Diagonal.Opacity := 0.15;
      Stack.Add(Diagonal);

      Footer := TPDFlibTextWatermark.Create;
      Footer.Text := 'Internal review copy';
      Footer.Position := wpBottomCenter;
      Footer.FontSize := 9;
      Footer.Opacity := 0.6;
      Stack.Add(Footer);

      Stack.PageRange := '1-3';   // children inherit this range
      Lib.ApplyWatermark(Stack);
      Lib.SaveToFile('draft-marked.pdf');
    finally
      Stack.Free;                 // composite frees its children
    end;
  finally
    Lib.Free;
  end;
end;

Có hai chi tiết cấu thành đáng được làm rõ. Thừa kế phạm vi là một chiều và không phá hủy: một watermark con với PageRange trống sẽ tạm thời thừa kế phạm vi của composite trong quá trình Apply, trong khi một watermark con tự thiết lập phạm vi riêng của nó sẽ giữ nguyên phạm vi đó, vì vậy bạn có thể ghim footer cho mọi trang trong khi dấu chéo CONFIDENTIAL chỉ bao phủ phần bản nháp. Và quyền sở hữu tuân theo composite: TPDFlibCompositeWatermark.Destroy giải phóng các watermark con của nó, vì vậy bạn chỉ cần giải phóng composite một lần và không cần làm gì khác, như trong ví dụ trên

Những giới hạn đáng lưu ý

Có hai giới hạn thực tế được áp dụng, và cả hai đều xuất phát từ cách thức hoạt động của cơ chế này. Thứ nhất, việc chụp chỉ đóng gói content stream và tài nguyên của trang. Các chú thích (annotation), liên kết (link), trường biểu mẫu (form field) và bình luận (comment) tồn tại dưới dạng các đối tượng riêng biệt trong dictionary của trang, không nằm trong content stream, vì vậy chúng không được chuyển vào XObject; bố cục ghép trang mang nội dung trang hiển thị được nhưng không mang theo lớp tương tác của các nguồn của nó. Thứ hai, việc định vị văn bản chín điểm chỉ là ước tính: nếu không có một layout engine đầy đủ, thư viện không thể biết chiều rộng dựng hình chính xác của một chuỗi, vì vậy nó ước tính hộp văn bản ở mức 40% chiều rộng trang khi giải quyết các vị trí như wpCenter. Điều đó đủ chính xác để căn giữa một nhãn ngắn, nhưng đối với việc đặt chính xác đến từng pixel của các chuỗi dài, hãy chuyển sang wpCustom và tự tính toán các tọa độ

Thu phóng không phải là một hạn chế thực tế ở đây, vì việc chụp và ghép trang thao tác trên các tham chiếu đối tượng thay vì mã hóa lại nội dung trang; nếu các tài liệu nguồn của bạn nặng hàng trăm megabyte, cùng một quy tắc kỷ luật được mô tả trong hướng dẫn gộp và tách file lớn với truy cập trực tiếp sẽ được áp dụng trước khi bạn ghép trang. Các hàm ghép trang và các class template watermark được hiển thị ở đây đi kèm trong losLab PDF Library hiện tại dành cho Delphi, C# và VB.NET, cùng với các hàm nguyên thủy cấp thấp hơn như CapturePage, DrawCapturedPage, và DrawCapturedPageMatrix khi bạn cần một bố cục mà ba lệnh gọi ghép trang dựng sẵn không bao gồm