Artykuł techniczny

Limity implementacji PDF/A i sprawdzenia kodowania czcionek

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

Diagram czterech inkluzywnych limitów implementacji z ISO 19005-1 Aneks C walidowanych czystym skanem bajtów z Delphi: tokeny nazw do 127 bajtów, 8191 elementów tablicy, 4095 wpisów słownika i 28 poziomów zagnieżdżenia kontenerów
Wszystkie cztery limity są włącznie — wartość graniczna sama się waliduje, a tylko następna jednostka zawodzi
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

Diagram zestawiający symboliczną czcionkę TrueType, której wpis /Encoding tworzy dwa konkurencyjne mapowania glifów, z niesymboliczną czcionką słusznie zachowującą WinAnsiEncoding, w sprawdzeniu PDFium Component dla Delphi
Kontrola czyta flagę symboliczną z deskryptora czcionki i odpala na sprzeczności, a nie na obecności 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