PDFlibPas řeší znaky, které zvolené písmo neumí vykreslit, tak, že prohledává řetězec náhradních (fallback) písem nainstalovaných v systému, a to po jednotlivých clusterech, přičemž zachovává tvarování textu i pořadí obousměrných běhů. Funkci zapnete metodou SetAutomaticFontFallback, řetězec rozšíříte metodou AddFontFallback a do souboru se vloží jen ta náhradní písma, která byla pro výstup skutečně použita
Řeší problém, na který narazí každý generátor dokumentů ve chvíli, kdy poprvé dorazí jméno zákazníka v písmu, se kterým šablona vůbec nepočítala. Selhání je tiché, a právě proto je drahé
Proč nepodporovaný text zmizí, místo aby vyvolal chybu?
Protože formát PDF vůbec nezná pojem písma, které neumí vykreslit daný znak. Jednoduché písmo mapuje bajtové kódy na názvy glyfů prostřednictvím kódování; složené písmo mapuje kódy přes CMap na indexy glyfů. Když si vyžádáte glyf, který dané písmo neobsahuje, dostanete index glyfu nula, .notdef, který většina písem vykreslí jako nic nebo jako prázdný obdélník. Soubor je strukturálně platný, textový operátor je správně zapsaný a stránka se vykreslí. Jen tam, kde mělo být jméno, zůstane prázdno
Norma ISO 32000-1 po producentovi nijak nevyžaduje, aby si toho všiml. Generátor, který zapisuje text bez kontroly pokrytí, vyprodukuje technicky vyhovující PDF, jež tiše ztratilo obsah, a ztráta se projeví na obrazovce zákazníka až o týdny později. Právě proto se funkce fallbacku a report chybějících glyfů dodávají společně: vyřešit to, co vyřešit lze, je jen polovina úkolu, druhou polovinou je nahlásit to, co vyřešit nešlo
Fallback probíhá po clusterech, ne po jednotlivých bodech kódu
Granularita je právě ten detail, který odlišuje funkční implementaci od jen zdánlivě funkční. Text není posloupnost nezávislých znaků. Slabika v dévanágarí, emoji s modifikátorem odstínu pleti, základní písmeno s kombinujícími znaménky – to všechno jsou jednotlivé clustery, které musí vykreslit jedno jediné písmo, protože rozhodnutí o tvarování uvnitř clusteru závisí na tabulkách právě v tomto písmu
PDFlibPas řeší celé clustery, takže cluster pokrytý náhradním písmem vykreslí od začátku do konce právě to písmo. Rozdělení uprostřed clusteru, kdy by se polovina vykreslila primárním písmem a polovina náhradním, by sice technicky poskytlo obsah, ale viditelně rozbitý, což je vlastně horší než prázdné místo, od kterého jste vyšli. Zachovává se i pořadí běhů, takže fallback uvnitř textu psaného zprava doleva okolní text nepřehodí; stejný mechanismus stojí i za svislým rozvržením popsaným v článku o svislém psaní pro japonštinu a čínštinu
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// Pořadí hledání: vyhrává první shoda, proto dejte nejobecnější písma na konec
Lib.AddFontFallback('Microsoft YaHei'); // zjednodušená čínština
Lib.AddFontFallback('Meiryo'); // japonština
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = vložit písmo do souboru
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;
Pořadí v řetězci volte záměrně. Řešení bere první písmo, které cluster pokrývá, takže široké pan-unikódové písmo umístěné na první místo vyhraje téměř všechno a vaše pečlivě vybraná písma pro konkrétní skripty nikdy nepřijdou na řadu. Konkrétní písma dejte na začátek a to univerzální až na konec
Report, nebo přerušení: jaký typ selhání chcete?
SetMissingGlyphPolicy přijímá buď PDF_MISSING_GLYPH_REPORT, což je kompatibilní výchozí hodnota, nebo PDF_MISSING_GLYPH_ABORT. Při politice reportu operace s textem proběhne, nevyřešitelné body kódu se jako dřív vynechají a každý z nich se zaznamená. Při politice přerušení je operace s textem odmítnuta ještě předtím, než se zapíše jakýkoli obsah, a LastErrorCode se nastaví na 521
Volbu odvíjejte od účelu dokumentu. Dávka interních reportů by měla vykreslování dokončit a mezery jen zalogovat, protože dnes mírně neúplný report je lepší než žádný. Právně závazná smlouva, faktura nebo cokoli, kde figuruje jméno, by naopak mělo skončit přerušením, protože tiše vynechaný znak ve jméně strany je vada, kterou chcete odhalit ve vlastním procesu, ne až ve sporu. Politika přerušení selže ještě před zápisem, takže po sobě nezanechá žádný napůl vytvořený obsahový proud
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... sestavení dokumentu ...
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;
Report je záměrně strojově čitelný a omezený. Každá událost nese stránku, index UTF-16 uvnitř řetězce, bod kódu v číselné podobě i ve tvaru U+XXXX, vybrané písmo, jeho typ a operaci, při které nastal problém, takže tiket podpory může pojmenovat přesný znak místo popisu příznaku. Sledovač uchovává posledních 256 událostí, což stačí k diagnóze dokumentu a zároveň je to dost málo na to, aby patologický běh nezměnil diagnostiku v problém s pamětí
Měření a kreslení se musí shodovat
Měření šířky používá stejná rozhodnutí o fallbacku po clusterech jako samotné kreslení. Zní to samozřejmě, a přesto je to přesně to, co většina domácích implementací fallbacku zpackává: opraví se jen cesta pro kreslení, měření zůstane na primárním písmu, a každý textový box, zarovnání doprava i sloupec tabulky se pak počítá z šířek, které neodpovídají tomu, co se skutečně vykreslilo
Protože obě cesty sdílejí stejné řešení, řetězec změřený před vykreslením zabere přesně tu šířku, na kterou byl změřen, včetně úseků řešených fallbackem. Právě to dělá z fallbacku funkci, kterou lze bezpečně zapnout globálně, a ne jen na místech, která jste ručně prověřili
Vloží se jen to, co jste opravdu použili
Náhradní písma se vkládají líně: písmo v řetězci, které nikdy nevyřešilo žádný cluster, do výstupu nepřispěje ničím. Dokument obsahující jeden čínský znak a 5000 latinských znaků nepotáhne za sebou celé CJK písmo; ponese jen to, co pro daný jeden glyf vyprodukoval proces vytváření podmnožin, což je chování popsané v článku o optimalizaci velikosti souboru a vytváření podmnožin písem
Právě tato lenost dělá z rozsáhlého řetězce levnou věc na konfiguraci. Zaregistrujte písma, která vaše sada dokumentů může potřebovat napříč všemi lokalizacemi, které obsluhujete, a každé jednotlivé PDF zaplatí jen za to, co skutečně použilo. U dokumentů, které jste sami nevygenerovali a kde chybějící písma už jsou uvnitř existujícího souboru, je postup opravy jiný a popisuje ho článek o vkládání chybějících písem do existujícího PDF
Jedno provozní upozornění stojí za to říct otevřeně: fallback řeší proti písmům nainstalovaným na stroji, kde kód běží. Server bez nainstalovaných CJK písem nemá na co se fallbackovat, a report vám to sdělí už u prvního dokumentu, ne až po první stížnosti. Písma, na kterých závisíte, si dodejte sami a ověřte si licenční podmínky pro jejich vkládání
PDFlibPas je knihovna pro práci s PDF pro Delphi, C++Builder a Lazarus s odpovídajícím rozhraním DLL a ActiveX, takže API pro fallback i pro chybějící glyfy je dostupné i pro volající mimo Pascal. Úplná dokumentace je na stránce PDFlibPas Delphi PDF library