Egy archívum-befogadó kapu elutasított egy tétel „PDF/A-2b” fájlt, amelyek jól nyíltak meg az asztalon lévő összes nézőkében. A szállító megesküdött, hogy megfelelnek a szabványnak. Nem feleltek meg: mindegyik tartalmazott egy katalógusba temetett JavaScript műveletet — olyasmit, amit egy alkalmi szemlélő soha nem vesz észre, de egy teljes PDF/A ellenőrző, mint a veraPDF, egy szempillantás alatt kiszűr. A bökkenő az, hogy senki sem akart Java eszközkészletet csatlakoztatni egy Delphi kötegelt szolgáltatáshoz, csak azért, hogy fájlonként egyetlen igen/nem kérdést megválaszoljon. Ezt az űrt tölti be a PDFium komponens ValidatePdfACompliance metódusa, és érdemes megérteni, hogyan hoz ítéletet anélkül, hogy valaha is teljesen elemezné a tartalomfolyamot
Miért nem képes maga a PDFium megválaszolni ezt
Az első dolog, amit őszintén ki kell mondani: a beépített pdfium.dll egyáltalán nem rendelkezik PDF/A képességekkel. A nyilvános felületén nincs sem ConvertToPDFA, sem OutputIntent író vagy XMP API. A PDF/A minden része ebben a könyvtárban — mind az írási, mind az ellenőrzési oldalon — tiszta Pascal nyelven van megírva az FPdfPdfa.pas-ban, és bájtszintű elemzéssel, valamint inkrementális frissítéssel működik. Tehát amikor meghívja az ellenőrzőt, nem a Chromium renderelőjét kérdezi meg. Egy Pascal token-szkennert futtat a fájl strukturális bájtjai felett
A nyilvános API szándékosan kicsi. Egy funkció a nullás pozíciótól olvassa be a folyamot, és egy rekordot ad vissza:
function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;
type
TPdfAValidationResult = record
Conformance: TPdfAConformance; // pacUnknown, pacNone, pac1b, pac2u, ...
Issues: TPdfAValidationIssues; // a set of TPdfAValidationIssue
function IsCompliant: Boolean; // True only when level <> unknown/none
end; // AND Issues is empty
Az IsCompliant kódolja azt a szabályt, amely egy kapunál számít: a fájl csak akkor megy át a vizsgálaton, ha valós megfelelőségi szintet észlelt a rendszer, és a hibák halmaza üres. A sikeres elemzés, amely nem talál pdfaid jelölőt, a pacNone értéket eredményezi, ami kifejezetten nem jelent megfelelést. Erre mutat rá kívülről a kötegelt preflight jelentés CLI-ről szóló cikk is: egy felismerhetetlen fájl üres találati listája nem jelenti azt, hogy a fájl hibátlan
Adatfolyam-testek leválasztása a token-vizsgálat előtt
Íme a legfontosabb implementációs részlet, és az, amelyet a legkönnyebb elrontani, ha saját szkennert ír. A detektor úgy találja meg a szabálysértéseket, hogy elhatárolt név-tokeneket keres, például /JavaScript, /LZWDecode, /BM. Ha a nyers fájlbájtokat szkenneli, a beágyazott bináris adatfolyam-testek, tömörített képek, ICC profilok, betűtípus-programok véletlenszerűen tartalmaznak majd olyan bájtsorozatokat, amelyek ezeknek a tokeneknek tűnnek. Azt fogja jelenteni, hogy /AA vagy /3D „található”, mert egy JPEG-en belüli három bájt történetesen ezt a betűsort alkotta. Ez egy téves riasztásgyár (false positive factory)
A megoldás a PdfStructureBytes: végigmegy a fájlon, és a stream és endstream kulcsszavak közötti bájtokat szóközökké alakítja, így a szótár szerkezete érintetlen marad. A vizsgálat csak ezután fut le. Az ellenőrzőben minden név-token ellenőrzés ezen a lecsupaszított másolaton működik. Ha csak egyetlen ötletet visz el ebből a cikkből, az ez legyen. Ugyanez a fegyelem tükröződik a PDF/UA ellenőrzőben is, amely saját másolatot tart fenn ebből a rutinszerű eljárásból, mivel a két szabvány egymástól függetlenül fejlődik
A 29 probléma és azok jelentése
A TPdfAValidationIssue egy dokumentált szerződés. A sorszámok rögzítettek, mert a DUnitX tesztek, a demók és a jelentésréteg mind tőlük függenek, így az új megállapítások mindig a végére kerülnek. A v1.63.0 verziótól kezdve 29 tagja van. Ezek néhány családba sorolhatók:
- Metaadatok és identitás:
pvaiMissingXmpMetadata,pvaiMissingPdfAIdentifier,pvaiMissingTrailerId(ISO 19005-1 6.1.3),pvaiMissingXmpDates - Szín és kimenet:
pvaiMissingOutputIntent,pvaiMissingIccProfileéspvaiMixedDeviceColorSpaces, amikor a DeviceRGB és a DeviceCMYK is megjelenik (6.2.3.3) - Szigorú tiltások minden részben:
pvaiEncryptionPresent(a/Encryptszótár teljesen tilos),pvaiJavaScriptPresent,pvaiForbiddenAction,pvaiAdditionalActions,pvaiLzwUsed,pvaiXfaPresent,pvaiNeedAppearancesTrue,pvaiForbiddenAnnotation - Betűtípusok:
pvaiFontNotEmbeddedés a szigorúbbpvaiUnembeddedFont, valamint apvaiUnicodeMappingMissinga/ToUnicodenélküli 'U' szintű állításokhoz - Címkézés:
pvaiLevelAStructureMissing, ha a conformance=A állítással rendelkező dokumentumból hiányzik a címkézett struktúra
A hat legújabb tag, amelyek a 24-től 29-ig terjedő sorszámokhoz kerültek, azokat a finom eseteket fedik le, amelyeken az ellenőrök valójában elbuknak: pvaiTrappedTrue (egy /Trapped /True az Info szótárban, ami egy „hamis barát”, mivel az értéknek False vagy Unknown-nak kell lennie), pvaiForbiddenActionSubtype (akcióként használt Sound vagy Movie, nem csak annotációként), pvaiTransparentColorSpace (nem normál keverési mód vagy 1.0-tól eltérő /CA//ca), pvaiAnnotationDictViolation, pvaiUnembeddedFont és pvaiMixedDeviceColorSpaces
Verzióérzékeny szűrés: az A-1 szigorú, az A-2 és A-3 megengedőbb
A PDF/A nem egyetlen szabálykönyv. Három dolog, amit a PDF/A-1 tilt, kifejezetten engedélyezett a PDF/A-2-től kezdődően: az átlátszóság (egy /Transparency csoport vagy egy aktív /SMask, 6.4), az opcionális tartalom (/OCProperties, 6.1.13) és a beágyazott fájlok (/EmbeddedFiles vagy /EF, 6.1.11). Egy naiv ellenőrző, amely mindhármat jelzi minden fájlnál, tömegesen utasítana el teljesen érvényes PDF/A-2 dokumentumokat
Így az ellenőrző leolvassa a rész számát a pdfaid jelölőből a PdfAPartOf segítségével, és ezeket az ellenőrzéseket a PartNo = 1 feltétel mögé helyezi. Az új transzparencia-problémák blend-mode és annotation-alpha ellenőrzései szintén csak az 1. részre vonatkoznak:
if PartNo = 1 then
begin
if PdfHasName(Struct, '/BM') then
if not PdfHasBMNormal(Struct) then // only /Normal or /Compatible allowed
Include(Result.Issues, pvaiTransparentColorSpace);
if PdfHasCaNotOne(Struct, '/CA') or PdfHasCaNotOne(Struct, '/ca') then
Include(Result.Issues, pvaiTransparentColorSpace);
end;
Egy konzervatív alapértelmezést érdemes megemlíteni: ha egyáltalán nincs pdfaid jelölő, a rendszer a részt 1-esként (a legszigorúbbként) kezeli. Ennek az az oka, hogy egy azonosítatlan fájlt a legszigorúbb szabályok szerint kell vizsgálni, ahelyett, hogy átengednénk. A JavaScript, a tiltott műveletek, az LZW, az XFA, a NeedAppearances, a tiltott annotációk és a beágyazatlan betűtípusok minden résznél tiltottak maradnak, így ezek az ellenőrzések soha nincsenek feltétel mögé rejtve
Objektumfolyamok kibontása, hogy semmi se bújhasson el
A PDF 1.5 bevezette a kereszthivatkozási folyamot (cross-reference stream) és az objektumfolyamot (/Type /ObjStm), és ezek vakfoltot hoznak létre a naiv bájtszkennerek számára. Egy katalógus, egy OutputIntent, egy akció-szótár — bármi, ami önmagában nem folyam — Flate-tömörítéssel tömöríthető egy ObjStm-en belül. Ha a nyers szerkezetet szkenneli, semmit sem fog látni ezekből, és hibátlannak jelent egy olyan fájlt, ami valójában nem az
A PdfExpandObjectStreams bezárja ezt a rést. Mielőtt bármilyen ellenőrzés futna, az ellenőrző végrehajtja a Data := PdfExpandObjectStreams(Data) műveletet. A rutin megtalál minden ObjStm-et, elolvassa annak /N és /First fejlécét, hogy megkapja a benne lévő objektumszámokat és eltolásokat, felfújja a törzset a PdfInflate (az RTL zlib, System.ZLib Delphiben és zstream FPC-ben) segítségével, és minden benne lévő objektumot normál N 0 obj ... endobj formátumban fűz a bájtok másolatának végéhez. A meglévő token-ellenőrzések ezután minden logika-módosítás nélkül megtalálják ezeket az objektumokat
Két korlátozás teszi ezt tisztává a törékenység helyett. Az adatfolyam-objektumok — a metaadatok, az ICC profil és a betűtípus-programok — nem élhetnek objektumfolyamban, csak a nem adatfolyam szótárak, így a kibontás mindig csak szótárakkal foglalkozik, és a hozzáfűzött objektumok nem hordoznak stream kulcsszót, amely megzavarná a test-eltávolító lépést. És mivel a hozzáfűzött tartalom a %%EOF után landol, a startxref-ből induló visszafelé keresés továbbra is megtalálja az eredeti trailert. Magát a kereszthivatkozási folyam trailerét már korábban, a v1.49.3-ban kezeltük a Root, Size és ID közvetlen beolvasásával a sima szöveges xref-stream szótárból — ezt a témát az objektum- és kereszthivatkozási folyamok érvényesítéséről szóló cikkben tárgyaljuk; az objektumfolyam-munkának csak a felfújási lépést kellett hozzáadnia, anélkül, hogy dekódolnia kellene a 2. típusú xref bejegyzéseket vagy vissza kellene fejtenie egy PNG prediktort
Egy bájtszintű ellenőrző becsületes korlátai
Ez egy preflight eszköz, nem pedig minősített ellenőrző, és a határai valósak. A betűtípus-beágyazás egy számlálási heurisztika, és a helyes működéshez szükség volt egy korrekcióra, amelyről érdemes tudni. Az eredeti ellenőrzés a PdfCountName('/FontDescriptor')-t használta, de minden betűtípus két /FontDescriptor tokennel járul hozzá: egy hivatkozással a betűtípus-szótárból, és egy /Type-pal magában a leíró objektumban, így a szám 2N volt az N beágyazott programmal szemben, és a teszt mindig igazat adott. A javítás az PdfCountDescriptorRefs, amely csak a /FontDescriptor N G R hivatkozási formát számolja, betűtípusonként egyet, és csak akkor jelzi a pvaiUnembeddedFont hibát, ha a beágyazott programok száma valóban kevesebb:
// betűtípus-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 dokumentum, ahol minden leírónak történetesen van valamilyen FontFile-ja, továbbra is átengedhet egy egyedi, nem megfelelő betűtípust. Az objektumfolyamok kibontásának is van egy ismert mellékhatása: feltárja azokat a standard-14 alapértelmezett erőforrásokat, amelyeket egy AcroForm /DR hordoz (például a /Helv-et), és a heurisztika kötelességtudóan beágyazatlanként jelenti őket, bár a veraPDF átengedi őket, mert valójában soha nem használják őket renderelésre. A tartalomfolyam-operátor szintű ellenőrzések (6.2.10) teljesen kívül esnek a hatókörön, mivel azokhoz teljes tartalomelemzésre lenne szükség a bájtszkennelés helyett. Kezelje az ellenőrzőt egy gyors, függőségmentes első kapuként, amely elkapja azokat a szabálysértéseket, amelyeket a jelölő-injektálás nem tud javítani, és tartsa fenn a teljes ellenőrzőt a végső tanúsításhoz
Ez a történet ellenőrző fele. A kiegészítő író oldal, ahol a SaveAsPdfA injektálja az XMP, OutputIntent és sRGB ICC profilt, és becsületesen leminősíti a címkézett struktúra nélküli 'A' szintű kérést, ugyanezen bájtszintű gépezetre épül. Mindkét fél a Delphihez készült PDFium komponensben érhető el, amely egyetlen VCL csomag egy tiszta Pascal PDF/A megvalósítás felett, külső telepítendő futtatókörnyezet nélkül