Artykuł techniczny

PDF Library for Delphi: PDF/A and PDF/UA preflight in Delphi

PDF/A i PDF/UA odpowiadają na dwa pytania, które nie mają ze sobą nic wspólnego, a traktowanie ich jako jednego pola wyboru "dostępność i archiwizacja" jest tym, jak uszkodzone pliki trafiają do archiwum, nosząc etykietę zgodności. PDF/A pyta, czy plik wciąż będzie renderował się wiernie za dwadzieścia lat. PDF/UA pyta, czy technologia wspomagająca potrafi go odczytać dzisiaj. Dokument może przejść jeden na zimno i oblać drugi, więc jedynym uczciwym werdyktem jest uruchomienie obu, i to przed zapisaniem pliku, a nie po tym, jak system niżej w potoku zaufa identyfikatorowi zgodności zaszytemu w jego metadanych. Ten identyfikator to samodeklaracja. Nic w formacie nie wymaga, by była prawdziwa, a aplikacja, która wpisuje "PDF/A-1b" do XMP bez walidacji wobec standardu, produkuje plik, który wygląda na zgodny dla każdego konsumenta czytającego tylko etykietę. losLab PDF Library (PDF Library for Delphi) zamyka tę lukę dla Delphi i C++Builder, wbudowując oba walidatory w bibliotekę, więc kontrola działa w tym samym procesie, bez zewnętrznej usługi do postawienia

Dwa standardy oblewające pliki z przeciwnych powodów

ISO 19005 (PDF/A) to kontrakt odtwarzalności. Zgodny plik musi renderować się identycznie za dekady na oprogramowaniu, które nigdy nie widziało systemu, który go wyprodukował, więc reguły atakują zależności zewnętrzne: każdą osadzoną czcionkę, kolor zakotwiczony w osadzonym OutputIntent ICC albo wyrażony w przestrzeni niezależnej od urządzenia, brak szyfrowania w PDF/A-1, brak JavaScript, metadane XMP zgodne ze słownikiem informacji o dokumencie. ISO 14289 (PDF/UA) to za to kontrakt semantyki. Technologia wspomagająca musi przejść przez dokument i wynieść z niego znaczenie, które mieszka w zupełnie innej warstwie: kompletne drzewo struktury, tekst alternatywny dla rysunków, tytuł dokumentu ustawiony do wyświetlania, poziomy nagłówków, które nie pomijają szczebli, relacje nagłówków tabeli, które przetrwają, gdy strona zejdzie z ekranu

Ponieważ oba standardy pilnują różnych warstw, plikami, które ugryzą, są te siedzące między nimi. Dokument doskonały archiwalnie może być milczący dla czytnika ekranu. Pięknie otagowany może odwoływać się do czcionki desktopowej, której za dziesięć lat już nie będzie. Publikacje sektora publicznego to zwykłe miejsce, gdzie oba wymogi lądują naraz, a potok tam nie może zwinąć ich w jedną bramkę. Wyniki trafiają do różnych ludzi. Nieosadzone czcionki to defekt w kodzie generującym PDF, podczas gdy brakujący tekst alternatywny należy do tego, kto włada szablonami treści, a raport mieszający oba po prostu zostaje przekazany dwukrotnie

To, którą część PDF/A obierasz za cel, ma tak samo duże znaczenie, jak to, czy w nią trafisz. PDF/A-1 jest zamrożony na PDF 1.4 i odrzuca przezroczystość oraz JPEG2000, po które nowoczesne wyjście raportujące sięga bez zastanowienia. PDF/A-2 (ISO 19005-2, zbudowany na ISO 32000-1) akceptuje oba i jest rozsądnym domyślnym wyborem dla nowego archiwum. PDF/A-3 idzie dalej i pozwala na osadzone pliki dowolnego typu, na czym opierają się regulowane formaty e-fakturowania. Zespół wciąż standaryzujący na PDF/A-1b w 2026 roku zwykle niesie wymóg, który ktoś napisał piętnaście lat temu, a renegocjacja docelowej części jest często tańsza niż usuwanie przezroczystości z każdego wykresu, który emituje system

Diagram PDF Library for Delphi porównujący kontrakt odtworzenia PDF/A z kontraktem semantyki PDF/UA plus macierz zaliczenia i porażki dokumentów spełniających jeden a obalających drugi
PDF/A zabezpiecza wierne renderowanie na dziesięciolecia, podczas gdy PDF/UA zabezpiecza czytanie wspomagane już dziś, a żadne z tych orzeczeń nie pociąga za sobą drugiego

Strukturalne wyniki w chwili przyjęcia

Punktem wejścia płaskiego API jest CheckFileCompliance, z selektorem testu 1 dla PDF/A i 2 dla PDF/UA. Zwraca uchwyt listy łańcuchów, której elementy to pojedyncze wyniki, jeden na linię, dokładnie taki kształt, po jakim chce przejść zautomatyzowana bramka:

function GateArchiveUpload(Pdf: TPDFlib; const FileName: string): Boolean;
var
  ListId, I: Integer;
begin
  ListId := Pdf.CheckFileCompliance(FileName, '', 1, 0);  // 1 = PDF/A
  if ListId = 0 then
  begin
    // 0 oznacza "brak wyników" ALBO "plik nieczytelny" -- rozróżnij, zanim przepuścisz
    Result := Pdf.LastErrorCode = 0;
    Exit;
  end;
  for I := 0 to Pdf.GetStringListCount(ListId) - 1 do
    LogFinding(FileName, Pdf.GetStringListItem(ListId, I));
  Pdf.ReleaseStringList(ListId);
  Result := False;
end;

Dwa szczegóły decydują, czy to działa bez nadzoru. Pierwszy to wartość zwracana, która znaczy dwie przeciwne rzeczy. CheckFileCompliance zwraca 0, gdy plik jest w pełni zgodny, a także gdy pliku w ogóle nie dało się otworzyć, bo wewnętrznie pusta lista wyników zwija się do 0 w obu przypadkach. Bramka, która czyta 0 jako przejście, machnie uszkodzone przesłania prosto do archiwum, więc rozróżnij za pomocą LastErrorCode, zanim zaufasz zeru, tak jak robi to powyższa bramka. Drugi dotyczy tego, gdzie plik jest w swoim cyklu życia. Program sprawdzający działa na strumieniowym czytniku biblioteki zamiast na pełnym modelu dokumentu, otwierając plik bezpośrednio z udostępnianiem do odczytu i nigdy nie wywołując LoadFromFile, dlatego potrafi przegryźć się przez kilkugigabajtowe wejście bez budowania drzewa obiektów. To samo strumieniowe otwarcie zawodzi, dopóki inny proces wciąż trzyma plik do zapisu, a trwające przesyłanie to dokładnie taki stan. Bramkuj po zakończeniu transferu

Projekt strumieniowy opłaca się ponownie pod obciążeniem. Każda kontrola otwiera swoje wejście tylko do odczytu i udostępnia je do czytania, więc audyt korpusu skaluje się poziomo między wątkami roboczymi albo procesami, z jedną instancją TPDFlib na worker i bez rywalizacji między nimi. Zasobem wymagającym dyscypliny jest sam uchwyt. Każdy niezerowy wynik z CheckFileCompliance pozostaje przydzielony, dopóki nie wywołasz ReleaseStringList, a długo działająca bramka, która zapomina je zwolnić, nie zawiesza się, po prostu powoli wykrwawia pamięć, dopóki ktoś nie zacznie szukać przyczyny

Raporty dla ludzi, diffy dla bramek buildu

Lista wyników ma właściwy kształt dla bramki, a niewłaściwy dla e-maila do zespołu szablonów. CreatePreflightReport renderuje tę samą analizę jako czytelną prozę, CreatePreflightReportEx dodaje selektor formatu raportu, a SavePreflightReport zapisuje go na dysk, żeby raport mógł podróżować wewnątrz dostarczanego pakietu dokumentów. Sporo kontraktów archiwalnych czyni ten raport samodzielnym elementem dostawy, a nie tylko wewnętrznym artefaktem

Członkiem tej rodziny, który po cichu zasługuje na swoje miejsce, jest ComparePreflightReports. Zgodność to powierzchnia regresji jak każdy inny kawałek zachowania. Poprawka szablonu, nowo licencjonowana czcionka firmowa albo aktualizacja biblioteki, każde z nich może wprowadzić wynik, którego nie było w poprzednim wydaniu, i żadne z nich się nie zapowiada. Trzymaj złote raporty dla zestawu reprezentatywnych dokumentów pod kontrolą wersji, regeneruj je po każdej zmianie i uruchamiaj ComparePreflightReports, żeby obliczyć różnicę. Pusty diff to artefakt wydania warty zachowania. Zaskakujący wynik oblewa build, co jest o wiele tańszym miejscem na jego odkrycie niż audyt

Generowanie wyjścia, które przechodzi za pierwszym razem

Preflight zarabia na siebie na plikach przychodzących skądinąd. Dla dokumentów, które produkuje twój własny kod, znajdowanie naruszeń po wygenerowaniu i łatanie ich z powrotem to powolna droga okrężna. PDF Library for Delphi niesie tryb po stronie generowania dla każdego standardu, i możesz włączyć oba naraz dla tego samego dokumentu:

Diagram PDF Library for Delphi skalowanego przyjmowania preflight z jedną instancją biblioteki strumieniowej na pracownika, otwarciami bramkowanymi po zakończeniu wysyłek i zwalnianymi listami ustaleń
Bramy otwierają się dopiero po zakończeniu transferów, każdy wątek roboczy strumieniuje wejście tylko do odczytu przez prywatną instancję biblioteki, a każdy zwrócony uchwyt wymaga zwolnienia
var
  Pdf: TPDFlib;
  Diag: WideString;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.NewDocument;
    Pdf.SetPDFAMode(1);
    Pdf.LoadOutputIntentProfile('sRGB-IEC61966-2.1.icc', 'RGB');
    Pdf.SetPDFUAMode('en-US');
    Pdf.SetInformation(1, 'Quarterly Statement');  // /Title: wymagane dla PDF/UA
    // ... narysuj tutaj otagowaną treść ...
    Diag := Pdf.GetPDFUADiagnostics;
    if Diag <> '' then
      Writeln('fix before shipping: ', Diag);
    Pdf.SaveToFile('statement.pdf');
    // preflight, który się liczy, działa na zapisanym pliku:
    Writeln(Pdf.CreatePreflightReport('statement.pdf', '', 1, 0));
  finally
    Pdf.Free;
  end;
end;

Pułapka chowa się w chwili zapisu. Kilka napraw zgodności zachodzi podczas serializacji dokumentu, a nie w chwili włączenia trybu: wymuszenie flagi druku na adnotacjach, zapisanie domyślnego AFRelationship dla osadzonych plików PDF/A-3, normalizacja kolejności tabulacji i opisów pól formularza dla PDF/UA. Dokument siedzący w pamięci nie jest identyczny bajt w bajt z tym, który ląduje na dysku, więc jedynym werdyktem preflight, który cokolwiek znaczy, jest ten obliczony z zapisanego pliku. Waliduj sam statement.pdf. Nie wnioskuj o zgodności z obiektu wciąż będącego w pamięci, bo bajty, które byś oceniał, nie są bajtami, które wysłałeś

Diagram PDF Library for Delphi poprawek zgodności nakładanych przy zapisie w trakcie serializacji, dlatego preflight należy stosować na zapisanym pliku PDF, a nie na modelu w pamięci
Serializacja wymusza flagi drukowania adnotacji, nadaje domyślną wartość AFRelationship i normalizuje kolejność tabulacji, więc inspekcja pamięci ocenia bajty, których nikt nigdy nie wysyła

Scenariusze faktur niosące czytelny maszynowo XML obok wizualnego dokumentu podążają za wzorcem ZUGFeRD i Factur-X, zbudowanym na PDF/A-3. Te powinny ustawić relację załącznika jawnie przez SetPDFA3DefaultAFRelationship, ponieważ ISO 19005-3 wymaga, by każdy osadzony plik deklarował swoją rolę względem dokumentu. Zostaw ją nieustawioną, a osadzony XML jest tylko blobem bez podanego celu, co walidator zauważa

Niezależni sędziowie: veraPDF i Acrobat

Producent nie powinien być jedynym sędzią własnego wyjścia. Programy sprawdzające PDF Library for Delphi dają ci szybkie, ustrukturyzowane werdykty w tym samym procesie, czego chcesz na gorącej ścieżce, ale bramka wydania dla partii archiwalnej wciąż powinna przepuścić wyjście przez walidator, którego nikt w twoim zespole nie napisał. veraPDF to utrzymywana przez społeczność implementacja referencyjna dla PDF/A i narzędzie, które większość archiwów wymienia w swoich kryteriach akceptacji, więc to ono jest tym, z którym trzeba się zgadzać. Profile preflight Acrobata stanowią użyteczny rozstrzygający głos, gdy veraPDF i kontrola wewnątrz procesu się nie zgadzają. Zapisuj nazwę walidatora i jego wersję obok każdego przechowywanego raportu. Twierdzenie, że plik przeszedł veraPDF, mówi bardzo mało bez numeru buildu, który go przepuścił, bo narzędzie zaostrza swoje reguły między wydaniami

Walidatory rzeczywiście nie zgadzają się na krawędziach standardów, a gdy tak się dzieje, odpowiedzią nie jest wybranie narzędzia, które lubisz. Zmniejsz plik do minimalnej próbki, która wciąż wywołuje niezgodę, i przeczytaj ją na tle tekstu standardu. Godzina takiej pracy zwykle wydobywa jedną z dwóch rzeczy: prawdziwy błąd narzędzia wart zgłoszenia w górę potoku, albo klauzulę, którą twój zespół czytał źle i powinien zapisać w notatkach o zgodności, żeby kolejna osoba nie musiała tego ponownie roztrząsać

Zaszyfrowane wejście dostaje skrót. Oba programy sprawdzające przyjmują argument hasła, ale plik PDF/A-1 ze słownikiem szyfrowania jest już niezgodny, bo ISO 19005-1 wprost zabrania szyfrowania, więc zaszyfrowane zgłoszenie można odrzucić, zanim uruchomi się jakakolwiek głębsza analiza. Ustalenie, co słownik szyfrowania faktycznie przyznaje, to osobne zadanie, omówione w audycie szyfrowania i uprawnień PDF

Wyniki PDF/UA niemal zawsze sprowadzają się do tego, jak drzewo struktury zostało pierwotnie napisane, a techniki tagowania stojące za tym żyją w budowaniu drzew struktury Tagged PDF w Delphi. Archiwa wymagające też podpisów cyfrowych powinny sparować tę bramkę z przepływem w podpisywaniu i walidacji PAdES. Pełna dokumentacja API preflight znajduje się na stronie produktu losLab PDF Library for Delphi