مقاله فنی

preflight در PDF/A و PDF/UA در Delphi با PDF Library for Delphi

PDF/A و PDF/UA به دو سوال پاسخ می‌دهند که با هم ربطی ندارند، و در نظر گرفتن آن‌ها به‌عنوان یک checkbox یکپارچهٔ accessibility-and-archiving، راهی است که fileهای شکسته با یک label compliance به یک archive می‌رسند. PDF/A می‌پرسد آیا یک file بیست سال بعد هنوز به‌درستی render می‌شود. PDF/UA می‌پرسد آیا assistive technology می‌تواند آن را امروز بخواند. یک document می‌تواند یکی را به‌سادگی pass کند و در دیگری fail شود، پس تنها honest verdict از اجرای هر دو می‌آید، و از اجرای آن‌ها قبل از اینکه file نوشته شود، نه بعد از اینکه یک سیستم پایین‌دست به conformance identifierی که در metadata آن bake شده اعتماد کند. آن identifier یک self-declaration است. هیچ‌چیز در format لازم نمی‌کند که true باشد، و یک application که «PDF/A-1b» را در XMP می‌نویسد بدون validate کردن در برابر standard، fileای تولید می‌کند که برای هر consumerای که فقط label را می‌خواند compliant به‌نظر می‌رسد. losLab PDF Library (PDF Library for Delphi) این فاصله را برای Delphi و C++Builder با ساختن هر دو validator در کتابخانه می‌بندد، تا check در-process اجرا شود بدون اینکه هیچ service خارجیای را راه‌اندازی کنید

دو standard که fileها را به دلایل مخالف fail می‌کنند

ISO 19005 (PDF/A) یک قرارداد reproduction است. یک file conforming باید دهه‌ها بعد روی softwareای که سیستم تولیدکننده را هرگز ندیده است، یکسان render شود، پس ruleها به external dependencyها حمله می‌کنند: هر font embedded، color به یک ICC OutputIntent embedded anchor شده یا در یک device-independent space بیان شده، هیچ encryptionی در PDF/A-1، هیچ JavaScriptای، XMP metadata که با document information dictionary موافق باشد. ISO 14289 (PDF/UA) در عوض یک قرارداد semantics است. assistive technology باید document را traverse کند و با meaning بیرون بیاید، که در یک لایهٔ کاملاً متفاوت زندگی می‌کند: یک structure tree کامل، alternate text روی figureها، یک document title برای display set شده، heading levelهایی که skip نمی‌کنند، table header relationshipهایی که وقتی page از screen رفت دوام می‌آورند

چون دو standard لایه‌های متفاوتی را police می‌کنند، fileهایی که شما را گاز می‌گیرند آن‌هایی هستند که بین آن‌ها نشسته‌اند. یک document archive-perfect می‌تواند برای یک screen reader بی‌صدا باشد. یک document زیبای tagged می‌تواند به یک desktop font reference دهد که در ده سال وجود نخواهد داشت. public-sector publishing جای معمولی است که هر دو نیاز هم‌زمان فرود می‌آیند، و یک pipeline آن‌جا نمی‌تواند آن‌ها را به یک gate واحد فروپاشد. findingها به افراد متفاوتی می‌روند. fontهای unembedded یک defect در codeای هستند که PDF را تولید می‌کند، در حالی که alternate text مفقود به هرکس تعلق دارد که content templateها را مالک است، و گزارشی که آن دو را مخلوط کند فقط دو بار forward می‌شود

اینکه کدام بخش از PDF/A را target می‌گیرید همان‌قدر اهمیت دارد که آیا به آن می‌رسید. PDF/A-1 روی PDF 1.4 frozen است و transparency و JPEG2000 را reject می‌کند، که هر دو را output reporting مدرن بدون فکر کردن درخواست می‌کند. PDF/A-2 (ISO 19005-2، ساخته‌شده روی ISO 32000-1) هر دو را می‌پذیرد و default معقول برای یک archive جدید است. PDF/A-3 جلوتر می‌رود و embedded file از هر نوع را اجازه می‌دهد، که e-invoicing formatهای regulated به آن تکیه می‌کنند. تیمی که هنوز در ۲۰۲۶ روی PDF/A-1b standard می‌کند، معمولاً نیازی را حمل می‌کند که کسی پانزده سال پیش نوشته، و مذاکرهٔ دوباره دربارهٔ target part اغلب ارزان‌تر از strip کردن transparency از هر chartی است که system بیرون می‌دهد

نمودار PDF Library for Delphi: مقایسه قرارداد بازتولید PDF/A با قرارداد معناشناسی PDF/UA به همراه ماتریس قبول-مردود اسنادی که یکی را برآورده و دیگری را رد می‌کنند
PDF/A رندر وفادار را دهه‌ها پیشِ رو تضمین می‌کند، در حالی که PDF/UA خوانش کمکی را امروز تضمین می‌کند، و هیچ‌کدام از دو حکم، دیگری را نتیجه نمی‌دهد

findingهای ساختاریافته هنگام ingestion

entry point در flat-API CheckFileCompliance است، با test selector ۱ برای PDF/A و ۲ برای PDF/UA. یک handle string-list برمی‌گرداند که itemهایش findingهای منفرد هستند، یکی per line، که دقیقاً شکل چیزی است که یک gate خودکار می‌خواهد طی کند:

function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0);  // 1 = PDF/A
  if ListId = 0 then
  begin
    // 0 یعنی "no findings" یا "file unreadable" — پیش از ارسال، این دو را از هم جدا کن
    Result := Pdf.LastErrorCode = 0;
    Exit;
  end;
  for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
    LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
  Pdf.ReleaseStringList(ListId);
  Result := False;
end;

دو detail تصمیم می‌گیرند آیا این بدون نظارت اجرا می‌شود. اولی یک return value است که دو چیز مخالف معنی می‌دهد. CheckFileCompliance وقتی file کاملاً compliant است و همچنین وقتی file اصلاً نمی‌توانست باز شود، ۰ برمی‌گرداند، چون در داخل یک result list خالی در هر دو حالت به ۰ collapse می‌شود. یک gate که ۰ را به‌عنوان pass می‌خواند uploadهای خراب را مستقیم به archive راه می‌دهد، پس قبل از اعتماد به صفر با LastErrorCode آن را disambiguate کنید، همان‌طور که gate بالا می‌کند. دومی به جایی file در lifecycleاش مربوط است. checker روی streaming reader کتابخانه به‌جای document model کامل اجرا می‌شود، file را مستقیماً با read sharing باز می‌کند و هرگز LoadFromFile را صدا نمی‌زند، که چرایی آن است که می‌تواند input چند-gigabyteای را بدون ساختن یک object tree chew کند. همان streaming open وقتی یک process دیگر هنوز file را برای writing نگه داشته شکست می‌خورد، و یک upload در حال انجام دقیقاً همان state است. پس از آنکه transfer تمام شد gate کنید

design streaming دوباره تحت load جواب می‌دهد. هر check input خود را read-only باز می‌کند و آن را برای reading share می‌کند، پس یک corpus audit با یک instance TPDFlib per worker و بدون contention بین آن‌ها روی worker threadها یا processها scale out می‌شود. resourceای که نیاز به discipline دارد خود handle است. هر result غیرصفر از CheckFileCompliance تا وقتی ReleaseStringList را صدا بزنید allocated می‌ماند، و یک gate طولانی‌مدت که release کردن آن‌ها را فراموش می‌کند crash نمی‌کند، فقط آرام memory bleed می‌کند تا اینکه کسی به دنبال چرایی برود

report برای انسان، diff برای build gate

یک finding list شکل درستی برای یک gate و شکل اشتباهی برای یک email به تیم template است. CreatePreflightReport همان analysis را به‌شکل prose قابل‌خواندن render می‌کند، CreatePreflightReportEx یک report-format selector اضافه می‌کند، و SavePreflightReport آن را روی disk می‌نویسد تا report بتواند داخل package document تحویل‌شده سفر کند. بسیاری از قراردادهای archival آن report را به‌عنوان یک deliverable مستقل می‌سازند، نه فقط یک artifact داخلی

عضو این خانواده که بی‌صدا جای خود را earn می‌کند ComparePreflightReports است. compliance یک regression surface مانند هر piece رفتار دیگری است. یک template tweak، یک corporate font تازه licensed، یا یک library upgrade هر کدام می‌توانند findingای معرفی کنند که در release قبل نبود، و هیچ‌کدام خود را اعلام نمی‌کنند. golden reportها را برای مجموعه‌ای از documentهای نماینده تحت version control نگه دارید، بعد از هر تغییر آن‌ها را دوباره تولید کنید، و ComparePreflightReports را برای محاسبهٔ delta اجرا کنید. یک diff خالی یک release artifact ارزش نگه‌داشتن است. یک finding غافلگیرکننده build را fail می‌کند، که جای بسیار ارزان‌تری برای کشف آن از audit است

تولید outputای که در اولین اجرا pass می‌شود

preflight ارزش خود را روی fileهایی از جای دیگری می‌آورد. برای documentهایی که code خودتان تولید می‌کند، پیدا کردن violationها بعد از generation و patch کردن آن‌ها به‌عقب راه دور است. PDF Library for Delphi یک generation-side mode برای هر standard حمل می‌کند، و شما می‌توانید هر دو را برای همان document روشن کنید:

نمودار PDF Library for Delphi از ورودی preflight مقیاس‌پذیر با یک نمونه کتابخانه streaming به ازای هر worker، باز کردن‌های گیت‌دار پس از پایان آپلودها، و فهرست‌های یافته آزادشده
دروازه‌ها فقط پس از کامل‌شدن انتقال‌ها باز می‌شوند، هر worker ورودی را فقط‌خواندنی از طریق یک نمونه خصوصی کتابخانه استریم می‌کند، و هر هندل برگشتی یک release بدهکار است
var
  Pdf: TPDFlib;
  Diag: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);
    Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
    Pdf.SetPDFUAMode('en-US');
    Pdf.SetInformation(1, 'Quarterly Statement');  // /Title: برای PDF/UA الزامی است
    // ... محتوای tag‌شده را اینجا رسم کن ...
    Diag := Pdf.GetPDFUADiagnostics;
    if Diag <> '' then
      Writeln('fix before shipping: ', Diag);
    Pdf.SaveToFile('statement.pdf');
    // preflightای که شمارش می‌کند روی فایل ذخیره‌شده اجرا می‌شود:
    Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
  finally
    Pdf.Free;
  end;
end;

تله در save time پنهان است. چندی از repairهای conformance هنگام serialize شدن document اتفاق می‌افتند نه وقتی mode را enable می‌کنید: forcing کردن print flag روی annotationها، نوشتن AFRelationship پیش‌فرض برای embedded fileهای PDF/A-3، normalize کردن tab order و form-field description برای PDF/UA. document نشسته در memory با آنچه روی disk land می‌شود byte-identical نیست، پس تنها preflight verdictای که معنی دارد آنی است که از file ذخیره‌شده محاسبه می‌شود. خود statement.pdf را validate کنید. compliance را از object هنوز در memory استنتاج نکنید، چون byteهایی که قضاوت می‌کردید byteهایی نیستند که ship کرده‌اید

نمودار PDF Library for Delphi از اصلاحات انطباق در زمان ذخیره که در حین serialization اعمال می‌شوند؛ به همین دلیل preflight به فایل PDF ذخیره‌شده تعلق دارد نه مدل درون‌حافظه‌ای
سریال‌سازی پرچم‌های چاپ حاشیه‌نویسی را تحمیل و AFRelationship را پیش‌فرض و ترتیب tab را نرمال می‌کند، پس بازرسی حافظه بایت‌هایی را قضاوت می‌کند که هیچ‌کس هرگز ارسال نمی‌کند

scenarioهای invoice که یک XML machine-readable در کنار document تصویری حمل می‌کنند از pattern ZUGFeRD و Factur-X پیروی می‌کنند، که روی PDF/A-3 ساخته شده. آن‌ها باید relationship attachment را صریحاً با SetPDFA3DefaultAFRelationship set کنند، چون ISO 19005-3 از هر embedded file می‌خواهد نقش خود را نسبت به document اعلام کند. آن را unset بگذارید و XML embedded فقط یک blob بدون purpose اعلام‌شده است، که validator آن را متوجه می‌شود

داوران مستقل: veraPDF و Acrobat

یک producer نباید تنها قاضی output خودش باشد. checkerهای PDF Library for Delphi verdictهای سریع و ساختاریافته در-process به شما می‌دهند، که همان چیزی است که در hot path می‌خواهید، اما release gate برای یک batch archival باید همچنان output را از یک validator که هیچ‌کس در تیمتان ننوشته عبور دهد. veraPDF reference implementation community-maintained برای PDF/A است و ابزاری است که بیشتر archiveها در acceptance criteria خود name می‌کنند، پس آنی است که باید match شوید. preflight profileهای Acrobat یک tiebreaker مفید می‌سازند وقتی veraPDF و check در-process مخالفت می‌کنند. نام validator و version آن را کنار هر report ذخیره‌شده ضبط کنید. یک claim که یک file از veraPDF عبور کرده بدون build numberای که از آن عبور کرده بسیار کمی می‌گوید، چون ابزار ruleهایش را بین releaseها tighten می‌کند

validatorها در لبه‌های standard با هم مخالفت می‌کنند، و وقتی می‌کنند پاسخ این نیست که ابزاری که دوست دارید انتخاب کنید. file را به یک sample مینیمال که هنوز disagreement را trigger می‌کند shrink کنید و آن را در برابر standard text بخوانید. یک ساعت از آن معمولاً یکی از دو چیز را ظاهر می‌کند: یک tool bug واقعی ارزش filing upstream، یا یک clause که تیمتان آن را اشتباه خوانده و باید در compliance noteها بنویسد تا نفر بعدی آن را دوباره بحث نکند

input encrypted یک shortcut می‌گیرد. هر دو checker یک argument password می‌گیرند، اما یک file PDF/A-1 با یک encryption dictionary از قبل non-conforming است، چون ISO 19005-1 encryption را outright forbid می‌کند، پس یک submission encrypted را می‌توان قبل از اینکه analysis عمیق‌تری اجرا شود رد کرد. فهمیدن اینکه یک encryption dictionary واقعاً چه می‌دهد یک task خودش است، که در audit کردن encryption و permissionهای PDF پوشش داده شده

findingهای PDF/UA تقریباً همیشه به چگونگی author شدن structure tree در درجهٔ اول trace می‌شوند، و techniqueهای tagging پشت آن در ساختن structure treeهای tagged PDF در Delphi زندگی می‌کنند. archiveهایی که همچنین digital signature می‌خواهند باید این gate را با workflow signing و validation در PAdES جفت کنند. مرجع کامل preflight API در page محصول losLab PDF Library برای Delphi زندگی می‌کند