مقاله فنی

اکشن‌های چرخه‌ی عمر PDF: کاتالوگ /AA در برابر صفحه /AA در Delphi

PDFlibPas، کتابخانه‌ی بومی PDF برای Delphi و C++Builder، به یک سند PDF دو مکان جداگانه می‌دهد برای آویختن رفتار خودکار: اکشن‌های چرخه‌ی عمر در سطح سند مثل WillClose، WillSave، DidSave، WillPrint و DidPrint، ذخیره‌شده در دیکشنری /AA کاتالوگ، و اکشن‌های چرخه‌ی عمر در سطح صفحه — Open و Close — ذخیره‌شده در دیکشنری /AA خودِ هر شیء Page به‌جای آن. اشتباه‌گرفتن این دو کانتینر رایج‌ترین راه است که یک اکشن چرخه‌ی عمر بی‌سروصدا هیچ کاری نمی‌کند

موردهای انگیزشی معمولی هستند. یک تیم مالی یک الگوی صورت‌حساب می‌خواهد که یک timestamp چاپ را مهر بزند و ثبت کند چه کسی آن را همان لحظه‌ای که چاپ واقعاً شروع می‌شود چاپ کرده، نه وقتی فایل صرفاً باز می‌شود. یک گردش‌کار پر از فرم به مقادیر فیلد نیاز دارد که به‌طور خودکار به یک سرور پوش شوند پیش از اینکه به کلاینت PDF خواننده اجازه‌ی بستن پنجره داده شود، پس یک تب بسته‌شده هرگز به‌معنای یک ویرایش گمشده نباشد. یک گزارش چندصفحه‌ای یک بنر مختص-صفحه می‌خواهد که فقط درحالی‌که آن صفحه روی صفحه است ظاهر شود. PDF واقعاً یک لایه‌ی سوم زیر سند و صفحه برای این نوع رفتار ارائه می‌دهد — اکشن‌های متصل به ورودی /A خودِ یک فیلد فرم یا لینک تکی، موضوع یک مقاله‌ی همراه درباره‌ی اکشن‌های فرم تعاملی و جاوااسکریپت — اما این مقاله در دو لایه‌ی بالای آن می‌ماند: کل سند، و یک صفحه‌ی تکی

چه محرک‌هایی روی /AA کاتالوگ سند زندگی می‌کنند؟

پنج محرک روی دیکشنری /AA کاتالوگ زندگی می‌کنند، و هر یک از آن‌ها برای یک رخداد شلیک می‌شود که کل سند را تحت‌تأثیر قرار می‌دهد، نه یک صفحه‌ی تکی. ISO 32000-1 §12.6.3 (رخدادهای محرک) کلیدهای سطح-سند را WC، WS، DS، WP و DP فهرست می‌کند — نام‌های دو-حرفی لفظی نوشته‌شده درون دیکشنری /AA — به‌ترتیب برای WillClose، WillSave، DidSave، WillPrint و DidPrint، و PDFlibPas دقیقاً آن مجموعه را در enumeration به نام TPDFlibDocumentActionTrigger آینه می‌کند: datWillClose، datWillSave، datDidSave، datWillPrint، datDidPrint. SetDocumentAction تنها نقطه‌ی ورود است که هر یک از آن پنج تا را متصل می‌کند، و پارامتر ActionKindای که می‌گیرد یکی از ده ثابت PDF_ACTION_BUILDER_* است که در سراسر هر فراخوانی action-builder در کتابخانه به اشتراک گذاشته شده، از یک URI ساده تا یک اسکریپت تا یک پرش مقصد. آنچه یک اکشن GoTo، فایل-دور، فایل-جاسازی‌شده یا Launch واقعاً وقتی ماشه‌کشیده می‌شود انجام می‌دهد سؤالی متفاوت از اینکه کجا متصل می‌شود است، و آن موضوع یک مقاله‌ی همراه درباره‌ی اکشن‌های GoTo، دور، جاسازی‌شده و launch است — این یکی با سؤال کانتینر می‌ماند، کاتالوگ یا صفحه، نه سؤال نوع-اکشن

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.AddStandardFont(4);
    Lib.DrawText(40, 700, 'Quarterly statement');
    Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
      'https://example.com/audit/will-save', '', 0, 0);
    Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_SUBMIT,
      'https://example.com/forms/submit', 'CustomerName;OrderTotal', 0, 0);
    Lib.SaveToFile('statement.pdf');
  finally
    Lib.Free;
  end;
end;

یک محرک در سطح-صفحه چطور با یکی در سطح-سند متفاوت است؟

یک محرک در سطح-صفحه فقط برای همان یک شیء Page که به آن متصل است شلیک می‌شود، و PDFlibPas آن را در دیکشنری /AA خودِ آن صفحه ذخیره می‌کند نه کاتالوگ. فقط دو محرک صفحه وجود دارد، Open و Close، متناظر با کلیدهای O و Cای که ISO 32000-1 برای دیکشنری اکشن‌های اضافی یک صفحه تعریف می‌کند، و PDFlibPas آن‌ها را به‌عنوان patOpen و patClose از طریق SetPageAction در معرض دید می‌گذارد، که به هر صفحه‌ای که در حال حاضر از طریق SelectPage انتخاب شده متصل می‌شود — یک جزئیات که اولین‌باری که در سراسر یک سند حلقه می‌زنید با انتظار اینکه یک فراخوانی همه‌جا اعمال شود اهمیت دارد، چون هرگز چنین نمی‌شود. متصل‌کردن هرکدام از نوع محرک هم حداقل نسخه‌ی PDF فایل را بالا می‌برد، و آن دو کانتینر کف‌های متفاوتی می‌خواهند: PDFlibPas سند را به حداقل PDF 1.4 بالا می‌برد اولین‌باری که یک ورودی /AA کاتالوگ می‌نویسد، و به حداقل PDF 1.5 اولین‌باری که یک ورودی /AA صفحه می‌نویسد، صرف‌نظر از اینکه کدام نوع اکشن درونش می‌نشیند. آن یک الزام در سطح-کانتینر است که روی هر چیزی که خودِ اکشن به‌تنهایی نیاز دارد لایه‌بندی شده، پس یک اکشن URI ساده که به‌تنهایی فقط به PDF 1.1 نیاز داشت، همچنان کل فایل را به PDF 1.5 می‌کشد به‌محض اینکه درون یک محرک page-open پیچیده شود

Lib.SelectPage(3);
Lib.SetPageAction(patOpen, PDF_ACTION_BUILDER_JAVASCRIPT,
  'app.alert("Section 3: internal review only");', '', 0, 0);
Lib.SetPageAction(patClose, PDF_ACTION_BUILDER_WEB,
  'https://example.com/analytics/page-3-closed', '', 0, 0);

خواندن و حذف اکشن‌های چرخه‌ی عمر

GetDocumentActionInfo و GetPageActionInfo هر دو یک رکورد TPDFlibActionInfo برمی‌گردانند، و فیلد Kind هر زمانی که آن محرک هیچ چیزی متصل نداشته باشد akNone برمی‌گردد، پس Kind را پیش از اعتمادکردن به هر فیلد دیگری روی رکورد بررسی کنید — URI، JavaScript، FileName و بقیه فقط برای همان یک نوع اکشنی معنادار هستند که Kind واقعاً گزارش می‌دهد، چون همان شکل رکورد در سراسر هر نوع اکشنی که builder می‌تواند تولید کند دوباره استفاده می‌شود. RemoveDocumentAction و RemovePageAction هرکدام یک محرک تکی را پاک می‌کنند و 1 را گزارش می‌دهند وقتی چیزی برای حذف پیدا کردند، 0 وقتی محرک از پیش خالی بود؛ وقتی ورودی حذف‌شده آخرین چیز باقی‌مانده در دیکشنری /AA بود، PDFlibPas خودِ /AA حالا-خالی را حذف می‌کند به‌جای اینکه یک کانتینر آویزان و بی‌معنا را روی کاتالوگ یا صفحه رها کند

var
  Info: TPDFlibActionInfo;
begin
  Info := Lib.GetDocumentActionInfo(datWillSave);
  if Info.Kind = akURI then
    WriteLn('WillSave calls out to: ', string(Info.URI));

  if Lib.RemoveDocumentAction(datWillSave) = 1 then
    Lib.SetDocumentAction(datWillSave, PDF_ACTION_BUILDER_WEB,
      'https://example.com/audit/will-save-v2', '', 0, 0);
end;

آیا PDF/A اصلاً اجازه‌ی اکشن‌های چرخه‌ی عمر می‌دهد؟

نه. مطابقت PDF/A کل کانتینر اکشن‌های اضافی را رد می‌کند، نه فقط انواع اکشنی که پرخطر به‌نظر می‌رسند، چون ISO 19005 مدل اکشن تعاملی PDF را با این فرض محدود می‌کند که یک فایل آرشیوی باید دهه‌ها از این پس همان‌طور رندر شود، بدون بستگی به یک موتور اسکریپت یا یک اتصال شبکه که ممکن است تا آن زمان وجود نداشته باشد. SetLifecycleAction، builder مشترک پشت هم SetDocumentAction و هم SetPageAction، پیش از اینکه اصلاً به ActionKind نگاه کند PDFAMode را بررسی می‌کند، پس یک اکشن URI که صرفاً یک صفحه‌ی وب شرکتی را باز می‌کند یا یک اکشن Named که فقط یعنی برو به صفحه‌ی بعدی در همان تور گرفتار می‌شود که یک اکشن خطرناک، هیچ چیزی که یک بازبین امنیتی به‌طور معمول پرچم بزند، در هر صورت مسدود شده، چون این محدودیت ساختاری است نه مورد-به-مورد. خطر عملی این است که رد‌شدن خاموش است: هم SetDocumentAction و هم SetPageAction بدون raise‌کردن یک استثنا 0 برمی‌گردانند، پس یک محل فراخوانی که هرگز مقدار بازگشتی را بررسی نمی‌کند سندی را عرضه می‌کند که بی‌سروصدا محرکی را که قرار بود حمل کند کم دارد

Lib.SetPDFAMode(2); // PDF/A-1b
if Lib.SetDocumentAction(datWillClose, PDF_ACTION_BUILDER_NAMED,
     '', '', 0, 0) = 0 then
  // rejected: PDF/A-1b forbids Catalog /AA, even a plain Named action
  WriteLn('lifecycle action not attached');

یک عدم‌تقارن ارزش به‌خاطرسپردن دارد. RemoveDocumentAction و RemovePageAction هرگز PDFAMode را بررسی نمی‌کنند، پس بارگذاری یک فایلی که از پیش اکشن‌های چرخه‌ی عمر غیرمنطبق حمل می‌کند و برداشتن آن‌ها در راه یک ذخیره‌ی منطبق-با-PDF/A دقیقاً همان‌طور که انتظار می‌رود کار می‌کند — فقط مسیر نوشتن، متصل‌کردن یک محرک جدید، به حالت مطابقت دروازه‌بندی شده

چاپ-در-باز-کردن بدون یک محرک WillOpen کجا جا می‌افتد؟

دیکشنری /AA کاتالوگ اصلاً هیچ ورودی WillOpenای ندارد، از نظر طراحی — /AA سطح-سند در ISO 32000-1 دقیقاً پنج کلید تعریف می‌کند، WillClose، WillSave، DidSave، WillPrint و DidPrint، و هیچ چیزی در آن فهرست فقط چون یک فایل باز شده شلیک نمی‌شود. قلاب زمان-باز-کردن در یک ورودی جداگانه‌ی کاتالوگ زندگی می‌کند، /OpenAction، که PDFlibPas از طریق خانواده‌ی خودش از فراخوانی‌ها در معرض دید می‌گذارد، SetOpenActionJavaScript، SetOpenActionDestination و SetOpenActionNamedDestination در میان آن‌ها، که هیچ‌کدام اصلاً دیکشنری /AA یا enumeration به نام TPDFlibDocumentActionTrigger را لمس نمی‌کنند. با این‌حال این دو سازوکار ترکیب می‌شوند، و آن معمولاً چیزی است که یک الگوی چاپ-در-باز-کردن واقعاً نیاز دارد: الگو را طوری بسازید که /OpenActionاش job چاپ را شروع کند، به‌طور معمول یک اکشن جاوااسکریپت که فرمان چاپ خودِ نمایشگر را فرا می‌خواند، و خودِ چاپ چیزی است که به WillPrint و DidPrint چیزی می‌دهد که در برابرش اجرا شوند — یک timestamp مهرشده پیش از اینکه صفحات spool شوند، یک ورودی ممیزی نوشته‌شده به‌محض اینکه تمام شوند

این محرک‌ها چقدر در سراسر نمایشگرهای PDF قابل‌اعتماد هستند؟

نه هر نمایشگری آن‌ها را اجرا می‌کند، حتی خارج از PDF/A، پس یک اکشن چرخه‌ی عمر را به‌عنوان یک درخواست در نظر بگیرید نه یک تضمین. Acrobat و اغلب خواننده‌های کامل دسکتاپ کل مجموعه را وفادارانه اجرا می‌کنند، اما سهم بزرگی از مصرف واقعی PDF هرگز اصلاً یک دیکشنری اکشن‌های اضافی را لمس نمی‌کند: نمایشگرهای جاسازی‌شده‌ی مرورگر، اغلب خواننده‌های موبایل، و تقریباً هر خط لوله‌ی رندر یا استخراج-متن سمت-سرور یا /AA را کاملاً نادیده می‌گیرند یا فقط یک برش باریک از آن را رعایت می‌کنند، با WillPrint و DidPrint که به‌طور معمول بدترین عملکرد را دارند چون تبدیل headless هیچ عملیات چاپی برایشان برای وصل‌شدن ندارد. اگر یک اکشن submit-form از نوع WillClose تنها مسیری است که داده‌ی فرم را می‌گیرد، مسیر قابل‌اعتمادی نیست — آن را با یک دکمه‌ی submit صریح جفت کنید، و محرک خودکار را به‌عنوان یک راحتی برای خواننده‌هایی که اتفاقاً آن را پشتیبانی می‌کنند در نظر بگیرید

محرک‌های سند، صفحه، و فیلد سه لایه از همان ماشین‌آلات دیکشنری-اکشن زیرین هستند، و به‌محض اینکه کانتینر روشن باشد، بقیه انتخاب ثابت ActionKind درست و بررسی کد بازگشتی است. این محرک‌های چرخه‌ی عمر، در کنار API گسترده‌تر action-builder که این مقاله به آن اشاره می‌کند، به‌عنوان بخشی از کتابخانه‌ی PDF از PDFlibPas برای Delphi استاندارد عرضه می‌شوند، با مرجع کامل محرک و نوع-اکشن در مستندات محصول