Bài viết kỹ thuật

Lớp PDF trong Delphi: Optional Content Group (OCG)

Một kỹ sư trắc địa mở bản vẽ mặt bằng và muốn ẩn đường đồng mức trong khi vẫn giữ hệ thống tiện ích hiển thị. Một người soát xét muốn ghi chú redline hiện trên màn hình nhưng biến mất khỏi bản in. Một tờ thông tin sản phẩm phát hành bằng ba ngôn ngữ từ một tệp duy nhất, và người đọc chọn ngôn ngữ được hiển thị. Cả ba đều là cùng một tính năng PDF, và bảng điều khiển chúng trong Acrobat được gọi là Layers. Tính năng nằm dưới bảng đó là optional content, và chính nó cho phép một trang duy nhất mang nhiều tầng hình ảnh độc lập mà trình xem có thể bật tắt

Optional content được đặc tả trong ISO 32000-1 §8.11. Đơn vị của tính hiển thị là một optional content group, gọi tắt là OCG, một dictionary kiểu /OCG có mang tên. Marked content trên trang được liên kết với một nhóm, và trình xem quyết định nhóm đó hiện đang được hiển thị hay không. Một cấu trúc liên quan, optional content membership dictionary hay OCMD, cho phép tính hiển thị phụ thuộc vào tổ hợp boolean của nhiều nhóm, nhưng trường hợp thường ngày là một nhóm có tên duy nhất đại diện cho một lớp duy nhất. Tài liệu gắn kết toàn bộ cơ chế này qua một mục trong catalog, /OCProperties, được mô tả ngay sau đây

Catalog phải mang những gì

Bản thân một OCG là trơ. Để trình xem liệt kê được một lớp và ghi nhớ trạng thái của nó, catalog của tài liệu cần một dictionary /OCProperties, và §8.11.4 quy định chính xác những gì nằm trong đó. Có một mảng /OCGs nêu tên mọi nhóm trong tệp, và có một mục /D chứa cấu hình mặc định. Cấu hình mặc định là phần mà trình đọc áp dụng khi tệp được mở lần đầu. Nó ghi lại nhóm nào bật lúc khởi đầu và nhóm nào tắt, mục nào bị khóa không cho người dùng bật tắt, và thông qua một mảng /Order, cách các tên lớp được sắp xếp và lồng nhau trong bảng

Cấu trúc các optional content group của PDF phía sau PDF Library for Delphi: mục OCProperties trong catalog liệt kê mọi lớp OCG và cấu hình mặc định của nó với trạng thái khởi đầu, khóa và thứ tự trong bảng, còn marked content trên trang gắn phần vẽ vào các nhóm
Lớp chỉ trở nên định địa chỉ được thông qua catalog: OCProperties liệt kê mọi OCG còn cấu hình mặc định ghi lại trạng thái khởi đầu, khóa và thứ tự trong bảng

Hệ quả thực tế là việc tạo một lớp không bao giờ là hành động thuần cục bộ. Nhóm phải được vẽ vào trên trang, và nó cũng phải được đăng ký trong một cấu trúc ở cấp catalog vốn trước đó chưa tồn tại. PDF Library for Delphi làm cả hai việc thay bạn. Lệnh gọi đầu tiên tạo ra một nhóm sẽ thêm mục /OCProperties vào catalog và khởi tạo cấu hình mặc định, nhờ đó lớp vừa được vẽ vừa được liệt kê mà bạn không phải ghi sổ riêng

Vì sao một chế độ tuân thủ có thể khước từ tính năng này

Trước khi bất kỳ đoạn mã lớp nào chạy, mục tiêu tuân thủ của tài liệu quyết định optional content có hợp lệ hay không. PDF/A-1, hồ sơ lưu trữ được định nghĩa trong ISO 19005-1, cấm thẳng mục /OCProperties tại §6.1.13. Lý lẽ này khớp với mục đích của định dạng. Một tệp lưu trữ phải render giống hệt nhau với mọi trình đọc trong tương lai xa, và nội dung mà trình xem có thể thay đổi tính hiển thị là nội dung có diện mạo không cố định, nên hồ sơ này cấm cấu trúc đó thay vì cho phép một bản lưu trữ mơ hồ. PDF/A-2 và PDF/A-3, được định nghĩa trong ISO 19005-2 và ISO 19005-3, có quan điểm ngược lại tại §6.9 của chúng và cho phép optional content, kèm quy tắc về tính hiển thị mặc định

Khác biệt đó lộ ra trực tiếp trong API. Khi tài liệu ở chế độ PDF/A-1, NewOptionalContentGroup từ chối tạo nhóm và trả về không, bởi đáp ứng yêu cầu đó sẽ tạo ra một tệp không đạt chính mức tuân thủ mà nó khai báo. Ở chế độ PDF/A-2 hoặc PDF/A-3, cũng như trong PDF thông thường không ràng buộc, cùng lệnh gọi ấy thành công và trả về một group ID khác không. Vì vậy kết quả bằng không không phải là một thất bại chung chung để bạn xem xét sau; đó là thư viện nói với bạn rằng mức tuân thủ đang bật không có chỗ cho tính năng này

var
  Pdf: TPDFlib;
  LayerID: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);                       // PDF/A-1a: OCProperties bị cấm

    LayerID := Pdf.NewOptionalContentGroup('Utilities');
    if LayerID = 0 then
      // bị từ chối trong PDF/A-1; không phải lỗi nhất thời, chế độ này cấm lớp
      ShowMessage('Optional content is not available in PDF/A-1 mode.');
  finally
    Pdf.Free;
  end;
end;

Hai trạng thái cho mỗi lớp, không phải một

Một lớp không đơn thuần là hiện hay ẩn. Cấu hình mặc định ghi lại trạng thái trên màn hình và một trạng thái in riêng biệt, bởi §8.11.4 phân biệt những gì trình xem hiển thị với những gì một pipeline in phát ra. Hai thứ này độc lập với nhau một cách có chủ đích. Một watermark nháp có thể hiện trên màn hình và bị bỏ khỏi giấy, còn một lớp đường cắt có thể ẩn trên màn hình mà vẫn được gửi tới máy vẽ. Gộp chúng lại sẽ buộc cái này chạy theo cái kia và đánh mất đúng khả năng kiểm soát mà tính năng này sinh ra để trao cho bạn

PDF Library for Delphi phơi bày cặp trạng thái này qua hai setter. SetOptionalContentGroupVisible nhận group ID và một cờ, trong đó một nghĩa là hiện và không nghĩa là ẩn, và chi phối trạng thái mặc định trên màn hình. SetOptionalContentGroupPrintable nhận group ID và một cờ cho biết lớp có được phát ra khi tài liệu được in hay không. Các getter tương ứng, GetOptionalContentGroupVisibleGetOptionalContentGroupPrintable, mỗi hàm trả về một hoặc không, nhờ đó bạn đọc lại được thiên hướng màn hình và thiên hướng in của một lớp một cách riêng rẽ thay vì suy ra cái này từ cái kia

Ma trận các trạng thái hiển thị màn hình và in độc lập cho lớp PDF được tạo bằng Delphi PDF Library: lớp tiện ích luôn hiện ở mọi nơi, ghi chú của người soát xét hiện trên màn hình nhưng không bao giờ in ra, còn lớp đường cắt cho máy vẽ ẩn trên màn hình mà vẫn được in
Tính hiển thị trên màn hình và việc phát ra khi in được đặt riêng cho từng nhóm, nên lớp tiện ích có thể xuất hiện ở mọi nơi trong khi ghi chú soát xét chỉ là ghi chú trên màn hình

Dựng hai lớp trên một trang

Việc tạo một lớp rồi lấp đầy nó theo một trình tự cố định. Bạn vẽ nội dung của lớp lên trang hiện hành, rồi gọi SetContentStreamOptional với group ID, lệnh này bọc content stream hiện tại của trang sao cho mọi thứ đã vẽ đến lúc đó thuộc về nhóm ấy. Vì lệnh gọi này thu lấy bất cứ thứ gì đang có trên stream tại thời điểm đó, kỷ luật cần theo là đặt xong các nét của một lớp, gán chúng, rồi mới bắt đầu lớp tiếp theo. Ví dụ bên dưới đặt tiện ích lên trang thứ nhất và ghi chú redline của người soát xét lên trang thứ hai, đặt trạng thái màn hình và in cho từng lớp, rồi lưu lại

var
  Pdf: TPDFlib;
  FontID, UtilLayer, RedlineLayer: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;                          // PDF không ràng buộc: cho phép lớp
    Pdf.SetPageDimensions(595, 842);          // A4 tính theo point
    FontID := Pdf.AddStandardFont(0);         // Helvetica
    Pdf.SelectFont(FontID);

    // Lớp 1: tiện ích, vẽ trước rồi gán vào nhóm riêng của nó
    Pdf.SetTextColor(0.10, 0.30, 0.65);
    Pdf.DrawText(72, 770, 'Utilities: water main, valve chamber');
    UtilLayer := Pdf.NewOptionalContentGroup('Utilities');
    Pdf.SetContentStreamOptional(UtilLayer);
    Pdf.SetOptionalContentGroupVisible(UtilLayer, 1);   // hiện trên màn hình
    Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // và trên giấy

    // Lớp 2: ghi chú redline của người soát xét trên một trang mới
    Pdf.InsertPages(2, 1);                     // chèn thêm một trang sau trang 1
    Pdf.SetTextColor(0.80, 0.10, 0.10);
    Pdf.DrawText(72, 770, 'REVIEW: revise valve spec before issue');
    RedlineLayer := Pdf.NewOptionalContentGroup('Reviewer markup');
    Pdf.SetContentStreamOptional(RedlineLayer);
    Pdf.SetOptionalContentGroupVisible(RedlineLayer, 1);    // hiện khi soát xét
    Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0);  // không bao giờ in ra

    Pdf.SaveToFile('SitePlan_Layers.pdf');
  finally
    Pdf.Free;
  end;
end;

Lớp redline là trường hợp đáng lưu ý. Nó hiện trên màn hình để người soát xét thấy ghi chú, và cờ printable của nó bằng không nên bản in của chính tệp đó không mang chữ nào của phần soát xét. Sự bất đối xứng ấy chính là toàn bộ lý do giữ hai trạng thái tách rời nhau

Đọc ngược lại cấu hình

Đọc lớp là một hành trình khác đi qua cùng cấu trúc đó. Sau khi một tệp được nạp, GetOptionalContentConfigCount cho biết tài liệu chứa bao nhiêu dictionary cấu hình; cấu hình mặc định đầu tiên có config ID bằng 1. Bên trong một cấu hình, GetOptionalContentConfigOrderCount cho ra số mục trong cây thứ tự, và bạn đánh chỉ mục chúng từ 1. Với mỗi mục, GetOptionalContentConfigOrderItemLabel trả về văn bản hiển thị của nó còn GetOptionalContentConfigOrderItemLevel trả về độ sâu lồng nhau, nhờ đó một sơ đồ bảng với các lớp con thụt vào dưới các tiêu đề có thể được dựng lại y nguyên

Mỗi mục cũng có một kiểu. GetOptionalContentConfigOrderItemType phân biệt một optional content group thực sự với một nhãn văn bản thuần túy chỉ tồn tại để đứng đầu một phần của cây. Phân biệt đó quan trọng vì các truy vấn trạng thái theo nhóm chỉ có nghĩa với nhóm thật. Với một mục là nhóm, GetOptionalContentConfigState cho biết cấu hình khởi động nó ở trạng thái bật, tắt, hay để nguyên, còn GetOptionalContentConfigLocked cho biết người dùng có bị chặn bật tắt nó hay không. Vòng lặp bên dưới kết xuất cây thứ tự kèm trạng thái và tình trạng khóa của từng nhóm, thụt lề theo cấp

var
  Pdf: TPDFlib;
  Cfg, Count, I, ItemType, GroupID, Indent: Integer;
  Line: string;
begin
  Pdf := TPDFlib.Create(nil);
  try
    if Pdf.LoadFromFile('SitePlan_Layers.pdf', '') = 0 then Exit;
    if Pdf.GetOptionalContentConfigCount = 0 then Exit;

    Cfg := 1;                                  // cấu hình mặc định
    Count := Pdf.GetOptionalContentConfigOrderCount(Cfg);
    for I := 1 to Count do
    begin
      Indent := Pdf.GetOptionalContentConfigOrderItemLevel(Cfg, I);
      Line := StringOfChar(' ', Indent * 2)
              + Pdf.GetOptionalContentConfigOrderItemLabel(Cfg, I);

      ItemType := Pdf.GetOptionalContentConfigOrderItemType(Cfg, I);
      if ItemType = 1 then                     // 1 = nhóm optional content
      begin
        GroupID := Pdf.GetOptionalContentConfigOrderItemID(Cfg, I);
        case Pdf.GetOptionalContentConfigState(Cfg, GroupID) of
          1: Line := Line + '  [on]';
          2: Line := Line + '  [off]';
          3: Line := Line + '  [unchanged]';
        end;
        if Pdf.GetOptionalContentConfigLocked(Cfg, GroupID) = 1 then
          Line := Line + ' (locked)';
      end;
      // ItemType = 2 là nhãn văn bản làm tiêu đề; nó không có trạng thái theo nhóm

      Writeln(Line);
    end;
  finally
    Pdf.Free;
  end;
end;

Hai chi tiết giữ cho vòng lặp này đúng. Chỉ mục thứ tự bắt đầu từ một, chạy từ 1 tới số lượng, khớp với cách thư viện đánh số cây ở bên trong. Và các lệnh gọi theo nhóm chỉ chạy khi kiểu của mục là nhóm, bởi một nhãn văn bản là tiêu đề có tên và có cấp nhưng không có trạng thái bật, tắt hay khóa nào để truy vấn. Bỏ qua chốt chặn đó là bạn đang hỏi một cái nhãn về trạng thái mà nó không hề có

Vòng lặp đọc ngược các cấu hình optional content trong PDF Library for Delphi: liệt kê các cấu hình, duyệt các mục thứ tự bắt đầu từ một, rồi rẽ nhánh theo ItemType để chỉ truy vấn trạng thái bật, tắt hay khóa với nhóm thật còn bỏ qua nhãn văn bản
Việc đọc lớp duyệt các mục thứ tự bắt đầu từ một và chỉ tra trạng thái với nhóm thật, bởi một nhãn tiêu đề không có trạng thái bật, tắt hay khóa nào để báo

Vị trí của cơ chế này

Lớp là một cơ chế trình bày, nên engine phải tôn trọng chúng trên mọi đường render một trang, và phía render được nói tới trong bài hướng dẫn của chúng tôi về render đa engine trong Delphi. Chúng cũng giao thoa với cấu trúc tài liệu, bởi tên một lớp là văn bản hướng tới tác giả và người đọc hưởng lợi từ một sơ đồ lớp có cấu trúc, điều này nối với nội dung trong bài viết của chúng tôi về tagged PDF và cấu trúc trợ năng. Cả hai đi cùng các API optional content được mô tả ở đây, vốn là một phần của Delphi PDF Library bên cạnh các tiện ích về trang, văn bản, font và tuân thủ được bàn ở nơi khác trên blog này