PDFlibPas implementuje PDF/VCR-1 jako profil tagované struktury: definice náhrady žije pod StructTreeRoot jako větev přímého potomka, kde kořen šablony nese /O /GTS_Template pro deklaraci množiny polí a každý placeholder nese /O /GTS_Replacement pro navázání jednoho datového pole. Nic z toho není slovník na úrovni stránky
Právě tahle jediná věta je část, kterou většina implementací pokazí, a pokazit ji stojí víc než neprošlá validace. Variabilní datová zakázka, jejíž plán náhrady se špatně přečte, nespadne. Vytiskne se, ve velkém, se špatným polem v nesprávném okénku, a první, kdo si toho všimne, je zákazník držící padesát tisíc dopisů
Kde definice náhrady skutečně žije
PDF/VCR-1 umisťuje svůj model náhrady do hierarchie logické struktury popsané v ISO 32000-1 §14.7, ne do soukromého slovníku stránky. Kořen šablony je strukturní element visící přímo na StructTreeRoot, odlišený vlastníkem atributu GTS_Template, a na jednom místě deklaruje celou slovní zásobu polí přes GTS_Fields plus volitelné GTS_Pages pojmenovávající pole, které řídí výběr stránky
Placeholdery jsou strukturní elementy vnořené dovnitř toho kořene s vlastníkem atributu GTS_Replacement. Každý nese GTS_Generator (který PDFlibPas vyžaduje jako PassThrough), hodnotu GTS_Data pojmenovávající přesně jedno deklarované pole a volitelný GTS_BBox. Navázání na stránku také není anotace stránky: element ukazuje na svou stránku přes /Pg a na sekvenci označeného obsahu přes /K. Volba struktury místo slovníku stránky pak nese důsledek, který se snadno vysloví a snadno podcení: strom struktury je obecný graf dosažitelný z katalogu, takže cokoli profil konzumuje, musí být procházeč stromu se všemi riziky, která to obnáší, ne čtečka slovníku. Pokud jste se strukturním stromem ještě nepracovali, mechanismy jsou vyloženy v tagované PDF struktuře a přístupnosti
Proč kontrola první větve nejvyšší úrovně propustí špatné soubory?
Protože StructTreeRoot může mít mnoho přímých potomků a validátor, který se zastaví po nalezení kořene šablony, ty ostatní nikdy nevidí. Tohle je přesně ten druh selhání, kterému se PDFlibPas staví vyhnout, a produkuje přesně ten typ falešného průchodu, který je horší než žádná kontrola: soubor, který hlásí shodu a obsahuje druhý kořen šablony, nebo placeholder sedící mimo větev šablony, kde ho žádný konzument neuzná
Takže ValidatePDFVCRStructure v PDFlibPas běží přes stejný strom dvěma průchody. První průchod vyjmenuje každého přímého potomka StructTreeRoot a spočítá ty s GTS_Template; cokoli jiného než přesně jeden je odmítnuto dřív, než se přečte jediný placeholder. Druhý průchod sestupuje do všech větví nejvyšší úrovně, ne jen té šablonové, a odmítá dvě věci najednou: vnořený element s GTS_Template, který není kořenem, a jakýkoli element GTS_Replacement nalezený mimo větev šablony
Procházení je omezeno na 64 úrovní hloubky a 1 000 000 uzlů a zaznamenává číslo objektu a generace každého navštíveného elementu, takže cyklus nebo opakovaný odkaz se nahlásí, místo aby se následoval. Tato čísla nejsou ladění výkonu. Strom struktury je graf objektů, který řídí útočník, a validátor rekurzující bez stropu je přetečení zásobníku s příponou souboru
Tvorba šablony a omezení, která API odmítá ohnout
PDFlibPas vystavuje tvorbu přes bracketing API, které vynucuje strukturální pravidla při zápisu, místo aby je objevovalo při ukládání. SetPDFVCRMode přepne dokument do profilu, který interně vyžaduje základní režim PDF/X-4 z ISO 15930-7 a zapíná příznak mark-info, protože soubor PDF/VCR-1 bez označeného obsahu nemá nic, na co by placeholder mohl ukazovat. Odtud BeginPDFVCRTemplate bere seznam polí a pole pro výběr stránky a je přísný způsobem, který stojí za to znát, než navrhnete své datové schéma. Jména polí procházejí PDFVCRValidFieldName, které přijímá jen ASCII písmena, číslice, podtržítko, spojovník a tečku, protože to jsou znaky, které se serializují do PDF jména bez nejednoznačnosti escapování. PDFVCRNormalizeFieldList drží seznam rozlišující velikost písmen a duplicity rovnou odmítá a pole pro výběr stránky se samo musí objevit v deklarovaném seznamu
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');
// Jeden placeholder váže přesně jedno deklarované pole.
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;
Všimněte si, co vám bracketing kupuje. BeginPDFVCRPlaceholder odmítne otevřít, pokud není aktivní šablona a zásobník tagů není přesně jednu úroveň hluboký, což je způsob, jakým PDFlibPas zaručuje, že placeholdery jsou přímými potomky kořene šablony a nikdy se do sebe nevnofují. Validují se i souřadnice a odmítají se NaN, nekonečno a převrácené obdélníky, takže špatný bounding box selže u volání, ne v preflight zprávě o týden později
Proč placeholder potřebuje vlastní cestu serializace MCID?
Protože PDF/VCR-1 chce /K jako holé celé číslo, a takhle obecný zapisovač tagovaného PDF serializuje propojení mezi strukturním elementem a jeho obsahem. Podle ISO 32000-1 §14.6 strukturní element běžně odkazuje na označený obsah buď přímým celočíselným MCID, když element už identifikuje svou stránku přes /Pg, nebo přes referenční slovník označeného obsahu, který stránku pojmenovává výslovně. Knihovna, která emituje tagovaný výstup pro mnoho účelů, má tendenci sjednotit se na slovníkové podobě, protože se lépe skládá napříč stránkami
PDFlibPas tu zvolil úzkou možnost místo obecné. SetCurrentStructElemDirectMCIDKid označí aktuální strukturní element a jen elementy s tímto označením serializují /K jako přímé MCID se stránkou nesenou na /Pg; všechno ostatní nechává stávající cestu MCR slovníku nedotčenou. Je to záměrný kompromis a je to ten typ, který stojí za to říct nahlas: přidání profilu by nemělo změnit bajty výstupu, který už produkuje každý jiný tagovaný dokument, takže profil dostává vlastní cestu místo globálního přepínače. Každý existující tagovaný soubor si drží svou předchozí serializaci a regresní plocha zůstává velká jako nová funkce
Jedno procházení, dva konzumenti
PDFlibPas odvozuje manifest náhrady i verdikt o shodě ze stejného dvouprůchodového procházení, a proto lze manifestu věřit jako plánu zakázky. Procházení plní záznam TPDFVCRManifestData, zatímco validuje: Fields z kořene šablony, PageSelectionField, když je deklarováno, a pole Placeholders s položkami TPDFVCRPlaceholderInfo nesoucími FieldName, PageNumber, MCID, HasBBox a BBox. Vstupním bodem shody je CheckCompliancePDFVCR(InputStream, Password, Options, ResultList) a své kontroly spouští v pořadí závislostí, ne všechny najednou: nejdřív plnou kontrolu PDF/X, pak GTS_PDFXVersion proti PDF/X-4, pak GTS_PDFVCRVersion čtené z namespace http://www.npes.org/pdfvcr/ns/id/ a teprve pak procházení struktury. Předání Options jako 1 zastaví na prvním problému, což chcete v dávkové bráně; předání 0 nasbírá všechno, což chcete ve zprávě
// Ověřte šablonu, kterou jste dostali, ne kterou jste vyrobili
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;
Manifest je smlouva, takže existuje jen pro vyhovující soubory
BuildPDFVCRManifest vrací JSON jen když se seznam problémů vrátí prázdný a jinak vrací prázdný řetězec. To není defenzivní kódování samo pro sebe. Manifest konzumuje kompoziční engine, který z něj bude řídit skutečný výstup, a vracet částečný seznam polí vytěžený ze souboru se dvěma kořeny šablon zve engine k tomu, aby nevyhovující strukturu bral jako důvěryhodný plán náhrady. Přes dokumentové API GetPDFVCRManifest(InputFileName, Password) obaluje stejné volání a nastaví poslední chybový kód na PDFLIB_ERROR_PDFVCR_COMPLIANCE (608), když soubor nevyhovuje, takže selhané vytěžení a prázdná šablona jsou rozlišitelné. JSON hlásí fields, pageSelectionField jako řetězec nebo null a pole placeholders, jehož položky dávají field, page, mcid a buď čtyřčíselný bbox, nebo null
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608 znamená, že soubor není shodný s PDF/VCR-1,
// 401 znamená, že soubor nešlo otevřít
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Jeden detail, který si odnést do svého konzumenta: hodnota page se řídí celoknihovní konvencí stránek číslovaných od nuly, ne číslováním od jedničky, které vidí člověk v prohlížeči. Záměna je tichá off-by-one chyba, která umístí obsah na špatný list, a žádný validátor ji nechytí, protože obě čísla jsou strukturálně platná
Kde jsou hranice
Podpora PDF/VCR-1 v PDFlibPas pokrývá tvorbu šablon, save gate, samostatný preflight a vytěžení manifestu. Samotnou náhradu neprovádí. Dosazování dat do placeholderů a výroba personalizovaných instancí je kompoziční krok a záměrně žije mimo knihovnu, protože manifest je přesně to rozhraní, které umožňuje napsat kompoziční engine nezávisle. Save gate také odmítá, místo aby opravoval. Na rozdíl od profilů, kde se špatný příznak anotace dá potichu normalizovat, každé omezení PDF/VCR-1, které PDFVCRReadyForSave vynucuje, je takové, kde by hádání změnilo, co dokument znamená: vymyslet kořen šablony nebo vybrat, ke kterému poli patří nenavázaný placeholder, vyrobí soubor, který validuje a lže. Stejná linie opravit versus odmítnout je probrána pro jiný profil v autorském režimu PDF/E-1 a ohraničeném preflightu a pracovní postup pro spuštění několika kontrol shody nad jedním dokumentem je popsán v pracovní ploše pro shodu a podepisování
Tvorba, ohraničené procházení struktury, save gate a extraktor manifestu jsou všechny součástí PDFlibPas Delphi PDF knihovny, což znamená, že šablonu lze vyrobit pod profilem a pak ověřit přes samostatnou cestu kódu, která nikdy neviděla stav zapisovače, a právě toto oddělení je jediný důvod, proč má tvrzení o shodě nějakou hodnotu