مقاله فنی

انطباق آرشیوی PDF/A در Delphi با PDFium VCL

شما یک converter می‌فرستید که هر فایل را PDF/A-1b علامت می‌زند، سیستم records مشتری آن‌ها را برای یک سال ingest می‌کند، و بعد یک audit کل batch را از veraPDF عبور می‌دهد و یک‌سوم آن‌ها non-conformant برمی‌گردند. هیچ چیز crash نکرده، هیچ exceptionی پرتاب نشده، فایل‌ها در هر viewer روی میز شما خوب باز می‌شوند. فقط استانداردی نبودند که رویشان مهر زده بودید. این failure mode عادیِ PDF آرشیوی است، و به همین دلیل «ما flag را set کردیم» هیچ‌وقت همان ادعای «validates می‌کند» نیست

اولین چیزی که دربارهٔ PDFium و PDF/A باید بفهمید این است که engine هیچ ربطی به آن ندارد. PDFium PDF را render، parse، و write می‌کند، اما surface عمومی آن نه ConvertToPDFA، نه OutputIntent writer، نه XMP API دارد. هر بخش از conformance آرشیوی، packet XMP، OutputIntent و ICC profile آن، markers کاتالوگ، validation، همه در خود PDFiumPas زندگی می‌کنند، در یک unit pure-Pascal حدوداً 2,000 خطی (FPdfPdfa.pas) که bytes ذخیره‌شده را parse می‌کند و از طریق incremental update بازنویسی می‌کند. دانستن اینکه کار کجا انجام می‌شود به شما می‌گوید bugها کجا پنهان می‌شوند، و آن‌ها در PDFium پنهان نمی‌شوند

PDF/A واقعاً چه می‌خواهد، و کجا گیر می‌دهد

PDF/A یک format واحد نیست. ISO 19005 سه part را تعریف می‌کند (PDF/A-1، -2، -3) و درون هر کدام، سطح‌های conformanceی را که چیزهای متفاوتی وعده می‌دهند. Level B (basic) فقط تضمین می‌کند ظاهر بصری قابل بازتولید است. Level A (accessible) روی B یک tagged structure tree و Unicode mapping اضافه می‌کند. Level U، که فقط برای partهای 2 و 3 وجود دارد، بین آن‌ها می‌نشیند: متن Unicode قابل اتکا بدون structure tree کامل. ISO 19005-1 اصلاً Level U ندارد، و library این محدودیت را مستقیم encode می‌کند

چند rule از این format همان چیزهایی هستند که در عمل گیر می‌دهند. encryption به‌طور کامل ممنوع است (ISO 19005-1 §6.1.3 و successorهای آن): یک فایل PDF/A نمی‌تواند یک /Encrypt dictionary حمل کند. سند باید یک condition رندر خروجی را از طریق یک OutputIntent که destination آن یک ICC profile معتبر است اعلام کند (§6.2.3.2). claim conformance خودش باید به‌صورت XMP metadata زیر schema شناسایی PDF/A ظاهر شود. Level A علاوه بر این §6.8 structure منطقی، یعنی tag treeای که سند را machine-readable می‌کند، می‌خواهد. هر کدام از این‌ها را از دست بدهید و یک conformance verifier فایل را رد می‌کند حتی اگر بی‌نقص render شود

یک call که یک archive تولید می‌کند

PDFiumPas کل pipeline پشت TPdf.SaveAsPdfA را expose می‌کند. overload ساده یک conformance هدف می‌گیرد و به PDF/A-1b پیش‌فرض می‌شود، که default درست برای حالت رایجِ «این را برای همیشه قابل رندر کن» است

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    // Default conformance is pac1b (PDF/A-1b)
    if Pdf.SaveAsPdfA('invoice_archive.pdf') then
      // file now carries XMP, sRGB OutputIntent, and catalog markers
    else
      raise Exception.Create('PDF/A save failed');
  finally
    Pdf.Free;
  end;
end;

در پشت صحنه این یک حرکت دو مرحله‌ای است. SaveAsPdfA اول از PDFium می‌خواهد سند را با FPDF_SaveAsCopy serialize کند، سپس آن byte stream را به InjectPdfAMarkers می‌دهد، که metadata XMP، OutputIntent sRGB با ICC profile embedded آن، و یک catalog بازنویسی‌شده را به‌صورت incremental update اضافه می‌کند. source از position صفر خوانده می‌شود و destination از position صفر نوشته می‌شود؛ tree شیء اصلی دست‌نخورده می‌ماند و markers بعد از %%EOF موجود وارد می‌شوند. اگر به bytes به‌جای فایل نیاز دارید، SaveAsPdfAToStream یک TStream و همان optionها را می‌گیرد

انتخاب conformance با options record

برای هدف گرفتن یک part و level مشخص، یک TPdfASaveOptions record بدهید. فیلد Conformance آن یک value از TPdfAConformance می‌گیرد. enum هر ترکیب معتبر و هیچ چیز دیگری را پوشش می‌دهد: pac1b, pac1a برای part 1؛ pac2b, pac2u, pac2a برای part 2؛ pac3b, pac3u, pac3a برای part 3، به‌علاوهٔ pacUnknown و pacNone برای سمت validation. هیچ pac1uی وجود ندارد، چون آن level در standard وجود ندارد

var
  Pdf: TPdf;
  Opts: TPdfASaveOptions;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('report.pdf');
    Opts := TPdfASaveOptions.Default;
    Opts.Conformance := pac2u;           // PDF/A-2u: reliable Unicode text
    Opts.Title := 'Quarterly Report 2026';
    Opts.Author := 'Finance';
    // Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
    if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
      raise Exception.Create('PDF/A-2u save failed');
  finally
    Pdf.Free;
  end;
end;

بیشتر record می‌تواند خالی بماند. TitleTitleAuthorAuthorSubjectSubjectKeywordsKeywordsCreatorCreatorProducer خالی بگذارید و SaveAsPdfA آن‌ها را از Info dictionary سند از طریق FPDF_GetMetaText پر می‌کند. CreationDate و ModDate خالی بگذارید و از زمان UTC فعلی برای هر دو تاریخ XMP استفاده می‌کند. DocumentId و InstanceId خالی بگذارید و library آن‌ها را از FPDF_GetFileIdentifier از پیش پر می‌کند، و اگر لازم شد به یک ID deterministic مشتق‌شده از bytes source برمی‌گردد. تنها فیلدی که شاید بخواهید عمداً override کنید IccProfileData است: خالی یعنی profile sRGB IEC61966-2.1 bundled، اما یک workflow CMYK یا grayscale باید profile خودش را بدهد

چرا Level A تنزل می‌کند، و چرا این انتخاب صادقانه است

این یک subtlety است که آدم‌هایی را که انتظار دارند یک flag ضمانت باشد گیر می‌اندازد. می‌توانید pac1a را روی سندی که هیچ tag tree ندارد درخواست کنید، اما PDF/A-1a §6.8 structure منطقی می‌خواهد، و library نمی‌تواند از یک PDF بدون tag tree یک structure tree بسازد. به‌جای اینکه فایلی بیرون بدهد که Level A را ادعا می‌کند اما آن را fail می‌کند، SaveAsPdfA یک tagged structure واقعی را check می‌کند (/StructTreeRoot به‌علاوهٔ /MarkInfo با /Marked true) و اگر غایب باشد claim را downgrade می‌کند: pac1a به pac1b تبدیل می‌شود، pac2a به pac2b، و همین‌طور در هر سه part. helperهای داخلی PdfAIsLevelA و PdfADowngradeToLevelB هستند

منطقش را باید صریح گفت: فایلی که صادقانه سطحی را که واقعاً meets می‌کند اعلام می‌کند، از فایلی که دربارهٔ سطحی که ندارد دروغ می‌گوید مفیدتر است. Level U به شکل دیگری handling می‌شود. تشخیص coverage واقعی Unicode یعنی یک test سادهٔ «آیا /ToUnicode دارد» می‌تواند documentهای legitimate را بیش‌ازحد degrade کند (WinAnsi و encodingهای مشابه معاف‌اند)، پس سمت save claim U را همان‌طور که caller اعلام کرده emit می‌کند و اختلاف را می‌گذارد روی سمت validation تا flag شود. اگر یک archive Level A تضمین‌شده می‌خواهید، سند را قبل از تبدیل tag کنید؛ converter structureای را که وجود ندارد اختراع نمی‌کند

دام ICC که فقط یک validator واقعی می‌گیرد

این failure همان درسی را داد که سخت‌تر از بقیه بود، چون checker خود library آن را pass کرد اما veraPDF، validator مرجع ISO 19005، نه. PDF/A می‌خواهد destination profile در OutputIntent یک stream معتبر ICCBased باشد، و §6.2.3.2 به verifier می‌گوید آن stream را به‌عنوان یک colour space validate کند. یک stream ICCBased باید /N را اعلام کند، یعنی تعداد مؤلفه‌های رنگ را. یک نسخهٔ اولیهٔ injector دیکشنری stream ICC را فقط با /Length و بدون /N می‌نوشت، و veraPDF نتیجه را با «The N entry (value null)... is missing» رد می‌کرد

آنچه آن را insidious می‌کرد این بود که رد شدن فقط برای PDF/A-1b و -1a فعال می‌شد. مدل‌های conformance part 2 و part 3 آن check خاص را روی destination profile اجرا نمی‌کردند، پس ساختار injected یکسان زیر pac2b، pac3b، و pac2u validate می‌شد اما زیر pac1b فقط به خاطر مقدار pdfaid:part fail می‌کرد. یک unit test هرگز نمی‌توانست آن را ببیند، چون checker خود library فقط چک می‌کرد که ValidatePdfACompliance key وجود دارد، نه اینکه داخل stream dictionary چه چیزی زندگی می‌کند. testهای داخلی سبز می‌ماندند، validation آرشیوی واقعی fail می‌کرد./DestOutputProfilefix این است

، که data colour-space signature را در offset 16 از ICC header می‌خواند و آن را به تعداد component map می‌کند: IccComponentCount یک component است، GRAY, RGB , و Lab سه تا هستند، XYZ چهار است، و یک profile ناشناخته به 3 پیش‌فرض می‌شود. آن count به‌عنوان CMYK وارد stream dictionary می‌شود. این مقدار محاسبه می‌شود، نه اینکه به‌صورت hard-coded روی 3 باشد، تا callerی که profile CMYK یا grayscale را از طریق /N می‌فرستد همچنان مقدار درست را بگیرد. درس کلی‌تر روش‌شناختی است: checker داخل library و یک validator مرجع هر دو blind spot دارند، و خروجی PDF/A باید end to end در برابر یک پیاده‌سازی مرجع مثل veraPDF تست شود نه اینکه به self-checks اعتماد کند. همان discipline incremental-update پشت آرشیوهای تمیز در IccProfileDataتأیید streamهای compressed object و xref پوشش داده شده است، که اهمیت دارد چون PDFهای مدرنی که injector مصرف می‌کند اغلب بر پایهٔ cross-reference streamها ساخته می‌شوند.Encryption، xref streamها، و لبه‌های دیگر

چون ISO 19005 encryption را ممنوع می‌کند، مسیر save آن را قبل از نوشتن strip می‌کند

هنگام serializing اعمال می‌کند، پس یک source رمزگذاری‌شده (که با password خودش load شده) در راه ورود به archive decrypt می‌شود. روی یک document بدون رمز این یک no-op است و هیچ چیز را عوض نمی‌کند. نتیجهٔ ضمنی همان محدودیتی است که HotXLS از سمت دیگر enforce می‌کند: یک فایل واحد نمی‌تواند هم encrypted باشد و هم PDF/A. وقتی یک workflow به هر دو نیاز دارد، پاسخ دو artifact است، یک copy رمزگذاری‌شده برای distribution و یک copy تمیز جداگانه برای archive.SaveAsPdfAیک لبهٔ دیگر تا وقتی نیش نزند نامرئی است: documentهای PDF 1.5+ که از pure cross-reference stream استفاده می‌کنند و هیچ FPDF_REMOVE_SECURITY keywordی ندارند. injector trailer را می‌خواند تا

source را پیدا کند و incremental update خودش را اضافه کند، و باید form xref-stream را بپذیرد، وگرنه چنین documentی با markers به‌طور بی‌صدا حذف می‌شود. ISO 32000-1 §7.5.6 صراحتاً اجازه می‌دهد یک incremental update کلاسیکِ trailer بعد از یک document xref-stream بیاید، با trailer pointing at the xref-stream offset، که دقیقاً همان ساختاری است که injector emit می‌کند. خود PDFium /Info همیشه یک trailer کلاسیک می‌نویسد، پس در pipeline معمول injector هیچ‌وقت با یک source خالص xref-stream روبه‌رو نمی‌شود، اما read path آن را برای documentهایی که از جای دیگری می‌آیند handling می‌کند./Prevقبل از اینکه به claim اعتماد کنید، verify کنیدFPDF_SaveAsCopylibrary یک checker سطح byte را ship می‌کند،

، که یک

برمی‌گرداند. فیلد TPdf.ValidatePdfA سطح detected را گزارش می‌کند و TPdfAValidationResult مجموعه‌ای از Conformance values است؛ method راحتیِ Issues فقط وقتی true است که یک level واقعی detected شده باشد و set issueها خالی باشند. آن را به‌عنوان gate سریع اول در batch اجرا کنید.TPdfAValidationIssueصادق باشید دربارهٔ اینکه این چه چیزی می‌خرد. checker سطح byte مشکلات ساختاری را با confidence بالا می‌گیرد (یک OutputIntent گم‌شده، یک action ممنوع، یک IsCompliant, transparency جایی که part 1 آن را منع می‌کند)، و تشخیص font-embedding از یک heuristic شمارش استفاده می‌کند که عمداً فقط یک signal با confidence بالا گزارش می‌کند و نه تعقیب coverage تک‌glyph. کاری که انجام نمی‌دهد تحلیل operatorهای content-stream است، که به یک content parser کامل نیاز دارد و از ابتدا خارج از scope بوده است. برای یک release gate، checker داخل library را با veraPDF جفت کنید: checker فوری است و بدون DLL همه‌جا اجرا می‌شود، veraPDF مرجع است. وصل کردن این جفت به یک batch run موضوع

var
  Pdf: TPdf;
  Res: TPdfAValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('invoice_archive.pdf');
    Res := Pdf.ValidatePdfA;
    if Res.IsCompliant then
      Writeln('Conformant: detected level ', Ord(Res.Conformance))
    else
      Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
  finally
    Pdf.Free;
  end;
end;

PDF/A batch preflight report CLI/Encrypt است، و همین‌جا این validation باید در یک workflow آرشیوی واقعی قرار بگیرد.batch preflight report CLI، که همین‌جا این validation باید در یک workflow آرشیوی واقعی قرار بگیرد

The SaveAsPdfA, InjectPdfAMarkers و ValidatePdfA APIs shown here ship with PDFium Component for Delphi, C++Builder, and Lazarus/FPC. The product page links the full API reference, including the complete conformance enumeration and the options record behind these examples