Artykuł techniczny

Druk danych zmiennych PDF/VT w Delphi z PDFium VCL

Drukarnia transakcyjna odsyła Twój przebieg wyciągów liczący 80 000 stron z jednozdaniowym odrzuceniem: „not PDF/VT, RIP cannot cache”. Plik otwiera się poprawnie w każdym viewerze na biurku, kolory są dobre, dane scaliły się prawidłowo. Tyle że nic z tego nie jest tym, o co prosiła prasa cyfrowa. Wysokowydajny druk danych zmiennych żyje albo umiera od tego, czy maszyna rozpozna, że blok z logo klienta na stronie 1 jest bajt w bajt tym samym obiektem co blok na stronie 40 000, wyrenderuje go raz i wykorzysta ponownie. PDF/VT to standard, który czyni tę obietnicę sprawdzalną maszynowo, a „wygląda poprawnie” jest dokładnie pułapką, bo struktura odczytywana przez RIP pozostaje niewidoczna na ekranie

PDFiumPas udostępnia tę strukturę przez niewielką powierzchnię na TPdf: SaveAsPdfVT ją zapisuje, a ValidatePdfVT ją sprawdza. Ten artykuł wyjaśnia, co te dwie metody faktycznie zapisują na dysk i co kontrolują, gdzie ISO 16612-2 okazuje się bardziej rygorystyczna, niż początkowo wygląda, oraz które elementy są uczciwymi kotwicami strukturalnymi, a nie pełnym preflight, za który można rozliczać klienta

Co standaryzuje PDF/VT i dlaczego najpierw pojawia się PDF/X

PDF/VT, ISO 16612-2:2010, nie jest nowym formatem pliku. To warstwa metadanych optymalizacyjnych dokręcona do pliku PDF/X i ta kolejność ma znaczenie konstrukcyjne. Standard definiuje trzy poziomy zgodności, ale tylko dwa z nich nazywają plik PDF: PDF/VT-1, czyli pojedynczy samowystarczalny dokument, oraz PDF/VT-2, czyli model zestawu plików, w którym strony odwołują się do współdzielonych zasobów zewnętrznych. Trzeci token, jaki możesz zobaczyć, PDF/VT-2s, w ogóle nie jest wartością na poziomie pliku; żyje w nagłówku strumienia MIME opisanym w Annex A. Jeśli znajdziesz kod wbijający GTS_PDFVTVersion = "PDF/VT-2s" do XMP dokumentu, ten kod jest błędny

Nienegocjowalną regułą dla pojedynczego pliku jest baza PDF/X. ISO 16612-2 §6.2.1 wymaga, aby każdy plik PDF/VT-1 był jednocześnie poprawnym plikiem PDF/X-4. Zestaw plików PDF/VT-2, zgodnie z §6.2.2, musi z kolei opierać się na PDF/X-4p, PDF/X-5g albo PDF/X-5pg. Właśnie dlatego writer PDF/VT nie może po prostu dopisać kilku kluczy identyfikacyjnych: musi nieść ze sobą cały zestaw znaczników PDF/X-4, co oznacza OutputIntent, osadzony docelowy profil ICC, pasujące wpisy XMP i Info dokumentu, trailer /ID i brak szyfrowania. Pomiń którykolwiek z tych elementów, a otrzymasz plik deklarujący PDF/VT i oblewający w chwili, gdy zgodny odbiorca sprawdzi bazę. PDFiumPas traktuje warstwę PDF/X-4 jako część zapisu PDF/VT, więc nie wywołujesz osobnego SaveAsPdfX najpierw; injector zapisuje obie warstwy w jednym przebiegu

Zapisywanie pliku przez SaveAsPdfVT

Minimalne wywołanie nie potrzebuje niczego poza aktywnym dokumentem, ponieważ TPdfVTSaveOptions.Default dostarcza wbudowany profil ICC sRGB oraz zgodność pvc1. Zapis wykonuje wewnętrznie trzy kroki: usuwa wszelkie zabezpieczenia, bo wstrzykiwanie znaczników plaintext do zaszyfrowanego strumienia obiektów uszkodziłoby plik, spina istniejący słownik Info dokumentu i trailer /ID z zestawem znaczników tak, aby wartości XMP i Info były zgodne, a następnie dopisuje obiekty PDF/X-4 i PDF/VT przez incremental update

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;

W prawdziwym wyjściu produkcyjnym prawie zawsze chcesz nadpisać OutputIntent charakterystyką swojej maszyny drukarskiej, a nie zostawiać ogólnego fallbacku sRGB. Dostarcz bajty ICC i identyfikatory warunku przez 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;

Jeden szczegół w tym fragmencie jest celową barierką ochronną, a nie ograniczeniem, z którym można się spierać. Ustawienie Opt.Conformance := pvc2 nie tworzy pliku PDF/VT-2. Writer normalizuje każde żądanie inne niż pvc1 z powrotem do pvc1, ponieważ PDF/VT-2 jest formatem zestawu plików, a writer pojedynczego pliku, dopisujący jeden dokument wyjściowy, fizycznie nie może złożyć zestawu zewnętrznych zasobów wymaganego przez §6.2.2. Wartość pvc2 istnieje dla ścieżki odczytu, żeby ValidatePdfVT mogło rozpoznać i zaraportować istniejący dokument typu file-set; nie jest celem zapisu

Drzewo DPart: struktura, którą RIP faktycznie czyta

Sercem PDF/VT jest hierarchia Document Part, DPart. To ona pozwala maszynie podzielić długi przebieg na rekordy, grupować rekordy według odbiorców albo pakietów wysyłkowych i dołączać Document Part Metadata, aby dalsze urządzenia mogły kierować i rozliczać każdą sztukę. ISO 16612-2 §6.5 rozpisuje to połączenie: katalog niesie /DPartRoot, korzeń DPart niesie /DPartRootNode oraz /NodeNameList nazywające każdy poziom hierarchii, liściaste DPart obejmują zakresy drzewa stron, a każda strona należąca do części wskazuje z powrotem swój liść przez wpis na poziomie strony /DPart

Gdy dokument źródłowy zawiera już użyteczną hierarchię, SaveAsPdfVT zachowuje ją. Gdy jej nie ma, writer syntetyzuje minimalną wersję: pojedynczy DPart na poziomie dokumentu obejmujący bieżące drzewo stron po kolei, z /DPart jako referencją zwrotną dopisaną do każdego żywego obiektu strony oraz jednopoziomowym /NodeNameList [/Document]. Warto uczciwie nazwać, czym jest takie minimalne drzewo. To kotwica strukturalna spełniająca wymagania kształtu z §6.5, a nie metadane biznesowe. Nie potrafi wymyślić odbiorców, granic przesyłek ani partii produktów, bo tych informacji nigdy nie było w źródle. Jeśli masz dane per odbiorca, powinieneś sam zbudować głębsze drzewo DPart i rozszerzyć /NodeNameList tak, by odpowiadało poziomom, które tworzysz

Walidacja wykraczająca poza samą obecność klucza

ValidatePdfVT zwraca rekord TPdfVTValidationResult z trzema elementami: wykrytym Conformance, zbiorem Issues oraz helperem IsCompliant, który zwraca true tylko wtedy, gdy zgodność oznacza rzeczywisty poziom, a zbiór problemów jest pusty. Enum problemów jest celowo precyzyjny, więc nieudany wynik mówi, którą klauzulę pominąłeś, zamiast rzucać tylko „invalid”:

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;

Dwie kontrole, które warto zrozumieć głębiej, to parowanie zgodności i przejście po DPart, ponieważ obie były kiedyś zbyt pobłażliwe i zostały zaostrzone tak, by odpowiadać specyfikacji. Po stronie parowania walidator wykonuje dopasowanie dokładne, a nie „dowolny PDF/X wystarczy”: plik PDF/VT-1 jest akceptowany tylko na bazie PDF/X-4, a plik PDF/VT-2 tylko na PDF/X-4p, PDF/X-5g albo PDF/X-5pg. Znacznik PDF/VT-1 osadzony na bazie PDF/X-1a jest raportowany, a nie przepuszczany machnięciem ręki

Przejście po DPart to miejsce, w którym mieszka większość rygoru. Sam fakt, że katalog ma klucz /DPartRoot, nie wystarcza, bo podrobiony pusty obiekt albo obiekt bez powiązań ze stronami nadal jest bezużyteczny. HasValidDPartHierarchy oraz rekurencyjne ValidateDPartNode śledzą całą strukturę: podążają po linkach rodziców, odrzucają duplikaty dzieci i cykle, wymuszają, aby /Start oraz /DParts były wzajemnie wykluczające się, a także wymagają, aby zakresy stron liści obejmowały drzewo stron w kolejności depth-first, przy czym /DPart każdej strony wskazuje liść, który ją zawiera. Wszystkie te wewnętrzne usterki zwijają się do pojedynczego bitu problemu pvviMissingDPartRoot zamiast rozszerzać publiczny enum, więc traktuj tę jedną flagę jako „hierarchia DPart jest bezużyteczna”, a nie dosłownie „brakuje klucza głównego”

Trzy pułapki składniowe, które walidator teraz wymusza

Kolejne przebiegi przeciwko §6.5 Table 4 ujawniły kształty, które wcześniejsze wersje akceptowały, ale standard już nie. To dokładnie ten typ rzeczy, które ręcznie budowane drzewo DPart robi źle, więc warto wymienić je wprost:

  • /DParts jest tablicą tablic, a nie tablicą płaską. Każdy element tablicy zewnętrznej sam musi być tablicą indirect reference. Płaskie /DParts [9 0 R] jest odrzucane; zgodny kształt to /DParts [[9 0 R] [10 0 R]]. To powstrzymuje strukturę niehierarchiczną przed podszywaniem się pod poprawny poziom
  • /End oznacza wyłącznie rzeczywisty zakres wielostronicowy. Liściasty DPart może nieść /End tylko wtedy, gdy ma również /Start, a /End musi wypadać później niż /Start w kolejności drzewa stron. Zdegenerowane /Start 3 0 R /End 3 0 R sprawia teraz, że hierarchia staje się bezużyteczna, zamiast być czytana jako część jednostronicowa
  • /NodeNameList nazwy muszą po odescapowaniu nazw PDF przetrwać jako XML NMTOKEN. Nazwa taka jak /Bad#20Name rozwija się do postaci zawierającej spację, a to nie jest poprawny token. Implementacja wykonuje lekką kontrolę ASCII, litery, cyfry, ., -, _, : oraz dodatkowo bajty nie-ASCII, co pozwala wychwycić błędy spacji i separatorów bez odrzucania prawidłowych nazw zlokalizowanych albo specyficznych dla dostawcy

Znaczniki XMP: dwa sposoby zapisu tej samej właściwości

Identyfikacja PDF/VT żyje w XMP pod przestrzenią nazw pdfvtid, konkretnie GTS_PDFVTVersion oraz GTS_PDFVTModDate, obok standardowych xmp:CreateDate oraz xmp:ModifyDate. Subtelność powodująca fałszywe raporty „missing” w naiwnych czytnikach polega na tym, że każdy z tych elementów może być serializowany na dwa sposoby: jako tekst elementu (<pdfvtid:GTS_PDFVTVersion>PDF/VT-1</pdfvtid:GTS_PDFVTVersion>) albo jako atrybut RDF na elemencie description. PDFiumPas odczytuje obie formy, więc plik zapisany przez inne narzędzie w stylu atrybutowym nie jest karany. Walidator wymusza też regułę spójności z §6.3, zgodnie z którą GTS_PDFVTModDate musi być równe xmp:ModifyDate; rozbieżność podnosi pvviModDateMismatch

Jeszcze jedna reguła z tej samej klauzuli: nieznana wartość GTS_PDFVTVersion jest zachowywana jako pvcUnknown, a nie składana z powrotem do pvcNone. To rozróżnienie ma znaczenie operacyjne. pvcNone oznacza „brak znacznika PDF/VT, zwykły PDF”, podczas gdy pvcUnknown oznacza „coś nabiło wersję, której ten walidator nie rozpoznaje”, w tym przypadek PDF/VT-2s. Zlewanie tych dwóch sytuacji ukryłoby błędny plik w tym samym koszyku co zwykły dokument

Gdzie kończy się gwarancja

Warto precyzyjnie wyznaczyć granicę tego, co obiecują te metody, bo zgodność druku danych zmiennych przekłada się na realne pieniądze. Kontrole DPart i parowania są walidacją strukturalną na poziomie bajtów. Potwierdzają, że szkielet optymalizacji, znaczniki bazy PDF/X-4, OutputIntent i XMP są obecne i wewnętrznie spójne. Nie są preflightem PDF/X-4 na poziomie treści: nie weryfikują, czy każdy kolor mieści się w zadeklarowanym warunku wyjściowym, czy wszystkie fonty są osadzone ani czy nie przemknął żaden zabroniony przypadek mieszania przezroczystości. Dla zadania, które trafia na prasę kontraktową, połącz walidację strukturalną PDFiumPas z dedykowanym silnikiem preflight PDF/X i z wydrukiem testowym, dokładnie tak samo, jak zdroworozsądkowo sprawdzałbyś każde inne twierdzenie o zgodności. Warstwa strukturalna wychwytuje usterki, które po cichu psują cache w RIP, ale stanowi tylko połowę kompletnej kontroli, nie całość

Jeśli wbudowujesz te kontrole w szerszy release gate, to ta sama metoda skanowania na poziomie bajtów stanowi podstawę pozostałych prac biblioteki związanych ze standardami, w tym walidację strumieni obiektów i cross-reference jeszcze zanim plik w ogóle dojdzie do preflight, oraz dyscyplinę współdzielonych obiektów stojącą za wielokrotnie używanymi stemplami stron opartymi na Form XObjects, która w ogóle czyni dokument przyjaznym dla RIP. Opisane tutaj API zapisu i walidacji PDF/VT oraz PDF/X są częścią komponentu PDFium VCL dla Delphi i C++Builder, a strona produktu zawiera pełne odniesienie do zgodności