מודד פותח תוכנית אתר ורוצה שקווי הגובה יהיו מוסתרים בעוד שהתשתיות יישארו גלויות. סוקר רוצה שהערות סימון אדום יהיו גלויות על המסך ולא יופיעו בהדפסה. דף מוצר נשלח בשלוש שפות מקובץ אחד, והקורא בוחר איזו שפה תוצג. כל השלושה הם אותה תכונה של PDF, והפאנל שמניע אותם ב-Acrobat נקרא שכבות (Layers). התכונה שמתחת לפאנל זה היא תוכן אופציונלי (optional content), והיא זו שמאפשרת לעמוד בודד לשאת מספר שכבות ויזואליות עצמאיות שהמציג מפעיל ומכבה
תוכן אופציונלי מוגדר בתקן ISO 32000-1 §8.11. יחידת הניראות היא קבוצת תוכן אופציונלית, OCG, מילון מטיפוס /OCG הנושא שם. תוכן מסומן (Marked content) בעמוד משויך לקבוצה, והמציג מחליט אם הקבוצה הזו מוצגת כעת. מבנה קשור, מילון חברות בתוכן אופציונלי או OCMD, מאפשר לניראות להיות תלויה בשילוב בוליאני של מספר קבוצות, אך המקרה היומיומי הוא קבוצה בודדת בעלת שם המייצגת שכבה אחת. המסמך קושר את המנגנון כולו יחד דרך ערך קטלוג בודד, /OCProperties, המתואר בהמשך
מה שהקטלוג חייב לשאת
קבוצת OCG כשלעצמה היא אינרטית. כדי שמציג יפרט שכבה ויזכור את מצבה, קטלוג המסמך זקוק למילון /OCProperties, וסעיף 8.11.4 מפרט בדיוק מה נכנס לתוכו. ישנו מערך /OCGs המציין כל קבוצה בקובץ, וישנו ערך /D המחזיק את תצורת ברירת המחדל. תצורת ברירת המחדל היא החלק שקורא מיישם כאשר הקובץ נפתח לראשונה. היא מתעדת אילו קבוצות מתחילות כפעילות ואילו כבויות, אילו ערכים נעולים בפני שינוי של המשתמש, ודרך מערך /Order, כיצד שמות השכבות מסודרים ומקוננים בפאנל
התוצאה המעשית היא שיצירת שכבה אינה רק פעולה מקומית. יש לצייר את הקבוצה בעמוד, והיא גם צריכה להירשם במבנה ברמת הקטלוג שלא היה קיים קודם לכן. PDFlibPas עושה את שניהם עבורך. הקריאה הראשונה שיוצרת קבוצה מוסיפה את ערך ה-/OCProperties לקטלוג ומציבה את תצורת ברירת המחדל, כך שהשכבה גם מצוירת וגם מפורטת ללא צורך בניהול רישום נפרד מצידך
מדוע מצב תאימות יכול למנוע את התכונה
לפני שקוד שכבות כלשהו רץ, יעד התאימות (conformance target) של המסמך קובע אם תוכן אופציונלי הוא בכלל חוקי. תקן 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 רגיל שאינו מוגבל, אותה קריאה מצליחה ומחזירה מזהה קבוצה שאינו אפס. תוצאת אפס אינה כישלון כללי שיש לבדוק מאוחר יותר; זו הספרייה שאומרת לך שלרמת התאימות הפעילה אין מקום לתכונה זו
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 מקבלת את מזהה הקבוצה ודגל, כאשר אחד פירושו גלוי ואפס פירושו מוסתר, ושולטת במצב ברירת המחדל על המסך. הפונקציה SetOptionalContentGroupPrintable מקבלת את מזהה הקבוצה ודגל לשאלה אם השכבה נפלטת בעת הדפסת המסמך. הפונקציות התואמות לקבלת המצב, GetOptionalContentGroupVisible ו-GetOptionalContentGroupPrintable, מחזירות כל אחת אחת או אפס, כך שתוכל לקרוא בחזרה את נטיית המסך וההדפסה של השכבה בנפרד במקום להסיק אחת מהשנייה
בניית שתי שכבות בעמוד
יצירת שכבה ומילויה מתבצעות בסדר קבוע. אתה מצייר את התוכן עבור השכבה על העמוד הנוכחי, ואז קורא ל-SetContentStreamOptional with the group ID, which wraps the page's current content stream so everything drawn so far belongs to that group. Because the call captures whatever is on the stream at that moment, the discipline is to lay down one layer's marks, assign them, and only then start the next layer. The example below puts utilities on the first page and a reviewer redline on a second page, sets each layer's screen and print state, and saves
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 מדווח כמה מילוני תצורה המסמך מחזיק; תצורת ברירת המחדל הראשונה היא מזהה תצורה 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, מ-1 ועד לספירה הכללית, ותואם לאופן שבו הספרייה ממספרת את העץ באופן פנימי. והקריאות לכל קבוצה רצות רק כאשר טיפוס הפריט הוא קבוצה, מכיוון שתווית טקסט היא כותרת עם שם ורמה אך ללא מצב פעיל, כבוי או נעול שניתן לתשאל. דלג על שומר זה ואתה תבקש מתווית מצב שאין לה
היכן זה מתאים
שכבות הן מנגנון הצגה, ולכן המנוע חייב לכבד אותן בכל נתיב המרנדר עמוד, והצד של הרינדור מכוסה במדריך שלנו על רינדור רב-מנועי ב-Delphi. הן גם נפגשות עם מבנה המסמך, מכיוון ששם השכבה הוא טקסט הפונה למשתמש והקורא מפיק תועלת ממתאר שכבות מובנה, מה שמתחבר לעבודה במאמר שלנו על tagged PDF ומבנה נגישות. שניהם מצטרפים לממשקי ה-API של תוכן אופציונלי המתוארת כאן, אשר נשלחים כחלק מספריית PDF עבור Delphi לצד יכולות העמוד, הטקסט, הגופן והתאימות הנדונות במקומות אחרים בבלוג זה