Egy archívum beviteli kapuja visszautasított egy köteg „PDF/A-2b” fájlt, amely az asztalon minden megjelenítőben rendben megnyílt. A beszállító esküdött rá, hogy szabványosak. Nem voltak azok: mindegyik egy JavaScript-műveletet rejtett a katalógusban, azt a fajtát, amelyet egy futó pillantás soha nem vesz észre, egy teljes PDF/A-ellenőrző, például a veraPDF viszont egy szempillantás alatt megjelöl. A bökkenő az volt, hogy senki nem akart Java eszközláncot csavarozni egy Delphi kötegelt szolgáltatásra pusztán azért, hogy fájlonként egyetlen igen-nem kérdésre válaszoljon. Ezt a hézagot tölti be a PDFium Component ValidatePdfACompliance függvénye, és érdemes megérteni, hogyan jut ítéletre anélkül, hogy valaha is teljesen elemezne egy tartalomstreamet
Miért nem tud erre maga a PDFium válaszolni
Az első dolog, amiben őszintének kell lenni: a mellékelt pdfium.dll egyáltalán nem tud PDF/A-t. Nincs ConvertToPDFA, nincs OutputIntent-író, nincs XMP API a nyilvános felületen. Ebben a könyvtárban a PDF/A minden része – az író és az ellenőrző oldal is – tiszta Pascalban él az FPdfPdfa.pas unitban, és bájtszintű elemzéssel meg növekményes frissítéssel dolgozik. Amikor tehát meghívjuk az ellenőrzőt, nem a Chromium megjelenítőmotorjától kérdezünk semmit. Egy Pascal tokenvizsgálót futtatunk a fájl szerkezeti bájtjai fölött
A nyilvános API szándékosan kicsi. Egyetlen függvény olvas egy streamet a 0. pozíciótól, és egy rekordot ad vissza:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // TPdfAValidationIssue értékek halmaza
function IsCompliant: Boolean; // csak akkor True, ha a szint <> unknown/none
end; // ÉS az Issues üres
Az IsCompliant azt a szabályt kódolja, amely egy kapunál számít: egy fájl csak akkor megy át, ha valódi megfelelőségi szintet ismertünk fel, és a hibahalmaz üres. Az a sikeres elemzés, amely nem talál pdfaid jelölőt, pacNone értékre oldódik fel, ami kifejezetten nem átmenés. Ugyanezt mondja kívülről a kötegelt előellenőrzési jelentés parancssori eszköze is: egy fel nem ismert fájlon az üres megállapításlista nem tiszta egészségügyi bizonyítvány
A streamtörzsek kiürítése minden tokenvizsgálat előtt
Itt jön a legfontosabb megvalósítási részlet, egyben az, amelyet a legkönnyebb elrontani, ha saját vizsgálót írunk. A felismerő úgy talál szabálysértéseket, hogy határolt névtokenekre keres, például a /JavaScript, /LZWDecode vagy /BM tokenre. Ha a nyers fájlbájtokat vizsgáljuk, a beágyazott bináris streamtörzsek – tömörített képek, ICC-profilok, betűkészletprogramok – véletlenszerűen tartalmazni fognak olyan bájtsorozatokat, amelyek ezekre a tokenekre hasonlítanak. Azt fogjuk jelenteni, hogy az /AA vagy a /3D „megvan”, mert egy JPEG belsejében három bájt véletlenül ezt betűzte ki. Ez tévesriasztás-gyár
A megoldás a PdfStructureBytes: végigjárja a fájlt, és minden stream és endstream kulcsszó közötti bájtot szóközre ürít, a szótárszerkezetet érintetlenül hagyva. A vizsgálat csak ezután fut le. Az ellenőrző minden névtoken-próbája ezen a kiürített másolaton dolgozik. Ha egyetlen gondolatot viszünk el ebből a cikkből, ez legyen az. Ugyanez a fegyelem tükröződik a PDF/UA ellenőrzőben, amely saját másolatot tart az eljárásból, mert a két szabvány egymástól függetlenül fejlődik
A 29 hibakód és jelentésük
A TPdfAValidationIssue dokumentált szerződés. A sorszámok be vannak fagyasztva, mert a DUnitX tesztek, a demók és a jelentésréteg mind rájuk támaszkodnak, ezért az új megállapítások mindig csak a végére kerülnek. A v1.63.0 verzió szerint 29 tagja van. Néhány családba sorolhatók:
- Metaadat és azonosság:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Szín és kimenet:
pvaiMissingOutputIntent,pvaiMissingIccProfile, valamintpvaiMixedDeviceColorSpaces, ha DeviceRGB és DeviceCMYK is előfordul (6.2.3.3) - Minden részre érvényes kemény tiltások:
pvaiEncryptionPresent(az/Encryptszótár egyenesen tilos),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Betűkészletek:
pvaiFontNotEmbeddedés a szigorúbbpvaiUnembeddedFont, valamintpvaiUnicodeMappingMissingarra az esetre, ha egy U szintű állítás mögött nincs/ToUnicode - Címkézés:
pvaiLevelAStructureMissing, amikor egy conformance=A állítás mögött nincs címkézett szerkezet
A hat legújabb tag, amely a 24–29. sorszámra került, azokat a finom eseteket fedi le, amelyekbe a lektorok valóban belebotlanak: pvaiTrappedTrue (egy /Trapped /True az Info szótárban, ez „hamis barát”, mert az értéknek False vagy Unknown lehet csak), pvaiForbiddenActionSubtype (Sound vagy Movie műveletként használva, nem csupán jegyzetként), pvaiTransparentColorSpace (nem Normal keverési mód vagy egy 1.0 értéktől eltérő /CA illetve /ca), pvaiAnnotationDictViolation, pvaiUnembeddedFont és pvaiMixedDeviceColorSpaces
Résztudatos kapuzás: az A-1 szigorú, az A-2 és A-3 enged
A PDF/A nem egyetlen szabálykönyv. Három olyan dolog, amelyet a PDF/A-1 tilt, a PDF/A-2 verziótól kezdve kifejezetten megengedett: az átlátszóság (egy /Transparency csoport vagy egy aktív /SMask, 6.4), a választható tartalom (/OCProperties, 6.1.13) és a beágyazott fájlok (/EmbeddedFiles vagy /EF, 6.1.11). Az a naiv ellenőrző, amely mindhármat minden fájlnál megjelöli, tömegével utasít vissza tökéletesen érvényes PDF/A-2 dokumentumokat
Az ellenőrző ezért a PdfAPartOf segítségével olvassa ki a részszámot a pdfaid jelölőből, és ezeket az ellenőrzéseket a PartNo = 1 feltétel mögé kapuzza. Az új átlátszósági hibákhoz tartozó keverésimód- és jegyzetáttetszőség-ellenőrzések hasonlóképpen csak az 1. részre vonatkoznak:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // csak a /Normal vagy /Compatible megengedett
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Egy óvatos alapértelmezés említést érdemel: ha egyáltalán nincs pdfaid jelölő, a rész 1-nek számít, vagyis a legszigorúbbnak. Az érvelés az, hogy egy azonosítatlan fájlt inkább a legszorosabb szabályokhoz kell mérni, mint átinteni. A JavaScript, a tiltott műveletek, az LZW, az XFA, a NeedAppearances, a tiltott jegyzetek és a be nem ágyazott betűkészletek minden részre tiltottak maradnak, így ezek az ellenőrzések soha nem kerülnek a kapu mögé
Objektumstreamek kibontása, hogy semmi ne rejtőzhessen el
A PDF 1.5 bevezette a kereszthivatkozási streamet és az objektumstreamet (/Type /ObjStm), ezek pedig vakfoltot okoznak egy naiv bájtvizsgálónak. Egy katalógus, egy OutputIntent, egy műveleti szótár – bármi, ami maga nem stream – Flate-tömörítve ülhet egy ObjStm belsejében. Ha a nyers szerkezetet vizsgáljuk, ebből semmit nem látunk, majd tisztának jelentünk egy fájlt, amely minden, csak nem az
A PdfExpandObjectStreams zárja be ezt a rést. Bármely ellenőrzés lefutása előtt az ellenőrző elvégzi a Data := PdfExpandObjectStreams(Data) lépést. Az eljárás megkeres minden ObjStm objektumot, beolvassa a /N és /First fejlécet a benne foglalt objektumszámokhoz és eltolásokhoz, kibontja a törzset a PdfInflate segítségével (az RTL zlib, Delphin a System.ZLib, FPC-n a zstream), majd minden benne foglalt objektumot szokásos N 0 obj ... endobj alakban a bájtok egy másolatának végére fűz. A meglévő tokenellenőrzések ezután a logikájuk bármiféle változtatása nélkül megtalálják ezeket az objektumokat
Két megkötés teszi ezt tisztává, nem törékennyé. A streamobjektumok – a Metadata, az ICC-profil és a betűkészletprogramok – nem élhetnek objektumstreamben, csak a nem stream típusú szótárak, ezért a kibontás mindig csak szótárakkal dolgozik, a hozzáfűzött objektumok pedig nem hordoznak stream kulcsszót, amely megzavarná a törzskiürítő menetet. És mivel a hozzáfűzött tartalom a %%EOF jelölés után landol, a startxref felől visszafelé induló keresés továbbra is megtalálja az eredeti trailert. Magát a kereszthivatkozási stream trailerét már korábban, a v1.49.3 verzióban kezeltük úgy, hogy a Root, a Size és az ID értékét közvetlenül a nyílt szövegű xref-stream szótárból olvassuk – ezt a témát az objektum- és kereszthivatkozási streamek ellenőrzéséről szóló testvércikk járja körbe; az objektumstreamek kezeléséhez már csak a kibontási lépést kellett hozzátenni, 2-es típusú xref-bejegyzések dekódolása és PNG-prediktor visszafejtése nélkül
Egy bájtszintű ellenőrző őszinte korlátai
Ez előellenőrző eszköz, nem hitelesített ellenőrző, és a határai valósak. A betűkészletek beágyazottsága számláló heurisztika, és a helyes működéséhez egy említésre méltó javításra volt szükség. Az eredeti ellenőrzés a PdfCountName('/FontDescriptor') hívást használta, de minden betűkészlet két /FontDescriptor tokent ad hozzá: egy hivatkozást a betűkészlet-szótárból és egy /Type bejegyzést magában a leíróobjektumban, így a szám N beágyazott program mellett 2N lett, a próba pedig mindig igaz volt. A javítás a PdfCountDescriptorRefs, amely csak a /FontDescriptor N G R hivatkozási alakot számolja, betűkészletenként egyet, és csak akkor emeli be a pvaiUnembeddedFont hibát, ha a beágyazott programok száma valóban kevesebb:
K := PdfCountDescriptorRefs(Struct); // betűkészlet-szótáranként egy
Emb := PdfCountName(Struct, '/FontFile')
+ PdfCountName(Struct, '/FontFile2')
+ PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
Include(Result.Issues, pvaiUnembeddedFont);
Még javítva is durva: egy vegyes dokumentumban, ahol minden leíróhoz történetesen tartozik valamilyen FontFile, egy-egy nem szabványos betűkészlet még mindig átcsúszhat. Az objektumstreamek kibontásának is van ismert mellékhatása: felfedi azokat a szabványos 14 alapértelmezett erőforrást, amelyeket egy AcroForm /DR bejegyzése hordoz, például a /Helv készletet, a heurisztika pedig kötelességtudóan be nem ágyazottként jelenti őket, jóllehet a veraPDF átengedi ezeket, mert valójában soha nem használatosak megjelenítésre. A tartalomstreamek operátorszintű ellenőrzései (6.2.10) teljesen kívül esnek a hatókörön, mert bájtvizsgálat helyett teljes tartalomelemzést igényelnének. Kezeljük az ellenőrzőt gyors, függőségmentes első kapuként, amely elkapja azokat a szabálysértéseket, amelyeket jelölő beszúrásával nem lehet elfedni, a végleges hitelesítésre pedig tartsunk fenn egy teljes ellenőrzőt
Ez a történet ellenőrző fele. A kiegészítő írói oldal, ahol a SaveAsPdfA beszúrja az XMP-t, az OutputIntent bejegyzést és az sRGB ICC-profilt, és őszintén visszaminősít egy A szintű kérést, amely mögött nincs címkézett szerkezet, ugyanerre a bájtszintű gépezetre épül. Mindkét fél a Delphihez készült PDFium Component részeként érkezik: egyetlen VCL csomag egy tiszta Pascal PDF/A megvalósítás fölött, telepítendő külső futtatókörnyezet nélkül