Műszaki cikk

Kötegelt PDF preflight jelentések Delphiben a PDFium Component CLI segítségével

A kötegelt preflight (előellenőrző) eszköz egy ablak nélküli konzolprogram, amely egy PDF-eket tartalmazó mappára mutat, ellenőrzi mindegyiket a megadott megfelelőségi szabványok szerint, és géppel olvasható bizonyítékot hagy hátra a találtakról. Senki sem ül és figyeli. Hajnali kettőkor fut a cron vagy a Windows Feladatütemező (Windows Task Scheduler) alatt, vagy kapuként (gate) egy CI csővezetékben (pipeline), és a következő személy, akit érdekel a kimenete, vagy egy ütemező (scheduler), amely a kilépési kódot (exit code) olvassa, vagy egy auditor, aki hetekkel később nyitja meg a jelentést. Ez megváltoztatja azt, hogy mit jelent a "helyes". A PDFium Component – amely egy forráskódú PDF-könyvtár Delphihez, C++Builderhez és Lazarushoz – preflight motorja a validációs hívásokat szinte triviálissá teszi. A munka, amely eldönti, hogy az eszköz megéri-e az árát, ezen hívások körül zajlik: melyik profilt ellenőrizte, mit mondott a kilépési kód az ütemezőnek, és hogy a jelentés, amely elcsípett volna egy hibát, még mindig létezik-e, amikor valaki elkezdi keresni

A szerződés: mit láthat valójában az ütemező

Egy CI futtató (runner) vagy a Windows Feladatütemező pontosan két dolgot lát az eszközéből: a kilépési kódot és azokat a fájlokat, amelyeket hátrahagyott. Naplósorok (log lines), konzol színek, folyamat kimenet (progress output): mindez az élőben figyelő embernek szól, és hajnali kettőkor senki sincs ott. Tehát rögzítse a kilépési kódok szótárát, mielőtt hozzáérne az API-hoz, és tartsa unalmasnak:

  • 0: minden fájl megfelelt minden kért profilnak
  • 1: legalább egy fájlnál validációs találatok (findings) jelentek meg
  • 2: maga az eszköz vallott kudarcot legalább egy fájlnál (sérült bemenet, zárolás, összeomlás)

Az 1-es és 2-es kódok közötti különbségtétel az, amit a csapatok gyakran átugranak, majd később megbánnak. Egy sérült PDF, amely nem nyílik meg, nem validációs hiba. Ha ezt az 1-es kódba sűríti, akkor egy teherautónyi sérült beolvasás hirtelen megfelelőségi összeomlásként jelenik meg az irányítópultjain (dashboards), és valakit elküld egy olyan szabványos regressziót üldözni, amely soha nem történt meg, miközben a valódi történet egy elromlott szkenner (scanner) az adatfolyam elején

Még két elem tartozik a szerződéshez. Az első a fájlonkénti időtúllépés (timeout). Egy patologikus PDF – több ezer oldal mélyen egymásba ágyazott objektumstruktúrákkal – perceken át képes feltartani egyetlen validációs menetet, és egy éjszakai ablaknak nincs türelme ehhez. Ölje meg a fájl feladatát a határidő lejártakor, számítsa eszközkudarcnak, és tartsa mozgásban a köteget. A második egy karantén (quarantine) könyvtár: minden időtúllépéses vagy megnyithatatlan bemenetet tegyen félre ahelyett, hogy a helyén hagyná. Néhány hónap alatt ez a könyvtár csendben felhalmozza a legrosszabb dokumentumokat, amelyeket a valódi ügyfelek küldenek, és ez a korpusz többet ér a kiadási tesztelés (release testing) során, mint bármilyen szintetikus minta, amit kézzel írhatna

Szabványok kiválasztása, és miért számít a megfelelőségi szint

A TPdfPreflightStandard enumeráció (felsorolás) lefedi a gyakorlatban felmerülő családokat: ppsPdfA az ISO 19005 archiválási megfelelőséghez, ppsPdfUa az ISO 14289 akadálymentesítéshez (accessibility), ppsPdfX a nyomdai cseréhez, plusz a ppsPdfE, ppsPdfR és ppsPdfVT a mérnöki, raszteres és változó adatú (variable-data) munkákhoz. Egy családon belül a motor leolvassa a dokumentum által állított megfelelőségi szintet, és szabványonként jelenti azt az eredmény ConformanceName tulajdonságában. A család megnevezése ritkán elég, mert a szint az, ahol a valódi különbség rejlik. A PDF/A-2b csupán vizuális reprodukálhatóságot ígér, semmi mást. A PDF/A-3a ehhez hozzáadja a logikai struktúra címkézésének (tagging) követelményét, és engedélyezi a beágyazott forrásfájlokat, amely sokkal nehezebben megugorható léc egy egyáltalán címkefával nem rendelkező szkennelt (beolvasott) anyag esetében. Ha ezt bármelyik irányba is elrontja, a köteg hazudni fog önnek. Ha a megőrzési házirendje (retention policy) valójában PDF/A-2b-t kér, de a hiányzó struktúracímkék miatt megbuktatja a fájlokat, a jelentés megtelik olyan találatokkal, amelyeket soha senki sem fog kijavítani. Ha elfogad bármilyen PDF/A címkét a szint ellenőrzése nélkül, olyan dokumentumokat ír alá, amelyek gyengébb lécet ütnek meg, mint amilyet ön ígért. A kormányzati vásárlóktól érkező akadálymentesítési (accessibility) mandátumok egyre inkább a PDF/UA-t halmozzák minderre, ami nem ad hozzá plusz költséget a futtatáshoz, mert a BuildPdfPreflightReport (az FPdfPreflightReport unitból) szabványok egy halmazát (set) veszi fel:

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

Egy hívás értékeli mindkét szabványt, és egyetlen, összevont jelentés (report) rekordot ad vissza

Miért nem jelentik az üres találatok a megfelelést

A jelentés felsorolja a találatokat szabványonként, és egy üres problémalista csupán azt jelenti: "nem találtunk problémát a ténylegesen lefuttatott szabványokban". Ez egy szűkebb állítás, mint az, hogy "a fájl megfelel az ön számára fontos szabványnak", és e kettő közötti szakadék az a hely, ahol a kötegelt preflight csendben elrohad. Egy konfigurációs elírás (typo), amely kiejti a ppsPdfA-t a halmazból, pontosan ugyanazt az üres problémalistát eredményezi, mint egy valóban tiszta fájl. Tehát kezelje a csendet gyanúsként! Járja be a Report.Results listát, és minden egyes ellenőrizni kívánt szabványnál állítson (assert) két dolgot: hogy egyáltalán létezik rá egy eredménybejegyzés, és hogy annak IsCompliant jelzője (flag) – amelyet a Status = pfsPass támogat – igaz (true). Egy éjszakai feladat, amely a "nincs találat" értéket az "archiválásra kész" értékkel teszi egyenlővé anélkül, hogy valaha is megerősítené, mely szabványok lettek értékelve, a klasszikus módja annak, hogy egy mappányi nem megfelelő fájl hónapokig átsuhanjon, egészen addig, amíg egy külső auditor meg nem nyit egyet a veraPDF-fel, és az egész archívum megkérdőjeleződik

Egy második csapda bújik meg abban, hogy egyáltalán mi is egy találat. Minden egyes TPdfPreflightIssue hordoz egy Code-ot (kódot), egy Category-t (kategóriát), egy Description-t (leírást) és egy Recommendation-t (javaslatot), és a megszegett szabályt nevezi meg, nem pedig egy oldalt vagy egy objektumot. Ez egy olyan tervezési döntés, amelynek következményei vannak a visszacsatolási hurokra (feedback loop) nézve. A jelentés elmondja az előállító csapatnak, hogy milyen osztályú hiba létezik – egy be nem ágyazott (unembedded) betűtípus vagy egy hiányzó XMP azonosító –, és az adott sértő objektum megtalálása a helyreállító eszköz (remediation tool) feladata a folyamatban (downstream), nem a validátoré (validator). Építse a jelentésfogyasztóit (report consumers) a stabil Code értékekre, és soha ne az ember által olvasható leíró szövegre, amelyet a kiadások (releases) között figyelmeztetés nélkül átfogalmazhatnak

Jelentésfájlok a gépek és az ügyeletes személy számára

A jelentés rekord ugyanazokat a találatokat öt formátumban írja meg: SaveJsonToFile, SaveCsvToFile, SaveHtmlToFile, SaveTextToFile és SaveMarkdownToFile, mindegyikhez tartozik egy megfelelő ToJson-stílusú függvény, amikor a karakterláncot a memóriában szeretné tudni, nem pedig a lemezen. Álljon ellen a kísértésnek, hogy csak egyet válasszon! Írjon JSON-t a csővezetéknek (pipeline), így a CI csatolhatja azt a feladatrekordhoz, és elemezheti (parse) a problémakódokat és a szabványonkénti állapotokat szöveg kaparása (scraping) nélkül. Írjon HTML-t az ember számára, akit beriasztanak (paged), mert bármelyik böngészőben megnyílik mindenféle eszköz (tooling) nélkül. A kettő együtt fájlonként egyetlen plusz sorba kerül, és megkíméli az ügyeletes mérnököt a kötegelt feldolgozás legrosszabb feladatától, ami a nyers JSON massza visszatervezése (reverse-engineering) hajnali kettőkor, hogy megtudja, melyik fájl tört el. Egy fegyelem (discipline) fontosabb, mint a formátum kiválasztása: minden jelentés nevét a bemeneti fájl nevéből származtassa, soha ne egy időbélyegzőből (timestamp), különben két párhuzamos futtatás olyan jelentéseket fésül össze, amelyeket többé nem tud összepárosítani a bemeneteikkel

A súlyossági küszöbértékek (severity thresholds) a konfigurációba tartoznak, nem pedig a kódba. Egy alternatív leírás nélküli annotáció (annotation) egy PDF/UA benyújtási portál (submission portal) számára kemény hiba (hard failure), egy belső archívum számára pedig egy figyelmen kívül hagyható megjegyzés (ignorable note), pedig mindkettőben azonos találatról van szó. Tegyen közzé egy megbuktatási szintet profilonként (fail-on level per profile), hogy a házirend újrafordítás (recompile) nélkül változhasson, és bélyegezze be az érvényben lévő szintet magába a feladatösszefoglalóba (job summary). A következő negyedévben senki sem fog emlékezni arra, hogy a tavaly októberi köteg milyen küszöbérték alatt futott, és az összefoglaló az egyetlen hely, ahol a memória fennmarad

Fájlok elkülönítése, hogy egy rossz PDF ne tudja elsüllyeszteni a köteget

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;

Három szándékos döntés él ebben a hurokban. Egy friss TPdf fájlonként garantálja, hogy egy olyan dokumentum, amely megrongálja a motor állapotát, nem tudja megmérgezni az őt követő fájlokat. A kifejezett Active ellenőrzés kiérdemli a helyét, mert az Active := True elnyeli a betöltési hibákat ahelyett, hogy felemelné (raising) őket; hagyja el az őrt (guard), és egy csonka (truncated) fájl elsodródik a validációs hívásba, mielőtt valahol lejjebb (downstream) megbukna egy félrevezető üzenettel. A belső try..except szándékosan a fájlonkénti hatókörön (scope) belül él, így egyetlen kivétel (exception) megemeli a hibaszámlálót, és a hurok megy tovább. Ön tiszta jelentéseket akar a 4 999 jó fájlhoz még akkor is, ha az 5 000. fájl darabokra van szakadva. És mindkét jelentésformátum a lemezre kerül az ítélet összeszámolása előtt, ami azt jelenti, hogy a bizonyíték túléli még akkor is, ha egy hiba később az összefoglaló logikában (summary logic) rosszul számol

A kilépési kód (exit-code) leképezése aztán néhány sorra omlik össze a projektfájlban:

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.

Amit a preflight nem fog megtenni önért

A motor észlel; nem javít. Egy be nem ágyazott betűtípusra (font) vagy egy eszközfüggő (device-dependent) színtérre (color space) vonatkozó találat munkamegrendelés annak, aki a fájlokat előállítja, és a validátornak nincs módja arra, hogy azt a helyén befoltozza (patch). Tervezze meg tehát a visszacsatolási hurkot szándékosan! A jelentéseknek ott kell landolniuk, ahol az előállító csapat ténylegesen elolvassa őket, különben ugyanazok a találatok minden este újra megjelennek, amíg végül valaki meg nem kérdezi, miért nem javul soha a megfelelőségi arány (conformance rate). Kifizetődő az ítéletek egy mintájának keresztellenőrzése is (cross-check) egy független validátorral – veraPDF a PDF/A-hoz vagy az Acrobat preflight-ja a PDF/X-hez –, mielőtt egy külső auditor ellenőrizné azokat ön helyett. Amikor két motor nem ért egyet egy valódi ügyfélfájlban, az a dokumentum nem bosszúság; az pontosan a regressziós eset, ami a kiadási tesztelésből (release testing) hiányzott. Tartsa meg, nevezze el, és futtassa minden fordításkor (build)

Még egy párosítást érdemes tudni. Ugyanez a validációs motor hajtja az interaktív ellenőrzéseket egy áttekintő (review) UI-n, így ez a fej nélküli (headless) CLI és egy elemzőnek szánt PDF beviteli áttekintő munkaasztal (PDF intake review workbench) közös validációs szótáron osztozhat ahelyett, hogy idővel elsodródnának egymástól. És mivel a [ppsPdfA, ppsPdfUa] egy menetben értékeli az akadálymentesítést (accessibility), a köteg PDF/UA oldala tisztán felsorakozik az olyan nézegető-oldali (viewer-side) munkák mellé, mint az akadálymentes PDF olvasó építése Delphiben. A profilok, jelentésformátumok és a teljes preflight API a PDFium Component termékoldalán vannak dokumentálva