Artykuł techniczny

Wsadowe raporty preflight PDF w Delphi z PDFium Component CLI

Narzędzie preflight wsadowe to program konsolowy bez okna, skierowany na folder PDF-ów, który waliduje każdy z nich według podanych przez ciebie standardów zgodności i pozostawia maszynoczytelny dowód tego, co znalazł. Nikt tego nie obserwuje. Działa o drugiej w nocy pod cron lub Harmonogramem zadań Windows, albo jako bramka w potoku CI, a następna osoba, która będzie dbała o jego wynik, to albo planista czytający kod wyjścia, albo audytor otwierający raport tygodnie później. To zmienia znaczenie słowa "poprawny". Silnik preflight PDFium Component, biblioteki PDF z kodem źródłowym dla Delphi, C++Builder i Lazarus, sprawia, że same wywołania walidacji są prawie trywialne. Praca decydująca o tym, czy narzędzie jest warte uwagi, leży wokół tych wywołań: który profil sprawdziłeś, co kod wyjścia powiedział planistowi, i czy raport, który mógłby wychwycić błąd, nadal istnieje, gdy ktoś po niego sięgnie

Kontrakt: co planista może faktycznie zobaczyć

Runner CI lub Harmonogram zadań Windows widzi dokładnie dwie rzeczy z twojego narzędzia: kod wyjścia i pliki, które pozostawiło. Linie logów, kolory konsoli, wyjście postępu: to wszystko jest dla człowieka obserwującego na żywo, a o drugiej w nocy nikogo nie ma. Więc ustal słownictwo kodów wyjścia przed dotknięciem API i zachowaj prostotę:

  • 0: każdy plik był zgodny z każdym żądanym profilem
  • 1: co najmniej jeden plik wyprodukował wyniki walidacji
  • 2: narzędzie samo zawiodło na co najmniej jednym pliku (uszkodzone wejście, blokada, awaria)

Różnica między kodami 1 i 2 to ta, którą zespoły pomijają i potem żałują. Uszkodzony PDF, którego nie można otworzyć, to nie błąd walidacji. Połącz go z kodem 1, a ciężarówka uszkodzonych skanów pojawi się w twoich dashboardach jako nagłe załamanie zgodności, wysyłając kogoś na tropienie regresji standardów, która nigdy nie nastąpiła, gdy prawdziwa historia to zepsuty skaner upstream

Dwie kolejne pozycje należą do kontraktu. Pierwsza to limit czasu per-plik. Patologiczny PDF, tysiące stron z głęboko zagnieżdżonymi strukturami obiektów, może zajmować jedno przejście walidacji przez minuty, a nocne okno nie ma na to cierpliwości. Zabij zadanie tego pliku po upływie terminu, policz jako awarię narzędzia i kontynuuj batch. Druga to katalog kwarantanny: przenieś każde wejście z limitem czasu lub nieotwieralne na bok zamiast pozostawiać je na miejscu. Przez kilka miesięcy ten katalog cicho gromadzi najgorsze dokumenty, jakie twoi prawdziwi klienci wysyłają, a ten korpus jest warty więcej dla testów wydania niż jakikolwiek syntetyczny przykład, który mógłbyś napisać ręcznie

Wybór standardów i dlaczego poziom zgodności ma znaczenie

Enumeracja TPdfPreflightStandard obejmuje rodziny, które pojawiają się w praktyce: ppsPdfA dla zgodności archiwalnej ISO 19005, ppsPdfUa dla dostępności ISO 14289, ppsPdfX dla wymiany drukowej, plus ppsPdfE, ppsPdfR i ppsPdfVT dla prac inżynieryjnych, rastrowych i ze zmiennymi danymi. W obrębie rodziny silnik odczytuje poziom zgodności, który deklaruje dokument, i raportuje go per-standard w ConformanceName wyniku. Sama nazwa rodziny rzadko wystarczy, bo poziom to miejsce, gdzie kryje się prawdziwa różnica. PDF/A-2b obiecuje wizualną reprodukowalność i nic więcej. PDF/A-3a dodaje wymóg tagowania struktury logicznej i umożliwia osadzanie plików źródłowych, co jest o wiele trudniejszą poprzeczką dla skanowanego materiału, który w ogóle nie ma drzewa tagów. Pomylenie tego w którymkolwiek kierunku sprawia, że batch kłamie. Jeśli twoja polityka retencji faktycznie chce PDF/A-2b, ale odrzucasz pliki za brak tagów struktury, raport wypełnia się wynikami, których nikt nigdy nie naprawi. Akceptowanie dowolnej etykiety PDF/A bez sprawdzenia poziomu oznacza zatwierdzanie dokumentów spełniających słabszy standard, niż obiecałeś. Mandaty dostępności od rządowych nabywców coraz częściej nakładają PDF/UA na to wszystko, co nie zwiększa kosztu uruchomienia, bo BuildPdfPreflightReport (z jednostki FPdfPreflightReport) przyjmuje zestaw standardów:

Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);

Jedno wywołanie ocenia oba standardy i zwraca jeden skonsolidowany rekord raportu

Dlaczego pusta lista wyników to nie zaliczenie

Raport wylicza wyniki per-standard, a pusta lista problemów oznacza tylko "brak problemów znalezionych w standardach, które faktycznie uruchomiono." To węższe twierdzenie niż "plik jest zgodny ze standardem, na którym ci zależy", a luka między nimi to miejsce, gdzie wsadowy preflight cicho gnije. Literówka w konfiguracji, która usuwa ppsPdfA z zestawu, produkuje dokładnie tę samą pustą listę problemów co prawdziwie czysty plik. Traktuj ciszę jako podejrzaną. Przejrzyj Report.Results i sprawdź dwie rzeczy dla każdego standardu, który zamierzałeś sprawdzić: że wpis wyniku dla niego w ogóle istnieje, i że jego flaga IsCompliant, wspierana przez Status = pfsPass, jest true. Nocne zadanie, które utożsamia "brak wyników" z "gotowe do archiwum" bez potwierdzenia, które standardy zostały ocenione, to klasyczny sposób, by folder niezgodnych plików przelatywał przez miesiące, aż zewnętrzny audytor otworzy jeden z veraPDF i cały archiwum stanie pod znakiem zapytania

Druga pułapka kryje się w tym, czym w ogóle jest wynik. Każdy TPdfPreflightIssue niesie Code, Category, Description i Recommendation, i wymienia naruszoną regułę, nie stronę ani obiekt. To celowa decyzja projektowa z konsekwencjami dla pętli informacji zwrotnej. Raport informuje producenta jaką klasę defektu nosi, osadzony font lub brakujący identyfikator XMP, a znalezienie konkretnego winnego obiektu to zadanie narzędzia naprawczego downstream, nie walidatora. Buduj konsumentów raportu na stabilnych wartościach Code, nigdy na czytelnym przez człowieka tekście opisu, który może zostać przeformułowany między wersjami bez ostrzeżenia

Pliki raportów dla maszyn i dla osoby na dyżurze

Rekord raportu zapisuje te same wyniki w pięciu formatach: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile i SaveMarkdownToFile, każdy z pasującą funkcją w stylu ToJson, gdy chcesz ciąg w pamięci zamiast na dysku. Oprzyj się pokusie wyboru jednego. Pisz JSON dla potoku, aby CI mógł dołączyć go do rekordu zadania i parsować kody problemów i statusy per-standard bez skrapania tekstu. Pisz HTML dla człowieka, który jest paginowany, bo otwiera się w każdej przeglądarce bez żadnych narzędzi. Oba razem kosztują jedną dodatkową linię per-plik i oszczędzają twojemu inżynierowi dyżurnemu jednego najgorszego zadania w przetwarzaniu wsadowym, czyli odwrotnej inżynierii surowego bloku JSON o drugiej w nocy, by dowiedzieć się, który plik się zepsuł. Jedna dyscyplina ma większe znaczenie niż wybór formatu: wyprowadzaj każdą nazwę raportu z nazwy pliku wejściowego, nigdy ze znacznika czasu, bo dwa równoległe przebiegi będą przeplatać raporty, których nie możesz już dopasować do ich wejść

Progi ważności należą do konfiguracji, a nie do kodu. Adnotacja bez alternatywnego opisu to twarda awaria dla portalu zgłoszeń PDF/UA i ignorowalna notatka dla archiwum wewnętrznego, a jednak jest to identyczny wynik w obu przypadkach. Udostępnij poziom fail-on per-profil, żeby polityka mogła się zmienić bez rekompilacji, i zapieczętuj poziom, który obowiązywał, do samego podsumowania zadania. W przyszłym kwartale nikt nie będzie pamiętał, przy jakim progu działał ostatni październikowy batch, a podsumowanie to jedyne miejsce, gdzie ta pamięć przeżywa

Izolowanie plików, żeby jeden zły PDF nie mógł zatopić batcha

procedure RunPreflightBatch(const InputDir, ReportDir: string;
  out FilesWithFindings, ToolFailures: Integer);
var
  SR: TSearchRec;
  Pdf: TPdf;
  Report: TPdfPreflightReport;
begin
  FilesWithFindings := 0;
  ToolFailures := 0;
  if FindFirst(InputDir + '*.pdf', faAnyFile, SR) = 0 then
  try
    repeat
      Pdf := TPdf.Create(nil);   // fresh instance per file: no state bleed
      try
        try
          Pdf.FileName := InputDir + SR.Name;
          Pdf.Active := True;
          if not Pdf.Active then  // load failures are silent, not raised
            raise EPdfError.Create('Cannot open ' + SR.Name);
          Report := BuildPdfPreflightReport(Pdf, [ppsPdfA, ppsPdfUa]);
          Report.SaveJsonToFile(ReportDir + ChangeFileExt(SR.Name, '.json'));
          Report.SaveHtmlToFile(ReportDir + ChangeFileExt(SR.Name, '.html'));
          if Report.TotalIssueCount > 0 then
            Inc(FilesWithFindings);
        except
          on E: Exception do
          begin
            Inc(ToolFailures);   // exit-code-2 territory, not a validation verdict
            WriteLn(ErrOutput, SR.Name + ': ' + E.Message);
          end;
        end;
      finally
        Pdf.Free;
      end;
    until FindNext(SR) <> 0;
  finally
    FindClose(SR);
  end;
end;

Trzy celowe wybory żyją w tej pętli. Świeże TPdf per-plik gwarantuje, że jeden dokument, który korumpuje stan silnika, nie może zatruwać następujących po nim plików. Jawne sprawdzenie Active zasługuje na swoje miejsce, ponieważ Active := True połyka błędy ładowania zamiast je rzucać; usuń ochronę, a obcięty plik dryfuje do wywołania walidacji przed zawiodą gdzieś downstream z mylącym komunikatem. Wewnętrzny try..except żyje wewnątrz zakresu per-plik celowo, więc pojedynczy wyjątek zwiększa licznik awarii i pętla kontynuuje. Chcesz czystych raportów dla 4 999 dobrych plików nawet gdy plik 5 000 jest poszarpany. I oba formaty raportów są zapisywane na dysk przed zebraniem werdyktu, co oznacza, że dowody przeżywają nawet jeśli błąd w późniejszej logice podsumowania zlicza niepoprawnie

Mapowanie kodów wyjścia redukuje się do kilku linii w pliku projektu:

begin
  RunPreflightBatch(ParamStr(1), ParamStr(2), Findings, Failures);
  if Failures > 0 then
    Halt(2)
  else if Findings > 0 then
    Halt(1);
  // falling through exits with 0: every file conformed
end.

Czego preflight nie zrobi za ciebie

Silnik wykrywa, nie naprawia. Wynik dotyczący nieosadzonej czcionki lub przestrzeni kolorów zależnej od urządzenia to zlecenie pracy dla kogokolwiek, kto produkuje pliki, a walidator nie może tego naprawić w miejscu. Planuj więc pętlę informacji zwrotnej celowo. Raporty muszą trafiać tam, gdzie producent faktycznie je czyta, albo te same wyniki będą pojawiać się każdej nocy, dopóki ktoś w końcu nie zapyta, dlaczego wskaźnik zgodności nigdy się nie poprawia. Warto też cross-sprawdzić próbkę werdyktów z niezależnym walidatorem, veraPDF dla PDF/A lub preflight Acrobata dla PDF/X, zanim zewnętrzny audytor zrobi to za ciebie. Gdy dwa silniki nie zgadzają się co do prawdziwego pliku klienta, ten dokument to nie kłopot; to dokładnie przypadek regresji, którego brakowało w testach wydania. Zachowaj go, nazwij i uruchamiaj na każdym buildzie

Jedno jeszcze parowanie warte poznania. Ten sam silnik walidacji napędza interaktywne sprawdzenia w UI recenzji, więc ten bezgłowy CLI i analityczny warsztат przeglądu PDF mogą dzielić jeden słownik walidacji zamiast rozbiegać się z czasem. I ponieważ [ppsPdfA, ppsPdfUa] ocenia dostępność w tym samym przebiegu, strona PDF/UA batcha zgadza się czysto z pracą po stronie przeglądarki jak budowanie dostępnego czytnika PDF w Delphi. Profile, formaty raportów i pełne API preflight są udokumentowane na stronie produktu PDFium Component