Archiwum faktur może renderować się czysto, drukować, osadzać każdy font i nieść poprawny output intent, a walidator PDF/A i tak je odrzuci — bo jeden słownik zasobów trzyma 4 096 wpisów. Limit to 4 095. PDF/A dziedziczy po PDF Reference małą tabelę limitów implementacji i plik o jedną jednostkę za którymkolwiek z nich zawodzi zgodność, choć żaden czytnik nie narzeka
Druga porażka wpadająca do tego samego koszyka „a przecież wygląda dobrze" to symboliczna czcionka TrueType niosąca wpis /Encoding. Pola wyboru, kody kreskowe i dingbaty wychodzą na ekranie dobrze, a walidator i tak flaguje słownik fontu. Oba problemy mieszkają w grafie obiektów, a nie w czymkolwiek, co człowiek widzi na stronie — więc warto sprawdzać je mechanicznie, zanim plik trafi do archiwum
Liczby, które PDF/A dziedziczy, i gdzie biegnie granica
PDFium Component sprawdza cztery limity kontenerów przy każdej walidacji PDF/A: token nazwy może mieć co najwyżej 127 bajtów, tablica co najwyżej 8 191 elementów, słownik co najwyżej 4 095 wpisów, a tablice i słowniki mogą zagnieżdżać się co najwyżej 28 poziomów. Wszystkie cztery granice są włączne. Nazwa 127-bajtowa przechodzi, a 128-bajtowa pada; tablica mająca dokładnie 8 191 elementów przechodzi, a jeden element więcej ją zawala
Limity nazw, tablic i słowników pochodzą z epoki, gdy od zgodnego czytnika wymagano tylko obsługi struktur do tych rozmiarów. Współczesna desktopowa przeglądarka może otworzyć dużo większą tablicę bez mrugnięcia, ale plik archiwalny ma się otwierać w czytnikach, których nikt jeszcze nie testował — w tym w starych, które system archiwizacji może nadal odpalać. Dlatego PDF/A trzyma historyczne sufity zamiast je poluzować
Włączność względem wyłączności to miejsce, w którym ręcznie pisane kontrole zwykle się mylą. Test napisany jako Count >= 8191 zamiast Count > 8191 odrzuca pliki zgodne, a walidator odrzucający pliki zgodne bywa brany za wiarygodny — więc generator zostaje „naprawiony" dla problemu, którego nigdy nie miał. Funkcja ValidatePdfACompliance w jednostce FPdfPdfa liczy ściśle ponad limitem i czyta bajty pliku bezpośrednio, więc kontrola biegnie bez wczytywania DLL PDFium
uses
System.Classes, FPdfPdfa;
procedure CheckContainerLimits(const FileName: string; Log: TStrings);
var
Src: TFileStream;
Res: TPdfAValidationResult;
begin
Src := TFileStream.Create(FileName, fmOpenRead or fmShareDenyWrite);
try
Res := ValidatePdfACompliance(Src); // skan bajtów, bez potrzeby DLL PDFium
if pvaiNameOverLimit in Res.Issues then
Log.Add('A name token is longer than 127 bytes');
if pvaiArrayOverLimit in Res.Issues then
Log.Add('An array holds more than 8191 elements');
if pvaiDictOverLimit in Res.Issues then
Log.Add('A dictionary holds more than 4095 entries');
if pvaiNestingOverLimit in Res.Issues then
Log.Add('Arrays and dictionaries nest deeper than 28 levels');
if Res.IsCompliant then
Log.Add('No PDF/A findings');
finally
Src.Free;
end;
end;
Te cztery flagi to składniki zbioru Issues na TPdfAValidationResult, obok każdego innego ustalenia PDF/A, a IsCompliant zwraca True tylko wtedy, gdy znaleziono identyfikację PDF/A i zbiór jest pusty. Plik nie niosący w ogóle identyfikacji PDF/A jest sprawdzany względem ścisłego zestawu reguł PDF/A-1, więc limity kontenerów i jego dotyczą
Które generatory wpadają na te limity?
Prawie zawsze te budujące strukturę w pętli. Formularz z kilkoma tysiącami pól produkuje tablicę AcroForm /Fields albo tablicę /Annots strony, która przechodzi za 8 191 wpisów. Silnik raportów rejestrujący jeden zasób fontu albo obrazu na wygenerowany obiekt, który nigdy nie używa ponownie istniejącego wpisu, rośnie ponad 4 095 kluczy w pod-słowniku /Resources. Dokument tagowany, którego drzewo struktury jest produkowane przez rekurencję po zagnieżdżonym modelu danych, może przekroczyć 28 poziomów, zanim ktokolwiek się zorientuje
Długie nazwy mają inne pochodzenie: dane upakowywane w tokeny nazw. Colorant separacyjny nazwany referencją klienta, grupa treści opcjonalnych nazwana ścieżką pliku, etykieta pola składana z sześciu poziomów hierarchii. Nazwy nic nie kosztują przy generowaniu, a 127 bajtów ucieka szybko, gdy w grę wchodzi etykieta UTF-8 ze znakami diakrytycznymi, bo każdy z tych znaków bierze dwa albo trzy bajty. Te 127 bajtów liczy się po wprowadzniku /, do następnego separatora albo białego znaku
Remedium jest za każdym razem strukturalne. Podziel tablicę, podziel słownik, spłaszcz rekurencję, skróć nazwę. Wstrzykiwanie znaczników PDF/A do pliku nie pomoże przy żadnym z tych ustaleń, bo problemem jest kształt grafu obiektów, a metadane XMP nie mówią o nim nic
Jak skan liczy elementy i wpisy
Reguła liczenia ma znaczenie, gdy próbujesz odtworzyć ustalenie w edytorze szesnastkowym. Zagnieżdżona tablica albo słownik liczy się jako jeden element rodzica — tak brzmi limit: tablica 10 elementów po tablicach 1000-elementowych jest w porządku, bo żadna pojedyncza tablica nie przekracza 8 191. Wewnątrz słownika liczone są tylko klucze, więc wartość będąca nazwą nie nadmuchuje sumy wpisów. Łańcuchy szesnastkowe, jak <FEFF0041>, są pomijane jako pojedyncze tokeny, więc ich nawiasy kątowe nigdy nie zostają wzięte za zagnieżdżenie słownikowe
Skan obejmuje strukturę obiektów całego pliku, włącznie z obiektami schowanymi w skompresowanych strumieniach obiektów, które są rozwijane przed liczeniem. Ciała strumieni — jak treść strony i dane obrazów — są najpierw wygaszane, więc ciąg bajtów binarnych, który przypadkiem zawiera / albo [, nie odpali fałszywego ustalenia nazwy albo tablicy
Istnieje sąsiedni limit numeryczny, który ta sama walidacja raportuje pod własną flagą. Gdy plik deklaruje PDF/A-2 albo późniejszą część, pvaiIntegerImplementationLimit odpala się dla każdego tokenu całkowitego poza 32-bitowym zakresem ze znakiem, czyli poza -2 147 483 648 do 2 147 483 647. Generatory piszące przesunięcia bajtowe albo duże liczniki jako gołe liczby całkowite to zwykłe źródło
Dlaczego symboliczna czcionka TrueType musi upuścić /Encoding?
ISO 19005-1 §6.3.7 dopuszcza wyłącznie wbudowaną cmap programu fontu jako mapowanie znaków dla symbolicznej czcionki TrueType. Symboliczny font mapuje kody na glify po swojemu; to deklaruje flaga Symbolic. Dodaj wpis /Encoding, a plik daje teraz dwie odpowiedzi na pytanie „który glif wybiera bajt 0x41", bez niczego w pliku, co mówiłoby, która wygrywa. Czytniki rozstrzygają konflikt różnie i to samo pole wyboru może wyjść jako ptaszek w jednej przeglądarce i jako litera A w drugiej
ValidatePdfACompliance znajduje każdy słownik fontu z /Subtype /TrueType, sprawdza, czy ma klucz /Encoding, po czym czyta bit 3 (wartość 4) wpisu /Flags w deskryptorze fontu. Deskryptor jest śledzony niezależnie od tego, czy jest zapisany inline, czy wskazany jako obiekt pośredni. Niesymboliczna czcionka TrueType zachowująca /WinAnsiEncoding albo /MacRomanEncoding nie jest flagowana przez tę kontrolę, bo dla fontów niesymbolicznych PDF/A oczekuje dokładnie takiego kodowania
// Res pochodzi z ValidatePdfACompliance jak w pierwszym przykładzie
if pvaiSymbolicTrueTypeEncoding in Res.Issues then
Log.Add('A symbolic TrueType font carries /Encoding; PDF/A admits only ' +
'the font''s built-in cmap (ISO 19005-1 6.3.7)');
// Sąsiednia reguła fontowa o innym wyzwalaczu: NIEsymboliczna
// czcionka TrueType, której słownik Encoding niesie /Differences
if pvaiTrueTypeDifferencesForbidden in Res.Issues then
Log.Add('A non-symbolic TrueType Encoding dictionary carries /Differences');
Fonty, które wywalają tę regułę, są przewidywalne: Symbol, Wingdings, fonty kodów kreskowych, fonty ikon. Pojawiają się w dokumentach biznesowych jako pola wyboru, gwiazdki ocen i kody kreskowe, a krok subsetowania traktujący każdą czcionkę TrueType tak samo pisze kodowanie i dla nich. To też fonty, na które nikt nie patrzy, gdy walidator powie „problem z fontem", bo fonty tekstu głównego są sprawdzane pierwsze
Odwrócona pomyłka też występuje na wolności: producent ustawia flagę Symbolic na zwykłym foncie tekstowym, który naprawdę jest kodowany WinAnsi. Kontrola PDF/A idzie za punktem takim, jaki jest, i raportuje ten font — a naprawa należy tu do deskryptora: popraw flagę w deskryptorze, zamiast usuwać kodowanie. Usunięcie /Encoding z fontu tekstowego, który został błędnie oetykietowany, zostawiłoby tekst zależnym od cmap, jaka akurat siedzi w podzbiorze — to gorszy plik niż ten wyjściowy. Obejrzyj zestaw glifów fontu, zanim wybierzesz, którą połowę sprzeczności usunąć
Czytanie ustaleń w raporcie preflight
Jednostka raportu preflight przyjeżdża z edycją Enterprise, podczas gdy ValidatePdfACompliance jest w każdej edycji. Raport w jednostce FPdfPreflightReport sortuje cztery limity kontenerów do kategorii Structure, a problem symbolicznego kodowania TrueType do kategorii Content. Ten podział jest celowy. Ustalenia strukturalne zwykle wracają do tego, kto utrzymuje generator, ustalenia treści — do tego, kto dostarczył fonty i szablony, a raport mieszający jedno i drugie wysyła obie osoby gonić problemy, które nie są ich
Każde zagadnienie niesie swoją nazwę enuma jako Code i rekomendację nazywającą remedium konkretnie, na przykład „Split dictionaries so none carries more than 4095 entries". Zauważ, że kategoria Structure trzyma też dwa inne ustalenia: brakującą strukturę tagowaną przy roszczeniu Level A oraz strumienie odniesień krzyżowych albo obiektów PDF 1.5 w pliku PDF/A-1 — więc filtruj po kodzie, gdy chcesz tylko cztery limity
uses
System.SysUtils, System.Classes, PDFium, FPdfPreflightReport;
procedure ReportStructureFindings(const FileName: string; Log: TStrings);
var
Pdf: TPdf;
Report: TPdfPreflightReport;
Issues: TPdfPreflightIssues;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := FileName;
Pdf.Active := True; // ta ścieżka wczytuje DLL PDFium
if not Pdf.Active then
Exit;
Report := BuildPdfPreflightReport(Pdf, [ppsPdfA]);
Issues := Report.FilterByCategory('Structure').FlattenIssues;
for I := 0 to High(Issues) do
Log.Add(Issues[I].Code + ': ' + Issues[I].Recommendation);
Report.SaveJsonToFile(ChangeFileExt(FileName, '.preflight.json'));
finally
Pdf.Free;
end;
end;
Te dwie ścieżki odpowiadają na nieco różne pytania. BuildPdfPreflightReport idzie przez TPdf.ValidatePdfA, które zapisuje otwarty dokument do kopii tymczasowej, biegnie tym samym skanem bajtów po tej kopii, po czym dokłada kontrole wymagające silnika PDFium — jak fonty używane na stronie, a nieosadzone. ValidatePdfACompliance ocenia dokładne bajty na dysku. Dla limitów implementacji zaufaną ścieżką jest bajtowa na oryginalnym pliku: raportuje o pliku, który zaraz zarchiwizujesz, i biegnie w kroku builda albo zdławionej usłudze, gdzie natywna DLL nie może być wdrożona. Ścieżkę raportu bierz, gdy chcesz też kontrole na poziomie silnika i wyjście JSON, SARIF albo JUnit dla potoku
Gdzie dalej
Uruchamianie tych kontroli na folderze, rozstrzyganie, które ustalenia blokują wydanie, i zamiana wyniku w raport, który ludzie przeczytają, są pokryte w przejściu przez artykuł walidacja preflight PDF/A w Delphi i w artykule CLI raportu preflight wsadowego. Którą część PDF/A i poziom zgodności celować na starcie, jest tematem notek o artykule zgodność archiwalna PDF/A; ten wybór rozstrzyga między innymi, czy limit liczb całkowitych obowiązuje
Walidatory pokazane tutaj przyjeżdżają z PDFium Component dla Delphi, C++Builder i Lazarus, obok renderowania, wypełniania formularzy i edycji na silniku PDFium. Strona produktu PDFium Component wypisuje wspierane standardy zgodności, edycje i wersje kompilatorów