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

Откриване на липсващи PDF глифи при рисуване в Delphi

Липсващ глиф в PDF не е грешка; Производителят иска символ, който избраният шрифт не може да съпостави, шрифтът връща глиф индекс нула и файлът, който излиза, е структурно валиден, отваря се навсякъде и показва празна кутия там, където трябва да е име или сума; Никой в генериращия конвейер не научава; Получателят научава; HotPDF затваря този цикъл с TrackUnresolvedGlyphs: включете го и пътят за рисуване на текст записва всеки кодов пункт, чието глиф търсене резолвира до индекс нула, изстрелвайки OnUnresolvedGlyph веднъж на уникална находка с кодовия пункт, шрифта, върху който се провали, скрипта, на който принадлежи, и предложение за шрифтове, които биха го покрили

Откриването е половината отговор; Другата половина е SetFontFallbackChain, която регистрира подреден списък от шрифтове за скрипт, така че честните случаи се разрешават сами и само истинските дупки достигат вашия handler; Заедно те превръщат клас дефекти, който преди се докладваше от клиенти, в проверка по време на build

Защо липсващ глиф не хвърля нищо?

Защото ISO 32000 не поставя задължение на производителя да верифицира покритието и глиф индекс нула е легитимен глиф; Той е .notdef, чиято контурна линия дизайнерът на шрифта избира: обикновено празен или кух правоъгълник, понякога нищо изобщо; Viewer, който го рисува, се държи правилно; Извличането на текст може дори да върне правилните символи, защото /ToUnicode съпоставянето е записано от source текста, а не от контурите, така че автоматична round-trip проверка с удоволствие ще мине документ, чийто видим текст има дупки

Диаграма защо липсващ PDF глиф остава мълчалив, докато глиф нула рисува празна кутия, а ToUnicode извличането минава round-trip проверки
Глиф нула е легитимен .notdef отговор и /ToUnicode е записан от source текста, така че нищо в конвейера не е уведомено за дупката

Практическата последица е, че покритието трябва да се провери в момента на рисуване, когато библиотеката все още знае кой кодов пункт е бил поиспан и кой глиф шрифтът действително е предложил; След това информацията е изгубена

Детекторът трябва да гледа subset състоянието, не device context-а

Това е мястото, където първата имплементация се обърка и причината си заслужава да се разбере, защото се отнася за всяка проверка на покритие, прикрепена към текстов конвейер; HotPDF има два текстови пътя; Единият излъчва през регистриран Unicode TrueType шрифт с in-memory символна карта, изградена в момента на регистрация; Другият е legacy GDI път, който създава пресен device context и font handle на символен run

Съдене на покритието от GDI пътя е безнадеждно; Неговото съпоставяне не е съпоставянето, което завършва в излъчения content поток и двете не са синхронизирани, така че детектор, четещ GDI резултати, докладва целия printable ASCII диапазон като неразрешен; Авторитетният отговор живее в регистрирания шрифт: символната карта, която RegisterUnicodeTTF парсва, запитвана през GetUnicodeGlyphForCodepoint; Детекторът следователно е ограден на subset-ready състоянието, не на каквото и да е GDI условие и той просто не тича върху документи, които никога не са регистрирали Unicode шрифт, което е правилно, защото тези документи и така са ограничени до стандартните кодировки

Втори капан седи до него; GDI фамилното име на шрифт и PostScript името, извлечено от шрифтовия двоичен файл при регистрация, са различни низове и не по начин, който можете да нормализирате: фамилия, казваща се Arial Unicode MS, носи PostScript името ArialMT; Всяка порта, написана като „дали текущо избраният шрифт е онзи, който регистрирахме“, сравнявана по име, е мъртъв код, който никога не изстрелва; Ограждайте по състояние, никога по шрифтови имена

Поток на откриване на неразрешени глифи в HotPDF, показващ subset-state портата, GetUnicodeGlyphForCodepoint запитването и OnUnresolvedGlyph обвързването на събитието
Покритието се съди от регистрираната Unicode шрифтова карта, а не от GDI и всеки уникален кодов пункт изстрелва едно събитие с предложение за шрифт

Не тествайте глиф детектор с emoji

Очевидният тестов случай е усмихнато лице и той ще ви убеди, че детекторът е счупен; Честни emoji кодови пунктове в астралните равнини резолвират през private-use синтезен път, който ги съпоставя директно към глиф индекс, така че те никога не достигат общия клон за покритие; Детекторът се държи правилно и тестът измерва грешния път

Използвайте вместо това неназован кодов пункт; 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;

Fallback веригите са за скрипт, не за шрифт

Причината fallback да е обхватен по скрипт, а не по source шрифт, е, че дупките в покритието се групират по писменост; Латински текстов шрифт липсва Devanagari, Thai, Han и emoji, всичко наведнъж и заместникът за всеки е различен шрифт; Декларирането на една верига за скрипт следователно описва реалното внедряване: един латински шрифт за основен текст, един CJK шрифт, един emoji шрифт, един catch-all

Диаграма на font fallback за скрипт, съпоставяща скриптове hfsCJK, hfsArabic, hfsEmoji и hfsOther към подредени вериги заместител-шрифтове в HotPDF
Всеки скрипт получава своя подредена верига, така че латински основен шрифт, липсващ Han, Arabic или emoji, пада върху шрифт, който го покрива
// THPDFFontScript covers hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji and 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']);

Fallback и откриването са комплементарни, а не алтернативни; Веригите обработват покритието, което сте предвидили; детекторът докладва покритието, което не сте, което върху система, обработваща произволни клиентски данни, е интересната половина; Обърнете внимание, че замяната на шрифт променя метриките, така че абзац, който падне обратно, може да пренареди; ако оформлението има значение, поведението на затваряне и subsetting на замениящия шрифт си заслужава да прочетете в статията за font subset closure, а скриптове, които се нуждаят от пренареждане или свързване, се обработват от shaping етапа, описан в complex script текстово оформяне

Как да добавите поведение ретроспективно, без да рискувате съществуващия път

Същото издание добави legacy kern таблица fallback за двойно разстояние и начинът, по който беше обхватен, е образец, който си заслужава да се копира; Вместо добавяне на нова точка на решение към kerning логиката, fallback-ът виси от early-exit клона, който вече съществуваше за шрифтове без GPOS таблица; Модерен шрифт с GPOS никога не го достига, така че неговото поведение е непроменено по конструкция, а не по тестване; Пътища, които не регистрират Unicode шрифт, произвеждат две нулеви отмествания, така че те също са непроменени

Това е общата форма на нискорисков retrofit в зряла rendering библиотека: намерете клона, който в момента не произвежда нищо и сложете новото поведение там; Това превръща „вярваме, че това не регресира нищо“ в „това не може да е регресирало нищо“, което е много по-добро нещо за казване върху текстов engine, през който минават чужди фактури

Направете го порта, не лог

Находките за покритие са полезни само ако нещо се проваля върху тях; В услуга за генериране на документи продуктивната уредба е tracking-ът да остане включен в нощната regression задача срещу корпус от реални клиентски имена, адреси и описания на продукти и задачата да се провали при всяка находка; Понеже събитието изстрелва веднъж на уникален кодов пункт, а не веднъж на срещане, изходът остава достатъчно малък за четене, дори целият скрипт да липсва

В продукция същият handler е по-добре да се използва като telemetry: запишете кодовия пункт и шрифта, продължете да сервирате документа и нека агрегатът ви каже кой скрипт да добавите следващия към deployment шрифтовия набор; Rendering поведението за вградени и замени шрифтове е разгледано допълнително в изобразяване на вградени шрифтови глифи, а пълният списък със свойства, включително TrackUnresolvedGlyphs, е документиран на продуктовата страница HotPDF Delphi PDF component