Dostarczasz konwerter, który oznacza każdy plik jako PDF/A-1b, system archiwizacji klienta przyjmuje je przez rok, a potem audyt przepuszcza cały pakiet przez veraPDF i okazuje się, że jedna trzecia plików nie spełnia normy. Nic się nie wywróciło, nie było wyjątku, pliki otwierają się poprawnie w każdym viewerze na Twoim biurku. Po prostu nie są standardem, którym je opatrzyłeś. To normalny tryb porażki przy archiwalnym PDF i właśnie dlatego "ustawiliśmy flagę" nigdy nie znaczy tego samego co "plik przechodzi walidację"
Pierwsza rzecz do zrozumienia o PDFium i PDF/A jest taka, że sam silnik nie ma z tym nic wspólnego. PDFium renderuje, parsuje i zapisuje PDF, ale jego publiczne API nie ma ConvertToPDFA, nie ma writer dla OutputIntent ani API XMP. Cała zgodność archiwalna, pakiet XMP, OutputIntent i jego profil ICC, znaczniki katalogu oraz walidacja żyją w samym PDFiumPas, w około 2000-liniowym module pure Pascal (FPdfPdfa.pas), który parsuje zapisane bajty i przepisuje je przez incremental update. Gdy wiesz, gdzie faktycznie dzieje się praca, wiesz też, gdzie kryją się błędy, a nie kryją się one w PDFium
Czego PDF/A naprawdę wymaga i gdzie to boli
PDF/A nie jest jednym formatem. ISO 19005 definiuje trzy części, PDF/A-1, -2 i -3, a wewnątrz każdej z nich poziomy zgodności obiecujące różne rzeczy. Poziom B, basic, gwarantuje tylko odtwarzalność wyglądu wizualnego. Poziom A, accessible, dodaje ponad B otagowane drzewo struktury i mapowanie Unicode. Poziom U, istniejący tylko w częściach 2 i 3, leży pomiędzy nimi: daje wiarygodny tekst Unicode bez pełnego drzewa struktury. ISO 19005-1 nie ma poziomu U i biblioteka koduje to ograniczenie wprost
Kilka reguł tego formatu to właśnie te, które realnie gryzą. Szyfrowanie jest zabronione wprost, ISO 19005-1 §6.1.3 i kolejne wersje, więc plik PDF/A nie może zawierać słownika /Encrypt. Dokument musi zadeklarować warunek renderowania wyjściowego przez OutputIntent, którego celem jest poprawny profil ICC (§6.2.3.2). Sama deklaracja zgodności musi pojawić się jako metadane XMP pod schematem identyfikacji PDF/A. Poziom A dodatkowo wymaga logicznej struktury z §6.8, czyli drzewa tagów czyniącego dokument czytelnym maszynowo. Pominięcie któregokolwiek z tych elementów sprawi, że walidator zgodności odrzuci plik, mimo że ten renderuje się idealnie
Jedno wywołanie, które tworzy archiwum
PDFiumPas wystawia cały pipeline za TPdf.SaveAsPdfA. Proste przeciążenie przyjmuje docelową zgodność i domyślnie ustawia PDF/A-1b, co jest właściwym domyślnym wyborem dla typowego przypadku "spraw, by to dało się renderować wiecznie"
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice.pdf');
// Default conformance is pac1b (PDF/A-1b)
if Pdf.SaveAsPdfA('invoice_archive.pdf') then
// file now carries XMP, sRGB OutputIntent, and catalog markers
else
raise Exception.Create('PDF/A save failed');
finally
Pdf.Free;
end;
end;
Pod spodem jest to operacja dwuetapowa. SaveAsPdfA najpierw prosi PDFium o serializację dokumentu przez FPDF_SaveAsCopy, po czym przekazuje ten strumień bajtów do InjectPdfAMarkers, które dołącza metadane XMP, sRGB OutputIntent z osadzonym profilem ICC oraz przepisany katalog jako incremental update. Źródło jest czytane od pozycji zero, a cel zapisywany od pozycji zero; oryginalne drzewo obiektów pozostaje nietknięte, a znaczniki są dopisywane po istniejącym %%EOF. Jeśli potrzebujesz bajtów zamiast pliku, SaveAsPdfAToStream przyjmuje TStream oraz te same opcje
Wybór zgodności przez rekord opcji
Aby celować w konkretną część i poziom, przekaż rekord TPdfASaveOptions. Jego pole Conformance przyjmuje wartość TPdfAConformance. Enum obejmuje wszystkie poprawne kombinacje i nic poza nimi: pac1b, pac1a dla części 1; pac2b, pac2u, pac2a dla części 2; pac3b, pac3u, pac3a dla części 3, plus pacUnknown oraz pacNone po stronie walidacji. Nie istnieje pac1u, ponieważ ten poziom nie istnieje w standardzie
var
Pdf: TPdf;
Opts: TPdfASaveOptions;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('report.pdf');
Opts := TPdfASaveOptions.Default;
Opts.Conformance := pac2u; // PDF/A-2u: reliable Unicode text
Opts.Title := 'Quarterly Report 2026';
Opts.Author := 'Finance';
// Leave IccProfileData empty to use the built-in sRGB IEC61966-2.1 profile
if not Pdf.SaveAsPdfA('report_a2u.pdf', Opts) then
raise Exception.Create('PDF/A-2u save failed');
finally
Pdf.Free;
end;
end;
Większość rekordu może pozostać pusta. Pozostaw Title, Author, Subject, Keywords, Creator oraz Producer puste, a SaveAsPdfA uzupełni je automatycznie na podstawie słownika Info dokumentu przez FPDF_GetMetaText. Pozostaw CreationDate oraz ModDate puste, a zostanie użyty bieżący czas UTC dla obu dat XMP. Pozostaw DocumentId oraz InstanceId puste, a biblioteka wstępnie wypełni je na podstawie FPDF_GetFileIdentifier, z fallbackiem do deterministycznego ID wyprowadzonego z bajtów źródłowych. Jednym polem, które możesz świadomie nadpisać, jest IccProfileData: pusty oznacza dołączony profil sRGB IEC61966-2.1, ale workflow CMYK albo grayscale powinien dostarczyć własny
Dlaczego Level A ulega degradacji i dlaczego to uczciwy wybór
Tu pojawia się subtelność, która potyka ludzi oczekujących, że flaga jest gwarancją. Możesz zażądać pac1a dla dokumentu bez drzewa tagów, ale PDF/A-1a wymaga logicznej struktury z §6.8, a biblioteka nie potrafi wyprodukować drzewa struktury z nieotagowanego PDF. Zamiast emitować plik, który deklaruje Level A, ale go nie spełnia, SaveAsPdfA sprawdza obecność rzeczywistej struktury otagowanej, /StructTreeRoot czyli /MarkInfo z /Marked true, i jeśli jej brak, obniża deklarowany poziom: pac1a staje się pac1b, pac2a staje się pac2b i tak dalej we wszystkich trzech częściach. Wewnętrzne funkcje pomocnicze to PdfAIsLevelA oraz PdfADowngradeToLevelB
Warto powiedzieć wprost, dlaczego: plik, który uczciwie deklaruje poziom, jaki naprawdę spełnia, jest bardziej użyteczny niż taki, który kłamie o poziomie, którego nie spełnia. Level U jest traktowany inaczej. Wykrywanie rzeczywistego pokrycia Unicode wymagałoby naiwnego testu "czy ma /ToUnicode" i taki test nadmiernie degradowałby prawidłowe dokumenty, bo WinAnsi i podobne kodowania są wyłączone z tego wymagania, więc strona zapisu emituje deklarację U dokładnie tak, jak zażądał wywołujący, a ewentualną rozbieżność pozostawia do wychwycenia na etapie walidacji. Jeśli potrzebujesz gwarantowanego archiwum Level A, otaguj dokument przed konwersją; konwerter nie wymyśli struktury, której tam nie ma
Pułapka ICC, którą łapie tylko prawdziwy walidator
To była porażka, która dała najtwardszą lekcję, bo własny checker biblioteki ją przepuszczał, a veraPDF, referencyjny walidator ISO 19005, już nie. PDF/A wymaga, aby profil docelowy OutputIntent był poprawnym strumieniem ICCBased, a §6.2.3.2 wymusza, aby walidator sprawdzał ten strumień jako przestrzeń barw. Strumień ICCBased musi deklarować /N, czyli liczbę składowych koloru. Wczesna wersja injector zapisywała słownik strumienia ICC tylko z /Length i bez /N, a veraPDF odrzucał wynik komunikatem "The N entry (value null)... is missing"
Podstępne było to, że odrzucenie pojawiało się tylko dla PDF/A-1b i -1a. Modele zgodności części 2 i 3 nie wykonywały tego konkretnego sprawdzenia dla profilu docelowego, więc identycznie wstrzyknięta struktura przechodziła walidację dla pac2b, pac3b oraz pac2u, ale oblewała przy pac1b wyłącznie z powodu wartości pdfaid:part. Test jednostkowy nie mógł tego zobaczyć, bo własny ValidatePdfACompliance sprawdzał jedynie, czy klucz /DestOutputProfile istnieje, a nie co naprawdę znajduje się w słowniku strumienia. Testy wewnętrzne pozostawały zielone, a prawdziwa walidacja archiwalna zawodziła
Poprawką jest IccComponentCount, które odczytuje sygnaturę data colour space na przesunięciu 16 w nagłówku ICC i mapuje ją na liczbę składowych: GRAY to 1, RGB , Lab oraz XYZ to 3, CMYK to 4, a nieznany profil domyślnie daje 3. Ta liczba trafia do słownika strumienia jako /N. Jest obliczana, a nie wpisana na sztywno jako 3, dzięki czemu wywołujący, który dostarcza profil CMYK albo grayscale przez IccProfileData, nadal dostaje poprawną wartość. Szersza lekcja jest metodologiczna: checker w bibliotece i autorytatywny walidator mają własne martwe pola, więc wyjście PDF/A trzeba testować end to end na implementacji referencyjnej takiej jak veraPDF, zamiast ufać samokontroli. Ta sama dyscyplina incremental update stojąca za poprawnymi archiwami jest omówiona w walidacji skompresowanych strumieni obiektów i xref, co ma znaczenie, bo współczesne PDF-y przetwarzane przez injector są często budowane na strumieniach cross-reference
Szyfrowanie, strumienie xref i inne brzegi
Ponieważ ISO 19005 zabrania szyfrowania, ścieżka zapisu usuwa je przed zapisaniem. SaveAsPdfA stosuje FPDF_REMOVE_SECURITY podczas serializacji, więc zaszyfrowane źródło, wczytane z hasłem, jest po drodze do archiwum odszyfrowywane. Dla dokumentu niezaszyfrowanego jest to no-op i nic nie zmienia. Wniosek jest taki sam jak ograniczenie wymuszane przez HotPDF z drugiej strony: pojedynczy plik nie może być jednocześnie zaszyfrowany i zgodny z PDF/A. Gdy workflow potrzebuje obu, odpowiedzią są dwa artefakty: zaszyfrowana kopia do dystrybucji i osobna czysta kopia do archiwum
Jeszcze jeden brzeg pozostaje niewidoczny, dopóki nie ugryzie: dokumenty PDF 1.5+ używające czystego strumienia cross-reference i nieposiadające słowa kluczowego trailer. Injector czyta trailer, aby znaleźć źródłowe /Info i dopisać swój incremental update, a więc musi akceptować formę xref-stream, bo inaczej taki dokument przeszedłby kopiowanie z cichym zgubieniem znaczników. ISO 32000-1 §7.5.6 wprost dopuszcza klasyczny incremental update oparty na trailerze po dokumencie xref-stream, z /Prev wskazującym na offset strumienia xref, i dokładnie taką strukturę emituje injector. Własne FPDF_SaveAsCopy w PDFium zawsze zapisuje klasyczny trailer, więc w normalnym pipeline injector nigdy nie spotyka czystego źródła xref-stream, ale ścieżka odczytu obsługuje je dla dokumentów przychodzących z zewnątrz
Weryfikacja, zanim zaufasz deklaracji
Biblioteka dostarcza checker na poziomie bajtów, TPdf.ValidatePdfA, który zwraca TPdfAValidationResult. Pole Conformance raportuje wykryty poziom, a Issues jest zbiorem wartości TPdfAValidationIssue; metoda pomocnicza IsCompliant zwraca true tylko wtedy, gdy wykryto rzeczywisty poziom i zbiór problemów jest pusty. Uruchamiaj to jako szybki pierwszy gate w batchu
var
Pdf: TPdf;
Res: TPdfAValidationResult;
begin
Pdf := TPdf.Create(nil);
try
Pdf.LoadFromFile('invoice_archive.pdf');
Res := Pdf.ValidatePdfA;
if Res.IsCompliant then
Writeln('Conformant: detected level ', Ord(Res.Conformance))
else
Writeln('Issues found: ', SizeOf(Res.Issues), ' flags set');
finally
Pdf.Free;
end;
end;
Trzeba uczciwie ocenić, co to daje. Checker na poziomie bajtów z dużą pewnością łapie problemy strukturalne, takie jak brakujący OutputIntent, zabroniona akcja czy obecny /Encrypt, przezroczystość tam, gdzie część 1 jej zabrania, a wykrywanie osadzania fontów używa heurystyki zliczania, która celowo raportuje tylko sygnał o wysokiej pewności zamiast śledzić pokrycie każdego glifu. Czego nie robi, to analiza operatorów content stream, bo wymagałoby to pełnego parsera zawartości i jest celowo poza zakresem. Dla release gate łącz checker z biblioteki z veraPDF: checker jest natychmiastowy i działa wszędzie bez DLL, veraPDF jest autorytatywny. Wpięcie tej pary w batch run jest tematem batch preflight report CLI, bo właśnie tam taka walidacja powinna żyć w prawdziwym workflow archiwalnym
API SaveAsPdfA, InjectPdfAMarkers oraz ValidatePdfA pokazane tutaj są dostarczane razem z PDFium Component dla Delphi, C++Builder i Lazarus/FPC. Strona produktu linkuje pełne API reference, w tym kompletny enum zgodności i rekord opcji stojący za tymi przykładami