Techninis straipsnis

PDF/A preflight validavimas Delphi aplinkoje su PDFium Component

Archyvo priėmimo vartai atmetė paketą „PDF/A-2b“ failų, kurie kiekvienoje peržiūros programoje ant stalo atsidarė be priekaištų. Tiekėjas prisiekė, kad jie atitinka standartą. Neatitiko: kiekvienas failas kataloge turėjo giliai paslėptą JavaScript veiksmą, tokio tipo dalyką, kurio atsitiktinis žvilgsnis nepastebi, bet pilnas PDF/A validatorius, kaip veraPDF, pažymi akimirksniu. Kabliukas tas, kad niekas nenorėjo prie Delphi paketinio serviso jungti visos Java įrankių grandinės vien tam, kad kiekvienam failui atsakytų vieną taip arba ne klausimą. Tą tarpą PDFium Component užpildo ValidatePdfACompliance, ir verta suprasti, kaip jis priima sprendimą nė karto pilnai neišanalizavęs content stream

Kodėl pats PDFium į tai negali atsakyti

Pirmas dalykas, kurį reikia sąžiningai pasakyti: komplektuojamas pdfium.dll apskritai neturi jokios PDF/A funkcijos. Jo viešajame paviršiuje nėra nei ConvertToPDFA, nei OutputIntent rašyklės, nei XMP API. Visa PDF/A logika šioje bibliotekoje, tiek rašymo, tiek tikrinimo pusė, gyvena grynajame Pascal modulyje FPdfPdfa.pas ir remiasi baitų lygio analize bei incremental update. Todėl kai kviečiate validatorių, jūs nieko neklausiate iš Chromium atvaizduotojo. Jūs paleidžiate Pascal token scanner ties failo struktūriniais baitais

Viešasis API čia sąmoningai mažas. Viena funkcija nuskaito srautą nuo 0 pozicijos ir grąžina įrašą:

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 koduoja taisyklę, kuri vartų scenarijuje iš tikrųjų svarbi: failas laikomas sėkmingu tik tada, kai aptiktas tikras atitikties lygis ir problemų aibė yra tuščia. Analizė, kuri pavyksta, bet neranda jokio pdfaid žymens, išsisprendžia kaip pacNone, ir tai aiškiai nelaikoma sėkme. Tą patį iš išorės pabrėžia ir batch preflight report CLI: tuščias radinių sąrašas neatpažintam failui nėra švarios sveikatos pažyma

Srautų kūnų pašalinimas prieš bet kokį token scan

Štai pati svarbiausia įgyvendinimo detalė ir kartu ta, kurią lengviausia sugadinti rašant savo skenerį. Pažeidimai aptinkami ieškant ribotų vardinių tokenų, tokių kaip /JavaScript, /LZWDecode, /BM. Jei skenuojate žalius failo baitus, įterpti dvejetainiai stream body, suspausti vaizdai, ICC profiliai ir šriftų programos atsitiktinai turės baitų sekas, kurios atrodo kaip šie tokenai. Jūs pranešite radę /AA arba /3D vien todėl, kad trys baitai JPEG viduje taip susidėliojo. Tai yra klaidingų teigiamų radinių fabrikas

Pataisa yra PdfStructureBytes: jis pereina per failą ir baitus tarp kiekvieno stream ir endstream raktažodžio užpildo tarpais, palikdamas žodyno struktūrą nepaliestą. Tik po to vyksta skenavimas. Kiekviena vardinio tokeno patikra validatoriuje veikia būtent su šia nuvalyta kopija. Jei iš šio straipsnio norite išsinešti vieną idėją, išsineškite šitą. Ta pati disciplina atkartota ir PDF/UA validatoriuje, kuris turi savo šios rutinos kopiją, nes abu standartai vystosi nepriklausomai

29 problemos ir ką kiekviena iš jų reiškia

TPdfAValidationIssue yra dokumentuota sutartis. Ordinalai įšaldyti, nes nuo jų priklauso DUnitX testai, demonstracinės programos ir ataskaitų sluoksnis, todėl nauji radiniai visada tik pridedami į enum pabaigą. Nuo v1.63.0 jų yra 29. Jie natūraliai susiskirsto į kelias šeimas:

  • Metaduomenys ir tapatybė: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates
  • Spalvos ir išvestis: pvaiMissingOutputIntent, pvaiMissingIccProfile ir pvaiMixedDeviceColorSpaces, kai kartu pasitaiko DeviceRGB ir DeviceCMYK (6.2.3.3)
  • Griežti draudimai visoms dalims: pvaiEncryptionPresent (bet koks /Encrypt žodynas draudžiamas iš principo), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Šriftai: pvaiFontNotEmbedded ir griežtesnis pvaiUnembeddedFont, taip pat pvaiUnicodeMappingMissing lygio U teiginiui be /ToUnicode
  • Žymėjimas: pvaiLevelAStructureMissing, kai conformance=A teiginys neturi žymėtos struktūros

Šeši naujausi nariai, pridėti į ordinalus 24-29, apima subtilius atvejus, kuriuos recenzentai realiai užkliudo: pvaiTrappedTrue (Info žodyne esantis /Trapped /True, klaidingas draugas, nes reikšmė turi būti False arba Unknown), pvaiForbiddenActionSubtype (Sound arba Movie naudojamas kaip veiksmas, ne tik kaip anotacija), pvaiTransparentColorSpace (blend mode, kuris nėra Normal, arba /CA//ca, nelygūs 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont ir pvaiMixedDeviceColorSpaces

Nuo dalies priklausantis filtravimas: A-1 griežtas, A-2 ir A-3 atleidžia daugiau

PDF/A nėra viena taisyklių knyga. Trys dalykai, kurie PDF/A-1 draudžiami, nuo PDF/A-2 į priekį jau leidžiami: skaidrumas (grupė /Transparency arba aktyvus /SMask, 6.4), optional content (/OCProperties, 6.1.13) ir įterpti failai (/EmbeddedFiles arba /EF, 6.1.11). Naivus validatorius, kuris visus tris žymi kiekvienam failui, masiškai atmes visiškai teisėtus PDF/A-2 dokumentus

Todėl validatorius nuskaito dalies numerį iš pdfaid žymens per PdfAPartOf ir šias patikras slepia už sąlygos PartNo = 1. Blend mode ir anotacijų alfa patikros, susijusios su naujais skaidrumo radiniais, taip pat taikomos tik 1 daliai:

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;

Viena konservatyvi numatytoji elgsena verta atskiro paminėjimo: jei pdfaid žymens išvis nėra, dalis laikoma 1, tai yra griežčiausia versija. Logika paprasta: neatpažintą failą geriau laikyti pagal griežčiausias taisykles, o ne praleisti ranka mostelėjus. JavaScript, draudžiami veiksmai, LZW, XFA, NeedAppearances, draudžiamos anotacijos ir neįterpti šriftai draudžiami visoms dalims, todėl šios patikros niekada nesėdi už jokio vartelio

Objektų srautų išplėtimas, kad niekas nepasislėptų

PDF 1.5 įvedė cross-reference stream ir object stream (/Type /ObjStm), ir tai sukuria akląją zoną naiviam baitų skeneriui. Katalogas, OutputIntent, veiksmų žodynas - bet kas, kas pats nėra srautas - gali būti Flate suspaustas ObjStm viduje. Jei skenuosite tik žalią struktūrą, jūs jų tiesiog nepamatysite ir paskelbsite failą švariu, nors jis toks visai nėra

PdfExpandObjectStreams uždaro šią spragą. Prieš paleidžiant bet kokią patikrą validatorius daro Data := PdfExpandObjectStreams(Data). Rutina suranda kiekvieną ObjStm, perskaito jo /N ir /First antraštę, kad gautų vidinių objektų numerius ir poslinkius, išpučia kūną per PdfInflate (RTL zlib, System.ZLib Delphi pusėje ir zstream FPC pusėje), o tada kiekvieną vidinį objektą kaip paprastą N 0 obj ... endobj prideda prie baitų kopijos galo. Esamos tokenų patikros tuomet tuos objektus randa visiškai nekeičiant savo logikos

Dvi sąlygos daro šį sprendimą švarų, o ne trapų. Stream objektai - metaduomenys, ICC profilis ir šriftų programos - negali gyventi objektų sraute, ten gali būti tik nesrautinių žodynų objektai, todėl išplėtimas dirba tik su žodynais, o pridėtuose objektuose nėra jokio stream raktažodžio, kuris sujauktų kūnų nuvalymo žingsnį. Kadangi pridėtas turinys nusėda po %%EOF, atgalinė paieška nuo startxref vis tiek randa originalų trailer. Pats cross-reference stream trailer jau buvo sutvarkytas anksčiau, v1.49.3, tiesiai iš aiškaus xref-stream žodyno skaitant Root, Size ir ID - tai aptarta gretimame tekste apie objektų ir kryžminių nuorodų srautų validavimą; objektų srautų darbui tereikėjo pridėti išpūtimo žingsnį, be jokio poreikio dekoduoti type-2 xref įrašų ar atsukti PNG predictor

Sąžiningos baitų lygio tikrintuvo ribos

Tai yra preflight įrankis, o ne sertifikuotas validatorius, ir jo ribos tikros. Šriftų įterpimo tikrinimas yra skaičiavimo euristika, o viena jos pataisa verta žinojimo. Originali patikra naudojo PdfCountName('/FontDescriptor'), bet kiekvienas šriftas prisideda dviem /FontDescriptor tokenais - viena nuoroda iš šrifto žodyno ir vienu /Type descriptor objekte, todėl skaičius gaudavosi 2N prieš N įterptų programų ir testas visada būdavo true. Pataisa yra PdfCountDescriptorRefs, kuris skaičiuoja tik /FontDescriptor N G R nuorodos formą, po vieną kiekvienam šriftui, ir pakelia pvaiUnembeddedFont tik tada, kai įterptų programų tikrai mažiau:

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);

Net ir po pataisos tai išlieka grubus metodas: mišrus dokumentas, kuriame kiekvienas descriptor atsitiktinai turi kokį nors FontFile, vis tiek gali praleisti vieną atskirą neatitinkantį šriftą. Objektų srautų išplėtimas taip pat turi žinomą šalutinį poveikį - jis iškelia standart-14 numatytuosius resursus, kuriuos AcroForm /DR nešasi, pavyzdžiui /Helv, ir euristika sąžiningai praneša juos kaip neįterptus, nors veraPDF juos praleidžia, nes jie faktiškai nenaudojami atvaizdavimui. Content-stream operatorių lygio patikros (6.2.10) išvis nepatenka į šio sluoksnio ribas, nes joms reikėtų pilno turinio parserio, o ne baitų skenavimo. Vertinkite šį validatorių kaip greitą, be priklausomybių veikiantį pirmą vartą, kuris pagauna tas klaidas, kurių marker injection ištaisyti negali, o pilną validatorių pasilikite galutinei sertifikacijai

Čia aprašyta tikrinimo istorijos pusė. Papildanti rašymo pusė, kur SaveAsPdfA įterpia XMP, OutputIntent ir sRGB ICC profilį bei sąžiningai pažemina lygio A prašymą, jei dokumentas neturi žymėtos struktūros, remiasi tais pačiais baitų lygio mechanizmais. Abi pusės tiekiamos kartu su PDFium Component for Delphi, vienu VCL paketu virš grynojo Pascal PDF/A įgyvendinimo, kuriam nereikia jokio išorinio runtime