مقاله فنی

چاپ داده‌های متغیر PDF/VT در Delphi با PDFium VCL

یک چاپخانهٔ تراکنشی در جواب اجرای صورت‌حساب 80,000 صفحه‌ای شما یک ردّیهٔ یک‌خطی می‌فرستد: «not PDF/VT, RIP cannot cache». فایل در هر viewer روی میز به‌خوبی باز می‌شود، رنگ‌ها درست‌اند، داده‌ها هم درست merge شده‌اند. هیچ‌کدام از این‌ها چیزی نیست که پرس دیجیتال خواسته است. چاپ دادهٔ متغیر پرسرعت فقط وقتی ممکن می‌شود که press بتواند تشخیص بدهد بلوک لوگوی مشتری در صفحهٔ 1 همان objectی است که در صفحهٔ 40,000 دیده می‌شود، آن را یک بار render کند و دوباره استفاده کند. PDF/VT استانداردی است که این وعده را machine-checkable می‌کند، و «درست به نظر می‌رسد» دقیقاً همان تله است، چون ساختاری که RIP می‌خواند روی صفحه دیده نمی‌شود

PDFiumPas این ساختار را از طریق یک surface کوچک روی TPdf: SaveAsPdfVT می‌نویسد، ValidatePdfVT بررسی می‌کند. این مقاله دربارهٔ این است که این دو روش در واقع چه چیزهایی را روی دیسک می‌گذارند و بررسی می‌کنند، ISO 16612-2 کجا از چیزی که در نگاه اول به نظر می‌رسد سخت‌گیرانه‌تر است، و کدام بخش‌ها لنگرهای ساختاریِ صادقانه‌اند نه یک preflight کامل که بتوانید بابتش از مشتری پول بگیرید

آنچه PDF/VT استاندارد می‌کند، و چرا PDF/X اول می‌آید

PDF/VT (ISO 16612-2:2010) یک فرمت فایل تازه نیست. یک لایهٔ metadata بهینه‌سازی است که روی یک فایل PDF/X سوار می‌شود، و همین ترتیب بار اصلی کار را به دوش می‌کشد. استاندارد سه سطح conformance تعریف می‌کند، اما فقط دو مورد از آن‌ها نام یک فایل PDF را می‌آورند: PDF/VT-1، یک document خودبسندهٔ واحد، و PDF/VT-2، یک modelِ file-set که در آن صفحه‌ها به resourceهای خارجی مشترک ارجاع می‌دهند. نشانهٔ سوم که ممکن است ببینید، PDF/VT-2s، اصلاً یک مقدار در سطح فایل نیست؛ در یک MIME stream header که در Annex A توضیح داده شده زندگی می‌کند. اگر در کد دیدید که GTS_PDFVTVersion = "PDF/VT-2s" را در XMP یک document stamp می‌کند، آن کد غلط است

قانون غیرقابل‌مذاکره برای یک فایل واحد، پایهٔ PDF/X است. ISO 16612-2 §6.2.1 الزام می‌کند که هر فایل PDF/VT-1 همچنین یک فایل PDF/X-4 معتبر باشد. file setِ PDF/VT-2 طبق §6.2.2 باید به‌جای آن روی PDF/X-4p، PDF/X-5g یا PDF/X-5pg بنشیند. به همین دلیل writerِ PDF/VT نمی‌تواند فقط چند کلید شناسه اضافه کند: باید کل مجموعهٔ markerهای PDF/X-4 را همراه خود حمل کند، یعنی یک OutputIntent، یک embedded ICC destination profile، ورودی‌های XMP و document Info متناظر، یک trailer /ID، و بدون encryption. هرکدام را جا بیندازید و فایلی خواهید داشت که ادعای PDF/VT می‌کند و به محض آن‌که یک consumer منطبق پایه را بررسی کند، رد می‌شود. PDFiumPas لایهٔ PDF/X-4 را بخشی از saveِ PDF/VT در نظر می‌گیرد، بنابراین یک SaveAsPdfX اول لازم نیست؛ injector هر دو لایه را در یک pass می‌نویسد

نوشتن یک فایل با SaveAsPdfVT

فراخوان حداقلی چیزی جز یک document فعال لازم ندارد، چون TPdfVTSaveOptions.Default یک sRGB ICC profile داخلی و conformance pvc1 را فراهم می‌کند. save سه مرحله را درون خود اجرا می‌کند: هر security را برمی‌دارد (inject کردن markerهای plaintext در یک encrypted object stream آن را خراب می‌کند)، document info dictionary و trailerِ موجود را /ID به marker set وصل می‌کند تا مقادیر XMP و Info با هم بخوانند، سپس objectهای PDF/X-4 و PDF/VT را از طریق یک incremental update اضافه می‌کند

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    if Pdf.LoadFromFile('statements-merged.pdf') then
    begin
      // Default options: built-in sRGB OutputIntent, PDF/VT-1, synthesised DPart
      if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
        Writeln('PDF/VT-1 written')
      else
        Writeln('Save failed (document not active?)');
    end;
  finally
    Pdf.Free;
  end;
end;

برای خروجی واقعیِ production تقریباً همیشه می‌خواهید OutputIntent را با characterization پرس خودتان جایگزین کنید، نه fallback عمومی sRGB. bytesهای ICC و شناسه‌های condition را از طریق TPdfVTSaveOptions:

var
  Pdf: TPdf;
  Opt: TPdfVTSaveOptions;
  Icc: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('directmail-merged.pdf');
    Icc := LoadIccProfile('GRACoL2013_CRPC6.icc');  // your own loader

    Opt := TPdfVTSaveOptions.Default;
    Opt.Conformance := pvc1;            // pvc2 is normalised to pvc1 on write
    Opt.IccProfileData := Icc;
    Opt.OutputConditionIdentifier := 'CGATS21_CRPC6';
    Opt.OutputCondition := 'Commercial print, coated, CRPC6';
    Opt.RegistryName := 'http://www.color.org';
    Opt.Title := 'Spring 2026 Direct Mail Run';
    Opt.Trapped := ptvFalse;           // PDF/X Info /Trapped state

    Pdf.SaveAsPdfVT('directmail-pdfvt.pdf', Opt);
  finally
    Pdf.Free;
  end;
end;

یک جزئیات در آن snippet بیش از آن‌که محدودیت باشد، یک guardrail عمدی است. تنظیم Opt.Conformance := pvc2 یک فایل PDF/VT-2 تولید نمی‌کند. writer هر درخواست غیرـpvc1 را به pvc1 برمی‌گرداند، چون PDF/VT-2 یک format file-set است و یک writer تک‌فایلی که فقط یک output document اضافه می‌کند، از نظر فیزیکی نمی‌تواند مجموعهٔ resource خارجیِ موردنیاز §6.2.2 را بسازد. مقدار pvc2 برای read path وجود دارد، تا ValidatePdfVT بتواند یک document file-set موجود را شناسایی و گزارش کند؛ این یک write target نیست

درخت DPart: ساختاری که RIP واقعاً می‌خواند

هستهٔ PDF/VT سلسله‌مراتب Document Part (DPart) است. همین است که به یک press اجازه می‌دهد یک run طولانی را به recordها بخش‌بندی کند، recordها را در recipientها یا mail bundleها گروه‌بندی کند، و Document Part Metadata را ضمیمه کند تا تجهیزات پایین‌دستی بتوانند هر قطعه را مسیردهی و bill کنند. ISO 16612-2 §6.5 wiring را این‌گونه مشخص می‌کند: catalog یک /DPartRoot را حمل می‌کند، root DPart node یک /DPartRootNode و یک /NodeNameList را که هر level سلسله‌مراتب را نام‌گذاری می‌کند حمل می‌کند، leaf DPartها بازه‌های page tree را پوشش می‌دهند، و هر صفحه‌ای که به یک part تعلق دارد از طریق یک /DPart سطح صفحه به leaf خودش اشاره می‌کند

وقتی سند مبدأ شما از قبل یک hierarchy قابل‌استفاده دارد، SaveAsPdfVT آن را حفظ می‌کند. وقتی چنین نیست، writer یک نمونهٔ حداقلی می‌سازد: یک DPart در سطح سند که page tree فعلی را به‌ترتیب پوشش می‌دهد، با یک /DPart back-reference که به هر page object زنده ضمیمه می‌شود و یک /NodeNameList [/Document]این درخت حداقلی را باید صادقانه همان چیزی دانست که هست: یک structural anchor که شکل موردنیاز §6.5 را برآورده می‌کند، نه business metadata. این tree نمی‌تواند recipientها، مرزهای mail-piece یا product batchها را اختراع کند، چون چنین اطلاعاتی هرگز در source وجود نداشته است. اگر دادهٔ per-recipient دارید، باید خودتان یک DPart tree عمیق‌تر بسازید و /NodeNameList را طوری گسترش دهید که با levelهایی که ایجاد کرده‌اید هماهنگ شود

اعتبارسنجی فراتر از وجودِ کلیدها

ValidatePdfVT یک TPdfVTValidationResult record با سه چیز برمی‌گرداند: Conformance conformance شناسایی‌شده، یک مجموعهٔ Issues issue، و یک IsCompliant helper که فقط وقتی conformance یک level واقعی است و issue set خالی است true می‌شود. enumerationِ issue عمداً دقیق است، بنابراین نتیجهٔ ناموفق به شما می‌گوید کدام clause را جا انداخته‌اید، نه فقط «invalid»:

var
  Pdf: TPdf;
  Res: TPdfVTValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.LoadFromFile('statements-pdfvt.pdf');
    Res := Pdf.ValidatePdfVT;

    if Res.IsCompliant then
      Writeln('PDF/VT compliant: ', VTLevelName(Res.Conformance))
    else
    begin
      if pvviMissingDPartRoot in Res.Issues then
        Writeln('DPart hierarchy missing or unusable');
      if pvviMissingPdfXIdentifier in Res.Issues then
        Writeln('PDF/X-4 base identifier absent');
      if pvviMissingOutputIntent in Res.Issues then
        Writeln('OutputIntent / ICC profile missing');
      if pvviEncryptionPresent in Res.Issues then
        Writeln('Encrypted - PDF/X forbids this');
    end;
  finally
    Pdf.Free;
  end;
end;

دو بررسی‌ای که ارزش دارد عمیق فهمیده شوند، جفت‌سازی conformance و walkِ DPart هستند، چون هر دو قبلاً بیش از حد سهل‌گیر بودند و برای هماهنگی با spec سخت‌تر شدند. در سمت pairing، validator تطابق دقیق انجام می‌دهد، نه «هر PDF/Xای قبول است»: یک فایل PDF/VT-1 فقط روی یک PDF/X-4 base پذیرفته می‌شود، و یک فایل PDF/VT-2 فقط روی PDF/X-4p، PDF/X-5g، یا PDF/X-5pg پذیرفته می‌شود. یک markerِ PDF/VT-1 که روی پایهٔ PDF/X-1a نشسته باشد گزارش می‌شود، نه این‌که عبور داده شود

walkِ DPart جایی است که بیشتر rigor در آن قرار دارد. داشتن یک /DPartRoot key برای catalog کافی نیست، چون یک object خالیِ جعلی یا یکی بدون page link هنوز هم قابل مصرف نیست. HasValidDPartHierarchy و recursive ValidateDPartNode کل structure را دنبال می‌کنند: parent linkها را تعقیب می‌کنند، childهای تکراری و cycleها را رد می‌کنند، enforce می‌کنند که /Start و /DParts متقابلاً exclusive باشند، و از leaf page rangeها می‌خواهند که page tree را به‌ترتیب depth-first پوشش دهند و هر page-level /DPart به leafی اشاره کند که آن را در بر می‌گیرد. همهٔ این faultهای داخلی به همان یک pvviMissingDPartRoot issue bit فرو می‌ریزند، نه این‌که public enum را بزرگ‌تر کنند، پس آن یک flag را به‌معنای «سلسله‌مراتب DPart قابل‌استفاده نیست» در نظر بگیرید، نه «کلید ریشه وجود ندارد»

سه تلهٔ نحوی که validator اکنون enforce می‌کند

تکرارهای پی‌درپی روی Table 4 از §6.5 شکل‌هایی را آشکار کردند که نسخه‌های قبلی قبول می‌کردند اما استاندارد نه. این‌ها از همان چیزهایی هستند که یک DPart tree دست‌ساز به‌سادگی اشتباه می‌کند، پس ارزش دارد صریحاً به آن‌ها اشاره شود:

  • /DParts یک array از arrayهاست، نه یک flat array. هر element از outer array باید خودش یک indirect-reference array باشد. یک /DParts [9 0 R] صاف رد می‌شود؛ شکل منطبق /DParts [[9 0 R] [10 0 R]] است. این کار اجازه نمی‌دهد یک structure غیرhierarchical خودش را به‌جای یک level معتبر جا بزند
  • /End فقط یک بازهٔ چندصفحه‌ای واقعی را علامت می‌زند. یک leaf DPart فقط وقتی می‌تواند /End داشته باشد که هم‌زمان /Start هم داشته باشد، و /End باید در orderِ page-tree بعدتر از /Start بیفتد. یک /Start 3 0 R /End 3 0 R degenerate اکنون hierarchy را unusable می‌کند، نه این‌که به‌عنوان یک part یک‌صفحه‌ای خوانده شود
  • /NodeNameListنام‌ها باید پس از PDF name unescaping به‌صورت XML NMTOKENها دوام بیاورند. نامی مثل /Bad#20Name به چیزی با یک فاصله expand می‌شود، که token معتبری نیست. پیاده‌سازی یک بررسی سبک ASCII انجام می‌دهد (حروف، رقم‌ها، ., -, _, :, plus non-ASCII bytes) که خطاهای whitespace و delimiter را می‌گیرد بدون این‌که نام‌های محلی‌سازی‌شده یا vendor-specific معتبر را رد کند

نشانه‌های XMP: دو راه برای نوشتن یک property یکسان

شناسایی PDF/VT در XMP زیر namespaceِ pdfvtid, به‌طور مشخص GTS_PDFVTVersion و GTS_PDFVTModDate، در کنار xmp:CreateDateهای استاندارد xmp:ModifyDate و <pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion> به‌شکل element text (GTS_PDFVTModDate) یا به‌صورت یک RDF attribute روی description element سریال‌سازی می‌شوند. PDFiumPas هر دو form را می‌خواند، پس فایلی که ابزار دیگری در styleِ attribute نوشته است جریمه نمی‌شود. این ابزار همچنین قاعدهٔ سازگاری §6.3 را enforce می‌کند که xmp:ModifyDate باید با pvviModDateMismatch برابر باشد؛ هر عدم‌تطابقی

یک rule دیگر از همان clause: یک GTS_PDFVTVersion ناشناخته به‌جای این‌که به pvcUnknown fold شود، به‌صورت pvcNone حفظ می‌شود. این تمایز از نظر عملیاتی مهم است. pvcNone یعنی «هیچ markerای برای PDF/VT وجود ندارد، یک PDF معمولی»، در حالی که pvcUnknown یعنی «چیزی یک version را stamp کرده که این validator آن را نمی‌شناسد» (از جمله caseِ PDF/VT-2s). یکی گرفتن این دو، یک فایل معیوب را در همان bucket یک document معمولی پنهان می‌کند

مرزِ پایانِ این تضمین کجاست

مهم است دقیق باشیم که این روش‌ها چه چیزی را وعده می‌دهند و چه چیزی را نه، چون انطباق چاپ دادهٔ متغیر پول واقعی در میان است. بررسی‌های DPart و pairing، اعتبارسنجی ساختاری در سطح byte هستند. آن‌ها تأیید می‌کنند که اسکلت بهینه‌سازی، markerهای پایهٔ PDF/X-4، OutputIntent و XMP وجود دارند و درونی با هم سازگارند. آن‌ها یک preflight محتوایی PDF/X-4 نیستند: بررسی نمی‌کنند که هر رنگ داخل شرط خروجی اعلام‌شده باشد، همهٔ فونت‌ها embed شده باشند، یا هیچ edge case ممنوعی از شفافیت و blending سر نخورده باشد. برای jobی که روی یک contract press می‌گذارید، اعتبارسنجی ساختاری PDFiumPas را با یک موتور preflight مخصوص PDF/X و یک test print جفت کنید، همان‌طور که هر ادعای انطباق دیگری را هم sanity-check می‌کنید. لایهٔ ساختاری failureهایی را می‌گیرد که cache شدن RIP را بی‌سروصدا می‌شکنند؛ این نیمی از یک check کامل است، نه همهٔ آن

اگر این بررسی‌ها را در یک release gate بزرگ‌تر می‌گذارید، همان approachِ اسکن در سطح byte زیربنای کار standards دیگر library هم هست، از جمله اعتبارسنجی object streamها و cross-reference streamها پیش از آن‌که یک فایل اصلاً به preflight برسد، و disciplineِ shared-object پشت تمپ‌های صفحهٔ قابل‌استفاده‌مجدد با Form XObjectها که یک document را از همان ابتدا RIP-friendly می‌کند. APIهای save و validationِ PDF/VT و PDF/X که اینجا توصیف شدند بخشی از PDFium VCL component برای Delphi و C++Builder هستند، که صفحهٔ محصولش مرجع کامل compliance را در خود دارد