Lista kontrolna PDF/A-3 zwykle mówi, że pakiet XMP musi nieść dwa wpisy identyfikacyjne, pdfaid:part i pdfaid:conformance. Poprawnie zapisany plik PDF/A-4 niesie pdfaid:part 4 i pdfaid:rev 2020, a dla profilu bazowego nie ma wpisu zgodności wcale. Dorzucenie litery, żeby "naprawić" plik, czyni go błędnym: ISO 19005-4 identyfikuje część bazową numerem i rokiem rewizji, a literę zachowują tylko dwa rozszerzenia
PDF/UA-2 ma podobną pułapkę poziom niżej, wewnątrz drzewa struktury. Zadeklarowanie własnej roli we własnej przestrzeni nazw wygląda na całość roboty, skoro przestrzenie nazw to funkcja, którą dodała część 2. To połowa roboty. Rola taka jak WidgetGroup w https://example.com/ns/widgets wciąż musi się zmapować na rolę standardową, zanim zgodny checker ją przyjmie, a nic na wyrenderowanej stronie nie zdradza, że mapowania brakuje
Wybieranie PDF/A-4 przez PDFACompliance
W HotPDF profile PDF/A-4 to trzy wartości istniejącej właściwości PDFACompliance: '4' dla części bazowej, '4E' dla dokumentów inżynieryjnych i '4F' dla plików niosących pliki osadzone. Przy wybranym '4' pakiet XMP dostaje pdfaid:part 4 i pdfaid:rev 2020 i nic więcej z przestrzeni pdfaid. '4E' i '4F' dodają pdfaid:conformance z E albo F. Funkcje dekodujące raportują ten sam podział: PDFAPart zwraca 4 dla wszystkich trzech wartości, a PDFAConformanceLevel zwraca E, F albo #0 dla profilu bazowego
Wszystkie trzy wymuszają nagłówek PDF 2.0. Podniesienie dzieje się wewnątrz EndDoc, więc dokument, który zaczął jako PDF 1.4, wychodzi jako 2.0 bez osobnego wywołania. Jedyny przypadek, który to zatrzymuje, to StrictVersionLock. Ta flaga zabrania automatycznych podwyższeń, więc EndDoc rzuca "PDF/A-4 requires PDF 2.0; StrictVersionLock prevents the version upgrade", zamiast zapisać plik 1.7, który udaje część 4 w metadanych. Baza kodu przypinająca wersję dla starego przepływu druku zobaczy dokładnie ten komunikat, gdy ktoś pierwszy raz przełączy profil
Dwie reguły PDF/A-4 zmieniają to, co ląduje w trailerze i katalogu. ISO 19005-4 §6.1.3 dopuszcza słownik informacji o dokumencie tylko wtedy, gdy katalog ma wpis PieceInfo, i nawet wtedy ogranicza go do ModDate, więc HotPDF nie pisze słownika Info dla części 4, a tytuł jedzie jako dc:title w XMP. Title i Lang pozostają obowiązkowe. Zostawienie któregokolwiek pustego kończy się wyjątkiem z EndDoc. Rzuca on też, gdy nie dodano intencji wyjścia przez AddPDFAOutputIntent, a EnableEncrypt odmawia biegu, gdy wybrany jest jakikolwiek profil PDF/A, bo zaszyfrowany plik archiwalny to sprzeczność, której norma nie dopuszcza
Pliki powiązane pod PDF/A-4f i 4e
Pliki powiązane to główny powód, by wybrać PDF/A-4f: faktura XML za wyrenderowaną stroną, ładunek JSON, który parsuje system po stronie dopływu. AddPDFAssociatedFile bierze nazwę pliku, typ MIME, opis, wartość AFRelationship i bajty, a zwraca pośredni słownik FileSpec, żebyś mógł dokleić własne opcjonalne klucze
var
Pdf: THotPDF;
Icc: TFileStream;
begin
Pdf := THotPDF.Create(nil);
Icc := TFileStream.Create('sRGB2014.icc', fmOpenRead or fmShareDenyWrite);
try
Pdf.FileName := 'invoice-archive.pdf';
Pdf.PDFACompliance := '4F'; // part 4, rev 2020, zgodność F
Pdf.Title := 'Invoice 2026-0731'; // zapisywany wyłącznie do XMP dc:title
Pdf.Lang := 'en-US';
Pdf.BeginDoc;
Pdf.AddPDFAOutputIntent('sRGB IEC61966-2.1', 'sRGB IEC61966-2.1',
Icc, 3, 'DeviceRGB');
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-0731');
// LoadInvoiceBytes to Twoja własna funkcja zwracająca TBytes
Pdf.AddPDFAssociatedFile('invoice.xml', 'text/xml',
'Structured invoice data', 'Data', LoadInvoiceBytes);
Pdf.EndDoc;
finally
Icc.Free;
Pdf.Free;
end;
end;
Każde wywołanie robi cztery rzeczy. Zapisuje strumień EmbeddedFile, którego /Subtype trzyma typ MIME jako nazwę PDF, ze słownikiem /Params z rozmiarem, datą modyfikacji i sumą kontrolną MD5. Buduje FileSpec z /F i /UF naraz, opisem jako /Desc i nazwą relacji. Potem rejestruje ten FileSpec dwukrotnie — w tablicy /AF katalogu i w drzewie nazw EmbeddedFiles. Obie rejestracje są wymagane. Plik zarejestrowany tylko w jednym miejscu potrafi się jeszcze otworzyć z listy załączników przeglądarki, dlatego szybka kontrola wzrokiem mówi bardzo niewiele o archiwum
Ciąg relacji musi być jednym z Source, Data, Alternative, Supplement albo Unspecified; cokolwiek innego rzuca, zanim zostanie zapisany bajt, podobnie jak pusty typ MIME. Sprawdzany jest też profil. AddPDFAssociatedFile przyjmuje PDF/A-3, PDF/A-4e i PDF/A-4f, a bazowy profil '4' rzuca, bo plików powiązanych nie dopuszcza. W drugą stronę AddDocumentAttachment odmawia biegu pod PDF/A-3, 4e i 4f, bo zwykły załącznik nie ma rejestracji /AF, której te profile wymagają. Kod pisany pod PDF/A-3 wokół AddPDFA3AssociatedFile dalej się kompiluje i zachowuje tak samo; AddPDFAssociatedFile to neutralna profilowo nazwa tej metody
Dwa rozszerzenia dzielą między siebie pozostałe uprawnienia. Pod '4F' EndDoc dopilnuje, by katalog niósł drzewo nazw EmbeddedFiles, a AddFileAttachmentAnnotation jest przyjmowane, podczas gdy profil bazowy i 4e go odrzucają. Pod '4E' dozwolone są Add3DAnnotation i AddRichMediaAnnotation, wraz z akcjami SetOCGState i GoTo3DView. Każdy inny profil PDF/A odrzuca wszystkie cztery
Dlaczego własna rola z przestrzenią nazw wciąż wali PDF/UA-2?
Własna rola przechodzi PDF/UA-2 tylko wtedy, gdy spełnione są dwa warunki: jej wpis /NS wskazuje na słownik przestrzeni nazw wypisany w korzeniu drzewa struktury, a rola rozwiązuje się przez RoleMapNS tej przestrzeni do roli w przestrzeni standardowej. HotPDF rozkłada tę robotę na trzy metody. RegisterStructureNamespace tworzy pośredni słownik /Type /Namespace, dokłada go do StructTreeRoot /Namespaces i zwraca; drugie wywołanie z tym samym URI zwraca istniejący słownik, zamiast dodawać kolejny. AddStructureElementNS tworzy element i ustawia jego /NS, przy okazji rejestrując przestrzeń nazw. AddStructRoleMapNS zapisuje mapowanie
const
PDF2NS = 'http://iso.org/pdf2/ssn';
WidgetNS = 'https://example.com/ns/widgets';
var
DocElem, Group: THPDFDictionaryObject;
begin
Pdf.PDFUACompliance := True;
Pdf.PDFUAPart := 2; // PDF/UA-2 wymaga PDF 2.0
Pdf.Title := 'Widget catalog'; // pusty Title rzuca przy EndDoc
Pdf.Lang := 'en-US';
Pdf.BeginDoc;
DocElem := Pdf.AddStructureElement('Document', nil); // związany z PDF2NS
Group := Pdf.AddStructureElementNS('WidgetGroup', WidgetNS, DocElem);
// bez tego mapowania EndDoc rzuca ua2.rolemap.unresolved
Pdf.AddStructRoleMapNS('WidgetGroup', WidgetNS, 'Div', PDF2NS);
Pdf.CurrentPage.SetFont('Arial', [], 18);
Pdf.CurrentPage.BeginMarkedContentMCID('H1', 0);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Widgets');
Pdf.CurrentPage.EndMarkedContent;
Pdf.AddStructureElement('H1', Group, 0, 0); // indeks strony 0, MCID 0
Pdf.EndDoc;
end;
Część tego wypisywania dzieje się bez pytania. Przy PDFUAPart ustawionym na 2 wywołanie AddStructureElement('Document', nil) wiąże element ze standardową przestrzenią nazw PDF 2.0, http://iso.org/pdf2/ssn, bo korzeń drzewa PDF/UA-2 musi być Documentem w tej przestrzeni. To samo wiązanie dotyczy każdej roli ze słownictwa PDF 2.0, więc H1 i P nie potrzebują argumentu przestrzeni nazw. Role istniejące tylko w zestawie PDF 1.7, jak Note, Code albo BlockQuote, zostają w przestrzeni 1.7
Cel mapowania to prawdziwy wybór. Puste przestrzenie nazw celu przekazane do AddStructRoleMapNS zapisują formę krótką — gołą nazwę roli rozwiązywaną względem zestawu PDF 1.7. Przekazanie URI PDF 2.0 zapisuje formę dwuelementową [rola przestrzeń]. W dokumencie PDF/UA-2 mapuj do przestrzeni PDF 2.0: całe drzewo rozwiązuje się wtedy do jednego słownictwa, a cele takie jak Aside albo FENote, które nie mają odpowiednika w 1.7, stają się dostępne. Dwa kształty mapowania są odrzucane od razu przy EndDoc: łańcuch zapętlający się w samego siebie i rola w jawnej przestrzeni nazw zmapowana na inną rolę w tej samej przestrzeni
Kolejność wywołań jest luźniejsza, niż wygląda. Mapowanie można dodać przed istnieniem elementu albo po, bo nic go nie sprawdza, dopóki drzewo się nie skończy. Od razu zawodzi natomiast AddStructureElementNS, gdy PDFUAPart wynosi 1. Część 1 nie ma przestrzeni nazw i wywołanie rzuca "structure namespaces require PDFUAPart=2" na miejscu
Co EndDoc sprawdza, zanim plik zostanie zapisany
Przy włączonym PDFUACompliance i PDFUAPart równym 2 EndDoc przeprowadza pełną walidację struktury PDF/UA-2, zanim cokolwiek zserializuje, i pierwszy błąd staje się wyjątkiem. Cztery z jego sprawdzeń strzegą szkieletu drzewa. Korzeń musi trzymać dokładnie jeden najwyższy element Document. Każdy słownik przestrzeni nazw musi być pośredni, typowany Namespace i nieść niepusty URI występujący tylko raz. Każda referencja /NS musi się rozwiązywać do słownika wypisanego w tablicy /Namespaces korzenia. I każda rola musi się rozwiązywać do przestrzeni standardowej — bezpośrednio albo przez RoleMap i RoleMapNS. Elementy, do których nie da się dojść od elementu Document, też są raportowane
Ten sam przebieg pokrywa wymagania na poziomie dokumentu: niepusty, ważny Lang; MarkInfo /Marked ustawione na true; włączone DisplayDocTitle; pakiet XMP z niepustym dc:title i pdfuaid:part 2. HotPDF sam ustawia flagę marked, preferencję przeglądarki i identyfikację XMP, gdy tylko PDF/UA jest włączony, i zapisuje pdfuaid:rev 2024 obok numeru części. Tytuł i język dostarczasz sam, a pusta wartość któregokolwiek rzuca, zanim walidator w ogóle ruszy. Nagłówki, listy, tabele, adnotacje i formularze są sprawdzane w tym samym biegu
Czekanie do EndDoc jest zamierzone. W połowie generowania drzewo legalnie jest niekompletne: element Document może nie mieć jeszcze dzieci, przestrzeń nazw może być zarejestrowana, zanim użyje jej jakikolwiek element, mapowanie może przyjść po swoim elemencie. Sprawdzanie przy każdym wywołaniu odrzucałoby te stany. Przy EndDoc drzewo jest skończone i wciąż w pamięci, a wyjątek nazywa kod reguły i ścieżkę elementu, który ją złamał
try
Pdf.EndDoc;
except
on E: Exception do
begin
// E.Message ma kształt
// PDF/UA-2 validation failed [ua2.heading.skipped at ...]:
// Poziomy nagłówków przeskakują z H2 na H4
LogBuildFailure(Pdf.FileName, E.Message); // Twój własny logger
raise;
end;
end;
Wyjście PDF/A-4 dostaje to samo traktowanie od własnego walidatora. Gdy PDFACompliance trzyma wartość części 4, EndDoc sprawdza wygenerowany graf względem ISO 19005-4 i rzuca z kodem reguły i punktem normy — na przykład pdfa4.filespec.relationship dla FileSpec bez ważnej relacji albo pdfa4.trailer.info_entries dla obcego klucza Info. Edycja Enterprise wystawia oba walidatory jako ValidatePDFUA2Structure i ValidatePDFA4, które wypełniają tablicę problemów wszystkimi zatrzymanymi diagnostykami, zamiast zatrzymywać się na pierwszym błędzie. Dla kroku CI, który ma wypisać pełną listę, to lepszy punkt wejścia
Role PDF 2.0 i nagłówki ponad H6
Typizowane wyliczenie THPDFStandardStructureType zyskuje sstDocumentFragment, sstAside, sstTitle, sstFENote, sstSub, sstEm, sstStrong i sstArtifact. Trzy z nich zmieniają tagowanie zwykłych dokumentów biznesowych. Aside daje panelom bocznym i wyróżnionym cytatom dom, który nie jest nadużytym Sect. FENote oznacza przypisy i przypisy końcowe jako noty, więc czytnik ekranu może zaoferować je na żądanie, zamiast czytać je w połowie zdania. Em i Strong niosą emfazę jako strukturę, tam gdzie drzewo PDF 1.7 mogło ją tylko owinąć w Span
Nagłówki w PDF 2.0 są otwarte. Wyliczenie zatrzymuje się na sstH6, ale przeciążenie łańcuchowe AddStructureElement przyjmuje H7, H8 i dalej, bo zestaw PDF 2.0 definiuje Hn dla dowolnego n od 1 wzwyż. PDF 1.7 zatrzymywał się na H6, co zmuszało głębokie dokumenty techniczne do spłaszczania konspektu albo recyklingu poziomów. Dla dokumentów normatywnych, kodeksów prawnych i katalogów części samo to może uzasadnić przeniesienie wyjścia na PDF 2.0
Głębsze nagłówki przychodzą z ostrzejszą regułą. Przebieg PDF/UA-2 wewnątrz EndDoc wymaga, by poziomy nagłówków były kolejne: pierwszy nagłówek musi być H1, a skok z H2 na H4 rzuca z ua2.heading.skipped. Hierarchia może zacząć się niżej tylko wewnątrz DocumentFragment, roli przeznaczonej dla treści wyjętych z większego dokumentu. Generator, który mapuje konspekt Markdown albo HTML wprost na role Hn, dziedziczy każdy poziom, który autor przeskoczył dla efektu wizualnego, więc znormalizuj konspekt przed tagowaniem
Zanim przeniesiesz wyjście produkcyjne na PDF 2.0
PDF 2.0 zaczyna się od zmiany nagłówka, a nagłówek czytają najpierw starsi konsumenci. Serwis ingestu, przepływ druku albo przeglądarka linii biznesowej przyjmująca najwyżej PDF 1.7 zatrzyma się na %PDF-2.0, jakkolwiek czysta byłaby reszta pliku. Wybranie jakiegokolwiek profilu PDF/A-4 albo PDF/UA części 2 wybiera PDF 2.0, czy prosisz o to, czy nie, więc potwierdź każdy konsumujący system przed przełączeniem, nie po
Najmniej ryzykowne wdrożenie trzyma PDF/A-3 dla dokumentów wychodzących do czytelników, których nie kontrolujesz, używa PDF/A-4f dla archiwów wewnętrznych, gdzie stronę ingestu masz pod sobą, i przyjmuje PDF/UA-2 tam, gdzie wymienia go polityka dostępności. StrictVersionLock służy przy okazji za strażnika w czasie przejścia: ustaw Version na pdf17 i blokadę w ścieżkach kodu, które muszą zostać przy 1.7, a przypadkowy wybór PDF/A-4 wali się przy EndDoc z komunikatem, zamiast dotrzeć do systemu, który pliku nie otworzy. Przewodniki po walidacji PDF/A, PDF/X i PDF/UA i po hybrydowych fakturach ZUGFeRD i Factur-X na PDF/A-3 pokrywają decyzje profilowe, które przychodzą przed numerem wersji, a notatki o automatycznym raportowaniu preflight pokazują, jak zrobić z werdyktu krok builda
Całość biegnie jako natywny kod VCL wewnątrz komponentu dla Delphi i C++Buildera, więc wyjście PDF/A-4 i PDF/UA-2 jedzie z Twoim plikiem wykonywalnym i nie potrzebuje zewnętrznego silnika. Obsługiwane profile i wersje RAD Studio są wypisane na stronie komponentu HotPDF dla Delphi