Звіт 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 нормалізує при завантаженні. 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, вона робить питання ширини безвідповідним
// /CIDToGIDMap — це імʼя /Identity або потік 16-бітових
// індексів гліфів у старшому байті, один на 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-рядки індексуються від одиниці
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 потребує, щоб потік надувся і кількість гліфів повернулася додатною, перш ніж тестується хоч один біт. Будь-яке виключення по дорозі згортається в «немає знахідки», а не в дефект
Це навмисний ухил до хибнонегативних, і варто сказати це прямо, а не ховати. Пошкоджена таблиця CFF, непідтримуваний варіант Type 1 чи /CIDSet коротший за діапазон гліфів — усе це породжує тишу замість діагностики. Міркування таке: аудити PDF/UA пересилаються авторам, які не будували інструментарій, і хибне обвинувачення коштує дорожче за пропущене: автор спалить день, доводячи, що відповідний файл відповідний, і перестане довіряти всьому звіту. Matterhorn Protocol робить те саме розрізнення в іншій формі, коли відділяє перевірки, які машина може вирішити, від перевірок, які мусить людина, і його контрольна точка Fonts (31) — саме там вони живуть. Якщо потрібне строже читання, запускайте PDFlibPas як швидкі ворота і спеціалізований валідатор як другу думку — та сама пара описана в провіднику preflight PDF/A та PDF/UA
Сторінковий /Contents — це список, а не потік
Найдорожча помилка в аудируванні потоків вмісту — сприймати /Contents як один потік. ISO 32000-1 §7.7.3.3 дозволяє сторінці тримати масив потоків, чия конкатенація, з пробілами між частинами, і є сторінковою програмою; продюсери ріжуть у довільних місцях, і BT може сидіти в одному члені, а його парний ET — у наступному. Процесор вмісту тримає стан — глибину вкладеності marked-content, шрифт, обраний останнім 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 насправді рахуються неструктурованими?
Тільки ті, які сторінка справді викликає, з місця виклику поза marked content, чиїй власний вміст показує текст. Діагностика 10040 застосовує ISO 14289-1 §7.20, записуючи три незалежні факти на номер обʼєкта — має текст, був викликаний, був викликаний всередині marked content — і звітуючи лише перетин перших двох мінус третій. Кожен з двох обхідних шляхів неправильний у спосіб, який ви б відгрузили: прапорювати кожен 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 коротко замикає на першій знахідці, що є тим, чого хочеш у build-воротах, а не в інструменті авторства. Для документа, ще відкритого в памʼяті, 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