Teknisk artikel

PDF/VT-variabeldatautskrift i Delphi med PDFium VCL

Ett transaktionstryckeri skickar tillbaka din körning med 80 000 sidor och ett kort avslag: "inte PDF/VT, RIP kan inte cacha." Filen öppnas utan problem i varje visningsprogram på skrivbordet, färgerna stämmer och data har slagits ihop korrekt. Inget av det är vad den digitala pressen begärde. Hög hastighet vid variabeldatautskrift hänger på att pressen kan se att kundlogoblocket på sida 1 är exakt samma objekt som på sida 40 000, rendera det en gång och återanvända det. PDF/VT är standarden som gör det löftet maskinverifierbart, och "ser rätt ut" är just fällan, eftersom strukturen som RIP:n läser är osynlig på skärmen

PDFiumPas exponerar den strukturen genom ett litet gränssnitt på TPdf: SaveAsPdfVT skriver den, ValidatePdfVT kontrollerar den. Den här artikeln handlar om vad de två metoderna faktiskt lägger på disk och inspekterar, var ISO 16612-2 är strängare än det först verkar, och vilka delar som är ärliga strukturella ankare snarare än en fullständig preflight du kan fakturera en kund för

Vad PDF/VT standardiserar, och varför PDF/X kommer först

PDF/VT (ISO 16612-2:2010) är inte ett nytt filformat. Det är ett lager av optimeringsmetadata som läggs ovanpå en PDF/X-fil, och den ordningen är avgörande. Standarden definierar tre överensstämmelsenivåer, men bara två av dem namnger en PDF-fil: PDF/VT-1, ett enda fristående dokument, och PDF/VT-2, en filuppsättningsmodell där sidor refererar till delade externa resurser. Den tredje beteckningen du kan se, PDF/VT-2s, är inte alls ett värde på filnivå; den lever i ett MIME-strömhuvud som beskrivs i bilaga A. Om du hittar kod som stämplar GTS_PDFVTVersion = "PDF/VT-2s" in i ett dokuments XMP, är den koden fel

Den icke förhandlingsbara regeln för en enskild fil är PDF/X-basen. ISO 16612-2 §6.2.1 kräver att varje PDF/VT-1-fil också är en giltig PDF/X-4-fil. PDF/VT-2:s filuppsättning måste enligt §6.2.2 i stället ligga på PDF/X-4p, PDF/X-5g eller PDF/X-5pg. Det är därför en PDF/VT-skrivare inte bara kan lägga till ett par identifieringsnycklar: den måste bära med sig hela PDF/X-4-marköruppsättningen, vilket betyder ett OutputIntent, en inbäddad ICC-destinationprofil, motsvarande XMP- och dokumentInfo-poster, en trailer /ID, och ingen kryptering. Hoppa över något av detta och du har en fil som påstår sig vara PDF/VT och misslyckas i samma stund som en kompatibel konsument kontrollerar basen. PDFiumPas behandlar PDF/X-4-lagret som en del av PDF/VT-sparningen, så du anropar inte först en separat SaveAsPdfX först; injektorn skriver båda lagren i ett enda pass

Skriva en fil med SaveAsPdfVT

Det minsta anropet kräver inget mer än ett aktivt dokument, eftersom TPdfVTSaveOptions.Default levererar en inbyggd sRGB-ICC-profil och överensstämmelsenivån pvc1. Sparningen kör tre steg internt: den tar bort all säkerhet (att injicera klartextmarkörer i en krypterad objektström skulle fördärva den), den kopplar dokumentets befintliga Info-ordbok och trailer /ID till marköruppsättningen så att XMP- och Info-värdena stämmer överens, och därefter lägger den till PDF/X-4- och PDF/VT-objekten genom en inkrementell uppdatering

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;

För verklig produktionsutskrift vill du nästan alltid åsidosätta OutputIntent med pressens karakterisering, inte den generiska sRGB-reserven. Ange ICC-bytena och konditionsidentifierarna via 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;

En detalj i det kodexemplet är en avsiktlig spärr snarare än en begränsning att argumentera mot. Att sätta Opt.Conformance := pvc2 ger inte en PDF/VT-2-fil. Skrivaren normaliserar alla icke-pvc1 förfrågningar tillbaka till pvc1, eftersom PDF/VT-2 är ett filuppsättningsformat och en skrivare för en enskild fil som lägger till ett enda utdata-dokument fysiskt inte kan sammanställa den externa resursmängd som §6.2.2 kräver. pvc2 värdet finns för lässidan, så ValidatePdfVT kan känna igen och rapportera ett befintligt filuppsättningsdokument; det är inte ett skrivmål

DPart-trädet: strukturen som RIP:n faktiskt läser

Kärnan i PDF/VT är hierarkin Document Part (DPart). Det är den som låter en press dela upp en lång körning i poster, gruppera poster i mottagare eller postbuntar, och koppla Document Part Metadata så att utrustningen längre ned i kedjan kan dirigera och fakturera varje exemplar. ISO 16612-2 §6.5 anger kopplingen: katalogen bär en /DPartRoot, rot-DPart-noden bär /DPartRootNode och en /NodeNameList som namnger varje hierarkinivå, löv-DPart-noder täcker intervall i sidträdet, och varje sida som hör till en del pekar tillbaka på sitt löv via en sidnivå-/DPart post

När källdokumentet redan innehåller en användbar hierarki, SaveAsPdfVT bevarar SaveAsPdfVT den. När det inte gör det skapar skrivaren en minimal variant: en ensam DPart på dokumentnivå som spänner över det aktuella sidträdet i ordning, med en /DPart bakåtreferens tillagd på varje levande sidobjekt och en en-nivå-/NodeNameList [/Document]. Var ärlig mot dig själv om vad det minimala trädet är. Det är ett strukturellt ankare som uppfyller §6.5:s formkrav; det är inte affärsmetadata. Det kan inte hitta på mottagare, gränser för utskicksenheter eller produktbatcher, eftersom den informationen aldrig fanns i källan. Om du har data per mottagare förväntas du själv bygga ett djupare DPart-träd och utöka /NodeNameList så att det matchar nivåerna du skapar

Validering som går längre än nyckelförekomst

ValidatePdfVT returnerar ett TPdfVTValidationResult record med tre saker: den upptäckta Conformance, en uppsättning Issues och en IsCompliant hjälpfunktion som bara är sann när överensstämmelsenivån är verklig och problemuppsättningen är tom. Problemuppräkningen är medvetet specifik, så ett misslyckat resultat talar om vilken klausul du missade i stället för bara "ogiltig":

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;

De två kontroller som är värda att förstå på djupet är överensstämmelsekopplingen och DPart-genomgången, eftersom båda tidigare var för tillåtande och skärptes för att matcha specifikationen. På kopplingssidan gör validatorn exakt matchning, inte "vilken PDF/X som helst går bra": en PDF/VT-1-fil accepteras bara på en PDF/X-4 bas, och en PDF/VT-2-fil bara på PDF/X-4p, PDF/X-5g eller PDF/X-5pg. En PDF/VT-1-markör som ligger på en PDF/X-1a-bas rapporteras, inte släpps igenom

Det är i DPart-genomgången som det mesta av noggrannheten finns. Det räcker inte att katalogen har en /DPartRoot nyckel, eftersom ett förfalskat tomt objekt eller ett utan sidlänkar ändå inte kan användas. HasValidDPartHierarchy och den rekursiva ValidateDPartNode följer hela strukturen: de följer föräldralänkar, avvisar dubbla barn och cykler, tvingar fram att /Start och /DParts utesluter varandra, och kräver att lövens sidintervall täcker sidträdet i djup-först-ordning med varje sidas /DPart pekande på det löv som innehåller den. Alla dessa interna fel kokar ned till den enda pvviMissingDPartRoot problemflaggan i stället för att utöka den publika uppräkningen, så se den flaggan som "DPart-hierarkin kan inte användas", inte bokstavligen "rot-nyckeln saknas"

Tre syntaktiska fällor som validatorn nu kräver

Upprepade genomgångar mot §6.5 tabell 4 visade former som tidigare versioner accepterade men standarden inte gör. Det här är sådant som ett handbyggt DPart-träd lätt får fel, så de är värda att lyfta fram tydligt:

  • /DParts är en array av arrayer, inte en platt array. Varje element i den yttre arrayen måste i sin tur vara en array med indirekta referenser. En platt /DParts [9 0 R] avvisas; den överensstämmande formen är /DParts [[9 0 R] [10 0 R]]. Detta hindrar en ohierarkisk struktur från att utge sig för att vara en giltig nivå
  • /End markerar bara ett verkligt intervall över flera sidor. Ett löv-DPart får bära /End endast när det också har /Start, och /End måste ligga senare än /Start i sidträdets ordning. En degenererad /Start 3 0 R /End 3 0 R gör nu hierarkin oanvändbar i stället för att läsas som en del med en sida
  • /NodeNameList namn måste överleva PDF-namnavkodning som XML NMTOKENs. Ett namn som /Bad#20Name expanderar till ett namn med ett mellanslag, vilket inte är ett giltigt token. Implementeringen gör en lätt ASCII-kontroll (bokstäver, siffror, ., -, _, :, plus icke-ASCII-byten) som fångar fel i blanksteg och avgränsare utan att förkasta legitima lokaliserade eller leverantörsspecifika namn

XMP-markörer: två sätt att skriva samma egenskap

PDF/VT-identifikation ligger i XMP under pdfvtid namnområdet, specifikt GTS_PDFVTVersion och GTS_PDFVTModDate, tillsammans med standardens xmp:CreateDate och xmp:ModifyDate. En finess som orsakar falska "saknas"-rapporter i naiva läsare är att alla dessa kan serialiseras på två sätt: som elementtext (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) eller som ett RDF-attribut på description-elementet. PDFiumPas läser båda formerna, så en fil som ett annat verktyg skrev i attributstil missgynnas inte. Den upprätthåller också §6.3-regeln om överensstämmelse att GTS_PDFVTModDate måste vara lika med xmp:ModifyDate; en avvikelse utlöser pvviModDateMismatch

Ytterligare en regel från samma klausul: ett okänt GTS_PDFVTVersion värde bevaras som pvcUnknown i stället för att vikas tillbaka till pvcNone. Den skillnaden spelar roll i drift. pvcNone betyder "ingen PDF/VT-markör alls, en vanlig PDF", medan pvcUnknown betyder "något stämplade en version som denna validator inte känner igen" (det PDF/VT-2s fallet bland dem). Att blanda ihop de två skulle gömma en felaktig fil i samma fack som ett vanligt dokument

Var garantin tar slut

Det är värt att vara exakt med gränsen för vad dessa metoder lovar, eftersom variabeldatautskriftens efterlevnad handlar om riktiga pengar. DPart- och parningskontrollerna är strukturell validering på byte-nivå. De bekräftar att optimeringsstommen, PDF/X-4-basmarkörerna, OutputIntent och XMP finns och är inbördes konsekventa. Det är inte en PDF/X-4-preflight på innehållsnivå: de verifierar inte att varje färg ligger inom den deklarerade utdataförutsättningen, att alla typsnitt är inbäddade eller att inget förbjudet transparensblandningskantfall smugit sig in. För ett jobb du ska köra hos ett kontraktstryckeri bör du para ihop PDFiumPas strukturella validering med en dedikerad PDF/X-preflightmotor och ett provtryck, på samma sätt som du skulle göra en rimlighetskontroll av varje annat överensstämmelsepåstående. Det strukturella lagret fångar de fel som tyst bryter RIP-cachelagringen; det är ena halvan av en fullständig kontroll, inte helheten

Om du bygger in dessa kontroller i en bredare releasegrind, ligger samma byte-nivå-skanningsmetod bakom bibliotekets andra standardarbete, inklusive validering av objekt- och korsreferensströmmar innan en fil ens når preflight, och den delade objektdisciplinen bakom återanvändbara sidstämplar med Form XObjects som gör ett dokument RIP-vänligt redan från början. De PDF/VT- och PDF/X-spar- och validerings-API:er som beskrivs här ingår i PDFium VCL-komponenten för Delphi och C++Builder, vars produktsida innehåller hela efterlevnadsreferensen