مقاله فنی

فایل‌های Associated سطح صفحه در PDF 2.0 با PDFlibPas

PDFlibPas یک فایل جاسازی‌شده را به یک صفحه مشخص گره می‌زند نه به کل سند، با نوشتن آرایه /AF در دیکشنری صفحه، در حالی که خود payload همان‌جا در name tree داخلی EmbeddedFiles سند ثبت‌شده می‌ماند. همین دوگانگی است که ISO 32000-2 §14.13 توصیفش می‌کند، و همین است که به reader اجازه می‌دهد به سؤالی جواب دهد که پیوست سطح سند از پسش برنمی‌آید: این داده متعلق به کدام صفحه است

موارد استفاده مشخص‌تر از پیوست‌های عمومی است. یک گزارش نقشه‌برداری که هر صفحه سری اندازه‌گیری خامِ پشت نمودارش را حمل می‌کند. یک دسته اسکن‌شده که هر صفحه نتیجه OCRای را نگه می‌دارد که لایه متنی‌اش را تولید کرده. یک مجموعه نقشه که هر شیت اکسترکت CADای را حمل می‌کند که از روی آن رندر شده. در هرکدام از این موارد، یک لیست پیوست سطح سند یعنی یک توده فایل با اسم‌هایی که شماره صفحه را در دل خودشان encode کرده‌اند، که یک قرارداد است نه یک ساختار

یک payload، دو جایی که به آن ارجاع می‌شود

نکته ساختاری مهم این است که انجمن سطح صفحه نسخه دومی از هیچ چیزی نمی‌سازد. فایل یک بار جاسازی می‌شود و در name tree داخلی EmbeddedFiles دقیقاً مثل پیوست سطح سند ثبت می‌شود، با همان سازوکار file specification. تفاوت در این است که ارجاع و کلید رابطه‌اش کجا نوشته می‌شوند: در دیکشنری صفحه به‌جای کاتالوگ سند

دو پیامد دارد. اول اینکه readerای که فقط پیوست سطح سند را می‌شناسد باز هم payload را پیدا می‌کند، چون payload در همان name treeای است که چنین readerای در آن جستجو می‌کند. دوم اینکه پاک‌کردن انجمن صفحه بایندینگ را حذف می‌کند نه فایل را. ClearPageAssociatedFiles صفحه را از فایل‌های Associated‌اش جدا می‌کند و payloadها را از طریق name tree قابل دسترس باقی می‌گذارد، که رفتار محافظه‌کارانه است: عملیاتی که می‌گوید انجمن را پاک کن نباید بی‌سروصدا داده‌ای را نابود کند که شاید بخش دیگری از سند به آن ارجاع دهد

ساختار یک فایل Associated سطح صفحه در سند PDF 2.0 نوشته‌شده با PDFlibPas: payload یک بار جاسازی می‌شود و در name tree داخلی EmbeddedFiles زیر کاتالوگ سند ثبت می‌شود، در حالی که دیکشنری صفحه یک آرایه /AF دارد که با کلید AFRelationship به همان file specification ارجاع می‌دهد، پس ClearPageAssociatedFiles بایندینگ را جدا می‌کند بدون آنکه داده را نابود کند
انجمن سطح صفحه یک ارجاع دوم اضافه می‌کند نه یک کپی دوم: readerهایی که فقط پیوست سطح سند را می‌شناسند باز هم payload را در name tree پیدا می‌کنند، و پاک‌کردن بایندینگ صفحه جریان جاسازی‌شده را قابل دسترس باقی می‌گذارد

این تابع یک شرط موفقیت عمداً باریک دارد که ارزش می‌شنیدن دارد. فقط وقتی گزارش موفقیت می‌دهد که صفحه واقعاً کلید /AF را داشته. صفحه‌ای که هیچ‌وقت انجمن نداشته به‌جای یک تأیید خوش‌خیم شکست برمی‌گرداند، تا caller نتواند یک no-op را با پاکسازی تمام‌شده اشتباه بگیرد

var
  Lib: TPDFlib;
  Idx, I: Integer;
begin
  Lib := TPDFlib.Create(nil);
  try
    Lib.LoadFromFile('survey-report.pdf');

    // اتصال سری اندازه‌گیری‌هایی که نمودار صفحه ۳ را تولید کرده‌اند
    Idx := Lib.AddPageAssociatedFileFromFile(3,
      'series-03.csv',            // فایل روی دیسک
      'measurements.csv',         // نام نمایشی درون PDF
      'text/csv',                 // نوع MIME
      'Raw measurement series for figure 3',
      'Data');                    // AFRelationship، ISO 32000-2 14.13

    if Idx < 0 then
      raise Exception.Create('page association refused');

    for I := 0 to Lib.GetPageAssociatedFileCount(3) - 1 do
      Writeln('page 3 associated file, embedded index ',
        Lib.GetPageAssociatedFileEmbeddedIndex(3, I));

    Lib.SaveToFile('survey-report-with-data.pdf');
  finally
    Lib.Free;
  end;
end;

رشته رابطه در عمل متن آزاد نیست. ISO 32000-2 یک واژگان تعریف می‌کند، Source، Data، Alternative، Supplement، EncryptedPayload، FormData، Schema و Unspecified، و consumerها به آن تکیه می‌کنند. Data برای عددهای پشت نمودار، Source برای سندی که یک صفحه از روی آن تولید شده، Alternative برای یک بازنمایی معادل. از همان واژگان انتخاب کنید حتی اگر هنوز هیچ‌جای خط لوله شما آن را نمی‌خواند، چون ابزار بعدیِ زنجیره شاید

چرا همان lookup در هر دو جهت به FollowRef نیاز دارد؟

چون دنبال‌کردن ارجاع به دو سؤال مختلف جواب می‌دهد و کد باید بداند کدام را می‌پرسد. یک key lookup که ارجاع‌های غیرمستقیم را دنبال می‌کند آبجکتی را برمی‌گرداند که ارجاع به آن اشاره می‌کند. lookupای که دنبال نمی‌کند خودِ ارجاع را برمی‌گرداند. هر دو درست‌اند، و استفاده از گزینه غلط به‌جای خطا یک بدرفتاری بی‌صدا تولید می‌کند

خواندن یک فایل Associated جهت اول را نشان می‌دهد. برای به‌دست‌آوردن شماره آبجکت جریان جاسازی‌شده پشت کلیدهای /EF و /F در file specification، lookup نباید دنبال کند، چون دنبال‌کردن، ارجاع را به آبجکت stream حل می‌کند و شماره آبجکت از دست می‌رود. قاعده کلی می‌شود: هر مسیر کدی که به هویت آبجکت نیاز دارد نه محتوای آبجکت، باید ارجاع خام را بگیرد

optional content جهت مقابل را نشان می‌دهد و پیدا کردنش گران‌تر تمام شد. دیکشنری properties محتوای اختیاری به‌صورت آبجکت غیرمستقیم در کاتالوگ نوشته می‌شود، پس کدی که بدون دنبال‌کردن آن را می‌خواند به‌جای دیکشنری یک ارجاع می‌گیرد. type check روی آن مقدار بعداً شکست می‌خورد، و شاخه fallback طبیعی — اگر پیکربندی نیست یکی بساز — اجرا می‌شود و پیکربندی‌ای را که از قبل آنجا بود بازنویسی می‌کند. هیچ‌چیز raise نمی‌شود. لایه‌هایی که در گروه‌های محتوای اختیاری و لایه‌ها توصیف شدند به‌سادگی وضعیت visibility پیش‌فرضشان را از دست می‌دهند

درس از هر دو مورد فراتر می‌رود. وقتی یک lookup می‌تواند هم ارجاع برگرداند هم خود آبجکت را، یک type check لخت مدیریت خطا نیست: یک شاخه است که بالاخره یک روز به بهانه اشتباهی اجرا می‌شود. صریح تصمیم بگیرید هر call site به چه چیزی نیاز دارد، و API عمومی‌ای که مستقیم به سؤال جواب می‌دهد — مثل یک property شمارنده محتوای اختیاری — را به زدن دست در accessor محافظت‌شده برای دیکشنری کاتالوگ ترجیح دهید

نقشه تصمیم برای دنبال‌کردن ارجاع در lookupهای PDF همان‌طور که در PDFlibPas پیاده شده: خواندن /EF و /F زیر file specification نباید ارجاع را دنبال کند چون شماره آبجکت جریان جاسازی‌شده خودِ جواب است، در حالی که دیکشنری غیرمستقیم /OCProperties در کاتالوگ باید دنبال شود وگرنه یک type check ناموفق بی‌سروصدا پیکربندی محتوای اختیاری موجود را بازنویسی می‌کند
همان lookup به دو سؤال مختلف جواب می‌دهد: هویت به ارجاع خام نیاز دارد، محتوا به آبجکت حل‌شده، و یک type check لخت به‌جای این تصمیم بالاخره شاخه غلط را بدون raise اجرا می‌کند
// پیوست‌های سطح سند و انجمن‌های سطح صفحه همزیستی دارند. یک
// فایل جاسازی‌شده را می‌توان در سطح سند هم associated علامت زد
if Lib.IsEmbeddedFileAssociated(0) = 0 then
  Lib.SetEmbeddedFileAssociated(0, 1, 'Supplement');

Writeln('document associated files: ', Lib.GetAssociatedFileCount);
Writeln('page 3 associated files  : ',
        Lib.GetPageAssociatedFileCount(3));

// پاک‌کردن بایندینگ صفحه را جدا می‌کند؛ payload در name tree می‌ماند
if Lib.ClearPageAssociatedFiles(3) > 0 then
  Writeln('page 3 associations removed, payloads still reachable');

حالت‌های انطباق با پیوست‌ها چه می‌کنند

پروفایل‌های آرشیوی محدود می‌کنند چه چیزی اجازه جاسازی دارد، و این محدودیت در نقطه ورود اعمال می‌شود نه در زمان ذخیره. PDF/A-1 فایل‌های جاسازی‌شده را کلاً ممنوع می‌کند، PDF/A-2 فقط اسناد PDF/A جاسازی‌شده را اجازه می‌دهد، و PDF/A-3 پروفایلی است که جاسازی را به انواع فایل دلخواه باز کرد، که دقیقاً دلیل این است که فرمت‌های فاکتور هیبریدی روی آن بنا شده‌اند

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

همین دلیل است که فایل‌های Associated این‌قدر در صورت‌حساب الکترونیکی دیده می‌شوند. یک فاکتور هیبریدی PDFای است که انسان می‌خواند با یک payload XML ماشین‌خوان که پیوست شده و با رابطه درست علامت خورده، و هم پروفایل ظرف و هم کلید رابطه بخشی از مشخصات‌اند نه قرارداد. این ساختار در ساخت فاکتورهای هیبریدی Factur-X و ZUGFeRD پوشش داده شده، و سمت metadata در شمای توسعه XMP در PDF/A-3

انجمن چه وقت باید به‌ازای هر صفحه باشد نه به‌ازای هر سند؟

وقتی consumer لازم دارد بداند داده متعلق به کدام صفحه است، و فقط در همان صورت. پیوست‌های سطح سند ساده‌ترند، توسط viewerهای بیشتری پشتیبانی می‌شوند، و هر جا که payload کل سند را توصیف می‌کند — یک XML فاکتور، یک manifest امضا، یک آرشیو سورس — کفایت می‌کنند. سراغ انجمن سطح صفحه بروید وقتی payload واقعاً page-scoped است و هویت صفحه بخشی از معنایش است

پشتیبانی، قید عملی است. فایل‌های Associated سطح صفحه یک سازه PDF 2.0 هستند و پشتیبانی viewerها از آن‌ها نازک‌تر از پیوست‌های سطح سند است. چون payload به هر حال در name tree می‌نشیند، viewerای که /AF روی صفحات را نادیده می‌گیرد باز هم فایل را در لیست پیوست‌هایش نشان می‌دهد، پس افت کیفیت باوقار است. ولی اگر بایندینگ صفحه برای consumer شما ضروری است نه صرفاً metadata مفید، readerای را که واقعاً هدف‌تان است راستی‌آزمایی کنید نه اینکه فرض

فایل‌های Associated سطح صفحه، پیوست‌های سطح سند و گیت‌های پروفایل آرشیوی که بر هر دو حکومت می‌کنند همراه PDFlibPas Delphi PDF library عرضه می‌شوند. اگر همزمان فایل‌های قدیمی‌تر را حین ورود تعمیر می‌کنید، کار metadata و انطباق در تبدیل به PDF/A با تعمیر metadata است که از اول تصمیم می‌گیرد کدام‌یک از این مسیرهای پیوست اصلاً در دسترس شماست