Відсутній гліф у PDF — не помилка. Творець просить символ, який вибраний шрифт не може відобразити, шрифт повертає індекс гліфа нуль, і файл, що виходить, структурно чинний, відкривається всюди і показує порожню коробку там, де мало бути ім'я чи сума. Ніхто в генерувальному конвеєрі не дізнається. Отримувач — так. HotPDF замикає той контур через TrackUnresolvedGlyphs: увімкніть — і шлях малювання тексту записує кожну кодову точку, чиїй пошуку гліфа розв'язок — індекс нуль, викидаючи OnUnresolvedGlyph по разу на унікальну знахідку з кодовою точкою, шрифтом, на якому вона зазнала невдачі, писемністю, до якої вона належить, і пропозицією шрифтів, які її покрили б
Виявлення — половина відповіді. Друга половина — SetFontFallbackChain, що реєструє впорядкований список шрифтів на писемність, тож звичайні випадки розв'язуються самі, і лише справжні прогалини доходять до вашого обробника. Разом вони перетворюють клас вад, про який раніше звітували клієнти, у перевірку на етапі збірки
Чому відсутній гліф не піднімає нічого?
Бо ISO 32000 не покладає на творця обов'язку перевіряти покриття, а індекс гліфа нуль — законний гліф. Це .notdef, чию обвідну обирає дизайнер шрифта: зазвичай порожній або порожнистий прямокутник, іноді нічого взагалі. Глядач, що малює його, поводиться правильно. Вилучення тексту може навіть повернути правильні символи, бо мапування /ToUnicode записується з вихідного тексту, а не з обвідних, тож автоматична перевірка туди й назад із радістю пропустить документ, чий видимий текст має дірки
Практичний наслідок: покриття треба перевіряти в момент малювання, коли бібліотека ще знає, яку кодову точку просили і який гліф шрифт справді запропонував. Після цього інформація зникає
Детектор мусить дивитися на стан субсету, а не на контекст пристрою
Ось де перша реалізація пішла неправильно, і причина варта розуміння, бо вона стосується будь-якої перевірки покриття, причепленої до текстового конвеєра. HotPDF має два текстові шляхи. Один виводить через зареєстрований Unicode-шрифт TrueType з мапою символів у пам'яті, збудованою на момент реєстрації. Другий — застарілий шлях GDI, що творить свіжий контекст пристрою та дескриптор шрифта на кожен рядок символів
Судити покриття з шляху GDI — безнадійно. Його мапування — не те мапування, що потрапляє у виведений потік вмісту, і вони не синхронізовані, тож детектор, що читає результати GDI, повідомляє весь друкований діапазон ASCII як нерозв'язаний. Авторитетна відповідь живе в зареєстрованому шрифті: мапа символів, яку розбирає RegisterUnicodeTTF, опитувана через GetUnicodeGlyphForCodepoint. Детектор тому перепонений на стані готовності субсету, а не на якійсь умові GDI, і він просто не працює на документах, що ніколи не реєстрували Unicode-шрифт, — правильно, бо ті документи обмежені стандартними кодуваннями й так
Друга пастка сидить поруч. Ім'я родини шрифта в GDI та ім'я PostScript, витягнуте з бінарника шрифта при реєстрації, — різні рядки, і не такі, які можна нормалізувати: родина під назвою Arial Unicode MS несе ім'я PostScript ArialMT. Будь-яка перепона, написана як «чи поточний вибраний шрифт — той, який ми зареєстрували», порівняна за назвою, — мертвий код, що ніколи не спрацює. Перепоняйте на стані, ніколи на назвах шрифтів
Не тестуйте детектор гліфів на емодзі
Очевидний тестовий випадок — усміхнене обличчя, і воно переконає вас, що детектор зламаний. Поширені кодові точки емодзі в астральних площинах розв'язуються через шлях приватної синтези, що мапує їх на індекс гліфа прямо, тож вони ніколи не досягають загальної гілки покриття. Детектор поводиться правильно, а тест міряє неправильний шлях
Використовуйте натомість нерозподілену кодову точку. U+0378 назавжди не виділений у Unicode, тож жоден шрифт не може його законно відобразити, і він вправляє точно ту гілку, яку ви хочете перевірити. Та відмінність між «можливість зламана» і «тест узяв вхід, що обходить можливість» коштує справжніх годин, а нерозподілені кодові точки — найдешевший спосіб її уникнути
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// Спрацьовує по разу на унікальну кодову точку, а не на входження
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// Підключення до генерувального завдання
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// Заваліть завдання замість випускати сторінку з коробками
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
Резервні ланцюги — на писемність, а не на шрифт
Причина, чому резерв обмежується писемністю, а не вихідним шрифтом: прогалини покриття гуртуються за системою письма. Латинський текстовий шрифт не має деванагарі, тайської, ханської та емодзі — усе водночас, і заміна кожного — інший шрифт. Оголошення одного ланцюга на писемність тому описує справжнє розгортання: один латинський шрифт для тіла тексту, один шрифт CJK, один шрифт емодзі, один універсальний
// THPDFFontScript охоплює hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji та hfsOther
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
Резерв і виявлення — комплементарні, а не альтернативні. Ланцюги обробляють покриття, яке ви передбачили; детектор звітує покриття, якого ви не передбачили, — а на системі, що обробляє довільні клієнтські дані, це цікава половина. Зауважте, що заміна шрифта змінює метрики, тож абзац, що відкотився в резерв, може переплисти; якщо компонування важить, поведінку закриття та субсетування заміненого шрифта варто вивчити в статті про закриття субсету шрифтів, а писемності, що потребують перевпорядкування чи зчеплення, обробляє стадія формування, описана в формуванні тексту складних писемностей
Як доробити поведінку, не ризикуючи наявним шляхом
Той самий випуск додав застарілий резерв таблиці kern для парних проміжків, і те, як його обмежили, — взірець, вартий копіювання. Замість додавання нової точки рішення до логіки кернінга резерв повісили на гілку раннього виходу, що вже існувала для шрифтів без таблиці GPOS. Сучасний шрифт з GPOS ніколи до неї не доходить, тож його поведінка незмінена за побудовою, а не за тестами. Шляхи, що не реєструють Unicode-шрифт, дають два нульові зсуви, тож вони теж незмінені
Це загальна форма малоризикового доробку в зрілій бібліотеці рендерингу: знайдіть гілку, яка зараз не творить нічого, і покладіть нову поведінку туди. Це перетворює «ми віримо, що нічого не регресувало» на «це не могло нічого регресувати», що набагато краще сказати про текстовий двигун, крізь який ідуть чужі рахунки
Зробіть це перепоною, а не журналом
Знахідки покриття корисні, лише якщо на них щось падає. У службі генерування документів продуктивний устрій — тримати відстеження увімкненим у нічному регресійному завданні проти корпусу справжніх клієнтських імен, адрес і описів продуктів і завалювати завдання на будь-якій знахідці. Бо подія спрацьовує по разу на унікальну кодову точку, а не на кожне входження, результат лишається достатньо малим для читання, навіть коли ціла писемність відсутня
У виробництві той самий обробник краще вжити як телеметрію: записуйте кодову точку та шрифт, продовжуйте обслуговувати документ і дозвольте агрегату сказати вам, яку писемність додати до шрифтового набору розгортання наступною. Поведінку рендерингу вбудованих і замінених шрифтів далі охоплено в рендерингу гліфів вбудованих шрифтів, а повний перелік властивостей, включно з TrackUnresolvedGlyphs, задокументовано на сторінці продукту HotPDF Delphi PDF component