Teknisk artikel

PDF/VT-variabel dataprinting i Delphi med PDFium VCL

En transaktionsudbyder sender din 80.000-siders udsendelse tilbage med en enlinjes afvisning: "not PDF/VT, RIP cannot cache." Filen åbner fint i alle fremvisere på skrivebordet, farverne er rigtige, data er flettet korrekt. Intet af det er, hvad det digitale trykpresse bad om. Højhastigheds-variabeldata-print lever og dør på, om pressen kan genkende, at kundelogo-blokken på side 1 er byte-for-byte det samme objekt som den på side 40.000, rendre det én gang og genbruge det. PDF/VT er standarden, der gør det løfte maskinkontrollerbart, og "ser rigtigt ud" er netop fælden, fordi strukturen, RIP'en læser, er usynlig på skærmen

PDFiumPas eksponerer den struktur gennem en lille overflade på TPdf: SaveAsPdfVT skriver den, ValidatePdfVT tjekker den. Denne artikel handler om, hvad de to metoder rent faktisk lægger på disk og inspicerer, hvor ISO 16612-2 er strengere, end det ser ud ved første øjekast, og hvilke dele der er ærlige strukturelle ankre frem for en fuld preflight, du kan fakturere en klient for

Hvad PDF/VT standardiserer, og hvorfor PDF/X kommer først

PDF/VT (ISO 16612-2:2010) er ikke et helt nyt filformat. Det er et lag af optimeringsmetadata boltet på en PDF/X-fil, og den rækkefølge er bærende. Standarden definerer tre overensstemmelsesniveauer, men kun to af dem navngiver en PDF-fil: PDF/VT-1, et enkelt selvstændigt dokument, og PDF/VT-2, en filsæt-model, hvor sider refererer til delte eksterne ressourcer. Det tredje token, du måske ser, PDF/VT-2s, er slet ikke en fil-niveau-værdi; det bor i en MIME-stream-header beskrevet i Annex A. Finder du kode, der stempler GTS_PDFVTVersion = "PDF/VT-2s" ind i et dokuments XMP, er den kode forkert

Den ikke-til-forhandling-regel for en enkelt fil er PDF/X-basen. ISO 16612-2 §6.2.1 kræver, at hver PDF/VT-1-fil også er en gyldig PDF/X-4-fil. PDF/VT-2's filsæt skal ifølge §6.2.2 i stedet hvile på PDF/X-4p, PDF/X-5g eller PDF/X-5pg. Det er derfor, en PDF/VT-writer ikke bare kan tilføje et par identifikatornøgler: den skal bære hele PDF/X-4-markørsættet med sig, hvilket betyder en OutputIntent, en indlejret ICC-destinationsprofil, de matchende XMP- og dokument-Info-poster, en trailer /ID, og ingen kryptering. Springer du et af dem over, har du en fil, der hævder PDF/VT og fejler, i det øjeblik en konform forbruger tjekker basen. PDFiumPas behandler PDF/X-4-laget som en del af PDF/VT-gemningen, så du kalder ikke en separat SaveAsPdfX først; injektoren skriver begge lag i ét pas

Diagram over PDF/VT-stakken bygget i Delphi, hvor optimeringsmetadata sidder på en obligatorisk PDF/X-4-base, med eksakt conformance-parring for PDF/VT-1, PDF/VT-2 og MIME-only PDF/VT-2s-tokenet
PDF/VT-metadata bærer kun last oven på en komplet PDF/X-base

Skriv en fil med SaveAsPdfVT

Det minimale kald behøver intet andet end et aktivt dokument, fordi TPdfVTSaveOptions.Default leverer en indbygget sRGB ICC-profil og overensstemmelse pvc1. Gemningen kører tre trin internt: den fjerner enhver sikkerhed (at injicere klartekstmarkører i en krypteret objektstream ville korrumpere den), den bygger bro mellem dokumentets eksisterende Info-dictionary og trailer /ID ind i markørsættet, så XMP- og Info-værdierne stemmer overens, og tilføjer så PDF/X-4- og PDF/VT-objekterne gennem en trinvis opdatering

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'statements-merged.pdf';
    Pdf.Active := True;
    // Standardindstillinger: indbygget sRGB OutputIntent, PDF/VT-1, syntetiseret DPart
    if Pdf.SaveAsPdfVT('statements-pdfvt.pdf') then
      Writeln('PDF/VT-1 written')
    else
      Writeln('Save failed (document not active?)');
  finally
    Pdf.Free;
  end;
end;

Til reelt produktionsoutput vil du næsten altid overstyre OutputIntenten med din presses karakterisering, ikke det generiske sRGB-fallback. Levér ICC-bytesene og betingelsesidentifikatorerne gennem TPdfVTSaveOptions:

var
  Pdf: TPdf;
  Opt: TPdfVTSaveOptions;
  Icc: TBytes;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'directmail-merged.pdf';
    Pdf.Active := True;
    Icc := LoadIccProfile('GRACoL2013_CRPC6.icc');  // din egen loader

    Opt := TPdfVTSaveOptions.Default;
    Opt.Conformance := pvc1;            // pvc2 normaliseres til pvc1 ved skrivning
    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-tilstand

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

Én detalje i det udsnit er et bevidst rækværk frem for en begrænsning, du kan diskutere. At sætte Opt.Conformance := pvc2 producerer ikke en PDF/VT-2-fil. Writeren normaliserer enhver ikke-pvc1-anmodning tilbage til pvc1, fordi PDF/VT-2 er et filsæt-format, og en enkeltfil-writer, der tilføjer ét outputdokument, fysisk ikke kan samle det eksterne ressourcesæt, §6.2.2 kræver. pvc2-værdien findes til læsestien, så ValidatePdfVT kan genkende og rapportere et eksisterende filsæt-dokument; den er ikke et skrivemål

DPart-træet: struktur som RIP faktisk læser

Hjertet i PDF/VT er Document Part (DPart)-hierarkiet. Det er, hvad der lader en presse opdele en lang kørsel i poster, gruppere poster i modtagere eller postbundter, og vedhæfte Document Part Metadata, så nedstrøms-udstyr kan rute og fakturere hvert stykke. ISO 16612-2 §6.5 lægger ledningsføringen ud: kataloget bærer en /DPartRoot, DPart-rodknuden bærer /DPartRootNode og en /NodeNameList, der navngiver hvert hierarkiniveau, blad-DParts dækker intervaller af sidetræet, og hver side, der hører til en del, peger tilbage på sit blad gennem en side-niveau /DPart-post

PDFium Component-diagram over et PDF/VT DPart-hierarki forbrugt af en RIP: catalog DPartRoot, rodknudens NodeNameList, blad-DParts, der dækker sideranges, og sideniveau DPart-bagreferencer
DPart-træet er den struktur, en RIP læser for at partitionere poster og cache delt indhold

Når dit kildedokument allerede indeholder et brugbart hierarki, bevarer SaveAsPdfVT det. Når det ikke gør, syntetiserer writeren et minimalt: én dokument-niveau-DPart, der spænder over det aktuelle sidetræ i rækkefølge, med en /DPart-tilbagereference tilføjet til hvert levende sideobjekt og en ét-niveau /NodeNameList [/Document]. Vær ærlig med dig selv om, hvad det minimale træ er. Det er et strukturelt anker, der opfylder §6.5's formkrav; det er ikke forretningsmetadata. Det kan ikke opfinde modtagere, postgrænser eller produktbatches, fordi den information aldrig var i kilden. Har du data pr. modtager, forventes det, at du selv bygger et dybere DPart-træ og udvider /NodeNameList til at matche de niveauer, du opretter

Validering, der går ud over nøgle-tilstedeværelse

ValidatePdfVT returnerer en TPdfVTValidationResult-record med tre ting: den registrerede Conformance, et sæt Issues, og en IsCompliant-hjælper, der kun er true, når overensstemmelsen er et reelt niveau, og problemsættet er tomt. Problem-enumeringen er bevidst specifik, så et fejlet resultat fortæller dig, hvilken klausul du missede, frem for bare "ugyldig":

var
  Pdf: TPdf;
  Res: TPdfVTValidationResult;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'statements-pdfvt.pdf';
    Pdf.Active := True;
    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 to tjek, det er værd at forstå i dybden, er overensstemmelsesparringen og DPart-gennemgangen, fordi begge plejede at være for slappe og blev strammet til at matche specifikationen. På parringssiden gør validatoren eksakt matchning, ikke "enhver PDF/X duer": en PDF/VT-1-fil accepteres kun på en PDF/X-4-base, og en PDF/VT-2-fil kun på PDF/X-4p, PDF/X-5g eller PDF/X-5pg. En PDF/VT-1-markør, der sidder på en PDF/X-1a-base, rapporteres, ikke vinkes igennem

DPart-gennemgangen er, hvor det meste af stringensen bor. Det er ikke nok, at kataloget har en /DPartRoot-nøgle, fordi et forfalsket tomt objekt eller ét uden sidelinks stadig ikke kan konsumeres. HasValidDPartHierarchy og den rekursive ValidateDPartNode sporer hele strukturen: de følger forælder-links, afviser duplikerede børn og cyklusser, håndhæver at /Start og /DParts gensidigt udelukker hinanden, og kræver, at blad-sideintervaller dækker sidetræet i depth-first-rækkefølge, med hver sides /DPart pegende på det blad, der indeholder den. Alle de interne fejl kollapser til det ene pvviMissingDPartRoot-problembit frem for at udvide den offentlige enum, så behandl det ene flag som "DPart-hierarkiet er ubrugeligt", ikke bogstaveligt "rodnøglen mangler."

Diagram over PDF/VT-validering i Delphi med PDFium VCL, hvor eksakt baseparring, et rekursivt DPart-gennemløb, PDF/X-4-markører og XMP datokontroller føder TPdfVTValidationResult-recorden
ValidatePdfVT rapporterer, hvilken klausul der fejlede, i stedet for en generisk ugyldig dom

Tre syntaktiske fælder, som validatoren nu håndhæver

Successive gennemløb mod §6.5 Tabel 4 afslørede former, som tidligere versioner accepterede, men som standarden ikke gør. Det er den slags, et håndbygget DPart-træ får galt, så de er værd at fremhæve eksplicit:

  • /DParts er et array af arrays, ikke et fladt array. Hvert element i det ydre array skal selv være et indirekte-reference-array. Et fladt /DParts [9 0 R] afvises; den konforme form er /DParts [[9 0 R] [10 0 R]]. Det forhindrer en ikke-hierarkisk struktur i at udgive sig for et gyldigt niveau
  • /End markerer kun et ægte flersidet interval. En blad-DPart må kun bære /End, når den også har /Start, og /End skal falde senere end /Start i sidetræ-rækkefølge. Et degenereret /Start 3 0 R /End 3 0 R gør nu hierarkiet ubrugeligt i stedet for at læses som en ét-sides del
  • /NodeNameList-navne skal overleve PDF-navne-unescaping som gyldige XML NMTOKENs. Et navn som /Bad#20Name udvides til ét, der indeholder et mellemrum, hvilket ikke er et gyldigt token. Implementeringen gør et let ASCII-tjek (bogstaver, cifre, ., -, _, :, plus ikke-ASCII-bytes), der fanger whitespace- og afgrænser-fejl uden at afvise legitime lokaliserede eller leverandørspecifikke navne

XMP-markører: to måder at skrive den samme egenskab

PDF/VT-identifikation bor i XMP under pdfvtid-namespacet, specifikt GTS_PDFVTVersion og GTS_PDFVTModDate, sammen med de standard xmp:CreateDate og xmp:ModifyDate. En finesse, der forårsager falske "manglende"-rapporter i naive læsere, er, at hver af disse kan serialiseres på to måder: som elementtekst (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) eller som en RDF-attribut på description-elementet. PDFiumPas læser begge former, så en fil, et andet værktøj skrev i attributstil, straffes ikke. Den håndhæver også §6.3-konsistensreglen om, at GTS_PDFVTModDate skal være lig med xmp:ModifyDate; en uoverensstemmelse rejser pvviModDateMismatch

Endnu en regel fra samme klausul: en ukendt GTS_PDFVTVersion-værdi bevares som pvcUnknown frem for at blive foldet tilbage til pvcNone. Den skelnen betyder noget operationelt. pvcNone betyder "intet PDF/VT-mærke overhovedet, en almindelig PDF," mens pvcUnknown betyder "noget stemplede en version, denne validator ikke genkender" (PDF/VT-2s-tilfældet blandt dem). At sammenblande de to ville skjule en fejlformet fil i den samme spand som et almindeligt dokument

Hvor garantien stopper

Det er værd at være præcis om grænsen for, hvad disse metoder lover, fordi variabeldata-printoverensstemmelse har rigtige penge knyttet til sig. DPart- og parringstjekkene er strukturel validering på byte-niveau. De bekræfter, at optimeringsskelettet, PDF/X-4-basemarkørerne, OutputIntenten og XMP'en er til stede og internt konsistente. De er ikke en indholds-niveau PDF/X-4-preflight: de verificerer ikke, at hver farve er inden for den deklarerede outputbetingelse, at alle fonte er indlejrede, eller at intet forbudt transparens-blending-kanttilfælde sneg sig ind. Til et job, du sætter på en kontraktpresse, skal du parre PDFiumPas's strukturelle validering med en dedikeret PDF/X-preflight-motor og en testprint, på samme måde som du ville sundhedstjekke enhver anden overensstemmelsespåstand. Det strukturelle lag fanger de fejl, der stille bryder RIP-caching; det er den ene halvdel af et komplet tjek, ikke det hele

Bygger du disse tjek ind i en bredere udgivelsesport, understøtter den samme byte-niveau-scanningstilgang bibliotekets øvrige standardarbejde, inklusive validering af objekt- og krydsreferencestreams, før en fil nogensinde når preflight, og den delt-objekt-disciplin bag genbrugelige sidestempler med Form XObjects, der overhovedet gør et dokument RIP-venligt. PDF/VT- og PDF/X-gemme- og valideringsAPI'erne beskrevet her er en del af PDFium VCL-komponenten til Delphi og C++Builder, hvis produktside indeholder den fulde overensstemmelsesreference