مقاله فنی

سوییچ کردن لایه‌های Optional Content با PDFium در Delphi

PDFium Component لایه‌های optional content در PDF (OCG) را در Delphi از طریق دو متد TPdf کنترل می‌کند: InspectOptionalContent هر لایه را همراه وضعیت دیداری‌ای که PDFium واقعاً رندر می‌کند فهرست می‌کند، و SaveAsOptionalContentConfigured یک کپی راستی‌آزمایی‌شده می‌نویسد که در آن لایه‌هایی که انتخاب می‌کنید روشن یا خاموش شده‌اند. متد دوم قواعد Usage و /AS را هم خنثی می‌کند که وگرنه بی‌سروصدا ویرایش شما را خنثی می‌کردند. هر دو روی سندی که از قبل در TPdf باز است کار می‌کنند، پس پارسر دومی وجود ندارد که لازم باشد با چیزی که viewer نشان می‌دهد sync بماند

درخواست معمولاً از یک شرکت CAD یا GIS می‌رسد: مجموعه نقشه‌ها با ابعاد و حاشیه‌نویسی‌ها و بلوک عنوان روی لایه‌های جدا ارسال می‌شود، و مشتری قبل از رفتنش به تأمین‌کننده یک کپی با ابعاد مخفی می‌خواهد. PDFium محتوای اختیاری را درست رندر می‌کند، ولی ABI عمومی‌اش هیچ تابعی برای فهرست کردن OCGها یا انتخاب یک configuration یا فلیپ کردن وضعیت یک لایه ندارد. پس به سطح آبجکت می‌روید، /OCProperties را ویرایش می‌کنید، ذخیره می‌کنید، دوباره لود می‌کنید، و لایه هنوز سر جایش است. دلیلش منطق دیداری PDFium است، و ارزش دارد قبل از دست زدن به هر بایتی بفهمیدش

چرا ویرایش /ON و /OFF چیزی را که PDFium رندر می‌کند عوض نمی‌کند؟

ویرایش آرایه‌های /ON و /OFF دیکشنری configuration کافی نیست، چون PDFium می‌گذارد وضعیت صریحی که داخل دیکشنری /Usage مال خود OCG است بر آن آرایه‌ها پیروز شود، و یک قاعدهٔ auto-state از نوع /AS بعد می‌تواند هر دو را override کند. ISO 32000-1 §8.11.4 configurationها و usage dictionaryها را دو مکانیسم جدا توصیف می‌کند؛ رندرکنندهٔ PDFium آن‌ها را در یک تصمیم واحد می‌پیچد، و InspectOptionalContent آن را به این ترتیب بازتولید می‌کند:

  • از /BaseState مربوط به configuration شروع کنید، جایی که /ON و /Unchanged هر دو دیدنی حساب می‌شوند و فقط /OFF پنهان می‌کند
  • آرایهٔ /ON مربوط به configuration را اعمال کنید، بعد آرایهٔ /OFF آن را، پس گروهی که در هر دو لیست شده در نهایت پنهان می‌شود
  • وضعیت Usage صریح گروه را برای usage درخواستی اعمال کنید، مثل /Usage << /View << /ViewState /OFF >> >> که بر همهٔ بالایی‌ها اولویت دارد
  • گروهی که /Intent آن نه /View دارد و نه /All را دیدنی بگیرید، چون در دیداری‌بودن با view-intent سهمی ندارد
  • در آخر آرایهٔ /AS مربوط به configuration انتخاب‌شده را اجرا کنید که مدخل‌هایش برای رخداد مچ‌شده وضعیت گروه‌هایی که لیست کرده‌اند را ست می‌کند
تصمیم دیداری پنج‌مرحله‌ای که PDFium Component به‌ازای هر گروه optional content در PDF در Delphi بازپخش می‌کند: BaseState نقطهٔ شروع را ست می‌کند، آرایه‌های ON و OFF مربوط به configuration به ترتیب اعمال می‌شوند، مدخل صریح Usage از نوع ViewState یا PrintState بر هر دو اولویت دارد، عدم مشارکت به دلیل Intent دیدنی حساب می‌شود، و آرایه AS آخر اجرا می‌شود
ویرایش آرایه‌های ON و OFF کافی نیست چون PDFium مقدار BaseState و هر دو آرایه و وضعیت Usage گروه و در آخر قواعد auto-state مربوط به AS را در یک حکم واحد می‌پیچد که InspectOptionalContent مرحله‌به‌مرحله بازتولیدش می‌کند

مرحلهٔ سوم همان است که آدم‌ها را می‌سوزاند. فایلی که یک ابزار page layout ذخیره کرده اغلب روی هر OCG یک /ViewState /ON دارد، و PDFium بعد آرایهٔ /OFF دقیق ویرایش‌شدهٔ شما را نادیده می‌گیرد: ذخیره موفق است، فایل تمیز دوباره باز می‌شود، و لایه هنوز رنگ می‌شود. برای Print و Export، OcExplicitUsageState اول PrintState یا ExportState را می‌خواند و وقتی مدخل مخصوص غایب است به ViewState برمی‌گردد، پس یک ViewState /ON تنها لایه را برای چاپ هم پین می‌کند. marked contentی که به یک OCMD ارجاع می‌دهد (§8.11.2.2) بعد بر اساس این نتایج به-ازای-هر-گروه حل می‌شود، از طریق سیاست /P یا در صورت وجود عبارت دیداری /VE

چطور لایه‌هایی را که PDFium واقعاً نشان می‌دهد فهرست کنید؟

TPdf.InspectOptionalContent یک TPdfOptionalContentInventory برمی‌گرداند که آرایهٔ Groups آن شمارهٔ آبجکت و نام و intentها و سه وضعیت Usage و زبان و بازهٔ zoom و پرچم Locked و اندیس radio-group و EffectiveVisible محاسبه‌شدهٔ هر OCG را حمل می‌کند. متد اول به PDFium می‌گوید سند درون-حافظهٔ فعلی را ذخیره کند، object streamها را باز می‌کند و نتیجه را اسکن می‌کند، پس ویرایش‌هایی که زودتر در همین session انجام شده‌اند منعکس می‌شوند. اندیس configuration با 0 همیشه دیکشنری پیش‌فرض /D است و مدخل‌های /Configs از اندیس 1 دنبال می‌شوند؛ آرگومان پیش‌فرض یعنی -1 اندیس 0 را برمی‌دارد. سندی بدون /OCProperties باعث می‌شود متد False برگرداند با دلیلش در ErrorMessage، به‌جای استثنا دادن

procedure TFormMain.ListLayers;
var
  Inv: TPdfOptionalContentInventory;
  G: TPdfOptionalContentGroup;
begin
  // Usage پیش‌فرضش ocuView است؛ -1 یعنی configuration 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 متوقف می‌شود و هر چه عمیق‌تر باشد پنهان گرفته می‌شود، تا یک عبارت خصمانه یا خود-ارجاع بررسی را به stack overflow نکشاند

نوشتن وضعیت جدید لایه با SaveAsOptionalContentConfigured

TPdf.SaveAsOptionalContentConfigured آرایه‌ای از رکوردهای TPdfOptionalContentStateChange می‌گیرد (شمارهٔ آبجکت گروه به‌علاوهٔ Visible) و سندی می‌نویسد که configuration انتخاب‌شده دقیقاً همان وضعیت را تولید می‌کند. configuration انتخاب‌شده /BaseState /ON می‌گیرد به‌علاوهٔ آرایه‌های کامل /ON و /OFF که همهٔ گروه‌ها را پوشش می‌دهند، و هر OCGی که از قبل دیکشنری Usage دارد یک ViewState صریح (یا PrintState / ExportState، پیرو Options.Usage) می‌گیرد که با وضعیت جدیدش جور است. با TPdfOptionalContentConfigureOptions.Default کلید /AS مربوط به configuration انتخاب‌شده حذف می‌شود تا یک رخداد open یا print یا export نتواند لایه‌ها را برگرداند

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;

  // configuration 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 را به‌عنوان پیشوند بایت‌به-بایت نگه می‌دارد و فقط مالک configuration بازنویسی‌شده و آبجکت‌های OCGی که دیکشنری Usage دارند را append می‌کند، با دنبال کردن یک سکشن xref و trailer جدید. قبل از اینکه حتی یک بایت به مقصد شما برسد، نتیجه در یک TPdf جداگانه زیر سیاست لود سخت‌گیرانه دوباره باز می‌شود، و متد شکست می‌خورد اگر جدول cross-reference راستی‌آزمایی نشود. overload فایلی یک قدم جلوتر می‌رود: در فایلی موقت کنار مقصد می‌نویسد و مقصد را فقط بعد از موفقیت راستی‌آزمایی جایگزین می‌کند، پس یک آپدیت ردشده هرگز نیمی از یک نقشهٔ نوشته‌شده جا نمی‌گذارد. همین رویکرد revision افزایشی راستی‌آزمایی‌شده‌ای است که ویرایشگر name tree و number tree در PDFium Component هم استفاده می‌کند

SaveAsOptionalContentConfigured در PDFium Component چطور یک PDF دلفی با لایه‌های سوییچ‌شده می‌نویسد: تغییرات وضعیت و گزینه‌ها داخل می‌روند، configuration انتخاب‌شده با آرایه‌های کامل ON و OFF و وضعیت‌های Usage بازنویسی می‌شود، revision افزایشی راستی‌آزمایی‌شده append می‌شود، و یک بازگشایی سخت‌گیرانه باید قبل از نوشتن هر چیزی validate شود
ذخیرهٔ configured ری‌سیو خود PDFium را به‌عنوان پیشوند بایت نگه می‌دارد، مالک configuration بازنویسی‌شده را به‌علاوهٔ یک سکشن xref جدید append می‌کند، و قبل از دست زدن به مقصد نتیجه را در یک TPdf جداگانه دوباره باز می‌کند

ذخیرهٔ configured از انجام چه کارهایی سر باز می‌زند؟

ذخیرهٔ configured از هر تغییری که خود سند منعش کرده یا نمی‌تواند امن نمایشش دهد سر باز می‌زند، و هر رد کردن قبل از دست زدن به مقصد اتفاق می‌افتد. شمارهٔ آبجکتی که در /OCGs نباشد یکسره شکست می‌خورد. عوض کردن گروهی که در آرایهٔ /Locked مربوط به configuration لیست شده شکست می‌خورد، هرچند بازگویی مقدار فعلی‌اش مجاز است. با EnforceRadioGroups روشن، هر مجموعهٔ /RBGroups که سرانجام بیش از یک عضو دیدنی داشته باشد رد می‌شود به‌جای اینکه بقیه بی‌سروصدا خاموش شوند. سندهای رمزشده رد می‌شوند چون آبجکت‌های افزایشی آشکار نمی‌توانند هندلر امنیتی فعال را حمل کنند. سندهای امضاشده EPdfError می‌دهند مگر اینکه AllowSignedDocument = True بدهید، چون عوض کردن چیزی که یک صفحه نشان می‌دهد می‌تواند پوشش امضا یا یک سیاست certification را بشکند

گیت‌های رد کردن که SaveAsOptionalContentConfigured در PDFium Component قبل از نوشتن یک PDF دلفی configured اعمال می‌کند: شمارهٔ آبجکت بیرون از OCGs شکست می‌خورد، گروه‌های locked شکست می‌خورند، مجموعه‌های 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;

قبل از سیم‌کشی این به یک batch job معاملاتش را بدانید. revision append‌شده روی ری‌سیو کامل PDFium می‌نشیند نه روی بایت‌های فایل اصلی شما، و دقیقاً به همین دلیل ورودی امضاشده رضایت صریح می‌خواهد. بازنویسی configuration انتخاب‌شده را هم به /BaseState /ON نرمال می‌کند، پس baseline از نوع /Unchanged یا /OFF نویسنده با آرایه‌های صریحی جایگزین می‌شود که دیداری حاصلشان یکی است. حذف /AS ترفندهای مخصوص-چاپ مثل لایهٔ واترمارکی که فقط روی کاغذ ظاهر می‌شود را برمی‌دارد؛ برای نگه داشتن آن قواعد DisableAutomaticState را روی False بگذارید و بپذیرید که ممکن است برای آن رخداد بر وضعیت درخواستی شما غلبه کنند. از سوی دیگر، PDF/A-2 (ISO 19005-2 بند 6.9) و PDF/UA (ISO 14289-1 بند 7.10) هر دو /AS را در دیکشنری‌های configuration منع می‌کنند، پس خروجی پیش‌فرض یک مورد را حذف می‌کند که وگرنه preflight اعتبارسنجی PDF/A با PDFium Component گزارشش می‌کرد

کنترل لایه در یک PDF viewer دلفی کجا می‌نشیند؟

در یک viewer، کنترل لایه یک چک‌لیست است که از inventory و یک reload از نتیجهٔ ذخیره‌شده تغذیه می‌شود. چک‌لیست را از Groups پر کنید، مدخل‌های Locked را غیرفعال کنید، اعضایی که RadioGroupIndex مشترک دارند را دوسویه-انحصاری بگیرید، و موقع apply در یک TMemoryStream بنویسید و همان stream را داخل TPdf لود کنید تا view وضعیت جدید را رنگ کند. سیم‌کشی بین TPdf و TPdfView در ساختن یک PDF viewer پرمایه با PDFium VCL در Delphi پوشش داده شده. مجوز و دانلود trial و بقیهٔ مجموعه فیچرها در صفحهٔ محصول PDFium Component برای Delphi است