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

PDFlibPas: автоматичен fallback на шрифтове за CJK

PDFlibPas разпознава знаци, които избраният шрифт не може да изчертае, като търси във верига от резервни, инсталирани шрифтове, клъстър по клъстър, докато запазва shaping-а и реда на двупосочните последователности. Включвате го с SetAutomaticFontFallback, разширявате веригата с AddFontFallback, а в изходния файл се вграждат само резервните шрифтове, реално използвани при извеждането

Проблемът, който решава, е такъв, с какъвто се сблъсква всеки генератор на документи в момента, в който името на клиент пристигне в писменост, която шрифтът на шаблона никога не е предвидил. Провалът е тих, а точно това го прави скъп

Защо неподдържан текст изчезва, вместо да предизвика грешка?

Защото PDF няма понятие за шрифт, който не може да изчертае знак. Обикновеният шрифт картографира байт кодове към имена на глифи чрез кодиране; композитният шрифт картографира кодове през CMap към индекси на глифи. Поискайте глиф, който шрифтът не съдържа, и получавате индекс на глиф нула, .notdef, който повечето шрифтове изчертават като нищо или като празно квадратче. Файлът е структурно валиден, текстовият оператор е добре оформен и страницата се рендира. Просто е празна там, където би трябвало да стои името

Нищо в ISO 32000-1 не изисква производителят да забележи това. Генератор, който записва текст, без да проверява покритието, произвежда технически съответстващ PDF, който тихо е загубил съдържание, а загубата излиза на екрана на клиента седмици по-късно. Точно затова функцията за fallback и докладът за липсващи глифи вървят заедно: разрешаването на онова, което може да бъде разрешено, е само половината от задачата, а докладването на онова, което не е могло да бъде разрешено, е другата половина

Fallback-ът се случва по клъстър, не по code point

Гранулярността е детайлът, който отличава работеща имплементация от правдоподобна. Текстът не е поредица от независими знаци. Деванагарска сричка, emoji с модификатор за цвят на кожата, базова буква с диакритични знаци: всяко от тях е един клъстър, който трябва да бъде изобразен от един шрифт, защото решенията за shaping вътре в него зависят от таблиците в този конкретен шрифт

PDFlibPas разрешава клъстъри, така че клъстър, покрит от резервен шрифт, се изчертава изцяло от този шрифт. Разделянето по средата на клъстър и изчертаването на половината от основния шрифт, а другата половина от резервен би дало технически присъстващ, но визуално развален резултат, което е спорно по-лошо от празното място, с което сте започнали. Редът на последователността също се запазва, така че fallback вътре в last-to-right последователност не пренарежда околния текст; същата механика стои и зад вертикалното оформление, описано в вертикалното писане за японски и китайски

var
  Lib: TPDFlib;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.SetAutomaticFontFallback(1);

    // Ред на търсене: първото съвпадение печели, затова сложете
    // най-широко покриващите шрифтове последни
    Lib.AddFontFallback('Microsoft YaHei');   // Опростен китайски
    Lib.AddFontFallback('Meiryo');            // Японски
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = вгради шрифта
    Lib.SetTextSize(11);
    Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
    Lib.DrawText(72, 700, 'Delivery status: on time');

    Lib.SaveToFile('invoice.pdf');
  finally
    Lib.Free;
  end;
end;

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

Доклад или прекъсване: кой провал предпочитате?

SetMissingGlyphPolicy приема PDF_MISSING_GLYPH_REPORT, съвместимата стойност по подразбиране, или PDF_MISSING_GLYPH_ABORT. При политиката за доклад текстовата операция продължава, неразрешимите code points се пропускат както преди и всеки от тях се записва. При политиката за прекъсване текстовата операция се отхвърля, преди каквото и да е съдържание да е записано, а LastErrorCode се задава на 521

Изберете според предназначението на документа. Партида вътрешни отчети трябва да продължи рендирането и да логва пропуските, защото леко непълен отчет днес е по-добър от никакъв отчет. Правно обвързващ договор, фактура или каквото и да е с нечие име на него трябва да прекъсва, защото тихо изпуснат знак в име на страна е дефект, който искате да откриете в собствения си процес, а не в спор. Политиката за прекъсване се задейства преди записа, така че никакъв полуготов content stream не остава след себе си

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... изграждане на документа ...

    if Lib.DrawText(72, 660, CustomerName) <> 1 then
      if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
      begin
        Report := Lib.GetMissingGlyphReportJSON;
        // {"valid":false,"policy":1,"eventCount":1,"events":[
        //   {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
        //    "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
        //    "fontType":"TrueType","operation":"DrawText"}]}
        EscalateToOperator(Report);
      end;
  finally
    Lib.Free;
  end;
end;

Докладът е съзнателно машинно четим и ограничен. Всяко събитие носи страницата, UTF-16 индекса вътре в низа, code point-а както в числова, така и в U+XXXX форма, избрания шрифт, неговия тип и операцията, при която е възникнал проблемът, така че поддръжката може да назове точния знак, вместо да описва симптом. Проследяването пази последните 256 събития, което е достатъчно за диагностика на документ и достатъчно малко, за да не превърне патологично изпълнение диагностиката в проблем с паметта

Измерването и изчертаването трябва да съвпадат

Измерването на ширината използва същите клъстърно съобразени решения за fallback като изчертаването. Това звучи очевидно и е нещото, което повечето самостоятелно писани fallback слоеве бъркат: те закърпват пътя на изчертаване, оставят измерването върху основния шрифт и всяка текстова кутия, дясно подравняване и колона в таблица завършва изчислена от ширини, които не съвпадат с реално рендираното

Тъй като двата пътя споделят разрешаването, низ, измерен преди изчертаване, заема ширината, при която е измерен, включително резервните последователности. Именно това прави fallback-а безопасен за глобално активиране, а не само на местата, които сте проверили ръчно

Вгражда се само онова, което сте използвали

Резервните шрифтове се вграждат мързеливо: шрифт от веригата, който никога не е разрешил клъстър, не допринася нищо в изхода. Документ с един китайски знак и 5000 латински не носи цял CJK шрифт; той носи онова, което подмножаването е произвело за този единствен глиф, което е поведението, описано в оптимизацията на размера на файла и подмножаването на шрифтове

Тази мързеливост прави широка верига евтина за конфигуриране. Регистрирайте шрифтовете, от които наборът ви документи може да се нуждае за всяка обслужвана локализация, и всеки отделен PDF плаща само за онова, което реално е използвал. За документи, които не сте генерирали вие, където липсващите шрифтове вече са вътре в съществуващ файл, пътят за поправка е различен и е разгледан във вграждането на липсващи шрифтове в съществуващ PDF

Едно предупреждение за внедряването си струва да бъде казано направо: fallback-ът разрешава спрямо шрифтове, инсталирани на машината, която изпълнява кода. Сървър без инсталирани CJK шрифтове няма към какво да прибегне, а докладът ще ви каже това още при първия документ, а не след първата жалба. Доставяйте шрифтовете, от които зависите, и потвърдете лицензирането за тяхното вграждане

PDFlibPas е PDF библиотека за Delphi, C++Builder и Lazarus със съответстващи DLL и ActiveX интерфейси, така че API-тата за fallback и липсващи глифи са достъпни и от извикващи извън Pascal. Пълната документация е на страницата на PDFlibPas PDF библиотеката за Delphi