Một nhân viên khảo sát mở một sơ đồ mặt bằng và muốn ẩn các đường đồng mức trong khi các tiện ích vẫn hiển thị. Một người phản biện muốn các ghi chú đường đỏ (redline annotation) hiển thị trên màn hình nhưng biến mất khỏi bản in. Một bảng thông số sản phẩm được gửi đi dưới dạng ba ngôn ngữ từ một tệp duy nhất, và người đọc chọn ngôn ngữ 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 bên dưới bảng điều khiển đó là nội dung tùy chọn (optional content), và đó là thứ cho phép một trang duy nhất mang nhiều tầng thị giác độc lập mà trình xem có thể bật và tắt.
Nội dung tùy chọn được chỉ định trong ISO 32000-1 §8.11. Đơn vị hiển thị là một nhóm nội dung tùy chọn, viết tắt là OCG (optional content group), một từ điển thuộc loại /OCG mang một tên gọi. Nội dung được đánh dấu (marked content) trên một trang được liên kết với một nhóm, và trình xem sẽ quyết định nhóm đó hiện có được hiển thị hay không. Một cấu trúc liên quan, từ điển tư cách thành viên nội dung tùy chọn hay OCMD (optional content membership dictionary), cho phép khả năng hiển thị phụ thuộc vào sự kết hợp boolean của nhiều nhóm, nhưng trường hợp hàng ngày là một nhóm duy nhất có tên đại diện cho một lớp duy nhất. Tài liệu liên kết toàn bộ cơ chế này lại với nhau thông qua một mục nhập catalog, /OCProperties, được mô tả tiếp theo.
Những gì catalog phải mang theo
Bản thân một OCG là trơ. Để trình xem liệt kê một lớp và ghi nhớ trạng thái của nó, catalog tài liệu cần có một từ điển /OCProperties, và mục §8.11.4 trình bày chính xác những gì đi vào đó. Có một mảng /OCGs đặt tên cho mọi nhóm trong tệp, và có một mục nhập /D giữ 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 tiên. Nó ghi lại những nhóm nào bắt đầu ở trạng thái bật và nhóm nào bắt đầu tắt, những mục nhập nào bị khóa để người dùng không thể chuyển đổi chúng, 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 điều khiển.
Hệ quả thực tế là việc tạo ra một lớp không bao giờ là một hành động thuần túy cục bộ. Nhóm này phải được vẽ trên trang, và nó cũng phải được đăng ký trong cấu trúc cấp catalog vốn chưa tồn tại trước đó. PDFlibPas thực hiện cả hai điều này cho bạn. Cuộc gọi đầu tiên tạo ra một nhóm sẽ thêm mục nhập /OCProperties vào catalog và gieo mầm cấu hình mặc định, do đó lớp vừa được vẽ vừa được liệt kê mà không cần sổ sách kế toán riêng biệt từ phía bạn.
Tại sao chế độ tuân thủ có thể từ chối tính năng này
Trước khi bất kỳ mã lớp nào chạy, mục tiêu tuân thủ của tài liệu sẽ quyết định xem nội dung tùy chọn 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 hoàn toàn mục nhập /OCProperties trong mục §6.1.13. Lập luận này phù hợp với mục đích của định dạng. Một tệp lưu trữ phải hiển thị giống hệt nhau đối với mọi người đọc trong tương lai xa, và nội dung mà người xem có thể thay đổi khả năng hiển thị là nội dung có giao diện không cố định, vì vậy hồ sơ này cấm cấu trúc đó thay vì cho phép một kho lưu trữ mơ hồ. PDF/A-2 and PDF/A-3, được định nghĩa trong ISO 19005-2 và ISO 19005-3, có quan điểm ngược lại trong mục §6.9 của chúng và cho phép nội dung tùy chọn, với các quy tắc về khả năng hiển thị mặc định.
Sự khác biệt đó hiển thị 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ề số không, vì việc thực hiện yêu cầu sẽ tạo ra một tệp không tuân thủ cấu hình đã khai báo của chính nó. Ở chế độ PDF/A-2 hoặc PDF/A-3, và trong tài liệu PDF thông thường không bị ràng buộc, cùng một cuộc gọi sẽ thành công và trả về ID nhóm khác không. Do đó, kết quả bằng không không phải là một lỗi chung để kiểm tra sau; đó là thư viện đang cho bạn biết mức độ tuân thủ đang hoạt động không có chỗ cho tính năng này.
var
LayerID: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.SetPDFAMode(1); // PDF/A-1a: OCProperties forbidden
LayerID := Pdf.NewOptionalContentGroup('Utilities');
if LayerID = 0 then
// refused under PDF/A-1; not a transient error, the mode bans layers
ShowMessage('Optional content is not available in PDF/A-1 mode.');
finally
Pdf.Free;
end;
end;
Hai trạng thái trên mỗi lớp, không phải một
Một lớp không đơn thuần là hiển thị hay vô hình. Cấu hình mặc định ghi lại trạng thái trên màn hình của nó và một trạng thái in riêng biệt, vì mục §8.11.4 phân biệt những gì trình xem hiển thị với những gì luồng xử lý in xuất ra. Cả hai độc lập có mục đích. Một hình mờ nháp (draft watermark) có thể được hiển thị trên màn hình và bỏ qua khi in ra giấy, và một lớp đường cắt (cut-line layer) có thể được ẩn trên màn hình nhưng vẫn được gửi đến máy vẽ. Việc hợp nhất hai trạng thái này sẽ buộc trạng thái này phải đi theo trạng thái kia và làm mất đi chính xác quyền kiểm soát mà tính năng này tồn tại để cung cấp.
PDFlibPas hiển thị cặp này thông qua hai setter. SetOptionalContentGroupVisible nhận ID nhóm và một cờ, trong đó một có nghĩa là hiển thị và không có nghĩa là ẩn, và quản lý trạng thái trên màn hình mặc định. SetOptionalContentGroupPrintable nhận ID nhóm và một cờ cho biết liệu lớp đó có được xuất ra khi tài liệu in hay không. Các getter phù hợp, GetOptionalContentGroupVisible and GetOptionalContentGroupPrintable, mỗi cái trả về một hoặc không, vì vậy bạn có thể đọc lại cách bố trí màn hình và in của một lớp một cách riêng biệt thay vì suy luận từ trạng thái này sang trạng thái kia.
Xây dựng hai lớp trên một trang
Tạo một lớp và điền nội dung vào đó tuân theo một thứ tự cố định. Bạn vẽ nội dung cho lớp lên trang hiện tại, sau đó gọi SetContentStreamOptional với ID nhóm, lệnh này đóng gói content stream hiện tại của trang để mọi thứ được vẽ cho đến nay đều thuộc về nhóm đó. Bởi vì lệnh gọi ghi lại bất cứ thứ gì trên stream tại thời điểm đó, kỷ luật ở đây là đặt các nét vẽ của một lớp xuống, gán chúng, và chỉ sau đó mới bắt đầu lớp tiếp theo. Ví dụ bên dưới đặt các tiện ích trên trang đầu tiên và đường đỏ phản biện trên trang thứ hai, đặt trạng thái hiển thị màn hình và in của 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; // unconstrained PDF: layers allowed
Pdf.SetPageDimensions(595, 842); // A4 in points
FontID := Pdf.AddStandardFont(0); // Helvetica
Pdf.SelectFont(FontID);
// Layer 1: utilities, drawn then assigned to its own group
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); // shown on screen
Pdf.SetOptionalContentGroupPrintable(UtilLayer, 1); // and on paper
// Layer 2: reviewer redline on a fresh page
Pdf.InsertPages(2, 1); // append one page after page 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); // visible while reviewing
Pdf.SetOptionalContentGroupPrintable(RedlineLayer, 0); // never printed
Pdf.SaveToFile('SitePlan_Layers.pdf');
finally
Pdf.Free;
end;
end;
Lớp đường đỏ (redline layer) là trường hợp đáng lưu ý. Nó được hiển thị trên màn hình để người phản biện thấy ghi chú, và cờ có thể in của nó là số không nên bản in của cùng một tệp sẽ không chứa văn bản đánh giá. Sự không đối xứng đó là toàn bộ mục đích của việc tách biệt hai trạng thái.
Đọc lại cấu hình
Đọc các lớp là một quá trình duyệt khác thông qua cùng một cấu trúc. Sau khi một tệp được tải, GetOptionalContentConfigCount báo cáo tài liệu chứa bao nhiêu từ điển cấu hình; cấu hình mặc định đầu tiên có ID cấu hình là 1. Trong một cấu hình, GetOptionalContentConfigOrderCount đưa ra số lượng mục nhập trong cây thứ tự (order tree), và bạn lập chỉ mục cho chúng từ 1. Đối với mỗi mục nhập, GetOptionalContentConfigOrderItemLabel trả về văn bản hiển thị của nó và GetOptionalContentConfigOrderItemLevel trả về độ sâu lồng nhau của nó, do đó sơ đồ bảng điều khiển với các lớp con được thụt lề dưới các tiêu đề có thể được tái cấu trúc nguyên văn.
Mỗi mục nhập cũng có một loại. GetOptionalContentConfigOrderItemType phân biệt một nhóm nội dung tùy chọn thực tế với một nhãn văn bản thuần túy chỉ tồn tại để làm tiêu đề cho một phần của cây. Sự phân biệt đó rất quan trọng vì các truy vấn trạng thái theo từng nhóm chỉ có ý nghĩa đối với các nhóm thực sự. Đối với mục nhập nhóm, GetOptionalContentConfigState báo cáo xem cấu hình bắt đầu bật nó, tắt nó hay giữ nguyên không đổi, và GetOptionalContentConfigLocked báo cáo xem người dùng có bị cấm chuyển đổi nó hay không. Vòng lặp bên dưới hiển thị cây thứ tự với trạng thái và trạng thái 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; // the default configuration
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 = optional content group
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 is a text label heading; it has no per-group state
Writeln(Line);
end;
finally
Pdf.Free;
end;
end;
Hai chi tiết giữ cho vòng lặp này chính xác. Chỉ mục thứ tự là dạng một-dựa-trên (one-based), từ 1 đến số lượng phần tử, khớp với cách thư viện đánh số cây bên trong. Và các lệnh gọi theo từng nhóm chỉ chạy khi loại mục nhập là một nhóm, bởi vì một nhãn văn bản là một tiêu đề có tên và cấp độ nhưng không có trạng thái bật, tắt hoặc khóa để truy vấn. Bỏ qua biện pháp bảo vệ đó và bạn sẽ yêu cầu một nhãn cung cấp trạng thái mà nó không có.
Nơi cấu trúc này phù hợp
Các lớp là một cơ chế trình bày, vì vậy công cụ phải tôn trọng chúng trên mọi đường dẫn kết xuất trang, và khía cạnh kết xuất được đề cập trong our walkthrough of multi-engine rendering in Delphi. Chúng cũng giao nhau với cấu trúc tài liệu, vì tên của một lớp là văn bản hướng tới người tác giả và người đọc được hưởng lợi từ một phác thảo lớp có cấu trúc, điều này kết nối với công việc trong our article on tagged PDF and accessibility structure. Cả hai đều kết hợp với các API nội dung tùy chọn được mô tả ở đây, vốn được cung cấp như một phần của Delphi PDF Library cùng với các tiện ích trang, văn bản, phông chữ và tính tuân thủ được thảo luận ở những nơi khác trên blog này.