Articol tehnic

Fallback automat de font pentru CJK și emoji în PDFlibPas

PDFlibPas rezolvă caracterele pe care fontul selectat nu le poate desena căutând într-un lanț de fallback de fețe instalate, cluster cu cluster, păstrând totodată shaping-ul și ordinea run-urilor bidirecționale. Activezi asta cu SetAutomaticFontFallback, extinzi lanțul cu AddFontFallback, iar doar fonturile de fallback efectiv folosite pentru output sunt incluse în fișier

Problema pe care o rezolvă este una pe care orice generator de documente o întâlnește prima dată când numele unui client sosește într-un script pe care fontul din șablon nu l-a anticipat niciodată. Eșecul este tăcut, ceea ce este exact ce îl face costisitor

De ce dispare textul nesuportat în loc să genereze o eroare?

Pentru că PDF nu are conceptul unui font care nu poate desena un caracter. Un font simplu mapează coduri de bytes la nume de glife printr-o codificare; un font compus mapează codurile printr-un CMap la indici de glife. Ceri o glifă pe care fața nu o conține și primești indicele de glifă zero, .notdef, pe care majoritatea fețelor îl desenează ca nimic sau ca o casetă goală. Fișierul este valid structural, operatorul de text este bine format, iar pagina se randează. Doar că este gol acolo unde ar fi trebuit să fie numele

Nimic din ISO 32000-1 nu cere unui producător să observe asta. Un generator care scrie text fără să verifice acoperirea produce un PDF tehnic conform care a pierdut silențios conținut, iar pierderea iese la suprafață pe ecranul unui client câteva săptămâni mai târziu. De aceea funcționalitatea de fallback și raportul de glife lipsă vin împreună: rezolvarea a ceea ce poate fi rezolvat este doar jumătate din treabă, iar raportarea a ceea ce nu a putut fi rezolvat este cealaltă jumătate

Fallback-ul se întâmplă pe cluster, nu pe punct de cod

Granularitatea este detaliul care separă o implementare funcțională de una doar plauzibilă. Textul nu este o secvență de caractere independente. O silabă devanagari, un emoji cu un modificator de ton al pielii, o literă de bază cu semne diacritice combinatorii: fiecare este un cluster care trebuie randat de un singur font, pentru că deciziile de shaping din interiorul lui depind de tabele din acea față

PDFlibPas rezolvă clustere, așa că un cluster pe care o față de fallback îl acoperă este desenat integral de acea față. Împărțirea la mijlocul unui cluster și desenarea unei jumătăți din fontul principal și a celeilalte dintr-un fallback ar produce un rezultat tehnic prezent, dar vizibil stricat, ceea ce este discutabil mai rău decât golul de la care ai pornit. Ordinea run-urilor este de asemenea păstrată, așa că un fallback în interiorul unui run de la dreapta la stânga nu reordonează textul din jur; același mecanism stă la baza layout-ului vertical descris în scrierea verticală pentru japoneză și chineză

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

    // Ordinea de căutare: primul potrivit câștigă, așa că pune fețele cele mai largi la final
    Lib.AddFontFallback('Microsoft YaHei');   // Chineză simplificată
    Lib.AddFontFallback('Meiryo');            // Japoneză
    Lib.AddFontFallback('Segoe UI Symbol');
    Lib.AddFontFallback('Segoe UI Emoji');

    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);

    Lib.AddTrueTypeFont('Arial', 1);          // 1 = include fața
    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;

Ordonează lanțul deliberat. Rezolvarea ia prima față care acoperă clusterul, așa că un font pan-Unicode larg plasat primul va câștiga aproape totul, iar fețele tale specifice pe scripturi, alese cu grijă, nu vor fi niciodată consultate. Pune fețele specifice primele și cea generală la final

Raport sau abandon: ce tip de eșec vrei?

SetMissingGlyphPolicy primește PDF_MISSING_GLYPH_REPORT, valoarea implicită compatibilă, sau PDF_MISSING_GLYPH_ABORT. Sub politica de raportare, operația de text continuă, punctele de cod nerezolvate sunt eliminate ca înainte, și fiecare este înregistrat. Sub politica de abandon, operația de text este respinsă înainte ca vreun conținut să fie scris, iar LastErrorCode este setat la 521

Alege în funcție de scopul documentului. Un lot de rapoarte interne ar trebui să continue randarea și să logheze golurile, pentru că un raport ușor incomplet astăzi este mai bun decât niciun raport. Un contract cu forță juridică, o factură, sau orice conține un nume ar trebui să abandoneze, pentru că un caracter eliminat silențios dintr-un nume de parte este un defect pe care vrei să îl descoperi în propriul tău proces, nu într-un litigiu. Politica de abandon eșuează înainte de scriere, așa că niciun content stream pe jumătate format nu rămâne în urmă

var
  Lib: TPDFlib;
  Report: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
    // ... construiește documentul ...

    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;

Raportul este în mod deliberat lizibil pentru mașini și mărginit. Fiecare eveniment poartă pagina, indexul UTF-16 din interiorul stringului, punctul de cod atât în formă numerică, cât și U+XXXX, fontul care a fost selectat, tipul lui și operația care a lovit problema, astfel încât un tichet de suport poate numi caracterul exact în loc să descrie un simptom. Tracker-ul păstrează cele mai recente 256 de evenimente, ceea ce este suficient pentru a diagnostica un document și suficient de mic încât o rulare patologică nu poate transforma diagnosticele într-o problemă de memorie

Măsurarea și desenarea trebuie să coincidă

Măsurarea lățimii folosește aceleași decizii de fallback conștiente de cluster ca și desenarea. Asta sună evident și este lucrul pe care majoritatea straturilor de fallback construite acasă îl greșesc: petic traseul de desenare, lasă măsurarea pe fontul principal, iar fiecare casetă de text, aliniere la dreapta și coloană de tabel ajunge calculată din lățimi care nu se potrivesc cu ce a fost randat

Pentru că ambele trasee împart rezolvarea, un string măsurat înainte de desenare ocupă lățimea la care a fost măsurat, inclusiv run-urile de fallback. Asta este ce face fallback-ul sigur de activat global, nu doar în locurile pe care le-ai auditat manual

Doar ce ai folosit ajunge inclus

Fonturile de fallback sunt incluse leneș: o față din lanț care nu a rezolvat niciodată vreun cluster nu contribuie cu nimic la output. Un document care conține un caracter chinezesc și 5.000 latine nu poartă o față CJK completă; poartă ce a produs pasul de subsetting pentru acea singură glifă, care este comportamentul descris în optimizarea dimensiunii fișierului și subsetting-ul de fonturi

Acea leneviciune face un lanț larg ieftin de configurat. Înregistrează fețele de care setul tău de documente ar putea avea nevoie pe fiecare locale pe care o servești, iar fiecare PDF individual plătește doar pentru ce a folosit efectiv. Pentru documentele pe care nu le-ai generat tu, unde fețele lipsă sunt deja în interiorul unui fișier existent, traseul de reparare este diferit și este acoperit în includerea fonturilor lipsă într-un PDF existent

Un avertisment de desfășurare merită menționat clar: fallback-ul se rezolvă față de fețele instalate pe mașina care rulează codul. Un server fără fonturi CJK instalate nu are la ce să recurgă, iar raportul îți va spune asta la primul document, nu după prima reclamație. Livrează fonturile de care depinzi, și confirmă licențierea pentru includerea lor

PDFlibPas este o bibliotecă PDF pentru Delphi, C++Builder și Lazarus cu interfețe DLL și ActiveX corespunzătoare, astfel încât API-urile de fallback și de glife lipsă sunt disponibile și pentru apelanți non-Pascal. Documentația completă este pe pagina bibliotecii PDFlibPas Delphi