veraPDF hlášení, že šířka glyfu nesouhlasí s vloženým fontovým programem, vám skoro nic neřekne o tom, který glyf to je, ani proč. PDFlibPas na tu otázku odpovídá tak, že každý znakový kód převádí přes vložený cmap na index glyfu, normalizuje metriku programu na 1000 jednotek na em a porovnává tam
Proč se šířky glyfů neshodují?
Protože obě porovnávaná čísla žijí v různých souřadných systémech a nic ve slovníku PDF vám konverzi neřekne. Fontový slovník zapisuje /Widths v prostoru glyfů, který PDF pevně stanovuje na tisícinu em (ISO 32000-1 §9.2.4). Tabulka hmtx uvnitř vloženého TrueType programu zapisuje posuny v designových jednotkách písma a tabulka head rozhoduje, kolik jich tvoří jedno em: 2048 u většiny TrueType písem, 1000 u písem odvozených od CFF, občas úplně něco jiného. Porovnáte-li syrové hodnoty, vypadá ve vašem korpusu každé písmo s 2048 upem jako rozbité. To je past, kterou ISO 14289-1 §7.21.5 staví komukoli, kdo se pokouší auditovat šířky čtením polí slovníku
PDFlibPas normalizuje hned při načtení. TPDFTrueTypeParser ukládá do pole šířek hodnotu Advance * 1000 div unitsPerEm, takže Parser.GetWidth(GID) už odpovídá ve stejných tisícinách em, jaké používá PDF, a GetRawWidth zůstává k dispozici, když potřebujete designové jednotky zpět. Tím zůstává těžší polovina: dostat se od znakového kódu k indexu glyfu. U jednoduchého TrueType písma závisí cesta na příznaku Symbolic ve FontDescriptor, bitu 3 /Flags
Parser := TPDFTrueTypeParser.Create;
try
Parser.LoadFromString(FontProgram);
if Symbolic then
begin
// Symbolická písma se adresují přímo přes cmap programu,
// s konvencí vysokého bajtu (3,0) jako zálohou
GID := Parser.GetGlyphIndex(Code);
if GID = 0 then
GID := Parser.GetGlyphIndex($F000 + Code);
end
else
begin
// Nesymbolická: kód -> název glyfu přes kódování, název -> Unicode
// přes Adobe Glyph List, Unicode -> GID přes cmap programu
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;
Dva detaily v tomto úryvku mají váhu. Tolerance je jedna jednotka, ne nula, protože normalizace je celočíselné dělení a korektně vyrobený soubor může o jednotku ulétnout; přesně to je formulace „do tisíciny em", kterou hlásí diagnostika 10036. A hlídka GID < Parser.GlyphCount není ozdoba. GetWidth je psáno shovívavě pro volající z renderování: index mimo rozsah zalamuje na poslední položku hmtx a při chybějící tabulce sahá po 750. Shovívavost se hodí při vykreslování a škodí při auditu, takže audit index odmítá dřív, než si o šířku řekne, místo aby se spoléhal na zalamování
CIDFontType2 přidává jedno zprostředkování navíc
Kompozitními písmy prochází PDFlibPas stejně, jen mezi CID a glyf vkládá /CIDToGIDMap. Šířky přicházejí v poli /W, kterému ISO 32000-1 §9.7.4.3 dává dva tvary volně se střídající v jednom poli: počáteční CID následovaný polem po sobě jdoucích šířek, nebo první CID, poslední CID a jediná šířka platná pro celé rozpětí. Audit parsuje obojí, každou vzniklou dvojici předává témuž srovnání a celkové součty hlásí pod diagnostikou 10037. Krok mapování je místo, kde se kompozitní písma liší, a proto je diagnostika chybějící mapy 10021 důležitá dřív, než začtete jakoukoli šířku — chybějící nebo poškozená /CIDToGIDMap neporušuje jen §7.21.3.2, ale činí otázku po šířce nezodpověditelnou
// /CIDToGIDMap je jméno /Identity nebo proud velkobajtových 16bitových
// indexů glyfů, jeden na CID (ISO 32000-1 oddíl 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; // řetězce v Pascalu indexují od 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;
Co má auditor dělat, když se fontový program nedekóduje?
Neříkat nic. Kontroly úplnosti /CharSet a /CIDSet, které vyžaduje ISO 14289-1 §7.21.4.2 — diagnostiky 10038 a 10039 — jsou místo, kde se z horlivého validátoru stává zátěž, protože hlášení „váš CharSet není úplný" rozezná čtenář od „náš dekodér Type 1 to vzdal" jen stěží. Proto PDFlibPas hlásí chybějící položku jen tehdy, když se podaří tři věci najednou: dekóduje se fontový program, rozliší se mapování kódu na glyf a dekóduje se samotná množina. TPDFType1Decoder.LoadPFBFromString musí vrátit True a vynést počet charstringů, než se jakýkoli název glyfu porovná s řetězcem /CharSet; cesta /CIDSet potřebuje, aby se proud rozbalil a počet glyfů vrátil kladný, než se otestuje jediný bit. Jakákoli výjimka na cestě se zredukuje na „bez nálezu", ne na závadu
To je vědomá preference falešných negativů a stojí za to ji vyslovit nahlas, ne zahrabat. Poškozená tabulka CFF, nepodporovaná varianta Type 1 nebo /CIDSet kratší, než je rozsah glyfů, všechny zaniknou v tichu místo v diagnostice. Úvaha je taková: audity PDF/UA putují k autorům, kteří nástroje nestavěli, a křivé obvinění stojí víc než propásnutý nález — autor spálí den tím, že dokazuje, že vyhovující soubor vyhovuje, a přestane věřit celému hlášení. Matterhorn Protocol dělá totéž rozlišení v jiné podobě, když odděluje kontroly, o kterých může rozhodnout stroj, od těch, které musí rozhodnout člověk, a jeho kontrolní bod Fonts (31) je místo, kde tyto kontroly bydlí. Kdyby se vám hodilo přísnější čtení, pusťte PDFlibPas jako rychlou bránu a specializovaný validátor jako druhý posudek — ta dvojice je ta samá, jakou popisuje průchod preflightem PDF/A a PDF/UA
Stránkový /Contents je seznam, ne proud
Nejdražší chyba při auditu proudů obsahu je brát /Contents jako jeden proud. ISO 32000-1 §7.7.3.3 umožňuje stránce držet pole proudů, jejichž spojení, s mezerou mezi částmi, je program stránky; producenti je dělí v libovolných bodech a BT může sedět v jednom členu, zatímco jeho ET v dalším. Procesor obsahu si drží stav — hloubku zanoření označeného obsahu, písmo vybrané posledním Tf, příznak textového objektu — a Process ten stav při vstupu vynuluje. Zavoláte-li ho pro každý člen pole zvlášť, každý proud po prvním startuje bez aktuálního písma, takže dokonale otagovaný text se čte jako neoznačený šum bez písma. PDFlibPas spojí nejdřív a zpracuje jednou
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;
// Jedno volání Process přes celé spojení, nikdy jedno volání na člen
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));
Které Form XObject se skutečně počítají jako nestrukturované?
Jen ty, které stránka skutečně vyvolá, z místa volání mimo označený obsah, jejichž vlastní obsah ukazuje text. Diagnostika 10040 vymáhá ISO 14289-1 §7.20 tím, že si pro každé číslo objektu zapisuje tři nezávislé fakty — má text, byl vyvolán, byl vyvolán uvnitř označeného obsahu — a hlásí jen průnik prvních dvou mínus třetí. Každá z těch dvou zkratek je špatná způsobem, který byste expedovali: označovat každý Form nesoucí text v /Resources trestá šablonovou knihovnu, z níž nikdo nekreslí, a označovat každý vyvolaný Form trestá vektorová loga bez textu, která žádné značkování nepotřebují. Místo vyvolání se rozlišuje podle čísla objektu, nikoli podle názvu zdroje, protože tentýž Form se běžně dostupuje pod různými názvy na různých stránkách. Sesterská diagnostika 10041 projde tímže spojeným programem pro §7.21.8, rozliší každý operand ukazující text přes písmo v rozsahu a počítá kódy, které dopadnou na .notdef, což je zakázané bez ohledu na režim vykreslování textu — včetně neviditelného režimu používaného za naskenovanými obrázky. Jak by přeživší Formy měly být zabaleny, je otázka stromu struktur, kterému se věnuje článek o stavbě tagovaného PDF
Písma bez jakéhokoli FontDescriptor
Nevložené písmo je legitimní vstup tohoto auditu, ne chybový stav, a každý pomocník pod kontrolou vložení ho musí přežít. Když PDFlibPas nenajde /FontDescriptor, nebo najde deskriptor bez FontFile, FontFile2 a FontFile3, zaznamená diagnostiku 10020 — respektive 10022, když jméno patří mezi Standard 14, které §7.21.4 NOTE 5 vyhraněně odmítá prominout — a pak pokračuje zbytkem souboru. V tom je celý smysl hlášení: autor chce všechny nálezy v jednom průchodu, ne jeden nález za běh. Deskriptorová reference, kterou dostávají pomocníci pro šířky, cmap, CharSet a CIDSet, proto může být Nil a každý z nich ji testuje na vstupu, místo aby předpokládal, že dřívější kontrola audit přerušila. Když je opravou vložit to, co chybí, mechanismy popisuje poznámka o vkládání chybějících písem do existujícího PDF
Spuštění auditu
Jedno volání, na souboru, který jste nemuseli vyrábět vy. TPDFlib.CheckFileCompliance přijímá volič testu shody — 2 pro PDF/UA-1 podle ISO 14289-1:2014 — a vrací buď nulu, nebo handle seznamu řetězců, jehož položky jsou číselný kód, dvojtečka a čitelné hlášení. Nálezy o písmech a proudech obsahu zde popsané zabírají v tom rozsahu 10020 až 10041, numericky oddělené od kódů PDF/A 00xxx, aby smíšený log zůstal čitelný. Hodnota 1 v Options zkratuje na prvním nálezu, což se hodí v build bráně spíš než v autorském nástroji. Pro dokument ještě otevřený v paměti provede GetPDFUADiagnostics rovnocennou prohlídku bez objížďky přes disk
var
Issues, Count, I: Integer;
begin
// ComplianceTest = 2 vybírá PDF/UA-1; Options = 0 hlásí každý nález
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)); // např. 10037 CIDFontType2 ...
end;
end;
Nic z toho nepotřebuje na stroji binární externí validátor, a právě v tom je rozdíl mezi kontrolou, která běží při každém buildu, a kontrolou, která běží, až si to někdo vzpomene. Compliance a diagnostická API popsaná zde putují ve standardní PDFlibPas Delphi PDF Library, jejíž produktová stránka nese úplnou tabulku diagnostických kódů pro PDF/UA-1 spolu se sadami testů PDF/A, PDF/X a PDF/E