PDFlibPas implementeert PDF/VCR-1 als een getagd structuurprofiel: de vervangingsdefinitie leeft onder StructTreeRoot als een directe kindertak, waarbij een template-wortel /O /GTS_Template draagt om de veldset te declareren en elke placeholder /O /GTS_Replacement draagt om één dataveld te binden. Niets ervan is een dictionary op paginaniveau
Die ene zin is het deel waar de meeste implementaties de fout ingaan, en de fout kost meer dan een mislukte validatie. Een variable-data-job waarvan het vervangingsplan verkeerd wordt gelezen crasht niet. Hij print, op volume, met het verkeerde veld in het verkeerde vakje, en de eerste die het merkt is de klant met vijftigduizend mailings in handen
Waar de vervangingsdefinitie werkelijk woont
PDF/VCR-1 plaatst zijn vervangingsmodel in de logische structuurhiërarchie die ISO 32000-1 §14.7 beschrijft, niet in een privé-pagina dictionary. De template-wortel is een structuurelement dat direct aan StructTreeRoot hangt, herkenbaar aan een attribute owner van GTS_Template, en hij declareert de hele veldvocabulaire op één plek via GTS_Fields plus een optionele GTS_Pages die het veld noemt dat de paginaselectie aanstuurt
Placeholders zijn structuurelementen die binnen die wortel genest zitten, met attribute owner GTS_Replacement. Elk draagt een GTS_Generator (die PDFlibPas op PassThrough vereist), een GTS_Data-waarde die exact één gedeclareerd veld noemt en een optionele GTS_BBox. De binding aan de pagina is ook geen pagina-annotatie: het element wijst via /Pg naar zijn pagina en via /K naar de marked-content-sequence. De keuze voor structuur boven een pagina-dictionary brengt dan een consequentie mee die makkelijk te stellen en makkelijk te onderschatten is: de structuurboom is een algemene graaf die vanuit de catalog bereikt wordt, dus alles wat het profiel consumeert moet een boomwandelaar zijn met alle gevaren van dien, in plaats van een dictionary-lezer. Heb je nog niet met de structuurboom gewerkt, dan liggen de mechanieken uiteen in getagde PDF-structuur en toegankelijkheid
Waarom laat het checken van de eerste top-level tak slechte bestanden door?
Omdat StructTreeRoot veel directe kinderen kan hebben, en een validator die stopt na het lokaliseren van de template-wortel de rest nooit ziet. Dat is precies het falen dat PDFlibPas is gebouwd om te vermijden, en het produceert exact het soort false pass dat erger is dan helemaal geen check: een bestand dat conformant rapporteert en een tweede template-wortel bevat, of een placeholder die buiten de template-tak zit waar geen enkele consumer hem honoreert
Daarom draait ValidatePDFVCRStructure in PDFlibPas twee passes over dezelfde boom. De eerste pass somt elk direct kind van StructTreeRoot op en telt de exemplaren die GTS_Template dragen; iets anders dan exact één wordt geweigerd voordat er één placeholder is gelezen. De tweede pass daalt af in alle top-level takken, niet alleen de template-tak, en wijst twee dingen tegelijk af: een genest element dat GTS_Template draagt maar niet de wortel is, en elk GTS_Replacement-element dat buiten de template-tak wordt gevonden
De traversal is begrensd op 64 niveaus diepte en 1.000.000 knopen, en hij registreert het object- en generatienummer van elk bezocht element zodat een cyclus of een herhaalde referentie wordt gerapporteerd in plaats van gevolgd. Die getallen zijn geen prestatieafstemming. Een structuurboom is een objectgraaf die een aanvaller controleert, en een validator die zonder plafond recursieert is een stack overflow met een bestandsextensie
Een template auteuren, en de constraints die de API weigert te buigen
PDFlibPas stelt auteuren bloot via een bracketing-API die de structuurregels bij het schrijven afdwingt in plaats van ze bij het saven te ontdekken. SetPDFVCRMode schakelt het document naar het profiel, wat intern de PDF/X-4-basismode van ISO 15930-7 vereist en de mark-info-flag aanzet, want een PDF/VCR-1-bestand zonder marked content heeft niets waar een placeholder naar kan wijzen. Van daaruit neemt BeginPDFVCRTemplate de veldlijst en het paginaselectieveld, en hij is streng op manieren die de moeite waard zijn om te kennen voordat je je dataschema ontwerpt. Veldnamen gaan door PDFVCRValidFieldName, dat alleen ASCII-letters, cijfers, underscore, koppelteken en punt accepteert, want dat zijn de tekens die zonder escaping-ambiguïteit in een PDF-name serialiseren. PDFVCRNormalizeFieldList houdt de lijst hoofdlettergevoelig en wijst duplicaten botweg af, en het paginaselectieveld moet zelf in de gedeclareerde lijst voorkomen
var
Lib: TPDFlib;
Diag: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
if Lib.SetPDFVCRMode(1) <> 1 then
raise Exception.Create('PDF/VCR mode was refused');
Lib.NewPage;
if Lib.BeginPDFVCRTemplate('Salutation,FullName,AccountNo',
'AccountNo') <> 1 then
raise Exception.Create('Field list rejected');
// Eén placeholder bindt exact één gedeclareerd veld.
Lib.BeginPDFVCRPlaceholder('FullName', 72, 690, 320, 712);
Lib.DrawText(72, 694, 'SAMPLE NAME');
Lib.EndPDFVCRPlaceholder;
Lib.EndPDFVCRTemplate;
Diag := Lib.GetPDFVCRDiagnostics;
if Diag <> '' then
begin
Writeln('PDF/VCR blockers: ', Diag);
Exit;
end;
Lib.SaveToFile('campaign-template.pdf');
finally
Lib.Free;
end;
end;
Let op wat de bracketing je oplevert. BeginPDFVCRPlaceholder weigert te openen tenzij een template actief is en de tag-stack exact één niveau diep is, zo garandeert PDFlibPas dat placeholders directe kinderen van de template-wortel zijn en nooit in elkaar nesten. Coördinaten worden ook gevalideerd, met afwijzing van NaN, oneindig en geïnverteerde rechthoeken, dus een slechte bounding box faalt bij de aanroep in plaats van in een preflight-rapport een week later
Waarom heeft een placeholder zijn eigen MCID-serialisatiepad nodig?
Omdat PDF/VCR-1 wil dat /K een kaal integer is, en dat is niet hoe een algemene tagged-PDF-writer de link tussen een structuurelement en zijn content serialiseert. Onder ISO 32000-1 §14.6 refereert een structuurelement marked content normaal gesproken ofwel via een direct MCID-integer, wanneer het element zijn pagina al via /Pg identificeert, ofwel via een marked-content-referentie-dictionary dat de pagina expliciet noemt. Een library die voor veel doeleinden getagde output emitteert neigt naar de dictionary-vorm te standaardiseren omdat die beter over pagina's heen composeert
PDFlibPas koos hier de smalle optie in plaats van de algemene. SetCurrentStructElemDirectMCIDKid flagt het huidige structuurelement, en alleen elementen die die flag dragen serialiseren /K als een direct MCID met de pagina op /Pg; al het andere houdt het bestaande MCR-dictionary-pad onaangeroerd. Dit is een bewuste afweging en het soort dat je hardop moet benoemen: een profiel toevoegen moet de bytes niet veranderen die elk ander getagd document al produceert, dus het profiel krijgt een eigen pad in plaats van een globale schakelaar. Elk bestaand getagd bestand houdt zijn vorige serialisatie, en het regressieoppervlak blijft zo groot als het nieuwe feature
Eén traversal, twee consumers
PDFlibPas leidt het vervangingsmanifest en het compliance-verdict af uit dezelfde twee-pass wandeling, en daarom mag je het manifest als jobplan vertrouwen. De traversal vult tijdens het valideren een TPDFVCRManifestData-record: Fields uit de template-wortel, PageSelectionField wanneer er één is gedeclareerd, en een Placeholders-array van TPDFVCRPlaceholderInfo-entries met FieldName, PageNumber, MCID, HasBBox en BBox. Het compliance-ingangspunt is CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), en het draait zijn checks in afhankelijkheidsvolgorde in plaats van allemaal tegelijk: eerst de volledige PDF/X-check, dan GTS_PDFXVersion tegen PDF/X-4, dan GTS_PDFVCRVersion gelezen uit de http://www.npes.org/pdfvcr/ns/id/-namespace, en pas daarna de structuurwandeling. Options als 1 doorgeven laat hem bij het eerste probleem stoppen, wat je wilt in een batch-gate; 0 doorgeven verzamelt alles, wat je wilt in een rapport
// Een ontvangen template verifiëren in plaats van een zelf geproduceerde
var
Issues: TStringList;
Stream: TFileStream;
I: Integer;
begin
Issues := TStringList.Create;
Stream := TFileStream.Create('supplier-template.pdf',
fmOpenRead or fmShareDenyWrite);
try
if CheckCompliancePDFVCR(Stream, '', 0, Issues) <> 100 then
for I := 0 to Issues.Count - 1 do
Writeln('PDF/VCR: ', Issues[I]);
finally
Stream.Free;
Issues.Free;
end;
end;
Het manifest is een contract, dus het bestaat alleen voor conformante bestanden
BuildPDFVCRManifest geeft alleen JSON terug wanneer de issue-lijst leeg terugkomt, en anders een lege string. Dat is geen defensief coderen om het coderen. Een manifest wordt geconsumeerd door een compositie-engine die er echte output uit gaat sturen, en een gedeeltelijke veldlijst teruggeven die uit een bestand met twee template-wortels is gehaald nodigt de engine uit een niet-conformante structuur als een betrouwbaar vervangingsplan te behandelen. Via de document-API wikkelt GetPDFVCRManifest(InputFileName, Password) dezelfde aanroep en zet hij de laatste errorcode op PDFLIB_ERROR_PDFVCR_COMPLIANCE (608) wanneer het bestand niet kwalificeert, zodat een mislukte extractie en een lege template van elkaar te onderscheiden zijn. De JSON rapporteert fields, pageSelectionField als string of null, en een placeholders-array waarvan de entries field, page, mcid en ofwel een vier-getallige bbox ofwel null geven
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608 betekent dat het bestand niet PDF/VCR-1-conformant is,
// 401 betekent dat het bestand niet kon worden geopend
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Eén detail om mee te nemen naar je consumer: de page-waarde volgt de library-brede zero-based pagina-conventie, niet de one-based nummering die een mens in een viewer ziet. Die door elkaar halen is een stille off-by-one die content op het verkeerde vel laat landen, en geen enkele validator vangt hem omdat beide getallen structureel valide zijn
Waar de grenzen liggen
PDF/VCR-1-ondersteuning in PDFlibPas dekt template-auteuren, de save-gate, standalone preflight en manifestextractie. Het voert de vervanging niet uit. Data in placeholders substitueren en de gepersonaliseerde instanties produceren is de compositiestap, en die woont bewust buiten de library, want het manifest is precies de interface die een compositie-engine onafhankelijk laat schrijven. De save-gate weigert ook liever dan te repareren. Anders dan bij profielen waar een verkeerde annotatie-flag stilletjes genormaliseerd kan worden, is elke PDF/VCR-1-constraint die PDFVCRReadyForSave handhaaft er één waar gokken zou veranderen wat het document betekent: een template-wortel verzinnen, of kiezen welk veld een ongebonden placeholder hoort, produceert een bestand dat valideert en liegt. Dezelfde repair-versus-refuse-lijn wordt voor een ander profiel besproken in de PDF/E-1 authormode en bounded preflight, en de workflow om meerdere conformance-checks over één document te draaien staat in de compliance- en signing-workbench
Auteuren, de begrensde structuurwandeling, de save-gate en de manifest-extractor zitten allemaal in de PDFlibPas Delphi PDF-library, wat betekent dat een template onder het profiel geproduceerd en daarna via een apart codepad geverifieerd kan worden dat de writer-state nooit zag, en die scheiding is de enige reden dat een conformance-claim iets waard is