Technical Article

طبقات PDF في Delphi: مجموعات المحتوى الاختياري (OCG)

يفتح مساح مخطط موقع ويريد إخفاء الخطوط الكنتورية بينما تظل المرافق ظاهرة. ويريد مراجع رؤية التعليقات التوضيحية ذات الخطوط الحمراء على الشاشة واختفائها من المطبوعات. وتأتي ورقة المنتج بثلاث لغات في ملف واحد، ويختار القارئ اللغة المعروضة. كل هذه الحالات الثلاث هي نفس ميزة PDF، وتسمى اللوحة التي تديرها في Acrobat باسم الطبقات (Layers). الميزة الكامنة وراء تلك اللوحة هي المحتوى الاختياري، وهو ما يسمح لصفحة واحدة بحمل عدة طبقات بصرية مستقلة يقوم برنامج العرض بتشغيلها وإيقافها.

يتم تحديد المحتوى الاختياري في القسم 8.11 من معيار ISO 32000-1. وحدة الرؤية هي مجموعة محتوى اختياري، OCG، وهي قاموس من نوع /OCG يحمل اسماً. يرتبط المحتوى المميز على الصفحة بمجموعة ما، ويقرر برنامج العرض ما إذا كانت هذه المجموعة معروضة حالياً أم لا. هناك بنية ذات صلة، وهي قاموس عضوية المحتوى الاختياري OCMD، تتيح اعتماد الرؤية على تركيبة منطقية لعدة مجموعات، ولكن الحالة اليومية هي مجموعة مسماة واحدة تمثل طبقة واحدة. يربط المستند الآلية بأكملها معاً من خلال إدخال كتالوج واحد، وهو /OCProperties، والموصوف لاحقاً.

ما يجب أن يحمله الكتالوج

تكون OCG بمفردها خاملة. لكي يقوم برنامج العرض بإدراج طبقة وتذكر حالتها، يحتاج كتالوج المستند إلى قاموس /OCProperties، ويوضح القسم 8.11.4 بالتفصيل ما يوضع فيه. هناك مصفوفة /OCGs تسمي كل مجموعة في الملف، وهناك إدخال /D يحتوي على التكوين الافتراضي. التكوين الافتراضي هو الجزء الذي يطبقه القارئ عند فتح الملف لأول مرة. حيث يسجل المجموعات التي تبدأ بالتشغيل والتي تبدأ بالإيقاف، والمدخلات المقفلة التي يمنع المستخدم من تغيير حالتها، ومن خلال مصفوفة /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 ويسمحان بالمحتوى الاختياري، مع وجود قواعد حول الرؤية الافتراضية.

يظهر هذا الاختلاف مباشرة في واجهة برمجة التطبيقات. عندما يكون المستند في وضع 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 الزوج من خلال وظيفتين للتعيين. يأخذ SetOptionalContentGroupVisible معرف المجموعة وعلامة، حيث يعني الرقم واحد مرئياً ويعني الصفر مخفياً، ويتحكم في الحالة الافتراضية على الشاشة. بينما يأخذ SetOptionalContentGroupPrintable معرف المجموعة وعلامة تفيد ما إذا كانت الطبقة ستصدر عند طباعة المستند. تعيد وظائف الاسترجاع المطابقة، GetOptionalContentGroupVisible وGetOptionalContentGroupPrintable، واحداً أو صفراً، بحيث يمكنك قراءة حالة شاشة وطباعة الطبقة بشكل منفصل بدلاً من استنتاج أحدهما من الآخر.

بناء طبقتين على صفحة

يتبع إنشاء طبقة وتعبئتها ترتيباً ثابتاً. تقوم برسم المحتوى للطبقة على الصفحة الحالية، ثم تستدعي SetContentStreamOptional مع معرف المجموعة، والذي يغلف تدفق المحتوى الحالي للصفحة بحيث ينتمي كل ما تم رسمه حتى الآن إلى تلك المجموعة. ونظراً لأن الاستدعاء يلتقط كل ما هو موجود في التدفق في تلك اللحظة، فإن الالتزام يقتضي وضع علامات طبقة واحدة وتعيينها، ثم البدء في الطبقة التالية فقط. يضع المثال أدناه المرافق في الصفحة الأولى وخطاً أحمر للمراجع في صفحة ثانية، ويضبط حالة الشاشة والطباعة لكل طبقة، ثم يحفظ.

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;

طبقة المراجع هي الحالة التي تستحق الملاحظة. يتم عرضها على الشاشة حتى يرى المراجع الملاحظة، وتكون علامة قابلية الطباعة الخاصة بها صفراً بحيث لا تحمل المطبوعات من نفس الملف أي نص للمراجعة. هذا التباين هو الغرض الأساسي من إبقاء الحالتين منفصلتين.

قراءة التكوين مجدداً

قراءة الطبقات هي رحلة مختلفة عبر نفس الهيكل. بعد تحميل الملف، يعيد 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 إلى العدد الإجمالي، وهو ما يطابق طريقة ترقيم المكتبة للشجرة داخلياً. وتعمل استدعاءات كل مجموعة فقط عندما يكون نوع العنصر مجموعة، لأن التسمية النصية هي عنوان يحمل اسماً ومستوى ولكن ليس لها حالة تشغيل، أو إيقاف، أو قفل للاستعلام عنها. تخط هذا الحارس وستسأل تسمية عن حالة لا تملكها.

أين يناسب هذا

الطبقات هي آلية عرض، لذا يتعين على المحرك احترامها في كل مسار يعرض صفحة، والجانب الخاص بالعرض مغطى في دليلنا حول العرض متعدد المحركات في Delphi. كما أنها تتقاطع مع بنية المستند، لأن اسم الطبقة هو نص يواجهه الكاتب ويستفيد القارئ من مخطط منظم للطبقات، وهو ما يرتبط بالعمل الموضح في مقالنا حول مستندات PDF الموسومة وبنية إمكانية الوصول. يقترن كلاهما بواجهات برمجة تطبيقات المحتوى الاختياري الموصوفة هنا، والتي يتم شحنها كجزء من مكتبة Delphi PDF إلى جانب إمكانيات الصفحة والنص والخط والمطابقة المناقشة في مكان آخر من هذه المدونة.