พนักงานรังวัดเปิดแผนที่ไซต์งานและต้องการซ่อนเส้นชั้นความสูงขณะที่โครงสร้างพื้นฐานยังคงแสดงอยู่ ผู้ตรวจสอบต้องการให้คำอธิบายประกอบแบบเส้นสีแดงแสดงบนหน้าจอแต่หายไปจากเอกสารที่พิมพ์ออกมา เอกสารข้อมูลผลิตภัณฑ์ที่ส่งออกเป็นสามภาษาจากไฟล์เดียว และผู้ใช้สามารถเลือกภาษาที่ต้องการแสดงผลได้ ทั้งสามกรณีนี้คือคุณสมบัติเดียวกันใน PDF ซึ่งใน Acrobat แผงที่ใช้จัดการเรื่องนี้จะเรียกว่าเลเยอร์ (Layers) คุณสมบัติที่อยู่เบื้องหลังแผงควบคุมนั้นคือเนื้อหาทางเลือก (optional content) ซึ่งเป็นสิ่งที่ช่วยให้หน้ากระดาษแผ่นเดียวสามารถเก็บเลเยอร์ของภาพที่เป็นอิสระต่อกันไว้ได้หลายชั้น โดยที่โปรแกรมอ่านสามารถเปิดและปิดการแสดงผลได้
เนื้อหาทางเลือกได้รับการกำหนดไว้ในมาตรฐาน ISO 32000-1 §8.11 หน่วยการแสดงผลคือกลุ่มเนื้อหาทางเลือก (optional content group) หรือ OCG ซึ่งเป็นพจนานุกรมประเภท /OCG ที่มีชื่อกำกับ เนื้อหาที่ทำเครื่องหมายไว้บนหน้ากระดาษจะเกี่ยวข้องกับกลุ่มใดกลุ่มหนึ่ง และโปรแกรมอ่านจะตัดสินใจว่ากลุ่มนั้นจะแสดงอยู่ในขณะนั้นหรือไม่ โครงสร้างที่เกี่ยวข้องอย่างพจนานุกรมสมาชิกเนื้อหาทางเลือก (optional content membership dictionary) หรือ OCMD จะช่วยให้การแสดงผลขึ้นอยู่กับเงื่อนไขการทำงานร่วมกันทางบูลีนของหลายกลุ่ม แต่ในกรณีทั่วไปมักใช้กลุ่มเนื้อหาทางเลือกที่มีชื่อเพียงกลุ่มเดียวเพื่อแทนเลเยอร์เดียว เอกสารจะเชื่อมโยงกลไกทั้งหมดนี้เข้าด้วยกันผ่านรายการแคตตาล็อกรายการเดียวคือ /OCProperties ซึ่งจะอธิบายถัดไป
สิ่งที่แคตตาล็อกต้องจัดเก็บ
OCG ด้วยตัวเองแล้วจะไม่มีการทำงานใดๆ เพื่อให้โปรแกรมอ่านสามารถแสดงรายการเลเยอร์และจดจำสถานะการทำงานได้ แคตตาล็อกเอกสารจำเป็นต้องมีพจนานุกรม /OCProperties ซึ่งหัวข้อ §8.11.4 ได้ระบุสิ่งที่ต้องมีอยู่ในนั้นอย่างชัดเจน โดยจะมีอาร์เรย์ /OCGs ที่ระบุชื่อกลุ่มทุกกลุ่มในไฟล์ และมีรายการ /D สำหรับเก็บการกำหนดค่าเริ่มต้น (default configuration) การกำหนดค่าเริ่มต้นคือส่วนที่โปรแกรมอ่านจะนำมาใช้เมื่อเปิดไฟล์ครั้งแรก มันจะบันทึกว่ากลุ่มใดบ้างที่เริ่มต้นด้วยสถานะเปิด และกลุ่มใดบ้างที่เริ่มต้นด้วยสถานะปิด รายการใดบ้างที่ถูกล็อกเพื่อไม่ให้ผู้ใช้สลับการแสดงผล และผ่านอาร์เรย์ /Order จะระบุวิธีจัดเรียงและซ้อนชื่อเลเยอร์ในแผงควบคุม
ผลที่ตามมาในทางปฏิบัติคือการสร้างเลเยอร์จะไม่ได้เกิดขึ้นแค่เฉพาะจุดเท่านั้น กลุ่มเนื้อหาทางเลือกจะต้องถูกวาดลงบนหน้ากระดาษ และต้องได้รับการลงทะเบียนในโครงสร้างระดับแคตตาล็อกที่ยังไม่มีอยู่ก่อนหน้านี้ด้วย PDFlibPas จะจัดการทั้งสองส่วนนี้ให้คุณ การเรียกใช้ฟังก์ชันครั้งแรกเพื่อสร้างกลุ่มจะเพิ่มรายการ /OCProperties ไปยังแคตตาล็อกและเตรียมค่าการกำหนดค่าเริ่มต้น เพื่อให้เลเยอร์นั้นถูกวาดและอยู่ในรายชื่อเลเยอร์โดยที่คุณไม่ต้องจัดการบันทึกข้อมูลแยกต่างหากด้วยตัวเอง
เหตุใดโหมดการปฏิบัติตามข้อกำหนดจึงสามารถจำกัดคุณสมบัตินี้ได้
ก่อนที่โค้ดเลเยอร์จะทำงาน เป้าหมายความเข้ากันได้ของเอกสารจะเป็นตัวตัดสินว่าเนื้อหาทางเลือกนั้นสามารถใช้งานได้ตามกฎหมายหรือไม่ มาตรฐาน PDF/A-1 ซึ่งเป็นโปรไฟล์สำหรับการจัดเก็บเอกสารระยะยาวที่กำหนดไว้ใน ISO 19005-1 จะห้ามใช้รายการ /OCProperties โดยสิ้นเชิงในหัวข้อ §6.1.13 เหตุผลนั้นสอดคล้องกับวัตถุประสงค์ของรูปแบบไฟล์ เอกสารจัดเก็บระยะยาวจะต้องแสดงผลเหมือนกันทุกประการสำหรับผู้ใช้งานในอนาคต และเนื้อหาที่ความสามารถในการมองเห็นเปลี่ยนไปตามโปรแกรมอ่านนั้นเป็นเนื้อหาที่ลักษณะภายนอกไม่คงที่ ดังนั้นโปรไฟล์นี้จึงสั่งห้ามโครงสร้างดังกล่าวแทนที่จะปล่อยให้ไฟล์ไม่ชัดเจน ส่วน PDF/A-2 และ PDF/A-3 ที่กำหนดไว้ใน ISO 19005-2 และ ISO 19005-3 มีมุมมองที่ตรงกันข้ามในหัวข้อ §6.9 โดยอนุญาตให้มีเนื้อหาทางเลือกได้ พร้อมด้วยกฎเกี่ยวกับการแสดงผลเริ่มต้น
ความแตกต่างดังกล่าวแสดงให้เห็นโดยตรงใน API เมื่อเอกสารอยู่ในโหมด PDF/A-1 ฟังก์ชัน NewOptionalContentGroup จะปฏิเสธที่จะสร้างกลุ่มและคืนค่ากลับเป็นศูนย์ เนื่องจากการยอมรับคำขอนี้จะส่งผลให้ไฟล์ไม่เป็นไปตามมาตรฐานการยอมรับที่ประกาศไว้ ในโหมด PDF/A-2 หรือ PDF/A-3 และใน PDF ทั่วไปที่ไม่มีข้อจำกัด การเรียกใช้งานแบบเดียวกันนี้จะทำงานสำเร็จและส่งกลับค่า ID ของกลุ่มที่ไม่ใช่ศูนย์ ผลลัพธ์ที่เป็นศูนย์จึงไม่ใช่ข้อผิดพลาดทั่วไปที่ต้องไปตรวจสอบในภายหลัง แต่เป็นคลังเครื่องมือที่กำลังแจ้งให้คุณทราบว่าระดับการปฏิบัติตามข้อกำหนดที่ทำงานอยู่นั้นไม่มีที่ว่างสำหรับคุณสมบัตินี้
var
Pdf: TPDFlib;
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;
เลเยอร์หนึ่งมีสองสถานะ ไม่ใช่สถานะเดียว
เลเยอร์ไม่ได้มีเพียงสถานะแสดงหรือซ่อนเท่านั้น การกำหนดค่าเริ่มต้นจะบันทึกสถานะการแสดงผลบนหน้าจอและสถานะการพิมพ์แยกต่างหาก เนื่องจากหัวข้อ §8.11.4 จะจำแนกสิ่งที่โปรแกรมอ่านแสดงผลออกจากสิ่งที่กระบวนการพิมพ์ส่งออกมา ทั้งสองส่วนนี้เป็นอิสระต่อกันโดยตั้งใจ ลายน้ำฉบับร่างสามารถแสดงบนหน้าจอและถูกตัดออกจากกระดาษที่พิมพ์ได้ และเลเยอร์เส้นตัดก็สามารถซ่อนบนหน้าจอแต่ส่งไปยังเครื่องพลอตเตอร์ได้ การรวมทั้งสองอย่างเข้าด้วยกันจะบังคับให้อันหนึ่งต้องดำเนินตามอีกอันหนึ่ง และสูญเสียการควบคุมอย่างเหมาะสมที่ฟีเจอร์นี้ตั้งใจให้มีตั้งแต่แรก
PDFlibPas เปิดเผยคู่นี้ผ่านฟังก์ชันกำหนดค่า (setters) สองตัว SetOptionalContentGroupVisible จะรับ ID ของกลุ่มและแฟล็ก โดยเลขหนึ่งหมายถึงแสดง และศูนย์หมายถึงซ่อน และควบคุมสถานะบนหน้าจอเริ่มต้น SetOptionalContentGroupPrintable จะรับ ID ของกลุ่มและแฟล็กว่าต้องการให้พิมพ์เลเยอร์นั้นออกมาร่วมด้วยหรือไม่เมื่อสั่งพิมพ์เอกสาร ส่วนฟังก์ชันดึงข้อมูลที่ตรงกัน ได้แก่ GetOptionalContentGroupVisible และ GetOptionalContentGroupPrintable จะส่งค่ากลับมาเป็นหนึ่งหรือศูนย์ ทำให้คุณสามารถตรวจสอบสถานะการแสดงผลบนจอและการพิมพ์ของเลเยอร์แยกกันได้ แทนที่จะคาดเดาค่าหนึ่งจากอีกค่าหนึ่ง
การสร้างสองเลเยอร์ในหนึ่งหน้า
การสร้างเลเยอร์และเติมเนื้อหาจะต้องทำตามลำดับขั้นตอนที่แน่นอน คุณวาดเนื้อหาสำหรับเลเยอร์นั้นลงบนหน้าปัจจุบัน จากนั้นเรียกใช้ SetContentStreamOptional พร้อมระบุ ID ของกลุ่ม ซึ่งจะห่อหุ้มกระแสเนื้อหา (content stream) ปัจจุบันของหน้า เพื่อให้ทุกสิ่งที่วาดจนถึงตอนนั้นเป็นของกลุ่มนั้น เนื่องจากฟังก์ชันจะจับสิ่งใดก็ตามที่อยู่ในกระแสเนื้อหา ณ ขณะนั้น ข้อควรปฏิบัติคือการวาดเครื่องหมายของเลเยอร์หนึ่งให้เสร็จ กำหนดกลุ่มให้เรียบร้อย แล้วจึงค่อยเริ่มทำเลเยอร์ถัดไป ตัวอย่างด้านล่างแสดงการใส่ข้อมูลโครงสร้างพื้นฐานในหน้าแรกและเส้นขีดของผู้เขียนคำนิยมในหน้าที่สอง ตั้งค่าสถานะการแสดงผลและสถานะการพิมพ์ของแต่ละเลเยอร์ แล้วทำการบันทึกไฟล์
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;
เลเยอร์เขียนบันทึกรีวิว (redline) คือกรณีที่ควรสังเกต มันจะแสดงบนหน้าเพื่อให้ผู้รีวิวสามารถมองเห็นบันทึกข้อความได้ และตั้งค่าแฟล็กพิมพ์ได้เป็นศูนย์ เพื่อไม่ให้เอกสารที่พิมพ์ออกมาจากไฟล์เดียวกันแสดงข้อความรีวิวนั้น ความไม่สมมาตรนี้คือเหตุผลทั้งหมดในการแยกสองสถานะนี้ออกจากกัน
การอ่านการกำหนดค่ากลับมา
การอ่านเลเยอร์คือการเดินทางอีกสายหนึ่งผ่านโครงสร้างแบบเดิม หลังจากโหลดไฟล์แล้ว GetOptionalContentConfigCount จะแจ้งจำนวนพจนานุกรมการกำหนดค่าที่เอกสารเก็บไว้ โดยการกำหนดค่าเริ่มต้นตัวแรกคือ ID การกำหนดค่า 1 ภายในการกำหนดค่า GetOptionalContentConfigOrderCount จะระบุจำนวนรายการในแผนผังลำดับ และคุณระบุดัชนีเริ่มต้นจาก 1 สำหรับแต่ละรายการ GetOptionalContentConfigOrderItemLabel จะส่งคืนข้อความแสดงผล และ GetOptionalContentConfigOrderItemLevel จะส่งคืนระดับความลึกของการซ้อน เพื่อให้สามารถสร้างเค้าโครงแผงควบคุมที่มีเลเยอร์ย่อยเยื้องอยู่ใต้หัวข้อขึ้นมาใหม่ได้อย่างถูกต้อง
แต่ละรายการยังมีประเภทของตัวเองด้วย GetOptionalContentConfigOrderItemType จะแยกความแตกต่างระหว่างกลุ่มเนื้อหาทางเลือกจริงกับป้ายกำกับข้อความธรรมดาที่มีไว้เพื่อเป็นหัวข้อของส่วนต่างๆ ในแผนผังเท่านั้น ความแตกต่างนี้มีความสำคัญเนื่องจากการสอบถามสถานะรายกลุ่มจะมีผลเฉพาะกับกลุ่มที่ใช้งานจริงเท่านั้น สำหรับรายการที่เป็นกลุ่ม GetOptionalContentConfigState จะแจ้งว่าการกำหนดค่าเริ่มต้นนั้นเปิด ปิด หรือปล่อยให้ไม่มีการเปลี่ยนแปลง และ GetOptionalContentConfigLocked จะแจ้งว่าผู้ใช้ถูกจำกัดไม่ให้สลับการแสดงผลหรือไม่ ลูปด้านล่างนี้จะแสดงผลแผนผังลำดับพร้อมสถานะเลเยอร์และสถานะการล็อกของแต่ละกลุ่ม โดยเยื้องหน้าตามระดับชั้น
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;
รายละเอียดสองประการที่ช่วยให้ลูปนี้ทำงานถูกต้อง ประการแรก ดัชนีลำดับจะเริ่มต้นด้วยหนึ่ง ตั้งแต่ 1 ถึงจำนวนทั้งหมด ซึ่งตรงกับวิธีที่คลังจัดลำดับแผนผังภายในตัว และประการที่สอง การเรียกใช้รายกลุ่มจะทำงานเมื่อประเภทของรายการเป็นกลุ่มเท่านั้น เนื่องจากป้ายกำกับข้อความเป็นหัวข้อที่มีชื่อและระดับ แต่ไม่มีสถานะเปิด ปิด หรือล็อกให้เรียกสอบถาม หากละเลยการดักจับข้อผิดพลาดนี้ คุณอาจสอบถามข้อความป้ายกำกับสำหรับสถานะที่มันไม่มี
จุดที่เหมาะสมในการใช้งาน
เลเยอร์เป็นกลไกการแสดงผล ดังนั้นเอ็นจินจึงต้องตรวจสอบสถานะของเลเยอร์ในทุกเส้นทางที่ทำการแสดงผลหน้ากระดาษ และส่วนของการแสดงผลนี้มีอธิบายไว้ในคู่มือการแสดงผลแบบหลายเอ็นจินใน Delphi เลเยอร์ยังมีความเกี่ยวข้องกับโครงสร้างเอกสาร เนื่องจากชื่อเลเยอร์เป็นข้อความที่ผู้ใช้เห็นได้ และผู้อ่านจะได้รับประโยชน์จากโครงสร้างเค้าโครงเลเยอร์ที่เป็นระเบียบ ซึ่งเชื่อมโยงกับเนื้อหาในบทความของเราเกี่ยวกับ tagged PDF และโครงสร้างการเข้าถึง ทั้งคู่ทำงานร่วมกับ API เนื้อหาทางเลือกที่อธิบายไว้ในที่นี้ ซึ่งเป็นส่วนหนึ่งของคลัง PDF สำหรับ Delphi ควบคู่ไปกับเครื่องมือจัดการหน้า ข้อความ ฟอนต์ และการปฏิบัติตามมาตรฐานที่อธิบายไว้ในส่วนอื่นของบล็อกนี้