PDFlibPas implementuje PDF/VCR-1 jako profil struktury tagowanej: definicja zamiany żyje pod StructTreeRoot jako bezpośrednia gałąź-dziecko, gdzie korzeń szablonu niesie /O /GTS_Template, żeby zadeklarować zestaw pól, a każdy placeholder niesie /O /GTS_Replacement, żeby związać jedno pole danych. Nic w tym nie jest słownikiem na poziomie strony
To pojedyncze zdanie to część, którą większość implementacji psuje, a spudłowanie kosztuje więcej niż nieudana walidacja. Zadanie variable-data z błędnie odczytanym planem zamiany nie crashuje. Drukuje się, na wielką skalę, ze złym polem w złym polu tekstowym, a pierwszą osobą, która to zauważy, jest klient trzymający pięćdziesiąt tysięcy mailerów
Gdzie faktycznie żyje definicja zamiany
PDF/VCR-1 umieszcza swój model zamiany w hierarchii struktury logicznej opisanej w ISO 32000-1 §14.7, nie w prywatnym słowniku strony. Korzeń szablonu to element struktury zwisający bezpośrednio z StructTreeRoot, wyróżniony właścicielem atrybutu GTS_Template, i deklaruje całe słownictwo pól w jednym miejscu przez GTS_Fields plus opcjonalne GTS_Pages nazywające pole sterujące wyborem strony
Placeholdery to elementy struktury zagnieżdżone wewnątrz tego korzenia, z właścicielem atrybutu GTS_Replacement. Każdy niesie GTS_Generator (który PDFlibPas wymaga, by był PassThrough), wartość GTS_Data nazywającą dokładnie jedno zadeklarowane pole oraz opcjonalne GTS_BBox. Powiązanie ze stroną też nie jest adnotacją strony: element wskazuje swoją stronę przez /Pg, a sekwencję marked-content przez /K. Wybór struktury zamiast słownika strony niesie konsekwencję łatwą do wypowiedzenia i łatwą do niedoszacowania: drzewo struktury to ogólny graf osiągalny z katalogu, więc wszystko, co konsumuje profil, musi być tree walkerem ze wszystkimi wynikającymi z tego zagrożeniami, a nie czytnikiem słownika. Jeśli nie pracowałeś wcześniej z drzewem struktury, mechanika jest rozłożona w strukturze tagowanej PDF i dostępności
Dlaczego sprawdzenie tylko pierwszej gałęzi najwyższego poziomu przepuszcza złe pliki?
Bo StructTreeRoot może mieć wiele bezpośrednich dzieci, a walidator, który zatrzymuje się po zlokalizowaniu korzenia szablonu, nigdy nie widzi reszty. To konkretna porażka, której uniknięcie jest celem PDFlibPas, i produkuje dokładnie ten rodzaj fałszywego zaliczenia, który jest gorszy niż żadne sprawdzenie: plik raportowany jako zgodny, zawierający drugi korzeń szablonu albo placeholder siedzący poza gałęzią szablonu, gdzie żaden konsument go nie uhonoruje
Więc ValidatePDFVCRStructure w PDFlibPas przechodzi dwa przebiegi po tym samym drzewie. Pierwszy przebieg wylicza każde bezpośrednie dziecko StructTreeRoot i liczy te z GTS_Template; cokolwiek innego niż dokładnie jeden jest odrzucane, zanim przeczytany zostanie jakikolwiek placeholder. Drugi przebieg schodzi do wszystkich gałęzi najwyższego poziomu, nie tylko szablonowej, i odrzuca dwie rzeczy naraz: zagnieżdżony element z GTS_Template, który nie jest korzeniem, oraz każdy element GTS_Replacement znaleziony poza gałęzią szablonu
Przechodzenie jest ograniczone do 64 poziomów głębokości i 1 000 000 węzłów, a przy tym rejestruje numer obiektu i generacji każdego odwiedzanego elementu, tak że cykl albo powtórzone odwołanie jest raportowane, a nie podążane. Te liczby to nie strojenie wydajności. Drzewo struktury to graf obiektów kontrolowany przez atakującego, a walidator rekurencyjny bez sufitu to stack overflow z rozszerzeniem pliku
Autorstwo szablonu i ograniczenia, których API nie zgina
PDFlibPas wystawia autorstwo przez API z nawiasowaniem, które egzekwuje reguły strukturalne w momencie zapisu, zamiast odkrywać je w momencie zapisu do pliku. SetPDFVCRMode przełącza dokument do profilu, co wewnętrznie wymaga bazowego trybu PDF/X-4 z ISO 15930-7 i włącza flagę mark-info, bo plik PDF/VCR-1 bez marked content nie ma niczego, na co placeholder mógłby wskazywać. Stamtąd BeginPDFVCRTemplate przyjmuje listę pól i pole wyboru strony, a jest restrykcyjna na sposoby, które warto znać, zanim zaprojektujesz swój schemat danych. Nazwy pól przechodzą przez PDFVCRValidFieldName, która przyjmuje tylko litery ASCII, cyfry, podkreślnik, łącznik i kropkę, bo to znaki serializujące się do nazwy PDF bez dwuznaczności escapowania. PDFVCRNormalizeFieldList trzyma listę z zachowaniem wielkości liter i odrzuca duplikaty wprost, a pole wyboru strony musi samo pojawić się na zadeklarowanej liście
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 wiąże dokładnie jedno zadeklarowane 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;
Zauważ, co daje ci to nawiasowanie. BeginPDFVCRPlaceholder odmawia otwarcia, dopóki szablon nie jest aktywny, a stos tagów ma dokładnie jeden poziom głębokości — tak PDFlibPas gwarantuje, że placeholdery są bezpośrednimi dziećmi korzenia szablonu i nigdy nie zagnieżdżają się w sobie nawzajem. Współrzędne też są walidowane, odrzucając NaN, nieskończoność i odwrócone prostokąty, więc zły bounding box pada przy wywołaniu, a nie w raporcie preflight tydzień później
Dlaczego placeholder potrzebuje własnej ścieżki serializacji MCID?
Bo PDF/VCR-1 chce, żeby /K było gołym integerem, a tak ogólny writer tagowanego PDF nie serializuje powiązania między elementem struktury a jego treścią. W myśl ISO 32000-1 §14.6 element struktury normalnie odwołuje się do marked content albo przez bezpośredni integer MCID, gdy element już identyfikuje swoją stronę przez /Pg, albo przez słownik-odwołanie do marked content nazywający stronę jawnie. Biblioteka emitująca tagowane wyjście do wielu celów ma tendencję do ujednolicenia formy słownikowej, bo lepiej komponuje się między stronami
PDFlibPas wybrał tu opcję wąską zamiast ogólnej. SetCurrentStructElemDirectMCIDKid oznacza aktualny element struktury, a tylko elementy z tym oznaczeniem serializują /K jako bezpośredni MCID ze stroną niesioną na /Pg; wszystko inne trzyma istniejącą ścieżkę słownika MCR nietkniętą. To świadomy kompromis i taki, który warto nazwać głośno: dodanie profilu nie powinno zmieniać bajtów wyjścia, które każdy inny tagowany dokument już produkuje, więc profil dostaje własną ścieżkę zamiast globalnego przełącznika. Każdy istniejący tagowany plik zachowuje swoją poprzednią serializację, a powierzchnia regresji zostaje wielkości nowej funkcji
Jedno przejście, dwóch konsumentów
PDFlibPas wyprowadza manifest zamiany i werdykt zgodności z tego samego dwuprzebiegowego przejścia, dlatego manifestowi można ufać jako planowi zadania. Przechodzenie wypełnia rekord TPDFVCRManifestData podczas walidacji: Fields z korzenia szablonu, PageSelectionField, gdy jedno jest zadeklarowane, oraz tablicę Placeholders wpisów TPDFVCRPlaceholderInfo niosących FieldName, PageNumber, MCID, HasBBox i BBox. Punktem wejścia zgodności jest CheckCompliancePDFVCR(InputStream, Password, Options, ResultList), który odpala swoje sprawdzenia w kolejności zależności, a nie wszystkie naraz: najpierw pełne sprawdzenie PDF/X, potem GTS_PDFXVersion względem PDF/X-4, potem GTS_PDFVCRVersion czytane z przestrzeni nazw http://www.npes.org/pdfvcr/ns/id/, a dopiero na końcu przejście po strukturze. Przekazanie Options jako 1 zatrzymuje go na pierwszym problemie, czego chcesz w bramie wsadowej; przekazanie 0 zbiera wszystko, czego chcesz w raporcie
// Zweryfikuj szablon, który otrzymałeś, a nie wyprodukowałeś
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 to kontrakt, więc istnieje tylko dla plików zgodnych
BuildPDFVCRManifest zwraca JSON tylko wtedy, gdy lista problemów wraca pusta, a w przeciwnym razie zwraca pusty string. To nie obronne kodowanie samo dla siebie. Manifest jest konsumowany przez silnik kompozycji, który będzie z niego napędzał prawdziwe wyjście, a oddanie częściowej listy pól wydobytej z pliku z dwoma korzeniami szablonu zaprasza silnik do traktowania niezgodnej struktury jako godnego zaufania planu zamiany. W API dokumentowym GetPDFVCRManifest(InputFileName, Password) opakowuje to samo wywołanie i ustawia ostatni kod błędu na PDFLIB_ERROR_PDFVCR_COMPLIANCE (608), gdy plik się nie kwalifikuje, więc nieudana ekstrakcja i pusty szablon są rozróżnialne. JSON raportuje fields, pageSelectionField jako string albo null oraz tablicę placeholders, której wpisy dają field, page, mcid i albo czteroliczbowy bbox, albo null
var
Lib: TPDFlib;
Manifest: WideString;
begin
Lib := TPDFlib.Create;
try
Manifest := Lib.GetPDFVCRManifest('campaign-template.pdf', '');
if Manifest = '' then
// 608 znaczy, że plik nie jest zgodny z PDF/VCR-1,
// 401 znaczy, że plik nie mógł zostać otwarty
Writeln('No manifest, error ', Lib.LastErrorCode)
else
Writeln(Manifest);
finally
Lib.Free;
end;
end;
Jeden szczegół do przeniesienia do twojego konsumenta: wartość page trzyma się bibliotecznej konwencji stron liczonej od zera, nie numeracji od jedynki, którą człowiek widzi w przeglądarce. Pomylenie tych dwóch to cichy off-by-one lądujący z treścią na złym arkuszu, a żaden walidator go nie złapie, bo obie liczby są strukturalnie poprawne
Gdzie są granice
Wsparcie PDF/VCR-1 w PDFlibPas pokrywa autorstwo szablonów, bramę zapisu, samodzielny preflight i ekstrakcję manifestu. Nie wykonuje zamiany. Podstawianie danych do placeholderów i produkcja spersonalizowanych instancji to krok kompozycji, i celowo żyje poza biblioteką, bo manifest to właśnie interfejs, który pozwala napisać silnik kompozycji niezależnie. Brama zapisu też odmawia zamiast naprawiać. Inaczej niż w profilach, gdzie złą flagę adnotacji można po cichu znormalizować, każde ograniczenie PDF/VCR-1 egzekwowane przez PDFVCRReadyForSave jest takie, że zgadywanie zmieniłoby znaczenie dokumentu: wymyślenie korzenia szablonu albo wybranie, do którego pola należy niepowiązany placeholder, produkuje plik, który waliduje się i kłamie. Ta sama linia naprawa-kontra-odmowa jest omówiona dla innego profilu w trybie autora PDF/E-1 i ograniczonym preflight, a workflow uruchamiania kilku kontroli zgodności nad jednym dokumentem jest pokryte w workbench zgodności i podpisywania
Autorstwo, ograniczone przejście po strukturze, brama zapisu i ekstraktor manifestu — wszystko to jest w bibliotece PDFlibPas Delphi PDF, co znaczy, że szablon może zostać wyprodukowany pod profilem, a potem zweryfikowany przez osobną ścieżkę kodu, która nigdy nie widziała stanu writera, i ta separacja to jedyny powód, dla którego roszczenie o zgodność jest w ogóle coś warte