Brama przyjmowania archiwów odrzuciła partię plików „PDF/A-2b”, które otwierały się poprawnie w każdym viewerze na biurku. Dostawca przysięgał, że są zgodne. Nie były: każdy z nich zawierał akcję JavaScript ukrytą w katalogu, czyli coś, czego pobieżne spojrzenie nigdy nie wychwyci, a pełny walidator PDF/A, taki jak veraPDF, oznacza natychmiast. Problem polegał na tym, że nikt nie chciał doklejać toolchainu Java do batch service napisanego w Delphi tylko po to, żeby odpowiedzieć na jedno pytanie tak albo nie dla każdego pliku. Tę lukę ValidatePdfACompliance w PDFium Component wypełnia i warto zrozumieć, jak dochodzi do werdyktu bez pełnego parsowania content stream
Dlaczego samo PDFium nie potrafi na to odpowiedzieć
Pierwsza rzecz, o której trzeba uczciwie powiedzieć: dołączone pdfium.dll nie ma żadnych możliwości PDF/A. Nie ma tam ConvertToPDFA, nie ma writer dla OutputIntent ani API XMP w publicznej powierzchni. Cała obsługa PDF/A w tej bibliotece, zarówno po stronie zapisu, jak i sprawdzania, żyje w pure Pascal w FPdfPdfa.pas i działa przez parsowanie na poziomie bajtów plus incremental update. Kiedy więc wywołujesz walidator, nie pytasz o nic renderer Chromium. Uruchamiasz skaner tokenów w Pascalu nad strukturalnymi bajtami pliku
Publiczne API jest celowo małe. Jedna funkcja czyta strumień od pozycji 0 i zwraca rekord:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
IsCompliant koduje regułę ważną dla gate: plik przechodzi tylko wtedy, gdy wykryto rzeczywisty poziom zgodności i zbiór problemów jest pusty. Parsowanie, które się powiedzie, ale nie znajdzie znacznika pdfaid, kończy się jako pacNone, co wprost nie oznacza sukcesu. To dokładnie ten sam punkt, który batch preflight report CLI podkreśla z zewnątrz: pusta lista findings dla nierozpoznanego pliku nie jest czystym świadectwem zdrowia
Usuwanie ciał strumieni przed każdym skanem tokenów
To najważniejszy szczegół implementacyjny i jednocześnie ten, który najłatwiej zepsuć, jeśli piszesz własny skaner. Detektor znajduje naruszenia przez wyszukiwanie ograniczonych tokenów nazw, takich jak /JavaScript, /LZWDecode, /BM. Jeśli skanujesz surowe bajty pliku, osadzone binarne ciała strumieni, skompresowane obrazy, profile ICC i programy fontów będą losowo zawierały sekwencje bajtów wyglądające jak takie tokeny. Zgłosisz wtedy /AA albo /3D jako „znalezione”, bo trzy bajty wewnątrz JPEG przypadkiem je zapisały. To fabryka false positive
Poprawką jest PdfStructureBytes: procedura przechodzi po pliku i zamienia na spacje bajty między każdym stream a endstream, zamieniając słowa kluczowe i pozostawiając strukturę słownika nienaruszoną. Dopiero potem uruchamia się skan. Każde sprawdzenie name-token w walidatorze działa na tej odchudzonej kopii. Jeśli masz zapamiętać z tego artykułu jedną ideę, niech będzie to ta. Tę samą dyscyplinę odbija walidator PDF/UA, który przechowuje własną kopię tej procedury, ponieważ oba standardy rozwijają się niezależnie
29 problemów i znaczenie każdego z nich
TPdfAValidationIssue to udokumentowany kontrakt. Ordinals są zamrożone, ponieważ zależą od nich testy DUnitX, dema i warstwa raportowa, więc nowe findings są tylko dopisywane na końcu. Od wersji v1.63.0 jest tam 29 pozycji. Dzielą się na kilka rodzin:
- Metadane i tożsamość:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Kolor i wyjście:
pvaiMissingOutputIntent,pvaiMissingIccProfileorazpvaiMixedDeviceColorSpaces, gdy jednocześnie występują DeviceRGB i DeviceCMYK (6.2.3.3) - Twarde zakazy dla każdej części:
pvaiEncryptionPresent(słownik/Encryptjest zabroniony wprost),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Fonty:
pvaiFontNotEmbeddedoraz bardziej rygorystycznepvaiUnembeddedFont, pluspvaiUnicodeMappingMissingdla deklaracji Level U bez/ToUnicode - Tagowanie:
pvaiLevelAStructureMissing, gdy deklaracja conformance=A nie ma otagowanej struktury
Sześć najnowszych pozycji, dodanych pod ordinalami od 24 do 29, obejmuje subtelne przypadki, o które recenzenci faktycznie się potykają: pvaiTrappedTrue (/Trapped /True w słowniku Info, fałszywy przyjaciel, bo wartość musi być False albo Unknown), pvaiForbiddenActionSubtype (Sound albo Movie użyte jako akcja, nie tylko jako adnotacja), pvaiTransparentColorSpace (tryb mieszania inny niż Normal albo /CA//ca różne od 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont oraz pvaiMixedDeviceColorSpaces
Bramkowanie zależne od części: A-1 jest surowe, A-2 i A-3 luzują
PDF/A nie jest jednym zbiorem reguł. Trzy rzeczy zabronione w PDF/A-1 są od PDF/A-2 wprost dozwolone: przezroczystość, czyli /Transparency grupa albo aktywne /SMask, 6.4), opcjonalna zawartość (/OCProperties, 6.1.13) oraz pliki osadzone (/EmbeddedFiles albo /EF, 6.1.11). Naiwny walidator, który oznacza wszystkie trzy przypadki dla każdego pliku, będzie masowo odrzucał całkowicie poprawne dokumenty PDF/A-2
Dlatego walidator odczytuje numer części ze znacznika pdfaid przez PdfAPartOf i chowa te kontrole za PartNo = 1. Sprawdzenia blend-mode i annotation-alpha dla nowych problemów z przezroczystością są podobnie ograniczone tylko do części 1:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Jedno konserwatywne ustawienie domyślne zasługuje na wzmiankę: gdy nie ma w ogóle znacznika pdfaid, część jest traktowana jako 1, czyli najbardziej rygorystyczna. Uzasadnienie jest proste: niezidentyfikowany plik powinien być oceniany według najtwardszych reguł, a nie przepuszczany na wiarę. JavaScript, zabronione akcje, LZW, XFA, NeedAppearances, zabronione adnotacje i nieosadzone fonty pozostają zabronione w każdej części, więc te kontrole nigdy nie trafiają za gate
Rozwijanie strumieni obiektów, żeby nic się nie ukryło
PDF 1.5 wprowadził strumień cross-reference i strumień obiektów (/Type /ObjStm), a to tworzy martwe pole dla naiwnego skanera bajtów. Katalog, OutputIntent, słownik akcji, wszystko co samo nie jest strumieniem, może być skompresowane Flate wewnątrz ObjStm. Skanujesz surową strukturę i nie widzisz nic z tego, po czym raportujesz czysty plik, który czysty wcale nie jest
PdfExpandObjectStreams zamyka tę lukę. Zanim ruszy jakiekolwiek sprawdzenie, walidator wykonuje Data := PdfExpandObjectStreams(Data). Procedura znajduje każdy ObjStm, odczytuje jego /N oraz /First z nagłówka, aby pobrać numery i offsety zawartych obiektów, dekompresuje ciało przez PdfInflate (zlib z RTL, System.ZLib w Delphi oraz zstream w FPC), a następnie dopisuje każdy zawarty obiekt jako zwykły N 0 obj ... endobj na końcu kopii bajtów. Istniejące testy tokenów znajdują potem te obiekty bez żadnej zmiany swojej logiki
Dwa ograniczenia sprawiają, że to rozwiązanie jest czyste, a nie kruche. Obiekty strumieniowe, Metadata, profil ICC i programy fontów nie mogą żyć w strumieniu obiektów, mogą tam być tylko słowniki niebędące strumieniami, więc rozwijanie dotyka wyłącznie słowników, a dopisane obiekty nie niosą słowa kluczowego stream, które mogłoby zaburzyć przebieg usuwania ciał strumieni. A ponieważ dopisana zawartość ląduje po %%EOF, odwrotne wyszukiwanie z startxref nadal znajduje oryginalny trailer. Sam trailer strumienia cross-reference był już obsłużony wcześniej, w v1.49.3, przez odczytywanie Root, Size i ID bezpośrednio z tekstowego słownika xref-stream, co omawia tekst uzupełniający o walidacji strumieni obiektów i cross-reference; praca nad object-stream musiała dołożyć tylko krok inflate, bez potrzeby dekodowania wpisów xref typu 2 ani odwijania predyktora PNG
Uczciwe granice checkera na poziomie bajtów
To jest narzędzie preflight, a nie certyfikowany walidator, i te granice są realne. Osadzanie fontów jest wykrywane heurystyką zliczającą, a doprowadzenie jej do poprawnego stanu wymagało korekty, o której warto wiedzieć. Oryginalna kontrola używała PdfCountName('/FontDescriptor'), ale każdy font wnosi dwa wystąpienia /FontDescriptor, jedno odwołanie ze słownika fontu i jedno /Type w samym obiekcie deskryptora, więc licznik dawał 2N wobec N osadzonych programów i test zawsze wychodził na true. Poprawką jest PdfCountDescriptorRefs, które zlicza tylko formę referencji /FontDescriptor N G R, po jednej na font, i podnosi pvaiUnembeddedFont tylko wtedy, gdy osadzonych programów jest naprawdę mniej:
K := PdfCountDescriptorRefs(Struct); // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Nawet po korekcie jest to podejście zgrubne: mieszany dokument, w którym każdy deskryptor akurat ma jakieś FontFile, nadal może przepuścić pojedynczy niezgodny font. Rozwijanie strumieni obiektów ma też znany efekt uboczny: odsłania domyślne zasoby standard-14, które niesie AcroForm /DR, takie jak /Helv, a heurystyka posłusznie raportuje je jako nieosadzone, mimo że veraPDF przepuszcza je dalej, bo nigdy nie są faktycznie używane do renderowania. Sprawdzenia operatorów na poziomie content stream, 6.2.10, są całkowicie poza zakresem, ponieważ wymagałyby pełnego parsowania treści zamiast skanu bajtów. Traktuj ten walidator jako szybki, pozbawiony zależności pierwszy gate, który wychwytuje naruszenia, których nie da się naprawić przez wstrzyknięcie markerów, a pełny walidator zostaw do ostatecznej certyfikacji
To jest połowa historii po stronie sprawdzania. Uzupełniająca strona zapisu, gdzie SaveAsPdfA wstrzykuje XMP, OutputIntent i profil ICC sRGB oraz uczciwie obniża żądanie Level A, jeśli nie ma otagowanej struktury, opiera się na tej samej maszynerii działającej na poziomie bajtów. Obie połowy są dostarczane w PDFium Component for Delphi, czyli pojedynczym pakiecie VCL nad implementacją PDF/A w pure Pascal, bez zewnętrznego runtime do instalowania