مقاله فنی

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

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

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

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

یک OCG به تنهایی بی‌اثر است. برای اینکه یک بیننده (viewer) یک لایه را لیست کند و وضعیت آن را به خاطر بسپارد، کاتالوگ سند به یک دیکشنری /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 خود دیدگاه متفاوتی را اتخاذ می‌کنند و به محتوای اختیاری اجازه می‌دهند، البته با قوانینی درباره دیده‌شدن پیش‌فرض

این تفاوت به طور مستقیم در 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 این جفت را از طریق دو متد تنظیم (setter) ارائه می‌دهد. SetOptionalContentGroupVisible شناسه گروه و یک پرچم (flag) را می‌گیرد، جایی که یک به معنای قابل‌مشاهده و صفر به معنای پنهان است، و حالت پیش‌فرض روی صفحه را کنترل می‌کند. SetOptionalContentGroupPrintable شناسه گروه و پرچمی را می‌گیرد که تعیین می‌کند آیا لایه هنگام چاپ سند منتشر می‌شود یا خیر. دریافت‌کننده‌های (getters) مطابق، 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 تعداد ورودی‌ها در درخت ترتیب را می‌دهد و شما آن‌ها را از ۱ نمایه‌گذاری می‌کنید. برای هر ورودی، 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;

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

جایگاه این مورد

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