Articol tehnic

Tipărirea datelor variabile PDF/VT în Delphi cu PDFium VCL

O tipografie tranzacțională îți trimite înapoi un tiraj de 80.000 de pagini cu un refuz de o singură linie: "nu este PDF/VT, RIP-ul nu poate face cache". Fișierul se deschide fără probleme în orice vizualizator de pe biroul tău, culorile sunt corecte, datele au fost îmbinate corect. Niciunul dintre aceste lucruri nu este ceea ce a cerut presa digitală. Tipărirea de date variabile la viteză mare depinde de faptul că presa poate recunoaște că blocul cu sigla clientului de pe pagina 1 este, octet cu octet, același obiect ca cel de pe pagina 40.000, îl redă o singură dată și îl reutilizează. PDF/VT este standardul care face această promisiune verificabilă automat, iar "arată corect" este exact capcana, pentru că structura pe care o citește RIP-ul este invizibilă pe ecran

PDFiumPas expune această structură printr-o suprafață mică pe TPdf: SaveAsPdfVT scrie, ValidatePdfVT verifică. Acest articol este despre ce pun de fapt pe disc și inspectează aceste două metode, unde ISO 16612-2 este mai strict decât pare la prima vedere și ce părți sunt ancore structurale oneste, nu o verificare preflight completă pe care i-o poți factura unui client

Ce standardizează PDF/VT și de ce PDF/X vine primul

PDF/VT (ISO 16612-2:2010) nu este un format de fișier nou. Este un strat de metadate de optimizare adăugat peste un fișier PDF/X, iar această ordine este esențială. Standardul definește trei niveluri de conformitate, dar doar două dintre ele numesc un fișier PDF: PDF/VT-1, un document unic, autonom, și PDF/VT-2, un model de set de fișiere în care paginile fac referire la resurse externe partajate. Al treilea token pe care îl poți vedea, PDF/VT-2s, nu este deloc o valoare la nivel de fișier; el trăiește într-un antet MIME al fluxului, descris în Anexa A. Dacă găsești cod care inserează GTS_PDFVTVersion = "PDF/VT-2s" în XMP-ul unui document, acel cod este greșit

Regula care nu se negociază pentru un singur fișier este baza PDF/X. ISO 16612-2 §6.2.1 cere ca fiecare fișier PDF/VT-1 să fie și un fișier PDF/X-4 valid. Setul de fișiere PDF/VT-2 trebuie, potrivit §6.2.2, să stea pe PDF/X-4p, PDF/X-5g sau PDF/X-5pg. De aceea un scriitor PDF/VT nu poate doar să adauge câteva chei de identificare: trebuie să poarte cu el întregul set de marcatori PDF/X-4, ceea ce înseamnă un OutputIntent, un profil ICC de destinație încorporat, intrările XMP și Info din document care se potrivesc, un trailer /ID, și fără criptare. Sari peste oricare dintre acestea și ai un fișier care pretinde că este PDF/VT și eșuează în clipa în care un consumator conform verifică baza. PDFiumPas tratează stratul PDF/X-4 ca parte a salvării PDF/VT, așa că nu apelezi mai întâi un SaveAsPdfX separat; injectorul scrie ambele straturi într-o singură trecere

Scrierea unui fișier cu SaveAsPdfVT

Apelul minim nu are nevoie de nimic în afară de un document activ, deoarece TPdfVTSaveOptions.Default furnizează un profil ICC sRGB încorporat și conformitate pvc1. Salvarea rulează intern în trei pași: elimină orice securitate (inserarea unor marcatori în clar într-un flux de obiecte criptat l-ar corupe), face puntea dintre dicționarul Info existent al documentului și trailer /ID în setul de marcatori, astfel încât valorile XMP și Info să fie aliniate, apoi adaugă obiectele PDF/X-4 și PDF/VT printr-o actualizare incrementală

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;

Pentru ieșirea reală de producție vei dori aproape întotdeauna să înlocuiești OutputIntent-ul cu caracterizarea tipografiei tale, nu cu fallback-ul sRGB generic. Furnizează octeții ICC și identificatorii condiției prin 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;

Un detaliu din acel fragment este o limitare de protecție deliberată, nu o restricție pe care să o poți contesta. Setarea Opt.Conformance := pvc2 nu produce un fișier PDF/VT-2. Scriitorul normalizează orice cerere care nu este pvc1 înapoi la pvc1, pentru că PDF/VT-2 este un format de set de fișiere, iar un scriitor pentru un singur fișier care adaugă un singur document de ieșire nu poate asambla fizic setul de resurse externe cerut de §6.2.2. Valoarea pvc2 există pentru calea de citire, astfel încât ValidatePdfVT poate recunoaște și raporta un document existent dintr-un set de fișiere; nu este o țintă de scriere

Arborele DPart: structura pe care RIP-ul chiar o citește

Nucleul PDF/VT este ierarhia Document Part (DPart). Ea permite presei să împartă un tiraj lung în înregistrări, să grupeze înregistrările în destinatari sau pachete poștale și să atașeze metadate Document Part, astfel încât echipamentele din aval să poată direcționa și factura fiecare piesă. ISO 16612-2 §6.5 descrie schema: catalogul poartă un /DPartRoot, nodul rădăcină DPart poartă /DPartRootNode și un /NodeNameList care denumește fiecare nivel al ierarhiei, DPart-urile frunză acoperă intervale din arborele de pagini, iar fiecare pagină care aparține unei părți trimite înapoi la frunza sa printr-o /DPartintrare la nivel de pagină

Când documentul sursă conține deja o ierarhie utilizabilă, SaveAsPdfVTo păstrează. Când nu o face, scriitorul sintetizează una minimă: un singur DPart la nivel de document care acoperă, în ordine, arborele curent al paginilor, cu o /DPart referință inversă atașată fiecărui obiect de pagină activ și o structură DPart de un singur nivel /NodeNameList [/Document]. Fii sincer cu tine însuți despre ce este acel arbore minim. Este o ancoră structurală care îndeplinește cerințele de formă din §6.5; nu este metadată de business. Nu poate inventa destinatari, limite de piese poștale sau loturi de produse, pentru că informația aceea nu a existat niciodată în sursă. Dacă ai date per destinatar, trebuie să construiești singur un arbore DPart mai adânc și să extinzi /NodeNameList pentru a se potrivi nivelurilor pe care le creezi

Validare care merge dincolo de simpla prezență a cheilor

ValidatePdfVT returnează un TPdfVTValidationResult record cu trei lucruri: nivelul de conformitate detectat, Conformance un set de Issues probleme, și un IsCompliant helper care este adevărat doar când conformitatea este un nivel real și setul de probleme este gol. Enumerarea problemelor este intenționat specifică, astfel încât un rezultat eșuat să îți spună ce clauză ai omis, nu doar "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;

Cele două verificări care merită înțelese în profunzime sunt asocierea conformității și parcurgerea DPart, pentru că ambele erau cândva prea permisive și au fost înăsprite ca să se potrivească specificației. Pe partea de asociere, validatorul face potrivire exactă, nu "orice PDF/X merge": un fișier PDF/VT-1 este acceptat doar pe o PDF/X-4 bază, iar un fișier PDF/VT-2 doar pe PDF/X-4p, PDF/X-5g, sau PDF/X-5pg. Un marcator PDF/VT-1 plasat pe o bază PDF/X-1a este raportat, nu trecut cu vederea

Parcurgerea DPart este zona unde se află cea mai mare parte a rigorii. Nu este suficient ca catalogul să aibă o /DPartRoot cheie, pentru că un obiect gol falsificat sau unul fără legături către pagini tot nu poate fi procesat. HasValidDPartHierarchy și parcurgerea recursivă ValidateDPartNode urmăresc întreaga structură: urmează legăturile părinte, resping nodurile copil duplicate și ciclurile, impun ca /Start și /DParts să fie reciproc exclusive și cer ca intervalele de pagini-frunză să acopere arborele de pagini în ordine depth-first, cu /DPart să indice frunza care o conține. Toate aceste erori interne se reduc la un singur pvviMissingDPartRoot bit de problemă în loc să extindă enumerația publică, așa că tratează acel singur indicator ca "ierarhia DPart este inutilizabilă", nu literalmente "cheia rădăcină lipsește"

Trei capcane sintactice pe care validatorul le impune acum

Parcurgerile succesive ale Tabelului 4 din §6.5 au scos la iveală forme pe care versiunile anterioare le acceptau, dar standardul nu le acceptă. Sunt genul de lucruri pe care un arbore DPart construit manual le greșește, așa că merită menționate explicit:

  • /DParts este un tablou de tablouri, nu un tablou plat. Fiecare element al tabloului exterior trebuie să fie el însuși un tablou de referințe indirecte. Un tablou /DParts [9 0 R] plat este respins; forma conformă este /DParts [[9 0 R] [10 0 R]]. Asta împiedică o structură nehierarhică să se dea drept un nivel valid
  • /End marchează doar un interval real de mai multe pagini. Un DPart-frunză poate include /End doar atunci când are și /Start, iar /End trebuie să fie mai târziu decât /Start în ordinea arborelui de pagini. Un /Start 3 0 R /End 3 0 R acum face ierarhia inutilizabilă în loc să fie interpretat ca o parte cu o singură pagină
  • /NodeNameList numele trebuie să supraviețuiască eliminării escape-urilor din numele PDF ca NMTOKEN-uri XML. Un nume precum /Bad#20Name se extinde într-unul care conține un spațiu, iar asta nu este un token valid. Implementarea face o verificare ASCII simplă (litere, cifre, ., -, _, :, plus biți non-ASCII) care depistează greșelile de spațiere și de delimitare fără a respinge nume localizate sau specifice furnizorului, legitime

Marcaje XMP: două moduri de a scrie aceeași proprietate

Identificarea PDF/VT se află în XMP sub pdfvtid namespace-ul, în special GTS_PDFVTVersion și GTS_PDFVTModDate, alături de standardul xmp:CreateDate și xmp:ModifyDate. O subtilitate care provoacă rapoarte false de "lipsă" în cititoarele naive este că oricare dintre acestea poate fi serializată în două moduri: ca text al elementului (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) sau ca un atribut RDF pe elementul de descriere. PDFiumPas citește ambele forme, deci un fișier scris de un alt instrument în stil de atribut nu este penalizat. De asemenea, impune regula de consistență din §6.3, conform căreia GTS_PDFVTModDate trebuie să fie egal cu xmp:ModifyDate; o neconcordanță declanșează pvviModDateMismatch

Încă o regulă din aceeași clauză: o valoare GTS_PDFVTVersion necunoscută este păstrată ca pvcUnknown în loc să fie redusă la pvcNone. Această diferență contează operațional. pvcNone înseamnă "niciun marcaj PDF/VT, un PDF obișnuit", în timp ce pvcUnknown înseamnă "ceva a marcat o versiune pe care acest validator nu o recunoaște" (cazul PDF/VT-2s printre ele). A le confunda ar ascunde un fișier formatat greșit în aceeași categorie cu un document simplu

Unde se oprește garanția

Merită să fim preciși în privința limitei a ceea ce promit aceste metode, pentru că respectarea cerințelor pentru tipărirea cu date variabile implică costuri financiare reale. Verificările DPart și de pairing sunt validări structurale la nivel de octeți. Ele confirmă că scheletul de optimizare, marcajele de bază PDF/X-4, OutputIntent și XMP sunt prezente și coerente intern. Nu sunt un preflight PDF/X-4 la nivel de conținut: nu verifică dacă fiecare culoare se află în condiția de ieșire declarată, dacă toate fonturile sunt încorporate sau dacă nu a apărut vreun caz limită interzis de transparență și blending. Pentru o lucrare pe care o dai la o tipografie contractată, combină validarea structurală a PDFiumPas cu un motor dedicat de preflight PDF/X și cu o probă tipărită, exact cum ai verifica la rece orice altă afirmație de conformitate. Stratul structural prinde defectele care rup în tăcere cache-ul RIP; este o jumătate a unei verificări complete, nu întregul ei

Dacă integrezi aceste verificări într-o poartă de lansare mai largă, aceeași abordare de scanare la nivel de octeți stă la baza celorlalte lucrări de standardizare ale bibliotecii, inclusiv validarea fluxurilor de obiecte și cross-reference înainte ca un fișier să ajungă la preflight, și disciplina obiectelor partajate din spatele ștampilelor de pagină reutilizabile cu Form XObjects care face ca un document să fie prietenos cu RIP încă de la început. API-urile de salvare și validare PDF/VT și PDF/X descrise aici fac parte din componenta PDFium VCL pentru Delphi și C++Builder, a cărei pagină de produs conține referința completă de conformitate