Műszaki cikk

PDF/A preflight ellenőrzés Delphiben PDFium VCL segítségével

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 és pvaiMixedDeviceColorSpaces, amikor a DeviceRGB és a DeviceCMYK is megjelenik (6.2.3.3)
  • Szigorú tiltások minden részben: pvaiEncryptionPresent (a /Encrypt szótár teljesen tilos), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Betűtípusok: pvaiFontNotEmbedded és a szigorúbb pvaiUnembeddedFont, valamint a pvaiUnicodeMappingMissing a /ToUnicode né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