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
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, GetOptionalContentGroupVisible và GetOptionalContentGroupPrintable, 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
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ị 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