مقال تقني

تبديل طبقات المحتوى الاختياري في PDF في Delphi عبر PDFium

يتحكم مكوّن PDFium في طبقات المحتوى الاختياري في PDF‏ (OCGs) في Delphi عبر أسلوبين من TPdf: يسرد InspectOptionalContent كل طبقة مع الرؤية التي سيرسمها PDFium فعلًا، ويكتب SaveAsOptionalContentConfigured نسخة متحقق منها بدلت فيها الطبقات التي تختار تشغيلها أو إطفاءها. والأسلوب الثاني يعطل أيضًا قواعد Usage و/AS التي كانت ستبطل تعديلك بهدوء لولا ذلك. وكلاهما يعمل على المستند المفتوح سلفًا في TPdf، فلا يوجد محلل ثانٍ تبقيه متزامنًا مع ما يعرضه العارض

الطلب يصل عادة من ورشة CAD أو GIS: تشحن حزمة الرسومات بأبعاد وتعليقات ولوحة عنوان على طبقات منفصلة، ويريد العميل نسخة الأبعاد مخفاة فيها قبل أن تذهب إلى مورد. يرسم PDFium المحتوى الاختياري بشكل صحيح، لكن ABI العام لديه لا يملك دالة لتعداد OCGs أو اختيار تهيئة أو قلب حالة طبقة. فتهبط إلى مستوى الكائنات، وتحرر /OCProperties، وتحفظ، وتعيد التحميل، والطبقة ما تزال هناك. والسبب منطق الرؤية في PDFium، ويستحق الفهم قبل لمس أي بايتات

لماذا لا يغير تحرير /ON و/OFF ما يرسمه PDFium؟

تحرير مصفوفتي /ON و/OFF في قاموس التهيئة لا يكفي، لأن PDFium يدع حالة صريحة داخل قاموس /Usage الخاص بالـ OCG نفسه تتغلب على تلك المصفوفات، وقاعدة auto-state في /AS تستطيع بعدها تجاوز الاثنين. تصف ISO 32000-1 §8.11.4 التهيئات وقواميس الاستخدام آليتين منفصلتين؛ لكن مُصيّر PDFium يطويهما في قرار واحد، ويعيد InspectOptionalContent إنتاجه بهذا الترتيب:

  • ابدأ من /BaseState في التهيئة، حيث تحسب /ON و/Unchanged كلتاهما مرئية ولا يخفي إلا /OFF
  • طبق مصفوفة /ON في التهيئة ثم مصفوفة /OFF فيها، فتنتهي مجموعة مسردة في كلتيهما مخفاة
  • طبق حالة Usage الصريحة للمجموعة للاستخدام المطلوب، مثل /Usage << /View << /ViewState /OFF >> >>، وهي تتجاوز كل ما سبق
  • عامل المجموعة التي لا يتضمن /Intent لديها /View ولا /All بوصفها مرئية، لأنها لا تشارك في رؤية قصد العرض
  • أخيرًا شغّل مصفوفة /AS في التهيئة المختارة، التي تضبط مداخيلها للحدث المطابق حالةَ المجموعات التي تسردها
قرار الرؤية ذو الخطوات الخمس الذي يعيد مكوّن PDFium تشغيله لكل مجموعة محتوى اختياري في PDF في Delphi: يحدد BaseState البداية، وتطبق مصفوفتا ON وOFF في التهيئة بالترتيب، ويتجاوز مدخل Usage الصريح ViewState أو PrintState كلتيهما، وتحسب عدم مشاركة Intent مرئية، وتشغل مصفوفة AS أخيرًا
تحرير مصفوفتي ON وOFF لا يكفي لأن PDFium يطوي BaseState والمصفوفتين وحالة Usage للمجموعة وقواعد AS التلقائية في حكم واحد يعيد InspectOptionalContent إنتاجه خطوة بخطوة

الخطوة الثالثة هي التي تحرق الناس. ملف حفظته أداة تخطيط كثيرًا ما يحمل /ViewState /ON على كل OCG، فيتجاهل PDFium حينها مصفوفة /OFF التي حررتها بعناية: الحفظ ينجح، والملف يعيد الفتح نظيفًا، والطبقة ترسم ما تزال. وللطباعة والتصدير، تقرأ OcExplicitUsageState‏ PrintState أو ExportState أولًا وتعود إلى ViewState عند غياب المدخل المحدد، فـ ViewState /ON وحدها تثبت الطبقة للطباعة أيضًا. والمحتوى الموسوم الذي يشير إلى OCMD‏ (§8.11.2.2) يحسم بعدها مقابل هذه النتائج لكل مجموعة، عبر سياسة /P أو، عند حضورها، تعبير الرؤية /VE

كيف تسرد الطبقات التي سيعرضها PDFium فعلًا؟

يعيد TPdf.InspectOptionalContent قيمة TPdfOptionalContentInventory تحمل مصفوفة Groups فيها رقم كائن كل OCG واسمه وقصوده وحالات Usage الثلاث ولغته ومدى التكبير وعلم Locked ومؤشر مجموعة الراديو وEffectiveVisible المحسوبة. يطلب الأسلوب أولًا من PDFium حفظ المستند الحالي في الذاكرة، وينشر تدفقات الكائنات، ويمسح الناتج، فتنعكس التعديلات التي أجريت سابقًا في الجلسة. مؤشر التهيئة 0 هو دائمًا قاموس /D الافتراضي وتليه مداخيل /Configs من المؤشر 1؛ والوسيط الافتراضي -1 يختار المؤشر 0. والمستند بلا /OCProperties يجعل الأسلوب يعيد False والسبب في ErrorMessage بدل رفع استثناء

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage افتراضها ocuView؛ و-1 يختار التهيئة 0، أي قاموس /D
  if not Pdf.InspectOptionalContent(Inv) then
  begin
    Memo1.Lines.Add('No usable layers: ' + Inv.ErrorMessage);
    Exit;
  end;
  Memo1.Lines.Add(Format('Configuration %d: %s',
    [Inv.SelectedConfigurationIndex,
     string(Inv.Configurations[Inv.SelectedConfigurationIndex].Name)]));
  for G in Inv.Groups do
    Memo1.Lines.Add(Format('obj %d  %s  visible=%s  locked=%s  radio=%d',
      [G.ObjectNumber, string(G.Name),
       BoolToStr(G.EffectiveVisible, True),
       BoolToStr(G.Locked, True), G.RadioGroupIndex]));
end;

تبلغ مصفوفة Memberships عن كل OCMD مع Policy خاصته‏ (ocmpAnyOn وocmpAllOn وocmpAnyOff وocmpAllOff) ونص VisibilityExpression الخام وEffectiveVisible خاصته. وبضعة قواعد حدية مقصودة. افتراض /P هو /AnyOn، وOCMD بلا مجموعات تحسب مرئية. والإشارة إلى رقم كائن ليس OCG معروفًا تعامل مرئية بدل إفشال التعبير كله. ويقف تقييم /VE عند عمق تداخل 32 ويعامل ما هو أعمق مخفيًا، مما يمنع تعبيرًا معاديًا أو ذاتي الإشارة من تحويل الفحص إلى طفح مكدس

كتابة حالة طبقة جديدة عبر SaveAsOptionalContentConfigured

يأخذ TPdf.SaveAsOptionalContentConfigured مصفوفة من سجلات TPdfOptionalContentStateChange‏ (رقم كائن المجموعة زائد Visible) ويكتب مستندًا تنتج فيه التهيئة المختارة تلك الحالة بالضبط. تنال التهيئة المختارة /BaseState /ON زائد مصفوفتي /ON و/OFF كاملتين تغطيان كل مجموعة، وينال كل OCG يملك قاموس Usage سلفًا حالة ViewState صريحة‏ (أو PrintState‏ / ExportState وفق Options.Usage) تطابق حالته الجديدة. ومع TPdfOptionalContentConfigureOptions.Default ينزع مفتاح /AS من التهيئة المختارة كي لا يقلب حدث فتح أو طباعة أو تصدير الطبقات مرة أخرى

procedure TFormMain.SaveWithoutDimensions(DimensionsObj, NotesObj: Integer);
var
  Changes: TPdfOptionalContentStateChanges;
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  SetLength(Changes, 2);
  Changes[0].GroupObjectNumber := DimensionsObj;
  Changes[0].Visible := False;
  Changes[1].GroupObjectNumber := NotesObj;
  Changes[1].Visible := True;

  // التهيئة 0، وocuView، وDisableAutomaticState وEnforceRadioGroups بقيمة True
  Options := TPdfOptionalContentConfigureOptions.Default;

  if not Pdf.SaveAsOptionalContentConfigured('C:\Out\Drawing-NoDims.pdf',
    Changes, Options, Report) then
    raise Exception.Create('Layer update rejected: ' + Report.ErrorMessage);

  Log(Format('%d of %d groups changed, %d Usage states rewritten, /AS removed: %s',
    [Report.ChangedGroupCount, Report.GroupCount,
     Report.UpdatedUsageStateCount,
     BoolToStr(Report.RemovedAutomaticState, True)]));
end;

يحفظ مسار الكتابة مخرج حفظ PDFium ذاته بادئةً بايتًا ببايت ويلحق فقط مالك التهيئة المعاد كتابته وكائنات OCG الحاملة لقواميس Usage، يليهما قسم xref جديد وtrailer. وقبل أن يبلغ بايت واحد وجهتك، يعاد فتح الناتج في TPdf منفصل تحت سياسة التحميل الصارمة، ويفشل الأسلوب إن لم يتحقق جدول الإشارات المرجعية. والتحميل الزائد للملفات يخطو خطوة أبعد: يكتب في ملف مؤقت بجوار الهدف ولا يستبدل الهدف إلا بعد نجاح التحقق، فلن يترك تحديث مرفوض رسمة نصف مكتوبة خلفه أبدًا. وهو أسلوب المراجعة التزايدية المتحقق منها نفسه الذي يستخدمه محرر شجرة الأسماء وشجرة الأرقام في PDF في مكوّن PDFium

كيف يكتب SaveAsOptionalContentConfigured في مكوّن PDFium ملف PDF لدلفي بطبقات مبدلة: تدخل تغييرات الحالة والخيارات، وتعيد كتابة التهيئة المختارة بمصفوفتي ON وOFF كاملتين وحالات Usage، وتلحق المراجعة التزايدية المتحقق منها، ويجب أن يتحقق إعادة فتح صارمة قبل كتابة أي شيء
يحفظ الحفظ المهيأ إعادة حفظ PDFium ذاتها بادئة بايتية، ويلحق مالك التهيئة المعاد كتابته زائد قسم xref جديد، ويعيد فتح الناتج في TPdf منفصل قبل لمس الوجهة

ما الذي يرفض الحفظ المهيأ فعله؟

يرفض الحفظ المهيأ أي تغيير يحظره المستند نفسه أو لا يستطيع تمثيله بأمان، وكل رفض يقع قبل لمس الوجهة. رقم كائن ليس في /OCGs يفشل تمامًا. تغيير مجموعة مسردة في مصفوفة /Locked في التهيئة يفشل، مع جواز إعادة ذكر قيمتها الحالية. ومع تفعيل EnforceRadioGroups، أي مجموعة /RBGroups ستنتهي بأكثر من عضو مرئي ترفض بدل إطفاء البقية بهدوء. والمستندات المشفرة ترفض لأن الكائنات التزايدية الصريحة لا تستطيع حمل معالج الأمان النشط. والمستندات الموقعة ترفع EPdfError ما لم تمرر AllowSignedDocument = True، لأن تغيير ما تعرضه صفحة قد يكسر تغطية التوقيع أو سياسة اعتماد

بوابات الرفض التي يطبقها SaveAsOptionalContentConfigured في مكوّن PDFium قبل كتابة ملف PDF مهيأ لدلفي: رقم كائن خارج OCGs يفشل، والمجموعات المقفلة تفشل، ومجموعات RBGroups بأكثر من عضو مرئي ترفض، والمستندات المشفرة لا تستطيع حمل كائنات تزايدية صريحة، والملفات الموقعة تطلب AllowSignedDocument
كل رفض يقع قبل لمس الوجهة، وسبب الفشل يهبط في Report.ErrorMessage بدل ترك رسمة نصف مكتوبة خلفه
function TFormMain.SavePrintPreset(Target: TStream;
  const Changes: TPdfOptionalContentStateChanges): Boolean;
var
  Options: TPdfOptionalContentConfigureOptions;
  Report: TPdfOptionalContentConfigureReport;
begin
  Options := TPdfOptionalContentConfigureOptions.Default;
  Options.Usage := ocuPrint;          // يكتب /Print << /PrintState ... >>
  Options.ConfigurationIndex := 1;    // أول مدخل في /Configs، لا /D
  try
    Result := Pdf.SaveAsOptionalContentConfigured(Target, Changes, Options,
      Report);                        // تبقى AllowSignedDocument بقيمة False
    if not Result then
      ShowMessage(Report.ErrorMessage);
  except
    on E: EPdfError do
    begin
      ShowMessage(E.Message);         // ملف موقّع: لا يكتب شيء في Target
      Result := False;
    end;
  end;
end;

اعرف المفاضلات قبل أن توصل هذا في عمل دفعي. المراجعة الملحقة تجلس فوق إعادة حفظ PDFium الكاملة، لا فوق بايتات ملفك الأصلي، وهذا بالضبط سبب حاجة المدخل الموقع إلى موافقة صريحة. والكتابة من جديد تطبع أيضًا التهيئة المختارة إلى /BaseState /ON، فخط الأساس /Unchanged أو /OFF من المؤلف يستبدل بمصفوفات صريحة تعطي الرؤية الناتجة نفسها. ونزع /AS يزيل حيل الطباعة فقط كطبقة علامة مائية تظهر على الورق وحده؛ واضبط DisableAutomaticState إلى False للاحتفاظ بتلك القواعد، مقبلًا بأنها قد تتجاوز الحالة التي طلبتها لذلك الحدث. ومن الجانب المضيء، تحرمان PDF/A-2‏ (ISO 19005-2 بند 6.9) وPDF/UA‏ (ISO 14289-1 بند 7.10) كلتاهما /AS في قواميس التهيئة، فيزيل المخرج الافتراضي مشكلة كانت التحقق المسبق PDF/A عبر مكوّن PDFium ستبلغ عنها لولا ذلك

أين يقع التحكم في الطبقات داخل عارض PDF لدلفي

في عارض، التحكم في الطبقات قائمة فحص تقودها المخزونات زائد إعادة تحميل للناتج المحفوظ. املأ القائمة من Groups، وعطل المداخيل المقفلة Locked، وعامل الأعضاء المتقاسمين RadioGroupIndex بوصفهم متعارضين، وعند التطبيق اكتب في TMemoryStream وحمّل ذلك التدفق مرة أخرى في TPdf لترسم العرض الحالة الجديدة. والربط بين TPdf وTPdfView مغطى في بناء عارض PDF غني بالميزات عبر PDFium VCL في دلفي. والترخيص وتنزيلات التجربة وبقية مجموعة الميزات على صفحة منتج مكوّن PDFium لدلفي