Tehnični članak

Preflight validacija PDF/A v Delphi z PDFium VCL

Vhodna arhivska zapora je zavrnila paket datotek "PDF/A-2b", ki so se brez težav odprle v vsakem pregledovalniku na mizi. Dobavitelj je prisegal, da so skladne. Niso bile: vsaka je nosila JavaScript dejanje, zakopano v katalogu, točno tisto vrsto težave, ki je bežen pogled nikoli ne opazi, popoln validator PDF/A, kot je veraPDF, pa jo označi v trenutku. Težava je bila v tem, da nihče ni želel na paketno storitev Delphi pritrditi orodnega sklopa Java samo zato, da bi za vsako datoteko odgovoril na eno vprašanje da ali ne. To je vrzel, ki jo ValidatePdfACompliance v PDFium Component zapolni, in vredno je razumeti, kako pride do odločitve, ne da bi kdaj v celoti razčlenil vsebinski tok

Zakaj sam PDFium na to ne more odgovoriti

Prva stvar, o kateri moramo biti pošteni: priloženi pdfium.dll sploh nima nobene zmožnosti za PDF/A. Ni ConvertToPDFA, ni zapisovalnika OutputIntent in v javni površini ni nobenega API-ja za XMP. Ves del PDF/A v tej knjižnici, tako zapisovalni kot preverjevalni, živi v čistem Pascalu v FPdfPdfa.pas in deluje z razčlenjevanjem na ravni bajtov ter incremental update. Ko torej pokličete validator, Chromiumovega izrisovalnika ne sprašujete ničesar. Nad strukturnimi bajti datoteke poganjate Pascalov pregledovalnik žetonov

Javni API je namenoma majhen. Ena funkcija prebere tok od položaja 0 in vrne 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, ki je pomembno na vhodni zapori: datoteka je uspešna samo takrat, ko je zaznana resnična raven skladnosti in je množica težav prazna. Razčlenitev, ki uspe, vendar ne najde nobenega označevalca pdfaid, se razreši v pacNone, kar izrecno ni uspešen rezultat. To je ista poanta, ki jo CLI za paketna preflight poročila sporoča navzven: prazen seznam ugotovitev pri neprepoznani datoteki ni čisto potrdilo o zdravju

Odstranjevanje teles tokov pred vsakim pregledom žetonov

Tukaj je najpomembnejša podrobnost implementacije in tista, pri kateri se najlažje zmotite, če napišete lasten pregledovalnik. Zaznavalnik išče kršitve tako, da išče razmejene imenske žetone, stvari, kot so /JavaScript, /LZWDecode, /BM. Če pregledujete surove bajte datoteke, bodo vdelana binarna telesa tokov, stisnjene slike, ICC profili in programi pisav naključno vsebovali zaporedja bajtov, ki so videti kot ti žetoni. Poročali boste, da ste našli /AA ali /3D, ker so tri bajti znotraj JPEG po naključju tvorili to črkovanje. To je tovarna lažnih pozitivnih rezultatov

Popravek je PdfStructureBytes: prehodi datoteko in bajte med vsako ključno besedo stream in endstream nadomesti s presledki, pri čemer struktura slovarja ostane nedotaknjena. Šele nato steče pregled. Vsako preverjanje imenskih žetonov v validatorju deluje na tej očiščeni kopiji. Če iz tega članka odnesete eno samo idejo, naj bo ta. Enaka disciplina se zrcali v validatorju PDF/UA, ki ohranja lastno kopijo te rutine, ker se standarda razvijata neodvisno

29 težav in kaj vsaka pomeni

TPdfAValidationIssue je dokumentirana pogodba. Ordinali so zamrznjeni, ker so od njih odvisni testi DUnitX, demoposnetki in plast poročanja, zato se nove ugotovitve vedno samo dodajo na konec. Od v1.63.0 jih je 29. Razpadejo v nekaj družin:

  • Metapodatki in identiteta: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates
  • Barva in izhod: pvaiMissingOutputIntent, pvaiMissingIccProfile in pvaiMixedDeviceColorSpaces kadar se hkrati pojavita DeviceRGB in DeviceCMYK (6.2.3.3)
  • Stroge prepovedi za vsak del: pvaiEncryptionPresent (slovar /Encrypt je neposredno prepovedan), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Pisave: pvaiFontNotEmbedded in strožji pvaiUnembeddedFont, plus pvaiUnicodeMappingMissing za trditev ravni U brez /ToUnicode
  • Označevanje: pvaiLevelAStructureMissing, ko trditev conformance=A nima označene strukture

Šest najnovejših članov, dodanih na ordinalih od 24 do 29, pokriva prefinjene primere, ob katerih se pregledovalci v resnici spotikajo: pvaiTrappedTrue (/Trapped /True v slovarju Info, lažni prijatelj, saj mora biti vrednost False ali Unknown), pvaiForbiddenActionSubtype (Sound ali Movie, uporabljena kot dejanje, ne samo kot anotacija), pvaiTransparentColorSpace (način mešanja, ki ni Normal, ali /CA//ca ni enak 1.0), pvaiAnnotationDictViolation, pvaiUnembeddedFont in pvaiMixedDeviceColorSpaces

Pravila po delih: A-1 je strog, A-2 in A-3 popustita

PDF/A ni ena sama knjiga pravil. Tri stvari, ki jih PDF/A-1 prepoveduje, so od PDF/A-2 naprej izrecno dovoljene: prosojnost (skupina /Transparency ali aktivni /SMask, 6.4), optional content (/OCProperties, 6.1.13) in vdelane datoteke (/EmbeddedFiles ali /EF, 6.1.11). Naiven validator, ki vse tri označi pri vsaki datoteki, bo množično zavrnil povsem veljavne dokumente PDF/A-2

Validator zato prek PdfAPartOf prebere številko dela iz označevalca pdfaid in ta preverjanja zameji z PartNo = 1. Preverjanja načina mešanja in alfa-vrednosti anotacij za nove težave s prosojnostjo so podobno omejena samo na del 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;

En konzervativen privzeti način si zasluži omembo: kadar označevalca pdfaid sploh ni, se del obravnava kot 1, torej najstrožje. Razlog je v tem, da je treba neidentificirano datoteko presojati po najstrožjih pravilih, ne pa jo spustiti skozi. JavaScript, prepovedana dejanja, LZW, XFA, NeedAppearances, prepovedane anotacije in nevdelane pisave ostanejo prepovedani v vsakem delu, zato ta preverjanja nikoli ne sedijo za vrati

Razširjanje object streams, da se nič ne skrije

PDF 1.5 je uvedel tok medsklicev in object stream (/Type /ObjStm), in to ustvari slepo pego za naiven pregled bajtov. Katalog, OutputIntent, slovar dejanj, karkoli, kar samo ni tok, je lahko Flate-stisnjeno znotraj ObjStm. Če pregledujete surovo strukturo, od tega ne boste videli ničesar, nato pa boste prijavili čisto datoteko, ki to nikakor ni

PdfExpandObjectStreams to vrzel zapre. Preden steče katerokoli preverjanje, validator izvede Data := PdfExpandObjectStreams(Data). Rutina poišče vsak ObjStm, prebere njegovo glavo /N in /First, da dobi številke vsebovanih objektov in njihove odmike, telo razširi z PdfInflate (RTL zlib, System.ZLib v Delphi in zstream v FPC), nato pa vsak vsebovani objekt kot navaden N 0 obj ... endobj pripne na konec kopije bajtov. Obstoječa preverjanja žetonov potem te objekte najdejo brez kakršnekoli spremembe svoje logike

Dve omejitvi to naredijo čisto namesto krhko. Pretočni objekti, Metadata, ICC profil in programi pisav ne morejo živeti v object stream, tam so lahko samo slovarji brez tokov, zato razširjanje vedno obravnava samo slovarje in pripeti objekti ne nosijo nobene ključne besede stream, ki bi motila prehod odstranjevanja teles. Ker se pripeta vsebina pojavi šele za %%EOF, obratno iskanje iz startxref še vedno najde izvirni trailer. Trailer samega toka medsklicev je bil obravnavan že prej, v v1.49.3, z branjem Root, Size in ID neposredno iz navadnega slovarja xref-stream, temo pa raziskuje spremljevalni prispevek o validiranju tokov objektov in medsklicev; delo z object stream je moralo dodati samo korak razširitve, brez potrebe po dekodiranju vnosov type-2 xref ali razpletanju PNG predictor

Poštene meje preverjalnika na ravni bajtov

To je orodje za preflight, ne certificiran validator, in te meje so resnične. Vdelanost pisav je hevristika štetja, in da jo spraviš prav, je bil potreben popravek, ki ga je vredno poznati. Prvotno preverjanje je uporabljalo PdfCountName('/FontDescriptor'), vendar vsaka pisava prispeva dva žetona /FontDescriptor, en sklic iz slovarja pisave in en /Type v samem objektu opisovalnika, zato je bilo štetje 2N proti N vdelanim programom in test je bil vedno resničen. Popravek je PdfCountDescriptorRefs, ki šteje samo obliko sklica /FontDescriptor N G R, po enega na pisavo, in sproži pvaiUnembeddedFont samo takrat, ko je vdelanih programov res manj:

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

Tudi po popravku je grobo: mešan dokument, v katerem ima vsak opisovalnik nekakšen FontFile, lahko še vedno spusti skozi posamezno neskladno pisavo. Razširjanje object streams ima tudi znan stranski učinek, razkrije privzete vire standard-14, ki jih nosi AcroForm /DR, na primer /Helv, hevristika pa jih vestno prijavi kot nevdelane, čeprav jih veraPDF sprejme, ker se za izris v resnici nikoli ne uporabijo. Preverjanja operatorjev na ravni vsebinskih tokov (6.2.10) so v celoti zunaj obsega, saj bi zahtevala popolno razčlenjevanje vsebine namesto pregleda bajtov. Validator obravnavajte kot hitro prvo zaporo brez odvisnosti, ki ujame kršitve, ki jih vbrizgavanje označevalcev ne more popraviti, popoln validator pa si prihranite za končno certifikacijo

To je preverjevalna polovica zgodbe. Dopolnilna zapisovalna polovica, kjer SaveAsPdfA vbrizga XMP, OutputIntent in ICC profil sRGB ter pošteno zniža zahtevo za raven A, kadar ni označene strukture, temelji na istem stroju na ravni bajtov. Obe polovici sta del PDFium Component za Delphi, enotnega paketa VCL nad čisto Pascalovo implementacijo PDF/A brez zunanjega izvajalnega okolja za namestitev