Tehnički članak

Provera usklađenosti PDF/A pre pokretanja u Delphiju uz PDFium VCL

Ulazni gate za arhivu odbio je seriju fajlova označenih kao "PDF/A-2b" koji su se otvarali sasvim normalno u svakom pregledaču na stolu. Dobavljač je tvrdio da su usklađeni. Nisu bili: svaki je nosio JavaScript akciju sakrivenu u katalogu, ono što usputno oko nikada ne uhvati, a pun PDF/A validator kao veraPDF prijavi u trenu. Problem je što niko nije želeo da zakači Java alatni lanac na Delphi batch servis samo da bi na jedno pitanje po fajlu odgovorio sa da ili ne. To je praznina ValidatePdfACompliance koju PDFium Component popunjava, i vredno je razumeti kako dolazi do zaključka bez potpunog parsiranja toka sadržaja

Zašto sam PDFium ne može da odgovori na ovo

Prvo što treba pošteno reći: ugrađeni pdfium.dll nema nikakvu PDF/A sposobnost. Nema ConvertToPDFA, nema OutputIntent upisivač, nema XMP API u javnom sučelju. Sav PDF/A deo u ovoj biblioteci, i na strani upisa i na strani provere, živi u čistom Pascal-u u FPdfPdfa.pas i radi kroz parsiranje na nivou bajta plus inkrementalno ažuriranje. Dakle, kada pozovete validator, ne pitate ništa Chromiumov renderer. Pokrećete Pascal skener tokena preko strukturnih bajtova fajla

Javni API je namerno mali. Jedna funkcija čita tok sa 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 kodira pravilo koje je važno u gate-u: fajl prolazi samo kada je otkriven stvarni nivo usklađenosti i kada je skup problema prazan. Parsiranje koje uspe, ali ne pronađe pdfaid marker, završava kao pacNone, što izričito nije prolaz. To je ista poenta koju CLI za batch preflight izveštaj pokazuje spolja: prazan skup nalaza na neprepoznatom fajlu nije čista zdravstvena potvrda

Skidanje tela toka pre bilo kakvog skeniranja tokena

Evo najvažnijeg detalja implementacije, i onog koji je najlakše pogrešiti ako pišete sopstveni skener. Detektor nalazi kršenja traženjem razdvojenih name tokena, stvari poput /JavaScript, /LZWDecode, /BM. Ako skenirate sirove bajtove fajla, ugrađena binarna tela tokova, komprimovane slike, ICC profile, programske fontove, nasumično će sadržati sekvence bajtova koje liče na te tokene. Prijavićete /AA ili /3D "pronađeno" zato što su tri bajta unutar JPEG-a slučajno tvorila tu reč. To je fabrika lažnih pozitivnih rezultata

Popravka je PdfStructureBytes: on prolazi kroz fajl i prazni bajtove između svakog stream i endstream keyword-a u razmake, ostavljajući strukturu rečnika netaknutom. Tek nakon toga kreće skeniranje. Svaka provera name-tokena u validatoru radi nad ovom očišćenom kopijom. Ako iz ovog članka ponesete jednu ideju, ponesite tu. Ista disciplina ogleda se u PDF/UA validatoru, koji čuva sopstvenu kopiju rutine jer se dva standarda razvijaju nezavisno

29 problema i šta svaki od njih znači

TPdfAValidationIssue je dokumentovan ugovor. Ordinali su zamrznuti zato što DUnitX testovi, demo primeri i report sloj zavise od njih, pa se novi nalazi samo dodaju na kraj. Od v1.63.0 postoji 29 članova. Oni spadaju u nekoliko porodica:

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

Šest najnovijih članova, dodati na ordinalima od 24 do 29, pokrivaju suptilne slučajeve o koje se recenzenti zaista sapliću: pvaiTrappedTrue (a /Trapped /True in the Info dictionary, a "false friend" since the value must be False or Unknown), pvaiForbiddenActionSubtype (Sound or Movie used as an action, not just an annotation), pvaiTransparentColorSpace (a non-Normal blend mode or a /CA//ca not equal to 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont, and pvaiMixedDeviceColorSpaces

Gating po delu: A-1 je strogo, A-2 i A-3 popuštaju

PDF/A is not one rulebook. Three things that PDF/A-1 forbids are explicitly permitted from PDF/A-2 onward: transparency (a /Transparency group or an active /SMask, 6.4), optional content (/OCProperties, 6.1.13), and embedded files (/EmbeddedFiles or /EF, 6.1.11). A naive validator that flags all three for every file will reject perfectly valid PDF/A-2 documents en masse

So the validator reads the part number from the pdfaid marker through PdfAPartOf and gates those checks behind PartNo = 1

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;

Podrazumevana oprezna vrednost zaslužuje pomen: kada pdfaid marker uopšte ne postoji, deo se tretira kao 1, najstroži. Razlog je taj što neidentifikovan fajl treba držati najtvrđim pravilima, a ne provući ga kroz sito. JavaScript, zabranjene akcije, LZW, XFA, NeedAppearances, zabranjene anotacije i neugašeni fontovi ostaju zabranjeni za svaki deo, pa te provere nikada ne stoje iza gate-a

Proširivanje object stream-ova da se ništa ne sakrije

PDF 1.5 je uveo cross-reference stream i object stream (/Type /ObjStm), i oni stvaraju slepu zonu za naivan bajt skener. Katalog, OutputIntent, rečnik akcije, bilo šta što samo po sebi nije tok, može biti Flate-kompresovano unutar ObjStm-a. Skenirajte sirovu strukturu i nećete videti ništa od toga, pa ćete prijaviti čist fajl koji to nije

PdfExpandObjectStreams zatvara tu rupu. Pre nego što bilo koja provera krene, validator radi Data := PdfExpandObjectStreams(Data) raspakivanje object stream-ova. Rutina pronalazi svaki ObjStm, čita njegovo /N i /First zaglavlje da bi dobila brojeve i offsete sadržanih objekata, naduvava telo pomoću PdfInflate (RTL zlib, System.ZLib na Delphiju i zstream na FPC-u), i dodaje svaki sadržani objekat kao običan N 0 obj ... endobj na kraj kopije bajtova. Postojeće provere tokena zatim pronalaze te objekte bez promene svoje logike

Dve restrikcije ovo čine čistim, a ne krhkim. Tok objekti, Metadata, ICC profil i programski fontovi, ne mogu da žive u object stream-u, samo ne-tok rečnici mogu, pa se proširivanje bavi samo rečnicima, a dodati objekti ne nose nikakav stream ključ koji bi poremetio prolaz za čišćenje tela toka. A pošto dodati sadržaj završava posle %%EOF, obrnuta pretraga od startxref i dalje nalazi originalni trailer. Sam trailer cross-reference stream-a već je bio obrađen ranije, u v1.49.3, čitanjem Root, Size i ID direktno iz plaintext xref-stream rečnika, tema obrađena u pratećem tekstu o validaciji object i cross-reference stream-ova; rad sa object stream-ovima je samo trebalo da doda korak naduvavanja, bez potrebe da se dekodiraju type-2 xref unosi ili poništava PNG predictor

Poštena ograničenja provere na nivou bajta

Ovo je alat za preflight, ne sertifikovani validator, i granice su stvarne. Otkrivanje ugradnje fontova je heuristika brojanja, a da bi bilo ispravno bila je potrebna ispravka koju vredi zapamtiti. Prvobitna provera je koristila PdfCountName('/FontDescriptor'), ali svaki font doprinosi sa dva /FontDescriptor tokena, jednom referencom iz rečnika fonta i jednom /Type u samom descriptor objektu, pa je brojanje bilo 2N naspram N ugrađenih programa i test je uvek bio tačan. Popravka je PdfCountDescriptorRefs, koja broji samo /FontDescriptor N G R referentni oblik, jedan po fontu, i podiže pvaiUnembeddedFont samo kada su ugrađeni programi zaista ređ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, ovo je grubo: dokument u kome svaki descriptor slučajno ima neki FontFile i dalje može da provuče pojedinačni neusklađeni font. Proširivanje object stream-ova ima i poznatu posledicu, izlaže standard-14 podrazumevane resurse koje nosi AcroForm /DR kao što je /Helv, a heuristika ih uredno prijavi kao neugašene iako ih veraPDF pušta dalje jer se nikada stvarno ne koriste za prikaz. Provere operatora toka sadržaja (6.2.10) su u potpunosti van opsega, jer bi zahtevale punu analizu sadržaja umesto skeniranja bajtova. Validator tretirajte kao brz, zavisan od nula biblioteka, prvi gate koji hvata kršenja koja ubacivanje markera ne može da popravi, a puni validator ostavite za završnu sertifikaciju

Ovo je deo priče o proveri. Dopunska strana upisa, gde SaveAsPdfA ubacuje XMP, OutputIntent i sRGB ICC profil i pošteno spušta zahtev nivoa A koji nema označenu strukturu, oslanja se na istu mašineriju na nivou bajta. Obe polovine dolaze u PDFium Component za Delphi, jednom VCL paketu preko čiste Pascal PDF/A implementacije bez spoljnog runtime-a za instalaciju