Technical Article

لایه‌های PDF در Delphi: گروه‌های محتوای اختیاری (OCG)

یک نقشه‌بردار یک پلان سایت را باز می‌کند و می‌خواهد خطوط تراز پنهان باشند در حالی که تاسیسات روشن بمانند. یک بازبین می‌خواهد یادداشت‌های خط قرمز روی صفحه نمایش داده شوند و از نسخه چاپی حذف گردند. یک برگه مشخصات محصول به سه زبان از یک فایل ارسال می‌شود و خواننده انتخاب می‌کند کدام زبان نشان داده شود. هر سه مورد یک ویژگی یکسان در PDF هستند و پنلی که آن‌ها را در Acrobat هدایت می‌کند Layers نام دارد. ویژگی زیرین این پنل محتوای اختیاری (optional content) است و چیزی است که به یک صفحه واحد اجازه می‌دهد چندین لایه بصری مستقل را حمل کند که بیننده آن‌ها را روشن و خاموش می‌کند.

محتوای اختیاری در استاندارد ISO 32000-1 بخش ۸.۱۱ مشخص شده است. واحد قابلیت دید، یک گروه محتوای اختیاری یا OCG است، دیکشنری از نوع /OCG که یک نام را حمل می‌کند. محتوای علامت‌گذاری شده روی صفحه با یک گروه مرتبط است و نمایشگر تصمیم می‌گیرد که آیا آن گروه در حال حاضر نشان داده شود یا خیر. یک ساختار مرتبط، دیکشنری عضویت محتوای اختیاری یا OCMD، اجازه می‌دهد قابلیت دید به ترکیب بولی چندین گروه وابسته باشد، اما مورد روزمره یک گروه نام‌گذاری شده واحد است که نشان‌دهنده یک لایه واحد است. سند کل این مکانیزم را از طریق یک ورودی کاتالوگ به نام /OCProperties که در ادامه توضیح داده می‌شود، به هم متصل می‌کند.

آنچه کاتالوگ باید حمل کند

یک OCG به تنهایی بی‌اثر است. برای اینکه یک نمایشگر یک لایه را لیست کرده و وضعیت آن را به خاطر بسپارد، کاتالوگ سند به یک دیکشنری /OCProperties نیاز دارد و بخش ۸.۱۱.۴ دقیقاً آنچه را که در آن قرار می‌گیرد مشخص می‌کند. یک آرایه /OCGs وجود دارد که نام هر گروه در فایل را ذکر می‌کند و یک ورودی /D وجود دارد که پیکربندی پیش‌فرض را نگه می‌دارد. پیکربندی پیش‌فرض بخشی است که خواننده هنگام اولین باز شدن فایل اعمال می‌کند. این بخش ثبت می‌کند که کدام گروه‌ها روشن و کدام خاموش شروع می‌شوند، کدام ورودی‌ها در برابر تغییر توسط کاربر قفل هستند و از طریق یک آرایه /Order، نام لایه‌ها چگونه در پنل مرتب شده و تو در تو قرار می‌گیرند.

نتیجه عملی این است که ایجاد یک لایه هرگز یک اقدام کاملاً محلی نیست. گروه باید روی صفحه کشیده شود و همچنین باید در یک ساختار در سطح کاتالوگ ثبت شود که قبلاً وجود نداشته است. PDFlibPas هر دو کار را برای شما انجام می‌دهد. اولین فراخوانی که یک گروه را می‌سازد، ورودی /OCProperties را به کاتالوگ اضافه کرده و پیکربندی پیش‌فرض را مقداردهی اولیه می‌کند، بنابراین لایه بدون نیاز به حسابرسی جداگانه از طرف شما، هم رسم می‌شود و هم در لیست قرار می‌گیرد.

چرا یک حالت انطباق می‌تواند این ویژگی را مسدود کند

قبل از اجرای هرگونه کد لایه، هدف انطباق سند تصمیم می‌گیرد که آیا محتوای اختیاری اصلاً قانونی است یا خیر. پروفایل آرشیوی PDF/A-1 تعریف شده در ISO 19005-1، ورودی /OCProperties را در بخش ۶.۱.۱۳ کاملاً ممنوع می‌کند. این استدلال با هدف فرمت مطابقت دارد. یک فایل آرشیوی باید برای هر خواننده‌ای در آینده دور کاملاً یکسان رندر شود و محتوایی که قابلیت دید آن توسط بیننده قابل تغییر است، محتوایی است که ظاهر آن ثابت نیست؛ بنابراین این پروفایل به جای اجازه دادن به یک آرشیو مبهم، این ساختار را ممنوع می‌کند. استاندارد‌های PDF/A-2 و PDF/A-3 تعریف شده در ISO 19005-2 و ISO 19005-3 در بخش ۶.۹ خود دیدگاه مخالفی دارند و محتوای اختیاری را با قوانینی در مورد قابلیت دید پیش‌فرض مجاز می‌دانند.

این تفاوت مستقیماً در 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;

دو وضعیت برای هر لایه, نه یکی

یک لایه به سادگی مرئی یا نامرئی نیست. پیکربندی پیش‌فرض وضعیت روی صفحه و وضعیت چاپ جداگانه‌ای را ثبت می‌کند, زیرا بخش ۸.۱۱.۴ آنچه را که نمایشگر نشان می‌دهد از آنچه خط لوله چاپ صادر می‌کند متمایز می‌سازد. این دو عمداً مستقل از یکدیگر هستند. یک واترمارک پیش‌نویس می‌تواند روی صفحه نشان داده شود و از روی کاغذ حذف گردد و یک لایه خط برش می‌تواند روی صفحه پنهان باشد اما به پلاتر ارسال شود. ادغام این دو باعث می‌شود که یکی از دیگری پیروی کند و دقیقاً کنترلی را که ویژگی برای ارائه آن وجود دارد از دست بدهد.

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 گزارش می‌دهد که سند چه تعداد دیکشنری پیکربندی را در خود نگه می‌دارد؛ اولین پیکربندی پیش‌فرض شناسه پیکربندی ۱ است. در یک پیکربندی، GetOptionalContentConfigOrderCount تعداد ورودی‌ها را در درخت ترتیب (order tree) نشان می‌دهد و شما آن‌ها را از ۱ ایندکس می‌کنید. برای هر ورودی، 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;

دو جزئیات این حلقه را درست نگه می‌دارند. ایندکس ترتیب مبتنی بر یک است، از ۱ تا تعداد کل، که با نحوه شماره‌گذاری درخت به صورت داخلی توسط کتابخانه مطابقت دارد. و فراخوانی‌های اختصاصی گروه تنها زمانی اجرا می‌شوند که نوع مورد یک گروه باشد، زیرا یک برچسب متنی سربرگی با نام و سطح است اما وضعیت روشن، خاموش یا قفل برای پرس‌وجو ندارد. این گارد را نادیده بگیرید تا از یک برچسب وضعیتی را بخواهید که ندارد.

جایگاه این ویژگی

لایه‌ها یک مکانیزم نمایش هستند، بنابراین موتور باید در هر مسیری که یک صفحه را رندر می‌کند به آن‌ها احترام بگذارد، و بخش رندر در راهنمای ما برای رندر چند‌موتوره در Delphi پوشش داده شده است. آن‌ها همچنین با ساختار سند تلاقی دارند، زیرا نام یک لایه متنی است که نویسنده با آن مواجه می‌شود و خواننده از یک ساختار لایه‌ای سازمان‌یافته بهره‌مند می‌شود که این موضوع به کار در مقاله ما در مورد PDF تگ‌شده و ساختار دسترس‌پذیری مرتبط است. هر دو با APIهای محتوای اختیاری توصیف‌شده در اینجا جفت می‌شوند که به عنوان بخشی از کتابخانه PDF Delphi در کنار امکانات صفحه، متن، فونت و انطباق که در بخش‌های دیگر این وبلاگ بحث شده‌اند، ارائه می‌شوند.