Műszaki cikk

PDF/UA font-audit Delphiben: szélesség, CharSet, CIDSet

Egy veraPDF jelentés, amely azt mondja, egy glyph szélessége nem egyezik a beágyazott betűtípusprogrammal, szinte semmit nem árul el arról, melyik glyphről van szó, és miért. A PDFlibPas úgy válaszol erre a kérdésre, hogy minden karakterkódot a beágyazott cmap útján glyph indexre old fel, a programmetrikát 1000 egységre normalizálja em-enként, és ott veti össze

Miért térnek el a glyph szélességek?

Mert a két összehasonlított szám különböző koordinátarendszerekben él, és semmi a PDF szótárban nem árulja el az átváltást. A betűtípus-szótár a /Widths-et glyph térben írja, amelyet a PDF az em ezredrészére rögzít (ISO 32000-1 §9.2.4). A beágyazott TrueType program hmtx táblája az előtolásokat font tervezőegységekben írja, a head tábla pedig eldönti, hány tesz ki egy em-et: 2048 a legtöbb TrueType változathoz, 1000 a CFF-ből származókhoz, olykor valami teljesen más. Hasonlítsa össze a nyers értékeket, és a korpusz minden 2048-upem betűtípusa sérültnek tűnik. Ez az a csapda, amelyet az ISO 14289-1 §7.21.5 mindenki számára állít, aki szótármezők olvasásával akarja auditálni a szélességeket

PDFlibPas glyph szélesség-audit Delphiben: egy /Widths bejegyzés a glyph térben és egy hmtx előtolás font tervezőegységekben úgy kerül ugyanarra a koordinátarendszerre, hogy a programmetrikát 1000 egységre skálázzák em-enként, még bármilyen összehasonlítás előtt
A PDFlibPas minden hmtx előtolást az em ezredrészére skáláz, mielőtt a szótár szélességével vetné össze, és csak azt jelenti, amely egynél több egységgel tér el

A PDFlibPas betöltéskor normalizál. A TPDFTrueTypeParser a Advance * 1000 div unitsPerEm-et tárolja a szélességtömbjében, így a Parser.GetWidth(GID) már ugyanabban az em-ezredrészben válaszol, amelyet a PDF használ, a GetRawWidth pedig elérhető marad, amikor tervezőegységekre van szüksége. Ez így is hagyja a nehezebb felét: az eljutást a karakterkódtól a glyph indexig. Egy egyszerű TrueType betűtípusnál az útvonal a FontDescriptor Symbolic jelzőjétől függ, a /Flags 3. bitjétől

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // A szimbolikus változatok közvetlenül a program cmap-én keresztül címezhetők,
    // a (3,0) magas bájt konvencióval tartalékként
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Nem szimbolikus: kód -> glyph név a kódolás útján, név -> Unicode
    // az Adobe Glyph List útján, Unicode -> GID a program cmap-je útján
    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;

Két részlet hordoz súlyt ebben a töredékben. A tűrés egy egység, nem nulla, mert a normalizálás egész osztás, és egy szabályosan előállított fájl eltérhet eggyel; ez pontosan az az em ezredrészén belüli megfogalmazás, amelyet a 10036-os diagnosztika jelent. És a GID < Parser.GlyphCount őr nem dísz. A GetWidth megbocsátóra van megírva a renderelést végző hívók számára, tartományon kívüli indexet a hmtx utolsó bejegyzésére szorít, és 750-re esik vissza, ha a tábla hiányzik. A megbocsátás jó a renderelésnek, rossz az auditnak, ezért az audit az indexet utasítja el, mielőtt szélességet kérne, ahelyett hogy a szorításra bízna

A CIDFontType2 egy további köztes lépést ad

A PDFlibPas ugyanígy járja be az összetett betűtípusokat, a /CIDToGIDMap-pel a CID és a glyph közé ékelve. A szélességek a /W tömbben érkeznek, amelyet az ISO 32000-1 §9.7.4.3 két alakban ad, és amelyek szabadon váltakoznak egy tömbben: egy kezdő CID, utána egymást követő szélességek tömbje, vagy egy első CID, egy utolsó CID, és egyetlen szélesség, amely a futamon át érvényes. Az audit mindkettőt értelmezi, majd minden keletkező párt ugyanahhoz az összehasonlításhoz ad, és az összesen a 10037-es diagnosztika alatt jelenik meg. A leképezési lépés az, ahol az összetett betűtípusok különböznek, és ezért fontos a hiányzó-térkép 10021-es diagnosztikája, mielőtt egyáltalán bármilyen szélességet olvasna — egy hiányzó vagy hibás /CIDToGIDMap nemcsak a §7.21.3.2-t sérti, hanem a szélesség kérdését megválaszolhatatlanná teszi

Három útvonal, amelyet a PDFlibPas a karakterkódtól a glyph indexig bejár Delphiben: a program cmap a szimbolikus TrueType betűtípusokhoz, egy kódolás és Adobe Glyph List kerülő a nem szimbolikusokhoz, valamint egy CMap plusz /CIDToGIDMap lépés a CIDFontType2-hez
A szélesség-összehasonlítás csak akkor kezdődhet, ha a karakterkód glyph indexre oldódik fel, és minden betűtípustípus más útvonalon jut el ahhoz az indexhez
// A /CIDToGIDMap a /Identity név vagy big-endian 16 bites glyph indexek streamje,
// CID-enként egy (ISO 32000-1 9.7.4.2 fejezet)
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;                       // a Pascal sztringek 1-alapúak
  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 tegyen az auditor, ha a betűtípusprogram nem dekódolható?

Ne mondjon semmit. A /CharSet és a /CIDSet teljesség-ellenőrzései, amelyeket az ISO 14289-1 §7.21.4.2 előír — a 10038-as és 10039-es diagnosztikák — azok a helyek, ahol a túl buzgó validátor terhessé válik, mert egy a CharSet-je hiányos jelentés az olvasó szemében megkülönböztethetetlen attól, hogy a Type 1 dekóderünk feladta. Ezért a PDFlibPas csak akkor jelent hiányzó bejegyzést, ha három dolog mind sikerül: a betűtípusprogram dekódol, a kód-glyph leképezés feloldódik, és maga a halmaz is dekódol. A TPDFType1Decoder.LoadPFBFromString-nek True-t kell visszaadnia és charstring számot kell adnia, mielőtt bármely glyph nevét a /CharSet sztringgel egybevetnék; a /CIDSet útvonalnak az streamet kicsomagoló és pozitív glyph számot visszaadó állapotra van szüksége, mielőtt egyetlen bitet tesztelne. Bármely kivétel az út során nincs-leletre omlik össze, nem hibára

A konzervatív jelentési szabály a PDFlibPas PDF/UA auditban: hiányzó /CharSet vagy /CIDSet bejegyzés csak akkor kerül jelentésre, ha a betűtípusprogram dekódol, a kód-glyph leképezés feloldódik és maga a halmaz is dekódol, bármely hiba pedig csendet eredményez
Három független siker kell, mielőtt hiányzó-bejegyzés lelet kerül kiadásra, így egy feladó dekóder hamis negatívat ok Önnek, nem hamis vádat

Ez szándékos torzítás a hamis negatívok felé, és érdemes egyszerűen kimondani, nem eltemetni. Egy sérült CFF tábla, egy nem támogatott Type 1 változat vagy egy a glyph tartománynál rövidebb /CIDSet mind csendet termel diagnosztika helyett. A gondolatmenet az, hogy a PDF/UA auditokat olyan szerzőknek továbbítják, akik nem építették az eszközt, és egy hamis vád többe kerül, mint egy kihagyott: a szerző napot éget el azzal, hogy bebizonyítja, egy megfelelő fájl megfelelő, majd felhagy a teljes jelentésbe vetett bizalommal. A Matterhorn Protocol ugyanezt a különbséget teszi más alakban, amikor szétválasztja azokat az ellenőrzéseket, amelyeket gép eldönthet, azoktól, amelyeket embernek kell, és ennek Fonts ellenőrzőpontja (31) az, ahol ezek élnek. Ha a szigorúbb olvasatra van szüksége, futtassa a PDFlibPas-t gyors kapuként, és dedikált validátort másodvéleményként — ez a párosítás ugyanaz, amelyet a PDF/A és PDF/UA preflight átvonulás ír le

Az oldal /Contents-je lista, nem stream

A legdrágább hiba a tartalomstream-auditban az, hogy a /Contents-t egyetlen streamként kezelik. Az ISO 32000-1 §7.7.3.3 engedi, hogy egy oldal streamek tömbjét hordozza, amelyek összefűzése, a részek közt whitespace-szel, az oldal programja; a termelők tetszőleges ponton darabolnak, és egy BT ülhet egy tagban, a hozzá illő ET a következőben. A tartalomfeldolgozó állapotot tart — a marked-content beágyazási mélységet, az utolsó Tf által kiválasztott betűtípust, a szövegobjektum jelzőt — és a Process belépéskor visszaállítja azt az állapotot. Hívja tömbtagenként egyszer, és minden stream az első után aktuális betűtípus nélkül indul, így a tökéletesen jelölt szöveg jelöletlen, betűtípus nélküli zajként olvasódik. A PDFlibPas előbb fűz össze, majd egyszer dolgoz fel

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;

// Egyetlen Process hívás a teljes összefűzésen, sosem tagenként egy hívás
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Melyek azok a Form XObjectek, amelyek ténylegesen struktúrálatlannak számítanak?

Csak azok, amelyeket egy oldal valóban meghív, marked-contenten kívüli hívási helyről, és amelyek saját tartalma szöveget mutat. A 10040-es diagnosztika az ISO 14289-1 §7.20-t lépteti érvénybe úgy, hogy objektumszámonként három független tényt rögzít — van szöveg, meg volt hívva, marked-contenten belül lett meghívva — és csak az első kettő metszetét jelenti a harmadik nélkül. A két rövidítés mindegyike úgy rossz, ahogy ki szállítaná: minden szöveget hordozó Form megjelölése a /Resources-ben megbüntet egy sablonkönyvtárat, amelyből senki nem merít, és minden meghívott Form megjelölése megbünteti azokat a vektoros logókat, amelyek nem hordoznak szöveget és nem is szorulnak jelölésre. A hívási hely objektumszám alapján oldódik fel, nem erőforrásnév alapján, mivel ugyanaz a Form szokásosan különböző neveken érhető el különböző oldalakon. A kísérő 10041-es diagnosztika ugyanazt az összefűzött programot járja be a §7.21.8 számára, minden szöveget mutató operandust a hatályban lévő betűtípus útján old fel, és megszámolja azokat a kódokat, amelyek a .notdef-re esnek, ami a szövegmegjelenítési módtól függetlenül tiltott — ideértve a szkennelt képek mögött használt láthatatlan módot is. Hogy a megmaradt Formeket hogyan kell becsomagolni, az szerkezetfa-kérdés, amelyet a tagged PDF szerkezet építéséről szóló cikk fed le

Betűtípusok, amelyeknek nincs FontDescriptor-je

A nem beágyazott betűtípus ennek az auditnak szabályos bemenet, nem hibaállapot, és a beágyazás-ellenőrzés alatti minden segédnek túlélnie kell. Amikor a PDFlibPas nem talál /FontDescriptor-t, vagy olyan leírót, amelynek nincs FontFile, FontFile2 vagy FontFile3, rögzíti a 10020-as diagnosztikát — vagy a 10022-est, ha a név a Standard 14 egyike, amelytől a §7.21.4 NOTE 5 nyilvánvalóan nem mentesít — majd folytatja a fájl többi részén. Ez a jelentés lényege: a szerző minden leletet egy menetben akar, nem egy leletet futásonként. Így a szélesség-, cmap-, CharSet- és CIDSet-segédnek átadott leíróhivatkozás Nil lehet, és mindegyik belépéskor erre tesztel, ahelyett hogy feltételezné, egy korábbi ellenőrzés megszakította az auditot. Ha a javítás a hiányzó beágyazása, ennek mechanikája a hiányzó betűtípusok beágyazása meglévő PDF-be jegyzetben van

Az audit futtatása

Egy hívás, olyan fájlon, amelyet nem feltétlenül Ön állított elő. A TPDFlib.CheckFileCompliance megfelelőségi tesztválasztót vesz át — 2 a PDF/UA-1-hez az ISO 14289-1:2014 alatt — és vagy nullát, vagy sztringlista-leírót ad vissza, amelynek bejegyzései egy numerikus kód, egy kettőspont és egy olvasható üzenet. Az itt tárgyalt betűtípus- és tartalomstream-leletek a 10020-tól 10041-ig foglalják el azt a tartományt, numerikusan elkülönítve a 00xxx PDF/A kódoktól, hogy a vegyes napló olvasható maradjon. Az Options-ban átadott 1 első leletnél rövidzárja a futást, ami build-kapuban az, amit akar, szerkesztőeszközben nem. A memóriában még nyitott dokumentumhoz a GetPDFUADiagnostics azonos vizsgálatot futtat, lemezre ugrás nélkül

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 a PDF/UA-1-et választja; Options = 0 minden leletet jelent
  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));   // pl. 10037 CIDFontType2 ...
  end;
end;

Mindehhez nincs szükség külső validátor binárisra a gépen, és ez a különbség aközött, hogy egy ellenőrzés minden builden fut, és aközött, hogy akkor fut, amikor valaki eszébe jut. Az itt ismertetett megfelelőségi és diagnosztikai API-k a standard PDFlibPas Delphi PDF Library-ben szállítanak, amelynek termékoldala a PDF/UA-1 teljes diagnosztikakód-tábláját hordozza a PDF/A, PDF/X és PDF/E tesztkészletek mellett