Tehnički članak

Automatski fallback fontova za CJK i emoji u PDFlibPas

PDFlibPas razrješava znakove koje odabrani font ne može nacrtati tako da pretražuje fallback lanac instaliranih fontova, klaster po klaster, uz očuvanje oblikovanja i redoslijeda dvosmjernih nizova. Uključujete ga pomoću SetAutomaticFontFallback, lanac proširujete pomoću AddFontFallback, a u datoteku se ugrađuju samo oni fallback fontovi koji su stvarno korišteni za izlaz

Problem koji rješava jest onaj s kojim se svaki generator dokumenata suoči prvi put kada stigne ime kupca napisano pismom koje predloženi font nikada nije predvidio. Kvar je tih, i upravo je to ono što ga čini skupim

Zašto nepodržani tekst nestane umjesto da izazove pogrešku?

Zato što PDF nema koncept fonta koji ne može nacrtati znak. Jednostavan font mapira bajt kodove u imena glifova putem kodiranja; složeni font mapira kodove putem CMap-a u indekse glifova. Zatražite glif koji font ne sadrži i dobit ćete indeks glifa nula, .notdef, koji većina fontova crta kao ništa ili kao praznu kutiju. Datoteka je strukturno valjana, operator teksta ispravno je oblikovan, a stranica se renderira. Samo je prazna ondje gdje bi trebalo biti ime

Ništa u ISO 32000-1 ne zahtijeva da proizvođač to primijeti. Generator koji piše tekst bez provjere pokrivenosti proizvodi tehnički sukladan PDF koji je tiho izgubio sadržaj, a gubitak se pojavi na kupčevu zaslonu tek tjednima kasnije. Zato značajka fallback fontova i izvještaj o nedostajućim glifovima dolaze zajedno: razrješavanje onoga što se može razriješiti samo je pola posla, a prijavljivanje onoga što se nije moglo razriješiti druga je polovica

Fallback se odvija po klasteru, a ne po kodnoj točki

Granularnost je detalj koji razdvaja funkcionalnu implementaciju od uvjerljive. Tekst nije niz nezavisnih znakova. Devanagari slog, emoji s modifikatorom tona kože, osnovno slovo s dijakritičkim znakovima: svaki je od njih jedan klaster koji mora nacrtati jedan font, jer odluke o oblikovanju unutar njega ovise o tablicama tog fonta

PDFlibPas razrješava klastere, pa klaster koji pokriva fallback font u cijelosti crta taj font. Razdvajanje usred klastera i crtanje pola iz primarnog fonta, a pola iz fallback fonta proizvelo bi tehnički prisutan, ali vizualno slomljen rezultat, što je gotovo sigurno gore od praznine s kojom ste započeli. Redoslijed nizova također se čuva, pa fallback unutar niza s desna na lijevo ne mijenja redoslijed okolnog teksta; isti mehanizam stoji iza okomitog rasporeda opisanog u članku okomito pisanje za japanski i kineski

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

    // Redoslijed pretrage: prvo podudaranje pobjeđuje, pa najšire fontove stavite na kraj
    Lib.AddFontFallback('Microsoft YaHei');   // pojednostavljeni kineski
    Lib.AddFontFallback('Meiryo');            // japanski
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = ugradi font
    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;

Lanac slažite namjerno. Razrješavanje uzima prvi font koji pokriva klaster, pa će široki pan-Unicode font stavljen na prvo mjesto pobijediti gotovo sve, a vaše pažljivo odabrane fontove specifične za pojedino pismo nikada neće biti konzultirani. Specifične fontove stavite prve, a univerzalni na kraj

Prijaviti ili prekinuti: koji kvar želite?

SetMissingGlyphPolicy prima PDF_MISSING_GLYPH_REPORT, kompatibilnu zadanu vrijednost, ili PDF_MISSING_GLYPH_ABORT. Kod politike izvještavanja operacija teksta nastavlja se, nerazrješive kodne točke izostavljaju se kao i prije, a svaka se zabilježi. Kod politike prekida operacija teksta odbacuje se prije nego što se zapiše ikakav sadržaj, a LastErrorCode postavlja se na 521

Birajte prema tome čemu dokument služi. Skup internih izvještaja trebao bi nastaviti s renderiranjem i bilježiti praznine, jer je danas malo nepotpun izvještaj bolji od nikakvog izvještaja. Pravno obvezujući ugovor, račun, ili bilo što s nečijim imenom na sebi trebalo bi prekinuti obradu, jer je tiho izostavljen znak u imenu stranke greška koju želite otkriti u vlastitom procesu, a ne u sporu. Politika prekida ne uspijeva prije pisanja, pa iza sebe ne ostavlja polunapisan tok sadržaja

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... izgradite dokument ...

    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;

Izvještaj je namjerno strojno čitljiv i ograničen. Svaki događaj nosi stranicu, UTF-16 indeks unutar niza, kodnu točku u brojčanom obliku i obliku U+XXXX, odabrani font, njegov tip i operaciju kod koje je nastao problem, tako da prijava podrške može navesti točan znak umjesto da opisuje simptom. Tracker čuva posljednjih 256 događaja, što je dovoljno za dijagnosticiranje dokumenta, a dovoljno malo da patološko izvođenje ne pretvori dijagnostiku u problem s memorijom

Mjerenje i crtanje moraju se slagati

Mjerenje širine koristi iste odluke fallback mehanizma svjesnog klastera kao i crtanje. To zvuči očito, a upravo je to ono što najviše kućnih fallback slojeva pogrešno rade: zakrpaju putanju crtanja, a mjerenje ostave na primarnom fontu, pa svaki tekstualni okvir, desno poravnanje i stupac tablice na kraju bude izračunat iz širina koje se ne podudaraju s onim što je zapravo renderirano

Budući da obje putanje dijele isto razrješavanje, niz izmjeren prije crtanja zauzima točno onu širinu na kojoj je izmjeren, uključujući fallback dijelove. Upravo to čini sigurnim globalno uključivanje fallback mehanizma, umjesto samo na mjestima koja ste ručno provjerili

Ugrađuje se samo ono što ste stvarno koristili

Fallback fontovi ugrađuju se lijeno: font u lancu koji nikada nije razriješio nijedan klaster ne pridonosi ničim izlazu. Dokument koji sadrži jedan kineski znak i 5000 latiničnih ne nosi cijeli CJK font; nosi samo ono što je prolaz izrade podskupa proizveo za taj jedan glif, što je ponašanje opisano u članku optimizacija veličine PDF datoteke i izrada podskupova fontova

Ta lijenost čini jeftinim konfiguriranje širokog lanca. Registrirajte fontove koji bi mogli trebati vašem skupu dokumenata za svaku lokalizaciju koju opslužujete, a svaki pojedinačni PDF plaća samo ono što je stvarno koristio. Za dokumente koje niste sami generirali, gdje su nedostajući fontovi već unutar postojeće datoteke, put popravka je drugačiji i obrađen je u članku ugrađivanje nedostajućih fontova u postojeći PDF

Jedno upozorenje vezano uz implementaciju vrijedi jasno izreći: fallback se razrješava u odnosu na fontove instalirane na stroju koji izvršava kod. Poslužitelj bez instaliranih CJK fontova nema na što pasti, a izvještaj će vam to reći već kod prvog dokumenta, a ne tek nakon prve pritužbe. Isporučite fontove o kojima ovisite i potvrdite licenciranje za njihovu ugradnju

PDFlibPas je PDF biblioteka za Delphi, C++Builder i Lazarus s pripadajućim DLL i ActiveX sučeljima, pa su API-ji za fallback i nedostajuće glifove dostupni i pozivateljima izvan Pascala. Potpuna dokumentacija nalazi se na stranici PDFlibPas Delphi PDF biblioteke