Tehnički članak

PDF/A preflight provjera u Delphiju uz PDFium VCL

Arhivski prijamni portal odbio je seriju datoteka označenih kao "PDF/A-2b" koje su se normalno otvarale u svakom pregledniku na radnom stolu. Dobavljač je tvrdio da su usklađene. Nisu bile: svaka je nosila JavaScript akciju skrivenu u katalogu, onakvu stvar koju usputni pogled nikad ne uhvati, a potpuni PDF/A validator poput veraPDF-a označi u trenu. Kvaka je u tome što nitko nije želio priključiti Java alatni lanac na Delphi batch servis samo da bi odgovorio na jedno pitanje da ili ne po datoteci. To je praznina ValidatePdfACompliance u PDFium Componentu popunjava, i vrijedi razumjeti kako donosi odluku bez ikad potpunog parsiranja content streama

Zašto sam PDFium ne može odgovoriti na ovo

Prvo što treba iskreno reći: priloženi pdfium.dll ima nimalo PDF/A mogućnosti. Nema ConvertToPDFA, nema pisca OutputIntenta, nema XMP API-ja u javnom sučelju. Svaki dio PDF/A u ovoj biblioteci, i zapisivanje i provjeravanje, živi u čistom Pascalu u FPdfPdfa.pas i radi kroz parsiranje na razini bajtova plus inkrementalni update. Dakle, kad pozovete validator, ne pitate ništa Chromiumov renderer. Pokrećete Pascal token skener nad strukturalnim bajtovima datoteke

Javni API je namjerno malen. Jedna funkcija čita stream od pozicije 0 i vraća zapis:

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

IsCompliant `IsCompliant` označava pravilo koje je važno na ulazu: datoteka prolazi samo kad je otkriven stvarni stupanj usklađenosti i skup problema je prazan. Parsiranje koje uspije, ali ne pronađe pdfaid oznaku, završava kao pacNone, što izričito nije prolaz. To je ista poanta koju batch preflight report CLI prenosi izvana: prazan popis nalaza na neprepoznatoj datoteci nije čista potvrda ispravnosti

Uklanjanje tijela streamova prije bilo kakvog skeniranja tokena

Evo najvažnijeg detalja implementacije, i najlakše ga je pogriješiti ako pišete vlastiti skener. Detektor pronalazi prekršaje traženjem razgraničenih name tokena, stvari kao /JavaScript, /LZWDecode, /BM. Ako skenirate sirove bajtove datoteke, ugrađena binarna tijela streamova, komprimirane slike, ICC profile i programski dijelovi fontova, slučajno će sadržavati nizove bajtova koji nalikuju tim tokenima. Prijavit ćete /AA ili /3D "pronađeno" jer su tri bajta unutar JPEG-a slučajno ispisala taj niz. To je tvornica lažnih pozitivnih rezultata

Rješenje je PdfStructureBytes: prolazi kroz datoteku i izravnava bajtove između svakog stream and endstream ključnog izraza u razmake, ostavljajući strukturu rječnika netaknutom. Tek nakon toga kreće skeniranje. Svaka provjera name tokena u validatoru radi nad ovom očišćenom kopijom. Ako iz ovog članka ponesete samo jednu ideju, neka to bude ta. Ista disciplina zrcali se u PDF/UA validatoru, koji zadržava vlastitu kopiju rutine jer se dva standarda razvijaju neovisno

29 problema i što svaki znači

TPdfAValidationIssue`TPdfAValidationIssue` je dokumentirani ugovor. Ordinali su fiksni jer DUnitX testovi, demo primjerci i sloj izvještaja ovise o njima, pa se novi nalazi uvijek dodaju na kraj. Od v1.63.0 postoji 29 članova. Dijele se u nekoliko skupina:

  • Metapodaci i identitet: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates
  • Boja i izlaz: pvaiMissingOutputIntent, pvaiMissingIccProfile, and pvaiMixedDeviceColorSpaces when both DeviceRGB and DeviceCMYK appear (6.2.3.3)
  • Stroge zabrane za svaki dio: pvaiEncryptionPresent (rječnik /Encrypt je potpuno zabranjen), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Fontovi: pvaiFontNotEmbedded i stroži pvaiUnembeddedFont, plus pvaiUnicodeMappingMissing za tvrdnju razine U bez /ToUnicode
  • Označavanje: pvaiLevelAStructureMissing kada tvrdnja o usklađenosti=A nema označenu strukturu

Šest najnovijih članova, dodanih na ordinalima 24 do 29, pokrivaju suptilne slučajeve u koje recenzenti zapravo upadaju: pvaiTrappedTrue (a /Trapped /True u Info rječniku, "false friend" jer vrijednost mora biti False ili Unknown), pvaiForbiddenActionSubtype (Sound ili Movie upotrijebljen kao akcija, a ne samo kao anotacija), pvaiTransparentColorSpace (nenormalan blend mode ili /CA//ca nije jednak 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, and pvaiMixedDeviceColorSpaces

Provjera prema dijelu: A-1 je stroga, A-2 i A-3 popuštaju

PDF/A nije jedna knjiga pravila. Tri stvari koje PDF/A-1 zabranjuje izričito su dopuštene od PDF/A-2 nadalje: prozirnost (grupa /Transparency ili aktivni /SMask, 6.4), opcijski sadržaj (/OCProperties, 6.1.13), i ugrađene datoteke (/EmbeddedFiles ili /EF, 6.1.11). Naivan validator koji sve tri označi za svaku datoteku odbacit će potpuno valjane PDF/A-2 dokumente u masi

Dakle, validator čita broj dijela iz pdfaid oznake preko PdfAPartOf i te provjere stavlja iza PartNo = 1. The blend-mode and annotation-alpha checks for the new transparency issues are similarly part-1 only:

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;

One conservative default deserves a mention: when there is no pdfaid marker at all, the part is treated as 1, the strictest. The reasoning is that an unidentified file should be held to the tightest rules rather than waved through. JavaScript, forbidden actions, LZW, XFA, NeedAppearances, forbidden annotations, and unembedded fonts stay forbidden for every part, so those checks never sit behind the gate

Širenje object streamova da se ništa ne sakrije

PDF 1.5 je uveo cross-reference stream i object stream (/Type /ObjStm), a oni stvaraju slijepo mjesto za naivni byte skener. Katalog, OutputIntent, akcijski rječnik, bilo što što samo po sebi nije stream, može biti Flate-komprimirano unutar ObjStm-a. Skenirajte sirovu strukturu i ništa od toga nećete vidjeti, a zatim ćete prijaviti čistu datoteku koja je sve samo ne to

PdfExpandObjectStreams zatvara tu prazninu. Prije nego što krene ijedna provjera, validator radi Data := PdfExpandObjectStreams(Data). Rutina pronalazi svaki ObjStm, čita njegovo /N i /First zaglavlje kako bi dobila brojeve i pomake sadržanih objekata, napuhuje tijelo pomoću PdfInflate (RTL-ov zlib, System.ZLib na Delphiju i zstream na FPC-u), te dodaje svaki sadržani objekt kao običan N 0 obj ... endobj na kraj kopije bajtova. Postojeće provjere tokena zatim pronalaze te objekte bez ikakve promjene njihove logike

Dvije stvari čine ovo čistim, a ne krhkim. Stream objekti, Metadata, ICC profil i programski dijelovi fontova ne mogu živjeti u object streamu, mogu samo ne-stream rječnici, pa širenje uvijek obrađuje samo rječnike, a dodani objekti ne nose stream ključnu riječ koja bi poremetila prolaz čišćenja tijela. A budući da dodani sadržaj završava nakon %%EOF, obrnuta pretraga od startxref i dalje pronalazi izvorni trailer. Sam xref-stream trailer već je ranije obrađen, u v1.49.3, čitanjem Root, Size i ID izravno iz plaintext xref-stream rječnika, tema obrađena u pratećem tekstu o provjeravanju object i cross-reference streamova; posao object-streama morao je samo dodati korak napuhavanja, bez potrebe za dekodiranjem type-2 xref unosa ili raspetljavanjem PNG prediktora

Poštene granice byte-level provjere

Ovo je alat za preflight, ne certificirani validator, i granice su stvarne. Ugradnja fontova je heuristika brojanja, a ispravljanje toga vrijedilo je zabilježiti. Izvorna provjera koristila je PdfCountName('/FontDescriptor'), ali svaki font doprinosi s dva /FontDescriptor tokena, jedna referenca iz font rječnika i jedna /Type u samom descriptor objektu, pa je broj bio 2N naspram N ugrađenih programa i test je uvijek bio true. Popravak je PdfCountDescriptorRefs, koja broji samo /FontDescriptor N G R referentni oblik, jedan po fontu, i podiže pvaiUnembeddedFont samo kada su ugrađeni programi doista rjeđi:

K := PdfCountDescriptorRefs(Struct);                 // one per font dict
Emb := PdfCountName(Struct, '/FontFile')
     + PdfCountName(Struct, '/FontFile2')
     + PdfCountName(Struct, '/FontFile3');
if (K > 0) and (Emb < K) then
  Include(Result.Issues, pvaiUnembeddedFont);

Čak i ispravljeno, to je grubo: mješoviti dokument u kojem svaki descriptor slučajno ima neki FontFile još uvijek može propustiti pojedini neusklađeni font. Širenje object streamova ima i poznatu nuspojavu: izlaže standard-14 zadane resurse koje nosi AcroForm /DR nosi, kao što je /Helv, a heuristika ih savjesno prijavljuje kao neugrađene iako ih veraPDF pušta jer se nikada zapravo ne koriste za iscrtavanje. Provjere operatora content streama (6.2.10) potpuno su izvan opsega, jer bi zahtijevale potpuno parsiranje sadržaja umjesto byte skena. Validator tretirajte kao brzi, od ovisnosti slobodan prvi filter koji hvata prekršaje koje marker injekcija ne može popraviti, a puni validator ostavite za završnu certifikaciju

Ovo je strana provjere u priči. Komplementarna strana zapisivanja, gdje SaveAsPdfA umeće XMP, OutputIntent i sRGB ICC profil i pošteno spušta zahtjev razine A bez označene strukture, temelji se na istoj byte-level mehanici. Obje polovice dolaze u PDFium Component for Delphi, jedan VCL paket nad čistom Pascal PDF/A implementacijom bez vanjskog runtimea za instalaciju