Tekninen artikkeli

PDF/A-esitarkistus Delphissä PDFium VCL:llä

Arkistoinnin sisäänottoportti hylkäsi erän "PDF/A-2b"-tiedostoja, jotka avautuivat moitteetta jokaisessa työpöydällä olevassa katseluohjelmassa. Toimittaja vannoi, että ne olivat vaatimustenmukaisia. Eivät olleet: jokaisessa oli JavaScript-toiminto haudattuna katalogiin, sellainen asia, jota pikainen silmäys ei koskaan huomaa mutta täysimittainen PDF/A-validaattori kuten veraPDF liputtaa silmänräpäyksessä. Kompastuskivi on, ettei kukaan halunnut liittää Java-työkaluketjua Delphi-eräpalveluun vain vastatakseen yhteen kyllä-tai-ei-kysymykseen tiedostoa kohden. Tämän aukon täyttää PDFium Componentin ValidatePdfACompliance, ja kannattaa ymmärtää, miten se päätyy tuomioon ilman, että se koskaan jäsentää sisältövirtaa täydellisesti

PDF/A-preflight-putki Delphille PDFium Componentilla: raaka PDF riisutaan virtarungoista, objektivirrat purataan, ja Pascal-tokeniskannaus rakennetavujen yli täyttää yhden TPdfAValidationResultin, jonka IsCompliant-portti vaatii havaitun tason ja tyhjän ongelmajoukon
ValidatePdfACompliance ei koskaan jäsennä sisältövirtaa: se skannaa rakennetavut streamin runkojen tyhjennettyinä ja objektistreamien laajennettuina, ja läpäisy edellyttää havaittua vaatimustasoa sekä tyhjää ongelmajoukkoa

Miksi PDFium itse ei voi vastata tähän

Ensimmäinen asia, joka kannattaa tunnustaa rehellisesti: mukana tuleva pdfium.dll ei tue PDF/A:ta lainkaan. Julkisessa pinnassa ei ole ConvertToPDFA-funktiota, ei OutputIntent-kirjoitinta, ei XMP-rajapintaa. Jokainen osa tämän kirjaston PDF/A-toteutuksesta, sekä kirjoitus- että tarkistuspuoli, elää puhtaana Pascal-koodina tiedostossa FPdfPdfa.pas ja toimii tavutason jäsennyksellä sekä inkrementaalisella päivityksellä. Kun siis kutsut validaattoria, et kysy Chromiumin renderöijältä mitään. Ajat Pascal-tokenskanneria tiedoston rakennetavujen yli

Julkinen rajapinta on tarkoituksella pieni. Yksi funktio lukee virran positiosta 0 ja palauttaa tietueen:

function ValidatePdfACompliance(Source: TStream): TPdfAValidationResult;

type
  TPdfAValidationResult = record
    Conformance: TPdfAConformance;        // pacUnknown, pacNone, pac1b, pac2u, ...
    Issues: TPdfAValidationIssues;        // joukko TPdfAValidationIssue-arvoja
    function IsCompliant: Boolean;        // True vain kun taso <> unknown/none
  end;                                    // JA Issues on tyhjä

IsCompliant kiteyttää säännön, jolla on merkitystä portissa: tiedosto läpäisee vain, kun todellinen vaatimustenmukaisuustaso havaittiin ja ongelmajoukko on tyhjä. Jäsennys, joka onnistuu mutta ei löydä pdfaid-merkkiä, ratkeaa arvoksi pacNone, mikä ei nimenomaisesti ole läpäisy. Tämä on sama asia, jonka erän preflight-raportin komentorivityökalu tekee selväksi ulkopuolelta: tyhjä löydöslista tunnistamattomalla tiedostolla ei ole puhtaan paperit

Virtojen sisällön karsinta ennen token-skannausta

Tässä on kaikkein tärkein toteutuksen yksityiskohta, ja se on helpoin mennä pieleen, jos kirjoitat oman skannerin. Tunnistin löytää rikkomukset etsimällä rajattuja nimitokeneita, kuten /JavaScript, /LZWDecode, /BM. Jos skannaat raakoja tiedostotavuja, upotetut binäärivirtojen sisällöt, pakatut kuvat, ICC-profiilit, fonttiohjelmat, sisältävät satunnaisesti tavusarjoja, jotka näyttävät noilta tokeneilta. Raportoit /AA- tai /3D-tokenin "löytyneen", koska kolme tavua JPEG-kuvan sisällä sattui tavaamaan sen. Se on väärien positiivisten tehdas

Korjaus on PdfStructureBytes: se käy tiedoston läpi ja tyhjentää tavut jokaisen stream- ja endstream-avainsanan välillä välilyönneiksi, jättäen sanakirjarakenteen koskemattomaksi. Vasta sen jälkeen skannaus ajetaan. Jokainen validaattorin nimitokentarkistus toimii tällä karsitulla kopiolla. Jos otat tästä artikkelista mukaasi vain yhden ajatuksen, ota tämä. Sama kuri toistuu PDF/UA-validaattorissa, joka pitää oman kopionsa rutiinista, koska kaksi standardia kehittyvät toisistaan riippumatta

29 ongelmaa ja mitä kukin niistä tarkoittaa

TPdfAValidationIssue on dokumentoitu sopimus. Järjestysluvut on jäädytetty, koska DUnitX-testit, demot ja raporttikerros riippuvat kaikki niistä, joten uudet löydökset lisätään aina vain loppuun. Versiosta 1.63.0 alkaen jäseniä on 29. Ne jakautuvat muutamaan perheeseen:

  • Metatiedot ja tunniste: pvaiMissingXmpMetadata, pvaiMissingPdfAIdentifier, pvaiMissingTrailerId (ISO 19005-1 6.1.3), pvaiMissingXmpDates
  • Väri ja tulostustarkoitus: pvaiMissingOutputIntent, pvaiMissingIccProfile, sekä pvaiMixedDeviceColorSpaces, kun sekä DeviceRGB että DeviceCMYK esiintyvät (6.2.3.3)
  • Ehdottomat kiellot jokaiselle osalle: pvaiEncryptionPresent (/Encrypt-sanakirja on suoraan kielletty), pvaiJavaScriptPresent, pvaiForbiddenAction, pvaiAdditionalActions, pvaiLzwUsed, pvaiXfaPresent, pvaiNeedAppearancesTrue, pvaiForbiddenAnnotation
  • Fontit: pvaiFontNotEmbedded ja tiukempi pvaiUnembeddedFont, sekä pvaiUnicodeMappingMissing Level U -väitteelle ilman /ToUnicode-kohdetta
  • Tagitus: pvaiLevelAStructureMissing, kun conformance=A-väitteellä ei ole tagattua rakennetta

Kuusi uusinta jäsentä, lisätty järjestysluvuille 24–29, kattavat hienovaraiset tapaukset, joihin tarkastajat oikeasti kompastuvat: pvaiTrappedTrue (/Trapped /True Info-sanakirjassa, "väärä ystävä", koska arvon täytyy olla False tai Unknown), pvaiForbiddenActionSubtype (Sound tai Movie käytettynä toimintona, ei vain merkintänä), pvaiTransparentColorSpace (muu kuin Normal-sekoitustila tai /CA//ca, joka ei ole 1,0), pvaiAnnotationDictViolation, pvaiUnembeddedFont ja pvaiMixedDeviceColorSpaces

Kaavio 29:stä TPdfAValidationIssue-koodista PDFium Component PDF/A -validoijassa Delphille, ryhmiteltynä metatietoon ja identiteettiin, väriin ja tulosteeseen, koviin kieltoihin, fonteihin, merkintään ja kuuteen uusimpaan löytöön kuten pvaiTrappedTrue ja pvaiTransparentColorSpace
29 ongelmakoodia jakautuu viiteen perheeseen plus kuuteen uusimpaan jäseneen, ja kovat kiellot, kuten salaus, JavaScript ja LZW, koskevat jokaista PDF/A-osaa

Parttikohtainen portitus: A-1 on tiukka, A-2 ja A-3 ovat väljempiä

PDF/A ei ole yksi sääntökirja. Kolme asiaa, jotka PDF/A-1 kieltää, on nimenomaisesti sallittu PDF/A-2:sta alkaen: läpinäkyvyys (/Transparency-ryhmä tai aktiivinen /SMask, 6.4), valinnainen sisältö (/OCProperties, 6.1.13) ja upotetut tiedostot (/EmbeddedFiles tai /EF, 6.1.11). Naiivi validaattori, joka liputtaa kaikki kolme jokaisessa tiedostossa, hylkää täysin kelvollisia PDF/A-2-asiakirjoja joukoittain

Siksi validaattori lukee osan numeron pdfaid-merkistä PdfAPartOf-funktion kautta ja porttaa nuo tarkistukset ehdon PartNo = 1 taakse. Uusien läpinäkyvyysongelmien sekoitustila- ja merkintä-alfa-tarkistukset ovat samoin vain osalle 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;

Yksi varovainen oletusarvo ansaitsee maininnan: kun pdfaid-merkkiä ei ole lainkaan, osa käsitellään arvona 1, tiukin. Perustelu on, että tunnistamaton tiedosto tulisi pitää tiukimpien sääntöjen alaisena sen sijaan, että se läpäistäisiin vilkaisulla. JavaScript, kielletyt toiminnot, LZW, XFA, NeedAppearances, kielletyt merkinnät ja upottamattomat fontit pysyvät kiellettyinä jokaiselle osalle, joten nuo tarkistukset eivät koskaan istu portin takana

Objektivirtojen avaaminen niin, ettei mikään jää piiloon

PDF 1.5 toi mukanaan ristiviittausvirran ja objektivirran (/Type /ObjStm), ja ne luovat sokean pisteen naiiville tavuskannerille. Katalogi, OutputIntent, toimintosanakirja, mikä tahansa, joka ei itse ole virta, voidaan Flate-pakata ObjStm:n sisään. Skannaa raaka rakenne, etkä näe siitä mitään, ja raportoit sitten puhtaan tiedoston, joka ei ole sitä lainkaan

PdfExpandObjectStreams sulkee tuon aukon. Ennen minkään tarkistuksen ajamista validaattori tekee Data := PdfExpandObjectStreams(Data). Rutiini löytää jokaisen ObjStm:n, lukee sen /N- ja /First-otsikot saadakseen sisältämiensä objektien numerot ja siirtymät, purkaa sisällön PdfInflate-funktiolla (RTL:n zlib, System.ZLib Delphissä ja zstream FPC:ssä) ja liittää jokaisen sisältämänsä objektin tavallisena N 0 obj ... endobj-lohkona tavukopion loppuun. Olemassa olevat tokentarkistukset löytävät sitten nuo objektit ilman muutoksia niiden logiikkaan

Kaksi rajoitetta tekevät tästä siistin, ei hauraan. Virtaobjektit, Metadata, ICC-profiili ja fonttiohjelmat, eivät voi elää objektivirrassa, vain ei-virtasanakirjat voivat, joten laajennus käsittelee aina vain sanakirjoja, eivätkä liitetyt objektit kanna stream-avainsanaa häiritsemässä sisällönkarsintakierrosta. Ja koska liitetty sisältö laskeutuu %%EOF:n jälkeen, käänteinen haku startxref:sta löytää edelleen alkuperäisen trailerin. Itse ristiviittausvirran traileri käsiteltiin jo aiemmin, versiossa 1.49.3, lukemalla Root, Size ja ID suoraan selväkielisestä xref-virran sanakirjasta, aihe, jota käsitellään rinnakkaisartikkelissa objekti- ja ristiviittausvirtojen validointi; objektivirtatyö tarvitsi vain purkuvaiheen lisäämisen, ilman tarvetta purkaa tyypin 2 xref-merkintöjä tai avata PNG-ennustinta

Osatietoinen PDF/A-portitus Delphissä: julistettu pdfaid-osa ruokkii PartNo = 1 -portin, joka mahdollistaa läpinäkyvyyden, valinnaisen sisällön ja upotettujen tiedostojen tarkistukset vain PDF/A-1:lle, kun taas JavaScript, LZW, XFA ja upottamattomat fontit pysyvät kiellettyinä jokaiselle osalle
Osatietoinen portitus lukee pdfaid-osanumeron ja soveltaa läpinäkyvyyden, valinnaisen sisällön ja upotetun tiedoston tarkistuksia vain PDF/A-1:een, pitäen merkintäton tiedostot tiukimpien sääntöjen alaisina

Byte-tason tarkistimen rehelliset rajat

Tämä on preflight-työkalu, ei sertifioitu validaattori, ja rajat ovat todellisia. Fonttien upotus on laskentaan perustuva heuristiikka, ja sen saaminen oikein vaati korjauksen, joka kannattaa tietää. Alkuperäinen tarkistus käytti funktiota PdfCountName('/FontDescriptor'), mutta jokainen fontti tuottaa kaksi /FontDescriptor-tokenia, yhden viittauksen fonttisanakirjasta ja yhden /Type-kentän itse deskriptoriobjektissa, joten määrä oli 2N N:ää upotettua ohjelmaa vastaan, ja testi oli aina tosi. Korjaus on PdfCountDescriptorRefs, joka laskee vain /FontDescriptor N G R -viittausmuodon, yhden per fontti, ja nostaa lipun pvaiUnembeddedFont vain, kun upotettuja ohjelmia on aidosti vähemmän:

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

Vaikka korjattu, se on karkea: sekamuotoinen asiakirja, jossa jokaisella deskriptorilla sattuu olemaan jokin FontFile, voi silti päästää yksittäisen ei-vaatimustenmukaisen fontin läpi. Objektivirtojen laajentamisella on myös tunnettu sivuvaikutus: se paljastaa standard-14-oletusresurssit, joita AcroForm /DR kantaa, kuten /Helv, ja heuristiikka raportoi ne tunnollisesti upottamattomiksi, vaikka veraPDF päästää ne läpi, koska niitä ei koskaan tosiasiassa käytetä renderöintiin. Sisältövirran operaattoritason tarkistukset (6.2.10) jäävät kokonaan soveltamisalan ulkopuolelle, koska ne vaatisivat täyden sisällönjäsennyksen tavuskannauksen sijaan. Kohtele validaattoria nopeana, riippuvuusvapaana ensimmäisenä porttina, joka nappaa ne rikkomukset, joita merkin injektointi ei voi korjata, ja varaa täysi validaattori lopulliseen sertifiointiin

Tämä on tarinan tarkistuspuoli. Täydentävä kirjoituspuoli, jossa SaveAsPdfA injektoi XMP:n, OutputIntentin ja sRGB ICC -profiilin ja alentaa rehellisesti Level A -pyynnön, jolla ei ole tagattua rakennetta, rakentuu samalle byte-tason koneistolle. Molemmat puoliskot toimitetaan PDFium Component for Delphi -komponentissa, yhdessä VCL-paketissa puhtaan Pascal-toteutuksen PDF/A-toiminnallisuuden päällä, ilman ulkoista ajonaikaista ympäristöä asennettavaksi