Artykuł techniczny

Dokładny zapis wersji PDF w Delphi: zgodność PDFiumPas

PDFiumPas, wrapper Delphi i C++Builder wokół silnika PDFium od Google, zapisuje dokument w dokładnej wersji PDF od 1.3 do 1.7 poprzez parametr PdfVersion metody TPdf.SaveAs. Własne wywołanie PDFium FPDF_SaveWithVersion przepisuje jedynie nagłówek %PDF-M.m, bez sprawdzania, czy rzeczywista zawartość dokumentu jest zgodna z prawem dla tej wersji. PDFiumPas zamyka tę lukę przebiegiem sprawdzania zgodności po zapisie, który przechodzi przez aktywny łańcuch rewizji odniesień krzyżowych i sprawdza deklaracje poziomu rozszerzenia Adobe, zanim plik opuści metodę

To rozróżnienie ma największe znaczenie w produkcji drukarskiej, gdzie profil PDF/X wskazuje dokładną wersję PDF, a narzędzie preflight lub RIP odrzuca wszystko, co po cichu jest niezgodne z własnym nagłówkiem — scenariusz omówiony od strony wyjściowej w walidacji dokumentów PDF/X gotowych do druku za pomocą PDFiumPas. SaveAs udostępnia cel jako wyliczenie TPdfVersion, pv13 do pv17 obok starszych wartości pv10 do pv12, plus niezależny TSaveOption dla zapisów przyrostowych lub pełnych. Przekaż PdfVersion, a PDFiumPas wykona dwa zadania w jednym wywołaniu: poprosi PDFium o wypisanie żądanego nagłówka, a następnie ponownie odczyta świeżo zapisane bajty i odmówi zwrócenia pliku, którego aktywna zawartość nie może zgodnie z prawem istnieć w tej wersji

var
  Pdf: TPdf;
begin
  Pdf:= TPdf.Create(nil);
  try
    Pdf.FileName:= 'source.pdf';
    Pdf.Active:= True;
    try
      Pdf.SaveAs('press-ready.pdf', saNoIncremental, pv17);
    except
      on E: Exception do
        // E.Message names the offending feature and the version or
        // extension level it actually needs, for example:
        // "RichMedia annotations and RichMediaExecute actions require
        // /Extensions /ADBE with /BaseVersion /1.7 and /ExtensionLevel 3
        // or newer."
        raise;
    end;
  finally
    Pdf.Free;
  end;
end;

Dlaczego ostatnia definicja obiektu w pliku to niewłaściwa rzecz, której warto zaufać?

Ostatni fizyczny obiekt o danym numerze w pliku PDF niekoniecznie jest obiektem, który zgodny ze specyfikacją czytnik rozwiązałby dziś dla tego numeru. Plik PDF, który przeszedł przez kilka aktualizacji przyrostowych, nie ma jednego grafu obiektów, ma historię takich grafów ułożonych warstwowo wewnątrz jednego pliku, a każdy cykl dołączania może zwolnić obiekt, redefiniować go pod nowym numerem generacji lub pozostawić jego stare fizyczne ciało leżące między dwoma znacznikami endobj, bez żadnego wpisu odniesienia krzyżowego wskazującego już na niego

PDFiumPas trafił dokładnie na ten tryb awarii, zanim jawnie śledził rewizje xref: adnotacja Redact osierocona przez późniejsze przepisanie obiektu strony, lub słownik /MarkInfo pozostawiony fizycznie obecny bez żadnego wpisu xref wskazującego na niego, wciąż mogły pojawić się przy skanowaniu bajtów i wciąż uruchamiać sprawdzenie funkcji zależnej od wersji, które już nie dotyczyło dokumentu, który czytnik faktycznie by otworzył. Kierunek awarii to fałszywe odrzucenie, nie fałszywa akceptacja: plik, który w swojej bieżącej rewizji naprawdę wyszedł poza jakąś funkcję, wciąż mógł zostać zablokowany przed zapisem w niższej wersji z powodu zawartości, do której nikt już nie mógł dotrzeć

Jak PDFiumPas ustala, które definicje obiektów są faktycznie aktywne?

PDFiumPas rozwiązuje aktywny zestaw obiektów tak samo jak zgodny ze specyfikacją czytnik, przechodząc przez łańcuch odniesień krzyżowych zamiast skanować bajty w poszukiwaniu nagłówków obiektów. Resolver zaczyna od ostatniego przesunięcia startxref w pliku i podąża wstecz za każdym łączem /Prev przez starsze rewizje, parsując po drodze klasyczne tabele odniesień krzyżowych, hybrydowe strumienie połączone przez /XRefStm oraz czyste strumienie odniesień krzyżowych. Przejście odbywa się od najnowszego do najstarszego i ustala każdy numer obiektu przy pierwszym napotkaniu, więc wolny wpis w późniejszej rewizji poprawnie przesłania ciało obiektu zapisane we wcześniejszej, a redefinicja pod nowym przesunięciem lub generacją zawsze wygrywa z tym, co zastępuje

Elementy strumienia obiektów otrzymują dodatkowe sprawdzenie, którego zwykłe wyszukiwanie po przesunięciu samo z siebie nie może zapewnić, mechanizm omówiony dokładniej w walidacji strumieni obiektów i odniesień krzyżowych za pomocą PDFiumPas. Skompresowany obiekt odzyskany z /ObjStm musi mieć potwierdzoną aktywność swojego strumienia nadrzędnego w tym samym przejściu, a jego indeks musi zgadzać się z pozycją elementu wewnątrz nagłówka tego strumienia, zanim PDFiumPas potraktuje go jako żywą zawartość. ISO 32000-1 sekcja 7.5.8.4 opisuje nawet przypadek hybrydowego odniesienia, gdzie klasyczna tabela zgodności oznacza obiekt jako wolny, podczas gdy wpis /XRefStm w trailerze jednocześnie definiuje ten sam obiekt jako skompresowany element gdzie indziej; PDFiumPas scala uzupełniający strumień xref w tę samą rewizję, zanim zastosowane zostaną klasyczne wpisy, więc definicja skompresowana wygrywa tak, jak zamierza specyfikacja

Poziomy rozszerzeń Adobe: brama nad numerem wersji

Nagłówek %PDF-1.7 obiecuje jedynie zestaw funkcji, który ISO 32000-1 ustandaryzowało w 2008 roku, podczas gdy kilka możliwości, na których dziś polegają producenci PDF, pojawiło się później jako suplementy wyłącznie od Adobe, nałożone na ten sam numer wersji. Adobe zarejestrowało każdy suplement jako parę BaseVersion i ExtensionLevel zapisaną w słowniku /Extensions katalogu dokumentu pod prefiksem dostawcy, ADBE dla własnych rozszerzeń Adobe, więc czytnik może odróżnić zwykły plik PDF 1.7 od takiego, który dodatkowo implementuje ponumerowany poziom rozszerzenia. Zapis w pv17 bez tej deklaracji sam w sobie nie jest błędem; staje się nim dopiero w chwili, gdy aktywna zawartość faktycznie zależy od funkcji, którą ta deklaracja ma obejmować

Które funkcje z wysokiej wersji uruchamiają jawną bramę wersji?

PDFiumPas sprawdza konkretną, opartą na specyfikacji listę, zamiast zgadywać wyłącznie na podstawie numeru wersji. Słowniki obrazów niosące jawny wpis /SMaskInData lub wartość /BitsPerComponent równą 16 wymagają PDF 1.5, przy czym przypadek szesnastobitowy podąża bezpośrednio za regułami komponentów obrazu z PDF Reference 1.5 sekcja 4.8. Adnotacje RichMedia i akcje RichMediaExecute wymagają /BaseVersion /1.7 z /ExtensionLevel 3 lub wyższym. Strumienie 3D PRC, identyfikowane przez słownik niosący zarówno /Type /3D, jak i /Subtype /PRC, wymagają tej samej wersji bazowej, ale tylko /ExtensionLevel 1. Słowniki Measure geoprzestrzenne i adnotacje Projection wymagają /BaseVersion /1.7 z /ExtensionLevel 3, tego samego suplementu Adobe, od którego zależy RichMedia

Sprawdzenie geoprzestrzenne niesie ze sobą szczegół lektury specyfikacji, warty poznania, jeśli kiedykolwiek zbudujesz własną logikę bramkowaną wersją na bazie PDFiumPas. Tabela 254 ISO 32000-1 oznacza wpis /Type słownika Measure jako opcjonalny, zaznaczając jedynie, że "jeśli obecny, powinien być Measure", podczas gdy Tabela 311 czyni /Type obowiązkowym dla słownika strumienia 3D, w którym mieszka zawartość PRC. Rzeczywisty wynik GeoPDF z narzędzi mapujących rutynowo pomija /Type w słowniku Measure i zapisuje tylko /Subtype /GEO, więc detektor geoprzestrzenny PDFiumPas dopasowuje wyłącznie po /Subtype, zamiast wymagać obu kluczy, tak jak bezpiecznie może to zrobić jego detektor 3D PRC. Wymaganie /Type na obu słownikach pozwoliłoby zgodnej zawartości GeoPDF prześlizgnąć się przez bramę niewykrytej, lądując w zwykłym pliku PDF 1.7 bez żadnej deklaracji poziomu rozszerzenia, która by ją wspierała

Czy PDFiumPas automatycznie obniża wersję nieobsługiwanych funkcji?

Nie jako ogólna zdolność, i założenie inaczej to błąd, którego warto tu unikać. SaveAs przekazuje docelową wersję przez wewnętrzną procedurę, ValidatePdfVersionCompliance, a gdy ta procedura znajdzie funkcję, której docelowa wersja lub jej deklaracja poziomu rozszerzenia nie mogą obsłużyć, SaveAs zgłasza wyjątek niosący tekst błędu z tej procedury zamiast zapisywać plik; wywołujący otrzymuje precyzyjny, nazwany po funkcji powód, nigdy po cichu przepisany dokument. Jedynym miejscem, w którym PDFiumPas automatycznie przepisuje zawartość, jest cel PDF 1.3, gdzie usuwa semantycznie neutralne wartości domyślne przezroczystości /BM /Normal, /CA 1 i /ca 1, które PDFium zawsze zapisuje w słownikach ExtGState niezależnie od wersji docelowej, ponieważ te konkretne wartości nie niosą żadnego znaczenia wizualnego, a PDF 1.3 poprzedza te klucze w ogóle

// PDF 1.3 targets rewrite the saved bytes to strip transparency
// defaults PDFium always emits, so incremental mode cannot apply
Pdf.SaveAs('legacy-archive.pdf', saIncremental, pv13);
// raises: PDF 1.3 normalization is incompatible with incremental
// save mode

Prawdziwa, niedomyślna przezroczystość i miękkie maski obrazu wciąż zawodzą bezwzględnie przy celu PDF 1.3, ponieważ ich usunięcie zmieniłoby to, jak strona faktycznie wygląda, a PDFiumPas nie podejmie tej decyzji w twoim imieniu. Dwa powiązane ograniczenia warto zaplanować, zanim dokładna wersja trafi do potoku wsadowego. Wyjście z jawną wersją nigdy nie niesie słownika /Encrypt; zapis zawodzi natychmiast, jeśli źródło jest chronione, co akurat pokrywa się z profilami PDF/X i PDF/A, które i tak zabraniają szyfrowania, ale oznacza to, że odszyfrowanie jest osobnym krokiem w twoim procesie roboczym, a nie czymś, co robi za ciebie SaveAs. PDFiumPas nie ma też żadnej publicznej metody do zapisania deklaracji /Extensions /ADBE w katalogu, więc plik źródłowy, który zawiera RichMedia, PRC 3D lub zawartość geoprzestrzenną, ale brakuje mu tej deklaracji, nie przejdzie przez bramę bez względu na to, jaki PdfVersion zażądasz; deklaracja musi już istnieć w źródle, zwykle dlatego, że napisało ją narzędzie autorskie, albo funkcja musi zostać usunięta przed zapisem. Warto sprawdzić tylko-do-odczytu właściwość TPdf.PdfVersion, zanim w ogóle spróbujesz zapisu z dokładną wersją, ponieważ rozwiązuje ona tę samą efektywną wersję świadomą katalogu, nagłówek lub nadpisanie /Version, którekolwiek jest aktualne, na której polega sam walidator w czasie zapisu

Pdf.FileName:= 'incoming.pdf';
Pdf.Active:= True;
// PdfVersion resolves the same catalog-aware effective version the
// save-time validator uses, so a mismatch here is worth investigating
// before spending a full SaveAs attempt on it
LogSourceVersion('incoming.pdf', Pdf.PdfVersion);

Traktuj wyjątek SaveAs przy celu z dokładną wersją jako raport preflight, a nie błąd: komunikat wskazuje dokładną klauzulę, którą narusza dokument źródłowy, co jest dokładnie tą informacją, jakiej potrzebuje drukarnia lub potok archiwizacji, zanim plik pójdzie dalej. Ścieżka zapisu z jawną wersją, resolver aktywnej rewizji xref oraz sprawdzenia poziomu rozszerzenia Adobe opisane tutaj są dostarczane jako część standardowego komponentu PDFiumPas dla Delphi i C++Buildera; strona produktu zawiera pełne odniesienie do TPdf.SaveAs obok reszty API zgodności i formularzy