Tehnični članak

Paketna preflight poročila za PDF v Delphiju: PDFium CLI

Orodje za paketno predhodno preverjanje (preflight) je konzolni program brez okna, usmerjen v mapo z datotekami PDF, ki vsako datoteko preveri glede na določene standarde skladnosti in ustvari strojno berljivo dokazilo o svojih ugotovitvah. Nihče ga ne spremlja v živo. Zagon se izvede ob dveh zjutraj s storitvijo cron ali Windows Task Scheduler ali pa kot kontrolna točka v cevovodu CI. Naslednji, ki ga zanima rezultat, je bodisi razporejevalnik opravil, ki prebere izhodno kodo, ali pa revizor, ki čez nekaj tednov odpre poročilo. To spremeni pomen besede "pravilno". Mehanizem za preflight v komponenti PDFium Component (knjižnici za PDF z izvorno kodo za Delphi, C++Builder in Lazarus) poskrbi, da so sami klici za preverjanje skoraj trivialni. Delo, ki določa vrednost tega orodja, pa se vrti okoli teh klicev: kateri profil ste preverili, kaj je izhodna koda sporočila razporejevalniku in ali poročilo, ki bi ujelo napako, še vedno obstaja, ko ga nekdo začne iskati

Pogodba: kaj razporejevalnik dejansko vidi

Zagonik CI ali Windows Task Scheduler iz vašega orodja vidi natanko dve stvari: izhodno kodo in datoteke, ki jih je orodje ustvarilo. Vrstice dnevnika, barve v konzoli in izpisi napredka: vse to je namenjeno človeku, ki spremlja izvajanje v živo, a ob dveh zjutraj tega nihče ne počne. Zato določite pomen izhodnih kod, še preden se dotaknete API-ja, in jih ohranite preproste:

  • 0: vsaka datoteka je bila skladna z vsemi zahtevanimi profili
  • 1: vsaj ena datoteka je imela opozorila o skladnosti
  • 2: orodje samo je odpovedalo pri vsaj eni datoteki (poškodovan vhod, zaklepanje datoteke, sesutje)

Razlikovanje med kodama 1 in 2 je tisto, kar ekipe pogosto izpustijo in pozneje obžalujejo. Poškodovan PDF, ki se ne odpre, ni napaka pri validaciji skladnosti. Če ga združete pod kodo 1, se bo na vaših nadzornih ploščah kup poškodovanih optično prebranih dokumentov prikazal kot nenaden padec skladnosti. To bo nekoga pognalo v iskanje sistemske napake, ki se sploh ni zgodila, medtem ko je dejanski razlog okvarjen skener na začetku procesa

V to pogodbo spadata še dve postavki. Prva je časovna omejitev (timeout) za posamezno datoteko. Patološki PDF s tisoči strani in globoko gnezdenimi strukturami objektov lahko za nekaj minut blokira celoten postopek preverjanja, nočno časovno okno pa nima potrpljenja za to. Ob prekoračitvi roka prekinite obdelavo te datoteke, jo zabeležite kot napako orodja in nadaljujte s paketom. Druga postavka je karantenski imenik: vse datoteke s potekom časa ali tiste, ki jih ni mogoče odpreti, premaknite na stran, namesto da jih pustite na mestu. V nekaj mesecih se bo v tem imeniku tiho nabrala zbirka najslabših dokumentov, ki jih pošiljajo vaše stranke, ta zbirka pa je za testiranje izdaj vredna več kot kateri koli umetni testni primer, ki bi ga lahko napisali sami

Izbira standardov in zakaj je raven skladnosti pomembna

Naštevanje TPdfPreflightStandard pokriva družine standardov, ki se pojavljajo v praksi: ppsPdfA za arhivsko skladnost ISO 19005, ppsPdfUa za dostopnost ISO 14289, ppsPdfX za grafično pripravo in tisk ter ppsPdfE, ppsPdfR in ppsPdfVT za inženiring, raster in variabilne podatke. Znotraj družine mehanizem prebere raven skladnosti, ki jo dokument navaja, in jo za vsak standard vrne v rezultatu ConformanceName. Sama družina standardov redko zadošča, saj se bistvo skriva v ravni skladnosti. PDF/A-2b zagotavlja vizualno ponovljivost in nič več. PDF/A-3a zahteva logično označevanje strukture in dovoljuje vgrajene izvorne datoteke, kar je precej težja ovira za optično prebrano gradivo brez drevesa oznak. Če raven razlagate napačno, bo paketno preverjanje dajalo zavajajoče rezultate. Če vaša politika hrambe zahteva PDF/A-2b, datotek ne zavračajte zaradi pravil, ki pripadajo višji ravni. Če sprejmete katero koli oznako PDF/A brez preverjanja ravni, boste potrdili dokumente, ki dosegajo nižji standard od obljubljenega. Zahteve vladnih kupcev vse pogosteje vključujejo tudi PDF/UA, funkcija BuildPdfPreflightReport iz enote FPdfPreflightReport pa sprejme množico standardov:

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

En sam klic ovrednoti oba standarda in vrne enoten združen zapis poročila

Zakaj prazen seznam ugotovitev ne pomeni uspeha

Poročilo navaja ugotovitve za posamezen standard, prazen seznam težav pa pomeni le "v standardih, ki so se dejansko izvedli, nismo našli težav". To je veliko ožja trditev kot "datoteka ustreza standardu, ki vas zanima", in v tej razliki se skriva tihi propad paketnega preverjanja. Tipkarska napaka v konfiguraciji, ki iz nabora izpusti ppsPdfA, ustvari enak prazen seznam težav kot resnično skladna datoteka. Zato obravnavajte tišino kot sumljivo. Pojdite skozi Report.Results in za vsak standard, ki ste ga želeli preveriti, potrdite dvoje: da vnos rezultata zanj sploh obstaja in da je njegova zastavica IsCompliant, podprta s stanjem Status = pfsPass, resnična. Nočno opravilo, ki enači "brez ugotovitev" z "pripravljeno za arhiv", ne da bi sploh potrdilo, kateri standardi so bili preverjeni, je klasičen način, da mapa neskladnih datotek nemoteno prehaja skozi proces mesece dolgo, dokler zunanji revizor ne odpre ene izmed njih z orodjem veraPDF in postavi pod vprašaj celoten arhiv

Druga past se skriva v tem, kaj sploh je posamezna ugotovitev. Vsaka TPdfPreflightIssue vsebuje podatke Code, Category, Description in Recommendation ter poimenuje pravilo, ki je bilo kršeno, ne pa same strani ali objekta. To je načrtovalska odločitev s posledicami za povratno zanko. Poročilo ekipi, ki ustvarja datoteke, pove, kateri razred napake je prisoten (npr. nevgrajena pisava ali manjkajoči identifikator XMP), iskanje konkretnega kršitelja pa je naloga orodij za odpravljanje napak v nadaljnjem procesu in ne samega validatorja. Svoje porabnike poročil zgradite na podlagi stabilnih vrednosti Code in nikoli na podlagi človeku berljivega opisa v Description, saj se slednji med različicami knjižnice lahko spremeni brez opozorila

Datoteke s poročili za stroje in za dežurnega inženirja

Zapis poročila zapiše iste ugotovitve v petih oblikah: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile in SaveMarkdownToFile, pri čemer ima vsaka ustrezno funkcijo tipa ToJson, ko želite niz v pomnilniku namesto na disku. Uprite se skušnjavi, da bi izbrali le eno. Zapišite JSON za cevovod, da ga lahko sistem CI priloži zapisu opravila ter razčleni kode težav in stanja standardov brez brskanja po besedilu. Zapišite HTML za človeka, ki prejme poziv ob napaki, saj se odpre v vsakem brskalniku brez kakršnih koli dodatnih orodij. Uporaba obeh skupaj stane le eno dodatno vrstico kode na datoteko in prihrani vašemu dežurnemu inženirju najhujše opravilo pri paketni obdelavi: ročno razčlenjevanje surovega JSON-a ob dveh zjutraj, da bi ugotovili, katera datoteka se je pokvarila. Ena stvar pa je pomembnejša od same izbire formata: ime vsakega poročila izpeljite iz imena vhodne datoteke in nikoli iz časovnega žiga, sicer bosta dva vzporedna zagona pomešala poročila, ki jih ne boste več mogli povezati z njihovimi vhodi

Pragi resnosti sodijo v konfiguracijo in ne v kodo. Anotacija brez alternativnega opisa je kritična napaka za portal za oddajo dokumentov PDF/UA, hkrati pa zanemarljiva opomba za interni arhiv, čeprav gre v obeh primerih za isto ugotovitev. Omogočite nastavitev praga zavrnitve za vsak profil posebej, da se politika lahko spremeni brez ponovnega prevajanja kode, prag, ki je bil v veljavi, pa zapišite v sam povzetek opravila. Naslednje četrtletje se nihče več ne bo spomnil, pod katerim pragom se je izvajal lanski oktobrski paket, povzetek pa je edino mesto, kjer se ta informacija ohrani

Izolacija datotek, da ena slaba datoteka PDF ne potopi celotnega paketa

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 tej zanki so tri premišljene odločitve. Sveža instanca TPdf za vsako datoteko zagotavlja, da dokument, ki bi morda pokvaril stanje mehanizma, ne more vplivati na datoteke, ki sledijo. Eksplicitno preverjanje lastnosti Active je nujno, ker nastavitev Active := True zadrži napake pri nalaganju, namesto da bi sprožila izjeme. Če to zaščito izpustite, bo skrajšana datoteka prešla v validacijski klic in odpovedala pozneje z zavajajočim sporočilom. Notranji blok try..except je znotraj obsega posamezne datoteke, zato ena sama izjema le poveča števec napak, zanka pa se nadaljuje. Tako dobite poročila za 4.999 dobrih datotek, tudi če je 5.000. datoteka povsem uničena. Obe obliki poročila se zapišeta na disk pred končnim štetjem, zato dokazi preživijo tudi napako pri poznejšem seštevanju

Preslikava izhodne kode se nahaja v nekaj vrsticah v datoteki projekta:

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.

Česa preflight ne bo naredil namesto vas

Mehanizem zaznava, ne popravlja. Opozorilo o nevgrajeni pisavi ali barvnem prostoru, odvisnem od naprave, je delovni nalog za tistega, ki datoteke ustvarja, in validator nima možnosti, da bi to popravil na mestu. Zato premišljeno načrtujte povratno zanko. Poročila morajo pristati tam, kjer jih ekipa, ki dokumente pripravlja, dejansko bere, sicer se bodo iste ugotovitve ponavljale vsako noč, dokler nekdo končno ne vpraša, zakaj se stopnja skladnosti nikoli ne izboljša. Prav tako se splača primerjati vzorec rezultatov z neodvisnim validatorjem (npr. veraPDF za PDF/A ali Acrobatov preflight za PDF/X), preden to namesto vas stori zunanji revizor. Ko se dva mehanizma ne strinjata glede dejanske datoteke stranke, ta dokument ni nadloga, temveč ravno tisti regresijski primer, ki je manjkal pri vašem testiranju izdaj. Shranite ga, poimenujte in zaženite ob vsaki gradnji programske opreme

Vredno je poznati še eno kombinacijo. Isti validacijski mehanizem poganja interaktivna preverjanja v pregledovalnem uporabniškem vmesniku, zato lahko ta konzolni CLI in analitikom namenjeno delovno okolje za pregled prejetih PDF-jev uporabljata enak validacijski besednjak, namesto da bi se sčasoma oddaljila. Ker [ppsPdfA, ppsPdfUa] ocenjuje dostopnost v istem prehodu, se stran PDF/UA v paketnem procesu lepo ujema z delom na strani pregledovalnika, kot je izdelava dostopnega bralnika PDF v Delphiju. Profili, formati poročil in celoten preflight API so dokumentirani na strani izdelka za PDFium Component