Техническа статия

Шрифтов одит на PDF/UA в Delphi: Widths, CharSet, CIDSet

Доклад на veraPDF, казващ, че ширина на глиф не съгласува с вградената шрифтова програма, не ти казва почти нищо кой глиф е или защо. PDFlibPas отговаря на въпроса, като разреши всеки код на знак през вградения cmap до индекс на глиф, нормализира метриката на програмата към 1000 единици на em и сравнява там

Защо ширините на глифовете не съгласуват?

Защото двете сравнявани числа живеят в различни координатни системи, а нищо в PDF речника не ти казва преобразуването. Шрифтов речник пише /Widths в глифово пространство, което PDF фиксира на една хилядна от em (ISO 32000-1 §9.2.4). Таблицата hmtx вътре във вградената TrueType програма пише напредванията в дизайн единици на шрифта, а таблицата head решава колко от тях правят em: 2048 за повечето TrueType лица, 1000 за произтичащите от CFF, от време на време нещо съвсем друго. Сравнете суровите стойности и всеки 2048-upem шрифт в корпуса ви изглежда повреден. Това е капанът, който ISO 14289-1 §7.21.5 слага на всеки, който се опита да одитва ширини, четейки полета на речника

Одит на ширина на глиф в PDFlibPas за Delphi: запис /Widths в глифово пространство и напредване hmtx в дизайн единици на шрифта са докарани в една координатна система чрез мащабиране на метриката на програмата към 1000 единици на em, преди да се направи сравнение
PDFlibPas мащабира всяко hmtx напредване към една хилядна от em, преди да го сравни с ширината на речника, и докладва само разликите от повече от една единица

PDFlibPas нормализира при зареждане. TPDFTrueTypeParser пази Advance * 1000 div unitsPerEm в своя масив с ширини, така че Parser.GetWidth(GID) вече отговаря в същите хилядни от em, които PDF използва, а GetRawWidth остава достъпен, когато ви трябват обратно дизайн единиците. Остава по-трудната половина: да стигнеш от код на знак до индекс на глиф. За прост TrueType шрифт маршрутът зависи от флага Symbolic в FontDescriptor, бит 3 на /Flags

Parser := TPDFTrueTypeParser.Create;
try
  Parser.LoadFromString(FontProgram);
  if Symbolic then
  begin
    // Символните лица се адресират направо през програмния cmap,
    // с (3,0) конвенцията за високия байт като резервен вариант
    GID := Parser.GetGlyphIndex(Code);
    if GID = 0 then
      GID := Parser.GetGlyphIndex($F000 + Code);
  end
  else
  begin
    // Несимволен: код -> име на глиф през кодировката, име -> Unicode
    // през Adobe Glyph List, Unicode -> GID през програмния cmap
    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;

Две подробности в този фрагмент носят тежест. Толерансът е една единица, не нула, защото нормализацията е целочислено деление и легитимно произведен файл може да приземи с единица встрани; това е точно формулировката „в рамките на една хилядна от em“, която диагностиката 10036 докладва. А предпазата GID < Parser.GlyphCount не е украса. GetWidth е писана да бъде снизходителна към извикващи за изобразяване, затяга индекс извън диапазона към последния запис в hmtx и пада обратно на 750, когато таблицата липсва. Снизходителността е правилна за изобразяване и грешна за одит, така че одитът отхвърля индекса, преди да поиска ширина, вместо да вярва на затягането

CIDFontType2 добавя още едно посредничество

PDFlibPas обхожда съставните шрифтове по същия начин, с вмъкнат /CIDToGIDMap между CID и глифа. Ширините идват в масива /W, на който ISO 32000-1 §9.7.4.3 дава две форми, свободно редуващи се в един масив: начален CID последван от масив с последователни ширини, или първи CID, последен CID и единична ширина, приложена върху цялата редица. Одитът разчита и двете, после подава всяка получена двойка на същото сравнение и докладва сбора под диагностика 10037. Стъпката на съпоставяне е мястото, където съставните шрифтове се различават, и затова липсващата карта, диагностика 10021, има значение, преди да сте прочели изобщо някоя ширина — липсващ или повреден /CIDToGIDMap не просто нарушава §7.21.3.2, той прави въпроса за ширината неотговорим

Три маршрута, по които PDFlibPas стига от код на знак до индекс на глиф в Delphi: програмният cmap за символни TrueType шрифтове, обход през кодировка и Adobe Glyph List за несимволните и стъпка CMap плюс /CIDToGIDMap за CIDFontType2
Сравнението на ширини не може да започне, докато кодът на знак не се разреши до индекс на глиф, а всеки вид шрифт стига този индекс по различен маршрут
// /CIDToGIDMap е името /Identity или поток от 16-битови глифови индекси
// в big-endian, по един на CID (ISO 32000-1, раздел 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 низовете се броят от 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;

Какво да направи одиторът, когато шрифтата програма не се декодира?

Да каже нищо. Проверките за пълнота на /CharSet и /CIDSet, които ISO 14289-1 §7.21.4.2 изисква — диагностики 10038 и 10039 — са мястото, където един прекалено ревностен валидатор се превръща в отговорност, защото доклад „вашият CharSet е непълен“ е неотличим, за човека, който го чете, от „нашият Type 1 декодер се предаде“. Затова PDFlibPas докладва липсващ запис само когато три неща успеят едновременно: шрифтата програма се декодира, съпоставянето код-към-глиф се разрешава и самият набор се декодира. TPDFType1Decoder.LoadPFBFromString трябва да върне True и да даде брой charstring, преди някое име на глиф да се провери срещу низа /CharSet; пътят /CIDSet иска потокът да се надуе и броят глифове да се върне положителен, преди единичен бит да бъде изпробван. Всяко изключение по пътя се срива до „без находка“, не до дефект

Консервативното правило за докладване в PDF/UA одита на PDFlibPas: липсващ запис /CharSet или /CIDSet се докладва само когато шрифтата програма се декодира, съпоставянето код-към-глиф се разрешава и самият набор се декодира, а всеки провал ражда тишина
Три независими успеха се изискват, преди находка за липсващ запис да бъде издадена, така че декодер, който се предаде, ви струва фалшиво отрицателно, а не фалшиво обвинение

Това е умишлено пристрастие към фалшиво отрицателните и си заслужава да се каже направо, вместо да се заравя. Повредена CFF таблица, неподдържан вариант на Type 1 или /CIDSet, по-къс от глифния диапазон, всички раждат тишина вместо диагностика. Разсъдъкът е, че PDF/UA одитите се препращат към автори, които не са строили инструментария, а фалшиво обвинение струва повече от пропуснато: авторът гори един ден да доказва, че съответстващият файл съответства, и спира да вярва на целия доклад. Протоколът Matterhorn прави същото разграничение в друга форма, като разделя проверките, които машина може да реши, от тези, които човек трябва, а неговата контролна точка Fonts (31) е където те живеят. Ако ви трябва по-строгото четене, пускайте PDFlibPas като бърза порта и отдаден валидатор като второ мнение — тази двойка е същата, описана в прегледа на preflight за PDF/A и PDF/UA

Станичният /Contents е списък, не поток

Най-скъпата единична грешка в одита на съдържателни потоци е третирането на /Contents като един поток. ISO 32000-1 §7.7.3.3 позволява страница да държи масив от потоци, чието съединение, с празни знаци между частите, е станичната програма; производителите режат на произволни места и BT може да седи в един член с неговият съответен ET в следващия. Процесор на съдържание пази състояние — дълбочината на влагане на маркирано съдържание, шрифтът, избран от последния Tf, флагът за текстов обект — а Process нулира това състояние при вход. Повикайте го по веднъж на член на масива и всеки поток след първия започва без текущ шрифт, така че текст, белязан съвсем правилно, се чете като небелязан, безшрифтов шум. PDFlibPas първо съединява и после обработва веднъж

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;

// Едно повикване Process върху цялото съединение, никога по едно на член
Scanner.Process(ContentObjectData(FDoc, PageDict.FindValueByKeyName('Contents')));

Кои Form XObjects действително броим като неструктурирани?

Само тези, които страница действително извиква, от място на повикване извън маркирано съдържание и чието собствено съдържание показва текст. Диагностика 10040 налага ISO 14289-1 §7.20, записвайки три независими факта на номер на обект — има текст, бил е извикан, бил е извикан вътре в маркирано съдържание — и докладва само пресечната точка на първите две минус третото. Всеки от двата къси пътя е грешен по начин, който бихте издали: бележенето на всеки текстоносен Form в /Resources наказва библиотека от шаблони, от която никой не чертае, а бележенето на всеки извикан Form наказва векторни логоа, които не носят текст и не се нуждаят от бележене. Мястото на повикването се разрешава по номер на обект, а не по име на ресурс, тъй като същият Form обикновено се достига чрез различни имена на различни страници. Съпътстващата диагностика 10041 обхожда същата съединена програма за §7.21.8, разрешавайки всеки операнд, показващ текст, през шрифта в обхват и броейки кодовете, паднали на .notdef, което е забранено независимо от режима на изобразяване на текста — включително невидимият режим, използван зад сканирани изображения. Как оцелелите Form трябва да бъдат обвити е въпрос на дърво от структура, покрит в статията за изграждане на бележена PDF структура

Шрифтове изобщо без FontDescriptor

Невграден шрифт е легитимен вход за този одит, а не състояние на грешка, и всеки помощник под проверката за вграждане трябва да го оцелее. Когато PDFlibPas не намери /FontDescriptor или дескриптор без FontFile, FontFile2 или FontFile3, той записва диагностика 10020 — или 10022, когато името е от Standard 14, от които §7.21.4 NOTE 5 изрично отказва да отстъпи — и после продължава през останалата част от файла. Това е целият смисъл на доклад: автор иска всяка находка в един проход, а не по една находка на пробег. Така референцията на дескриптора, подадена на помощниците за ширина, cmap, CharSet и CIDSet, може да е Nil, и всеки от тях го изпробва на вход, вместо да предполага, че по-ранна проверка е прекъснала одита. Ако поправката е да вградиш липсващото, механиката е в бележката за вграждане на липсващи шрифтове в съществуващ PDF

Пускане на одита

Едно повикване, върху файл, който не непременно сте произвели вие. TPDFlib.CheckFileCompliance приема селектор на тест за съответствие — 2 за PDF/UA-1 под ISO 14289-1:2014 — и връща или нула, или дръжка към списък от низове, чиито записи са числов код, двоеточие и четимо съобщение. Шрифтовите и съдържателно-потоковите находки, обсъдени тук, заемат 10020 до 10041 в този диапазон, държани числово отделно от кодовете 00xxx на PDF/A, така че смесен лог остава четим. Подаване на 1 в Options прекъсва на първата находка, което искате в порта на строене, а не в инструмент за авторство. За документ все още отворен в паметта GetPDFUADiagnostics пуска еквивалентната проверка без обиколка през диска

var
  Issues, Count, I: Integer;
begin
  // ComplianceTest = 2 избира PDF/UA-1; Options = 0 докладва всяка находка
  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));   // напр. 10037 CIDFontType2 ...
  end;
end;

Нищо от това не се нуждае от външен валидаторен бинар на машината, което е разликата между проверка, вървяща на всяко строене, и проверка, вървяща когато някой си спомни. API за съответствие и диагностика, описани тук, идват в стандартната PDFlibPas Delphi PDF Library, чиято продуктова страница носи пълната таблица с диагностични кодове за PDF/UA-1 заедно с тестовите пакети PDF/A, PDF/X и PDF/E