Technical Article

Dávkové reporty PDF Preflight v Delphi s využitím PDFium Component CLI

Nástroj na dávkový preflight je konzolový program bez grafického rozhrania. Ukážete mu priečinok so súbormi PDF, program každý z nich skontroluje voči zadaným štandardom zhody a zanechá po sebe strojovo čitateľný dôkaz o svojich zisteniach. Nikto pri ňom nesedí a nesleduje ho. Spúšťa sa o druhej ráno cez cron alebo plánovač úloh Windows (Task Scheduler), prípadne ako kontrolný krok v CI pipeline. Ďalšou osobou, ktorú zaujíma jeho výstup, je buď plánovač čítajúci návratový kód (exit code) alebo audítor otvárajúci report o niekoľko týždňov neskôr. To mení význam slova „správny“. Validačné jadro komponentu PDFium Component, knižnice PDF so zdrojovým kódom pre Delphi, C++Builder a Lazarus, robí samotné volania validácie takmer triviálnymi. Práca, ktorá rozhoduje o tom, či je nástroj skutočne užitočný, sa točí okolo týchto volaní: ktorý profil ste kontrolovali, čo návratový kód povedal plánovaču a či report, ktorý mal odhaliť chybu, stále existuje, keď ho niekto začne hľadať.

Zmluva: čo plánovač v skutočnosti vidí

CI runner alebo plánovač úloh Windows vidí z vášho nástroja presne dve veci: návratový kód a súbory, ktoré po sebe zanechal. Riadky denníka (logy), farby konzoly, výstup o priebehu - to všetko je pre človeka sledujúceho priebeh naživo, no o druhej ráno pri ňom nikto nie je. Preto si ujasnite významy návratových kódov skôr, než sa dotknete API, a udržujte ich štandardné:

  • 0: každý súbor vyhovel každému požadovanému profilu
  • 1: aspoň jeden súbor vygeneroval nálezy validácie
  • 2: samotný nástroj zlyhal na aspoň jednom súbore (poškodený vstup, zámok súboru, pád)

Rozdiel medzi kódmi 1 a 2 je bod, ktorý tímy často vynechávajú a neskôr to ľutujú. Poškodené PDF, ktoré sa nedá otvoriť, nie je chybou zhody s normou. Ak ho zaradíte pod kód 1, hromada poškodených skenov sa vo vašich prehľadoch zobrazí ako náhly kolaps zhody. To pošle niekoho hľadať chybu v nastavení noriem, hoci skutočným problémom je pokazený skener na vstupe.

Do zmluvy patria ešte dve položky. Prvou je časový limit (timeout) pre každý súbor. Patologické PDF s tisíckami strán a hlboko vnorenými štruktúrami objektov môže zamestnať jeden prechod validácie na celé minúty, pričom nočné okno na to nemá trpezlivosť. Ukončite spracovanie takéhoto súboru pri dosiahnutí limitu, započítajte ho ako zlyhanie nástroja a pokračujte v dávke.

Druhou položkou je karanténny adresár: presuňte každý expirovaný alebo neotvoriteľný vstup bokom namiesto toho, aby ste ho nechali na pôvodnom mieste. Počas niekoľkých mesiacov tento adresár potichu nahromadí tie najhoršie dokumenty, aké vám vaši skutoční zákazníci pošlú, a tento súbor dát má pre testovanie nových verzií väčšiu hodnotu než akákoľvek umelo vytvorená vzorka.

Výber noriem a prečo záleží na úrovni zhody

Enumerácia TPdfPreflightStandard pokrýva skupiny noriem, ktoré sa vyskytujú v praxi: ppsPdfA pre archívnu zhodu ISO 19005, ppsPdfUa pre prístupnosť ISO 14289, ppsPdfX pre tlačovú výmenu, plus ppsPdfE, ppsPdfR a ppsPdfVT pre inžiniersku prácu, rastrové dáta a premenlivé dáta.

V rámci jednej rodiny noriem jadro načíta úroveň zhody, ktorú dokument deklaruje, a nahlási ju pre každú normu vo vlastnosti výsledku ConformanceName. Pomenovanie samotnej rodiny málokedy stačí, pretože skutočný rozdiel je práve v úrovni. PDF/A-2b zaručuje vizuálnu reprodukovateľnosť a nič viac. PDF/A-3a pridáva požiadavku na logické tagovanie štruktúry a umožňuje vnorené zdrojové súbory, čo je oveľa vyššia prekážka pre naskenovaný materiál, ktorý nemá žiadny strom tagov. Ak to v ktoromkoľvek smere nastavíte nesprávne, dávka vám bude klamať. Ak vaše pravidlá archivácie vyžadujú PDF/A-2b, ale vyradíte súbory pre chýbajúce tagy štruktúry, report sa zaplní nálezmi, ktoré nikto nikdy nebude opravovať. Naopak, ak akceptujete akýkoľvek štítok PDF/A bez kontroly úrovne, schválite dokumenty vyhovujúce slabšej norme, než ste sľúbili. Požiadavky na prístupnosť od štátnych odberateľov k tomu všetkému čoraz častejšie pridávajú PDF/UA, čo nezvyšuje nároky na beh programu, pretože metóda BuildPdfPreflightReport (z jednotky FPdfPreflightReport) prijíma sadu noriem:

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

Jedno volanie vyhodnotí obe normy a vráti jeden konsolidovaný záznam reportu.

Prečo prázdny zoznam nálezov neznamená úspešnú validáciu

Report vymenúva nálezy pre každú normu a prázdny zoznam problémov znamená iba „v spustených normách sa nenašli žiadne problémy“. To je užšie tvrdenie ako „súbor vyhovuje norme, na ktorej vám záleží“, a rozdiel medzi nimi je miestom, kde dávkový preflight potichu zlyháva. Chyba v konfigurácii, ktorá vynechá ppsPdfA zo sady, vyprodukuje presne rovnaký prázdny zoznam problémov ako skutočne vyhovujúci súbor. Považujte preto ticho za podozrivé. Prejdite Report.Results a overte dve veci pre každú normu, ktorú ste chceli skontrolovať: že záznam o jej výsledku vôbec existuje a že jej príznak IsCompliant (podporený stavom Status = pfsPass) je pravdivý. Nočná úloha, ktorá stotožňuje „žiadne nálezy“ s „pripravené na archiváciu“ bez toho, aby kedykoľvek overila, ktoré normy boli reálne vyhodnocované, je klasickým spôsobom, ako priečinok nevyhovujúcich súborov prechádza mesiacmi bez povšimnutia. Až kým externý audítor neotvorí jeden z nich cez veraPDF a nespochybní celý archív.

Druhá pasca sa skrýva v samotnej podstate nálezu. Každý záznam TPdfPreflightIssue obsahuje kľúčové hodnoty Code, Category, Description a Recommendation, pričom menuje porušené pravidlo, nie konkrétnu stránku alebo objekt. Ide o návrhové rozhodnutie s dôsledkami pre spätnú väzbu. Report hovorí tímu zodpovednému za tvorbu dokumentu, aká trieda chyby sa v ňom nachádza (napr. nevložené písmo alebo chýbajúci identifikátor XMP), a vyhľadanie konkrétneho problematického objektu je prácou pre opravné nástroje v ďalších krokoch, nie pre samotný validátor. Postavte spracovanie reportov na stabilných hodnotách Code, nikdy nie na popise určenom pre ľudí, ktorý sa môže medzi verziami zmeniť bez predchádzajúceho upozornenia.

Súbory reportov pre stroje aj pre osobu v pohotovosti

Záznam reportu zapisuje rovnaké zistenia v piatich formátoch: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile a SaveMarkdownToFile, pričom každý má zodpovedajúcu funkciu typu ToJson, ak chcete mať reťazec v pamäti a nie na disku. Odolajte nutkaniu vybrať si len jeden. Zapisujte JSON pre procesy, aby ho CI systém mohol pripojiť k záznamu o úlohe a analyzovať kódy problémov a stavy noriem bez parsovania textu. Zapisujte HTML pre človeka, ktorý má pohotovosť, pretože sa dá otvoriť v akomkoľvek prehliadači bez špeciálnych nástrojov. Obe možnosti spoločne stoja len jeden riadok kódu navyše na súbor a ušetria vášmu inžinierovi v pohotovosti tú najhoršiu prácu v dávkovom spracovaní: lúštenie surového JSON bloku o druhej ráno, aby zistil, ktorý súbor zlyhal. Jedna zásada je dôležitejšia ako voľba formátu: odvodzujte názov každého reportu od názvu vstupného súboru, nikdy nie od časovej pečiatky. V opačnom prípade by dva paralelné behy vytvorili reporty, ktoré by ste už nevedeli priradiť k ich vstupom.

Prahové hodnoty závažnosti (severity thresholds) patria do konfigurácie, nie do kódu. Anotácia bez alternatívneho popisu je kritickou chybou pre portál na odosielanie PDF/UA dokumentov a zanedbateľnou poznámkou pre interný archív, hoci v oboch prípadoch ide o identický nález. Sprístupnite úroveň chybovosti (fail-on level) pre každý profil samostatne, aby sa pravidlá mohli zmeniť bez opätovnej kompilácie, a zapíšte platnú úroveň priamo do súhrnu úlohy. O tri mesiace si už nikto nebude pamätať, pod akým prahom bežala minuloročná októbrová dávka, a súhrn je jediným miestom, kde táto informácia zostane zachovaná.

Izolácia súborov, aby jedno chybné PDF nepotopilo celú dávku

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;

V tomto cykle sa nachádzajú tri zámerné rozhodnutia. Nová inštancia TPdf pre každý súbor zaručuje, že jeden dokument, ktorý poškodí stav jadra, neovplyvní nasledujúce súbory. Explicitná kontrola vlastnosti Active má svoje miesto, pretože Active := True potláča chyby načítania namiesto ich vyvolania. Ak by ste túto kontrolu vynechali, poškodený súbor by prešiel až k volaniu validácie a zlyhal by neskôr s mätúcim hlásením. Vnútorný blok try..except sa nachádza zámerne v rozsahu jedného súboru, takže jediná výnimka iba zvýši počítadlo zlyhaní a cyklus pokračuje ďalej. Chcete mať čisté reporty pre 4 999 dobrých súborov, aj keď je súbor 5 000 poškodený. A oba formáty reportov sa zapíšu na disk ešte pred spočítaním výsledkov, čo znamená, že dôkaz prežije aj v prípade, ak by neskoršia chyba v sumarizačnej logike nesprávne spočítala výsledky.

Priradenie návratových kódov sa potom redukuje na niekoľko riadkov v súbore 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.

Čo za vás preflight neurobí

Jadro chyby iba zisťuje, neopravuje ich. Nález o nevloženom písme alebo farebnom priestore závislom od zariadenia je zadanie pre toho, kto súbory vytvára, a validátor nemá možnosť opraviť ho priamo na mieste. Naplánujte preto spätnú väzbu cielene. Reporty musia končiť tam, kde ich produkčný tím reálne číta. V opačnom prípade sa rovnaké nálezy budú objavovať každú noc, kým sa niekto neopýta, prečo sa miera zhody s normami nikdy nezlepšuje.

Taktiež sa oplatí porovnať vzorku výsledkov s nezávislým validátorom (napríklad veraPDF pre PDF/A alebo preflight v Acrobate pre PDF/X) predtým, než ich pre vás skontroluje externý audítor. Keď sa dve jadrá nezhodnú na reálnom súbore zákazníka, tento dokument nie je na obtiaž. Je to presne ten prípad regresie, ktorý chýbal vo vašom testovaní vydania. Uložte si ho, pomenujte ho a spúšťajte ho pri každom zostavení.

Stojí za to vedieť o ešte jednom prepojení. Rovnaké validačné jadro poháňa interaktívne kontroly v používateľskom rozhraní, takže toto CLI bez rozhrania a analytický nástroj na kontrolu prijatých PDF môžu zdieľať rovnakú validačnú slovnú zásobu namiesto toho, aby sa časom rozchádzali.

A keďže kombinácia [ppsPdfA, ppsPdfUa] vyhodnocuje aj prístupnosť v rovnakom kroku, strana PDF/UA v dávke plynule nadväzuje na prácu na strane prehliadača, ako je vytvorenie prístupnej čítačke PDF v Delphi. Profily, formáty reportov a kompletné preflight API sú zdokumentované na produktovej stránke PDFium Component.