Een transactionele drukker stuurt je 80.000 pagina's statement-run terug met een afwijzing van één regel: "not PDF/VT, RIP cannot cache." Het bestand opent prima in elke viewer op je bureau, de kleuren kloppen, de data is correct samengevoegd. Geen van dat alles is waar de digitale pers naar vroeg. High-speed variable-data printing staat of valt met het vermogen van de pers om te herkennen dat het klantlogo-blok op pagina 1 byte-voor-byte hetzelfde object is als dat op pagina 40.000, het één keer te renderen en opnieuw te gebruiken. PDF/VT is de standaard die die belofte machineverifieerbaar maakt, en "ziet er goed uit" is precies de valkuil, omdat de structuur die de RIP leest onzichtbaar is op het scherm
PDFiumPas legt die structuur bloot via een kleine surface op TPdf: SaveAsPdfVT schrijft hem weg, ValidatePdfVT controleert hem. Dit artikel gaat over wat die twee methoden precies op schijf zetten en inspecteren, waar ISO 16612-2 strenger is dan het eerst lijkt, en welke delen eerlijke structurele ankers zijn in plaats van een volledige preflight die je aan een klant kunt doorberekenen
Wat PDF/VT standaardiseert, en waarom PDF/X eerst komt
PDF/VT (ISO 16612-2:2010) is geen nieuw bestandsformaat. Het is een laag optimalisatiemetadata bovenop een PDF/X-bestand, en die volgorde is dragend. De standaard definieert drie conformiteitsniveaus, maar slechts twee daarvan benoemen een PDF-bestand: PDF/VT-1, een enkel zelfstaand document, en PDF/VT-2, een file-setmodel waarin pagina's verwijzen naar gedeelde externe resources. Het derde token dat je misschien ziet, PDF/VT-2s, is helemaal geen bestandswaarde; het leeft in een MIME stream header die in Annex A wordt beschreven. Als je code ziet die GTS_PDFVTVersion = "PDF/VT-2s" in de XMP van een document stampt, is die code fout
De ononderhandelbare regel voor een enkel bestand is de PDF/X-basiskant. ISO 16612-2 §6.2.1 vereist dat elk PDF/VT-1-bestand ook een geldig PDF/X-4-bestand is. De file set van PDF/VT-2 moet volgens §6.2.2 juist rusten op PDF/X-4p, PDF/X-5g of PDF/X-5pg. Daarom kan een PDF/VT-writer niet simpelweg een paar identifier-sleutels toevoegen: hij moet de volledige PDF/X-4-marker-set meenemen, en dat betekent een OutputIntent, een ingesloten ICC-destination profile, de bijpassende XMP- en document-Info-items, een trailer /ID en geen encryptie. Sla je iets daarvan over, dan heb je een bestand dat PDF/VT claimt en faalt zodra een conforme consumer de basis controleert. PDFiumPas behandelt de PDF/X-4-laag als onderdeel van het PDF/VT-schrijfpad, dus je roept niet eerst een aparte SaveAsPdfX aan; de injector schrijft beide lagen in één keer
Een bestand schrijven met SaveAsPdfVT
De minimale aanroep heeft niets nodig behalve een actief document, omdat TPdfVTSaveOptions.Default een ingebouwd sRGB ICC-profiel en conformiteit pvc1 levert. Het opslaan loopt intern in drie stappen: het haalt eventuele security eraf, want plaintext markers injecteren in een versleutelde objectstream zou die beschadigen; het verbindt het bestaande Info-dictionary en trailer /ID van het document met de markerset zodat XMP- en Info-waarden overeenkomen; daarna voegt het de PDF/X-4- en PDF/VT-objecten toe via een incremental update
Voor echte productie-output wil je bijna altijd de OutputIntent overschrijven met de karakterisering van je pers, niet met de generieke sRGB-fallback. Geef de ICC-bytes en de conditiespecificaties door via TPdfVTSaveOptions:
Eén detail in dat fragment is een bewuste vangrail en geen beperking waar je over kunt onderhandelen. Opt.Conformance := pvc2 instellen produceert geen PDF/VT-2-bestand. De writer normaliseert elke niet-pvc1-aanvraag terug naar pvc1, omdat PDF/VT-2 een file-setformaat is en een single-file writer die één outputdocument toevoegt fysiek niet de externe resource-set kan samenstellen die §6.2.2 eist. De waarde pvc2 bestaat voor het leespad, zodat ValidatePdfVT een bestaand file-setdocument kan herkennen en rapporteren; het is geen write-doel
De DPart-tree: structuur die de RIP echt leest
De kern van PDF/VT is de Document Part, DPart, hiërarchie. Die laat een pers een lange run verdelen in records, records groeperen in ontvangers of mailbundels, en Document Part Metadata koppelen zodat downstream apparatuur elk stuk kan routeren en factureren. ISO 16612-2 §6.5 beschrijft de bedrading: de catalog draagt een /DPartRoot, de root DPart-node draagt /DPartRootNode en een /NodeNameList die elk hiërarchieniveau benoemt, leaf DParts dekken bereiken van de page tree, en elke pagina die bij een part hoort wijst via een pagina-level /DPart-entry terug naar het leaf waar hij onder valt
Wanneer je brondocument al een bruikbare hiërarchie bevat, behoudt SaveAsPdfVT die. Wanneer dat niet zo is, syntheseert de writer een minimale variant: één document-level DPart die de huidige page tree in volgorde beslaat, met een /DPart-back-reference die aan elk live page object wordt toegevoegd en een éénregelige /NodeNameList [/Document]. Wees eerlijk over wat die minimale tree is. Het is een structureel anker dat aan de vormvereisten van §6.5 voldoet; het is geen businessmetadata. Het kan geen ontvangers, mail-piecegrenzen of productbatches verzinnen, omdat die informatie nooit in de bron zat. Als je per ontvanger data hebt, wordt van je verwacht dat je zelf een diepere DPart-tree bouwt en de /NodeNameList uitbreidt zodat die overeenkomt met de niveaus die je maakt
Validatie die verder gaat dan sleutel-aanwezigheid
ValidatePdfVT geeft een TPdfVTValidationResult-record terug met drie dingen: de gedetecteerde Conformance, een set Issues en een IsCompliant-helper die alleen waar is wanneer de conformiteit een echt niveau is en de issue-set leeg is. De issue-enumeratie is bewust specifiek, zodat een fout resultaat je vertelt welke clausule je hebt gemist in plaats van alleen "invalid":
De twee controles die je diep moet begrijpen zijn de conformiteitskoppeling en de DPart-wandeling, omdat beide vroeger te soepel waren en zijn aangescherpt om aan de standaard te voldoen. Aan de koppelkant doet de validator exact matchen, niet "elke PDF/X is goed": een PDF/VT-1-bestand wordt alleen geaccepteerd op een PDF/X-4-basis, en een PDF/VT-2-bestand alleen op PDF/X-4p, PDF/X-5g of PDF/X-5pg. Een PDF/VT-1-marker op een PDF/X-1a-basis wordt gerapporteerd, niet doorgewuifd
De DPart-wandeling is waar de meeste strengheid zit. Het is niet genoeg dat de catalog een /DPartRoot-sleutel heeft, want een vervalst leeg object of een object zonder paginalinks kan nog steeds niet worden geconsumeerd. HasValidDPartHierarchy en de recursieve ValidateDPartNode traceren de hele structuur: ze volgen parent-links, wijzen duplicate kinderen en cycles af, dwingen af dat /Start en /DParts elkaar uitsluiten, en eisen dat leaf page ranges de page tree in depth-first order afdekken, waarbij elke pagina's /DPart wijst naar het leaf dat hem bevat. Al die interne fouten vallen terug op de ene issue-bit pvviMissingDPartRoot in plaats van de publieke enum uit te breiden, dus behandel die ene vlag als "de DPart-hiërarchie is onbruikbaar", niet letterlijk als "de root-sleutel ontbreekt"
Drie syntactische valkuilen die de validator nu afdwingt
Opeenvolgende passes op §6.5 Table 4 brachten vormen aan het licht die eerdere versies accepteerden maar de standaard niet toestaat. Dit zijn precies de dingen die een met de hand gebouwde DPart-tree fout doet, dus het is de moeite waard ze expliciet te noemen:
/DPartsis een array van arrays, geen vlakke array. Elk element van de outer array moet zelf een indirect-reference array zijn. Een vlakke/DParts [9 0 R]wordt geweigerd; de conforme vorm is/DParts [[9 0 R] [10 0 R]]. Zo voorkom je dat een niet-hiërarchische structuur zich voordoet als een geldig niveau/Endmarkeert alleen een echte meerpagina-range. Een leaf DPart mag/Endalleen dragen wanneer het ook/Startheeft, en/Endmoet later vallen dan/Startin page-tree-volgorde. Een degenerate/Start 3 0 R /End 3 0 Rmaakt de hiërarchie nu onbruikbaar in plaats van dat het leest als een éénpagina-part/NodeNameList-namen moeten PDF-name-unescaping overleven als XML NMTOKENs. Een naam als/Bad#20Namewordt uitgebreid naar een naam met een spatie, en dat is geen geldig token. De implementatie doet een lichte ASCII-check, letters, cijfers,.,-,_,:, plus niet-ASCII-bytes, die whitespace- en delimiterfouten vangt zonder legitieme gelokaliseerde of vendorspecifieke namen te weigeren
Deze checks dekken samen de structurele fouten waar de preflight vroeger doorheen keek. De DPart-wandeling maakt van een onbruikbare boom een expliciete issue, zodat de writer niet stilletjes iets produceert wat een pers alsnog afwijst
Waar de garantie eindigt
Het is belangrijk om precies te zijn over de grens van wat deze methoden beloven, omdat variabele-data-printconformiteit echt geld vertegenwoordigt. De DPart- en koppelcontroles zijn structurele byte-level validatie. Ze bevestigen dat het optimalisatieskelet, de PDF/X-4-basismarkers, de OutputIntent en de XMP aanwezig zijn en intern consistent zijn. Het is geen content-level PDF/X-4-preflight: het controleert niet of elke kleur binnen de opgegeven outputconditie valt, of alle fonts embedded zijn, of er geen verboden transparantie-edge case tussendoor glipt. Voor een job die op een contractpers draait, combineer je de structurele validatie van PDFiumPas met een dedicated PDF/X-preflightengine en een proefdruk, net zoals je elke andere conformiteitsclaim zou sanity-checken. De structurele laag vangt de fouten die RIP-caching stil breken; het is één helft van een complete controle, niet het geheel
Als je deze controles in een bredere releasegate wilt hangen, onderbouwt dezelfde byte-level scanning-aanpak ook de andere standaarden in de bibliotheek, waaronder validating object and cross-reference streams voordat een bestand ooit preflight bereikt, en de discipline rond gedeelde objecten achter reusable page stamps with Form XObjects die een document in de eerste plaats RIP-vriendelijk maakt. De PDF/VT- en PDF/X-save- en validatie-API's die hier worden beschreven, maken deel uit van de PDFium VCL component voor Delphi en C++Builder, waarvan de productpagina de volledige conformiteitsreferentie bevat
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;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;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;