Tekninen artikkeli

PDF/UA-fonttiauditointi Delphissä: Widths, CharSet, CIDSet

veraPDF-raportti, joka sanoo merkin leveyden eroavan upotetusta fonttiohjelmasta, ei kerro juuri mitään siitä, mikä merkki on kyseessä tai miksi. PDFlibPas vastaa kysymykseen ratkaisemalla jokaisen merkkikoodin upotetun cmapin kautta merkin indeksiin, normalisoimalla ohjelman metriikan 1000 yksikköön per em ja vertaamalla siellä

Miksi merkkien leveydet eroavat?

Koska verrattavat luvut elävät eri koordinaatistoissa, eikä PDF-sanakirja kerro muunnosta. Fonttisanakirja kirjoittaa /Widthsin merkkitilaan, jonka PDF kiinnittää tuhannesosaan emistä (ISO 32000-1 §9.2.4). Upotetun TrueType-ohjelman sisäinen hmtx-taulukko kirjoittaa edunsäät fontin suunnitteluyksiköissä, ja head-taulukko päättää, montako niistä muodostaa emin: 2048 useimmille TrueType-leikkeille, 1000 CFF-peräisille, satunnaisesti jotain aivan muuta. Vertaa raaka-arvoja, ja jokainen korpuksesi 2048-upem-fontti näyttää rikkineen. Se on ansa, jonka ISO 14289-1 §7.21.5 asettaa jokaiselle, joka yrittää auditoida leveyksiä lukemalla sanakirjakenttiä

PDFlibPasin merkkileveysauditointi Delphissä: /Widths-tietue merkkitilassa ja hmtx-edunsää fontin suunnitteluyksiköissä tuodaan samaan koordinaatistoon skaalaamalla ohjelman metriikka 1000 yksikköön per em ennen minkäänlaista vertailua
PDFlibPas skaalaa jokaisen hmtx-edunsään tuhannesosaan emistä ennen kuin vertaa sitä sanakirjan leveysarvoon, ja raportoi vain, mikä on yli yhden yksikön päässä

PDFlibPas normalisoi latauksen yhteydessä. TPDFTrueTypeParser tallentaa Advance * 1000 div unitsPerEmin leveysataulukkoonsa, joten Parser.GetWidth(GID) vastaa jo samoissa tuhannesosissa emistä, joita PDF käyttää, ja GetRawWidth pysyy käytettävissä, kun tarvitset suunnitteluyksiköt takaisin. Se jättää yhä jäljelle vaikeamman puolen: pääsyn merkkikoodista merkki-indeksiin. Yksinkertaiselle TrueType-fontille reitti riippuu FontDescriptorin Symbolic-lipusta, /Flagsin bitistä 3

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Symbolisia leikkeitä osoitetaan suoraan ohjelman cmapin kautta,
    // ja (3,0) high-byte-käytäntö toimii varajäsenenä
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Ei-symbolinen: koodi -> merkin nimi koodauksen kautta, nimi -> Unicode
    // Adobe Glyph Listin kautta, Unicode -> GID ohjelman cmapin kautta
    UnicodeValue := GetGlyphUnicode(EncodingNames[Code and $FF]);
    if UnicodeValue = 0 then
      Continue;
    GID := Parser.GetGlyphIndex(UnicodeValue);
  end;
  if (GID > 0) and (GID < Parser.GlyphCount) then
    if Abs(PDFWidth - Parser.GetWidth(GID)) > 1 then
      Inc(MismatchCount);
finally
  Parser.Free;
end;

Katkelmassa kantavat painoa kaksi yksityiskohtaa. Toleranssi on yksi yksikkö, ei nolla, koska normalisointi on kokonaislukujako ja kelvollisesti tuotettu tiedosto voi hajahtua yksikön verran; juuri sitä tarkoittaa diagnoosin 10036 muotoilu "yhtä tuhannesosaa emistä". Ja GID < Parser.GlyphCount-tarkistus ei ole pelkkää koristetta. GetWidth on kirjoitettu sietäväksi piirtäjäkutsuja varten: se rajaa alueen ulkopuolisen indeksin hmtx-taulukon viimeiseen alkioon ja palauttaa 750, kun taulukko puuttuu. Sietävyys sopii piirtämiseen ja on väärin auditoinnissa, joten auditointi hylkää indeksin ennen kuin kysyy leveyttä, sen sijaan että luottaisi rajaukseen

CIDFontType2 lisää yhden epäsuoran vaiheen

PDFlibPas kulkee yhdistelmäfontit läpi samalla tavalla, kun /CIDToGIDMap on lisätty CID:n ja merkin väliin. Leveydet saapuvat /W-taulukossa, jolla ISO 32000-1 §9.7.4.3 määrittää kaksi muotoa, jotka vuorottelevat vapaasti samassa taulukossa: aloitus-CID, jota seuraa peräkkäisten leveyksien taulukko, tai ensimmäinen CID, viimeinen CID ja yksi leveys, jota sovelletaan koko jaksoon. Auditointi jäsentää molemmat, antaa jokaisen syntyneen parin samalle vertailulle ja raportoi summan diagnoosissa 10037. Kartoitusvaihe on kohta, jossa yhdistelmäfontit eroavat toisistaan, ja siksi puuttuvan kartan diagnoosilla 10021 on väliä ennen kuin luet yhtään leveyttä — puuttuva tai epäkelpo /CIDToGIDMap ei pelkästään loukkaa kohtaa §7.21.3.2 vaan tekee leveyskysymyksestä vastaamattoman

Kolme reittiä, joilla PDFlibPas päättyy merkkitunnuksesta merkki-indeksiin Delphissä: ohjelman cmap symbolisille TrueType-fonteille, koodauksen ja Adobe Glyph Listin kierto ei-symbolisille fonteille sekä CMap- ja /CIDToGIDMap-vaihe CIDFontType2:lle
Leveysvertailu ei voi alkaa ennen kuin merkkitunnus on ratkennut merkki-indeksiksi, ja jokainen fonttityyppi saapuu indeksiin omalla reitillään
// /CIDToGIDMap on joko nimi /Identity tai big-endian-koodattu 16-bittisten
// merkin indeksien tietovirta, yksi per CID (ISO 32000-1 kohta 9.7.4.2)
Obj := DerefIndRef(FDoc, CIDFont.FindValueByKeyName('CIDToGIDMap'));
if (Obj is TPDFName) and (TPDFName(Obj).Name = 'Identity') then
begin
  GID := CID;
  Result := True;
end
else if Obj is TPDFStream then
begin
  Data := TPDFStream(Obj).GetDecodedStream;
  P := CID * 2 + 1;                       // Pascal-merkkijonot alkavat indeksistä 1
  if (P >= 1) and (P + 1 <= Length(Data)) then
  begin
    GID := (Integer(Byte(Data[P])) shl 8) or Integer(Byte(Data[P + 1]));
    Result := True;
  end;
end;

Mitä auditorin pitää tehdä, kun fonttiohjelma ei purkaudu?

Älä sano mitään. ISO 14289-1 §7.21.4.2 vaatimat /CharSet- ja /CIDSet-kattavuustarkistukset — diagnoosit 10038 ja 10039 — ovat paikka, jossa liian innokas validoija muuttuu ongelmaksi, koska lukijalle raportti "CharSet on puutteellinen" on erottamaton lauseesta "meidän Type 1 -purkumme luovutti". Siksi PDFlibPas raportoi puuttuvan tietueen vain, kun kolme asiaa onnistuu: fonttiohjelma purkautuu, koodista merkkiin -kartoitus ratkeaa ja joukko itsessään purkautuu. TPDFType1Decoder.LoadPFBFromString on palautettava True ja tuotettava charstringien lukumäärä ennen kuin yhtään merkin nimeä verrataan /CharSet-merkkijonoon; /CIDSet-polku vaatii, että tietovirta purkautuu ja merkkien lukumäärä palaa positiivisena ennen yhdenkään bitin testaamista. Mikä tahansa matkan varrella osuva poikkeus kutistuu tulokseksi "ei havaintoa", ei viaksi

PDFlibPasin PDF/UA-auditoinnin varovainen raportointisääntö: puuttuva /CharSet- tai /CIDSet-tietue raportoidaan vain, kun fonttiohjelma purkautuu, koodista merkkiin -kartoitus ratkeaa ja joukko itsessään purkautuu, ja mikä tahansa epäonnistuminen tuottaa hiljaisuutta
Kolme riippumatonta onnistumista vaaditaan, ennen kuin puuttuvan tietueen havainto annetaan, joten luovuttava purkaja tuottaa sinulle vääriä negatiivisia tuloksia eikä vääriä syytöksiä

Tuo on tahallinen kallistuminen vääriin negatiivisiin tuloksiin, ja sen kannattaa sanoa suoraan eikä haudata sitä. Rikkinäinen CFF-taulukko, tukematon Type 1 -muunnos tai merkkialuetta lyhyempi /CIDSet tuottavat kaikki hiljaisuutta diagnoosin sijaan. Perustelu kuuluu: PDF/UA-auditoinnit välitetään kirjoittajille, jotka eivät rakentaneet työkaluja, ja väärä syytös maksaa enemmän kuin ohitettu havainto. Kirjoittaja polttaa päivän todistaakseen, että kelvollinen tiedosto on kelvollinen, ja lopettaa luottamasta koko raporttiin. Matterhorn Protocol tekee saman eron toisessa muodossa erotellessään tarkistukset, jotka kone voi ratkaista, ja tarkistukset, jotka jäävät ihmiselle, ja sen Fonts-tarkistuskohta (31) on paikka, jossa nämä asuvat. Jos tarvitset tiukemman tulkinnan, aja PDFlibPas nopeana porttina ja erillinen validoija toisena mielipiteenä — sama paritus on kuvattuna artikkelissa PDF/A- ja PDF/UA-esitarkistuskierros

Sivun /Contents on lista, ei tietovirta

Kallein yksittäinen virhe sisältövirtojen auditoinnissa on käsitellä /Contents yhtenä tietovirtana. ISO 32000-1 §7.7.3.3 sallii sivun pitää taulukkoa tietovirtoja, joiden yhdistetty kokonaisuus osien välisine välilyönteineen on sivun ohjelma; tuottajat jakavat sisällön mielivaltaisista kohdista, ja BT voi sijaita yhdessä jäsenessä, kun taas sen pari ET istuu seuraavassa. Sisällönkäsittelijä ylläpitää tilaa — merkityn sisällön pesityssyvyyttä, viimeisen Tf-kutsun valitsemaa fonttia, tekstiobjektilippua — ja Process nollaa tilan alussa. Kutsu sitä kerran per taulukon jäsen, ja jokainen ensimmäisen jälkeinen tietovirta alkaa ilman voimassa olevaa fonttia, joten täydellisesti tagitettu teksti luetaan tagittomana ja fontittomana kohinana. PDFlibPas yhdistää ensin ja käsittelee kerran

function ContentObjectData(FDoc: TSmartPDFDocument; Obj: TPDFObject): AnsiString;
var
  I: Integer;
begin
  Result := '';
  Obj := DerefIndRef(FDoc, Obj);
  if Obj is TPDFStream then
    Result := TPDFStream(Obj).GetDecodedStream
  else if Obj is TPDFArray then
    for I := 0 to TPDFArray(Obj).Count - 1 do
      Result := Result + ContentObjectData(FDoc, TPDFArray(Obj).Item[I]) + #10;
end;

// Yksi Process-kutsu koko yhdisteelle, ei koskaan yhtä kutsua per jäsen
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Mitkä Form XObjectit oikeasti lasketaan rakenteettomiksi?

Vain ne, jotka sivu oikeasti kutsuu kutsupaikasta, joka on merkityn sisällön ulkopuolella, ja joiden oma sisältö näyttää tekstiä. Diagnoosi 10040 panee täytäntöön ISO 14289-1 §7.20:n tallentamalla kolme riippumatonta tosiasiaa objektinumeroa kohden — sisältää tekstiä, kutsuttiin, kutsuttiin merkityn sisällön sisällä — ja raportoi vain kahden ensimmäisen leikkauksen vähennettynä kolmannella. Kummankin oikotien virhe on sellainen, jonka voisit viedä tuotantoon: kaikkien /Resources-tietueen tekstiä kantavien Formien liputtaminen rankaisee mallikirjastoa, josta kukaan ei piirrä mitään, ja kaikkien kutsuttujen Formien liputtaminen rankaisee vektorilogoja, jotka eivät kanna tekstiä eivätkä tarvitse tagitusta. Kutsupaikka ratkaistaan objektinumerolla eikä resurssin nimellä, sillä sama Form tavoitetaan tavallisesti eri nimillä eri sivuilla. Rinnakkainen diagnoosi 10041 kulkee saman yhdistetyn ohjelman läpi kohdan §7.21.8 takia, ratkaisee jokaisen tekstiä näyttävän operandin vaikutuspiirin fontin kautta ja laskee koodit, jotka osuvat .notdef-merkkiin, mikä on kielletty riippumatta tekstin piirtotavasta — mukaan lukien skannattujen kuvien takana käytetty näkymätön tila. Miten jäljelle jäävät Formit pitää kääriä, on rakennepuun kysymys, jonka käsittelee artikkeli tagged PDF -rakenteen rakentaminen

Fontit, joissa ei ole FontDescriptoria lainkaan

Upottamaton fontti on tämän auditoinnin laillinen syöte, ei virhetila, ja jokaisen upotustarkistuksen alapuolella olevan apurin on selviydyttävä siitä. Kun PDFlibPas ei löydä /FontDescriptoria, tai löytää deskriptorin, jossa ei ole FontFile-, FontFile2- eikä FontFile3-kenttää, se kirjaa diagnoosin 10020 — tai 10022, kun nimi kuuluu Standard 14 -joukkoon, jota §7.21.4 HUOMAUTUS 5 määrätietoisesti kieltäytyy vapauttamasta — ja jatkaa sitten tiedoston loppuun. Juuri tuo on raportin tarkoitus: kirjoittaja haluaa kaikki havainnot yhdellä ajolla, eikä yhtä havaintoa per ajo. Siksi leveys-, cmap-, CharSet- ja CIDSet-apureille annettava deskriptoriviite voi olla Nil, ja kukin niistä testaa asian alussa sen sijaan, että olettaisi aiemman tarkistuksen keskeyttäneen auditoinnin. Jos korjauksena on puuttuvan upottaminen, tekniikka on kuvattu muistiinpanossa puuttuvien fonttien upottaminen olemassa olevaan PDF-tiedostoon

Auditoinnin ajaminen

Yksi kutsu, tiedostoon, jota et välttämättä ole itse tuottanut. TPDFlib.CheckFileCompliance ottaa vastaan vaatimustestivalitsimen — 2 valitsee PDF/UA-1:n ISO 14289-1:2014:n alla — ja palauttaa joko nollan tai merkkijonolistan kahvan, jonka alkiot muodostuvat numeerisesta koodista, kaksoispisteestä ja luettavasta viestistä. Täällä käsitellyt fontti- ja sisältövirtahavainnot sijoittuvat alueelle 10020–10041, numeerisesti erilleen PDF/A:n 00xxx-koodeista, jotta sekaisin kirjoitettu loki pysyy luettavana. Arvon 1 antaminen Options-parametrissa pysäyttää suorituksen ensimmäiseen havaintoon, mikä on toivottavaa koontiportissa eikä niinkään kirjoitustyökalussa. Muistissa edelleen olevalle dokumentille GetPDFUADiagnostics tekee vastaavan tarkastuksen ilman kierrosta levyn kautta

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 valitsee PDF/UA-1:n; Options = 0 raportoi kaikki havainnot
  Issues := PDF.CheckFileCompliance('delivery.pdf', '', 2, 0);
  if Issues = 0 then
    WriteLn('delivery.pdf: PDF/UA-1 conformant')
  else
  begin
    Count := PDF.GetStringListCount(Issues);
    for I := 1 to Count do
      WriteLn('  ', PDF.GetStringListItem(Issues, I));   // esim. 10037 CIDFontType2 ...
  end;
end;

Mikään tästä ei vaadi ulkoista validoijabinaaria koneelle, ja juuri siinä on ero tarkistuksen, joka ajetaan jokaisessa koontiversiossa, ja tarkistuksen, joka ajetaan kun joku muistaa. Täällä kuvatut vaatimustenmukaisuus- ja diagnoosi-API:t kuuluvat vakiopakettiin PDFlibPas Delphi PDF Library, jonka tuotesivulla on koko PDF/UA-1-diagnoosikooditaulu PDF/A-, PDF/X- ja PDF/E-testisarjojen rinnalla