Articol tehnic

Modelare text conectabilă: Uniscribe și HarfBuzz în Delphi

Modelarea textului în componenta PDFium trece printr-un singur obiect instalabil. ConfigureTextShaper instalează shaper-ul prin care fiecare punct de intrare de modelare se dirijează, înlocuind și eliberând orice era acolo; ActiveTextShaper returnează cel instalat și creează implicitul platformei la prima folosire; ActiveTextShaperName raportează ce backend este activ; ClearTextShaper renunță la instalare și lasă implicitul să fie creat din nou. Pe Windows implicitul este TPdfUniscribeTextShaper. Sub Free Pascal există TPdfHarfBuzzTextShaper, care leagă libharfbuzz la execuție, astfel încât o bibliotecă lipsă este o condiție raportată, nu un eșec de încărcare

Arhitectura de modelare de text conectabilă în componenta Delphi PDFium: ConfigureTextShaper, ActiveTextShaper și ClearTextShaper gestionează un singur backend instalat, Uniscribe pe Windows și un HarfBuzz legat la execuție sub Free Pascal
Fiecare apel de modelare se dirijează prin singurul obiect shaper instalat, cu un implicit de platformă pe fiecare țintă

O interfață, două backend-uri care împart munca complet diferit. Înțelegerea acelei asimetrii este ceea ce împiedică calea portabilă să producă text modelat corect și poziționat greșit

De ce backend-ul Windows este o clasă, iar cel portabil trei bucăți?

Pentru că Uniscribe sunt patru API-uri care se prefac a fi unul. ScriptItemize segmentează un șir după script și rezolvă nivelurile bidirecționale; ScriptShape cartografiază caractere la glife; ScriptPlace calculează avansurile și offset-urile; ScriptLayout pune trecerile rezultate în ordine vizuală. Un backend construit pe el nu mai are deci nimic de adăugat, ceea ce este motivul pentru care shaper-ul Windows este o singură clasă cu o singură metodă

HarfBuzz acoperă cele două din mijloc. El modelează și plasează o trecere a cărei direcție și script au fost deja decise de apelant, și nu are nicio părere despre cum se împarte un paragraf în treceri sau în ce ordine apar acele treceri. Deci backend-ul portabil furnizează restul: algoritmul bidirecțional rezolvă nivelurile de înglobare, funcțiile Unicode HarfBuzz segmentează textul după script, iar trecerile sunt aranjate în ordinea vizuală pe care o produce regula L2 din UAX #9. Jumătatea bidirecțională este suficient de substanțială încât să fie o unitate proprie, descrisă în articolul despre nivelurile de înglobare UAX #9

Comparație de pipeline de modelare pentru text PDF: Uniscribe furnizează ScriptItemize, ScriptShape, ScriptPlace și ScriptLayout într-o singură clasă, în timp ce HarfBuzz acoperă doar modelarea și plasarea în jurul etapelor UAX #9 proprii componentei
Uniscribe acoperă toate cele patru etape; calea portabilă trebuie să furnizeze ea însăși itemizarea și ordinea vizuală

Shaper-ul nu rezolvă fonturi, și asta este deliberat

Uniscribe citește binarul fontului dintr-un context de dispozitiv GDI. Nu există un echivalent portabil pentru asta, iar inventarea unuia în interiorul unei unități de modelare ar însemna a decide, în numele fiecărei aplicații, dacă fonturile vin din fontconfig, din CoreText, dintr-un dosar de fonturi al aplicației sau dintr-o bază de date. Deci backend-ul HarfBuzz primește un resolver: un callback care cartografiază un nume de font la octeții TrueType sau OpenType. Returnarea lui False face cererea de modelare să eșueze în același fel în care o face un font GDI ilizibil pe Windows

uses
  FPdfTextShaping
{$IFDEF FPC}
  , FPdfTextShapingHb
{$ENDIF}
  ;

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Politica dumneavoastră: fontconfig, CoreText, un dosar de fonturi
  // al aplicației, o bază de date
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Proprietatea trece la unitate; apelați o dată la pornire,
  // înainte ca ceva să modeleze text
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // Pe Delphi implicitul platformei (Uniscribe) este creat la cerere,
  // deci nu este necesară deloc o instalare
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Păstrarea descoperirii de fonturi în afara shaper-ului are un al doilea beneficiu care apare în servere: același proces poate modela cu un set de fonturi înglobat care nu are nimic de-a face cu ceea ce este instalat pe mașină, ceea ce vreți când rezultatul trebuie să fie reproductibil octet cu octet între gazde. Componenta expune de asemenea un furnizor de fonturi de sistem gazdă pentru cazurile în care vreți într-adevăr fonturile instalate, tratat în articolul despre furnizorul de fonturi de sistem

Înregistrarea de rezultat este neutru față de backend, iar clusterele sunt motivul

Ambele backend-uri umple același TPdfShapedText: textul sursă, numele fontului, mărimea, octeții fontului, un vector de treceri, lățimea totală, numărul de glife și numărul de caractere logice. Fiecare TPdfShapedRun poartă segmentul său în textul sursă, poziția sa vizuală X, lățimea sa, nivelul său bidirecțional și un fanion dreapta-stânga, plus glifele lui. Fiecare TPdfShapedGlyph poartă un identificator de glifă, un avans, offset-uri X și Y, și clusterul căruia îi aparține ca un început și o lungime în textul sursă

Câmpurile de cluster acelea sunt ceea ce face înregistrarea folosibilă, nu doar informativă. Modelarea nu este o cartografiere unu-la-unu: o silabă Devanagari devine o glifă din patru caractere, o ligatură arabă îmbină două, iar un singur caracter poate produce mai multe semne. Fără segmente de cluster nu puteți plasa un cursor, nu puteți testa la atingere un clic sau evidenția o selecție, deoarece nu puteți spune cărui caracter aparține o glifă. Cu ele, aritmetica este locală, iar același cod funcționează pentru ambele backend-uri

Segmente de cluster de glife în TPdfShapedText: o glifă de silabă Devanagari din patru caractere, o ligatură arabă din două și o bază plus semn dintr-un caracter, fiecare cartografiat înapoi prin ClusterStart și ClusterLength
Segmente de cluster cartografiază fiecare glifă înapoi la caracterele sale sursă, astfel încât cursoarele, testele de atingere și selecțiile funcționează
var
  Shaped: TPdfShapedText;
  R, G: Integer;
begin
  if ShapePdfText(Line, 'Noto Sans Arabic', 14, ptdAuto, Shaped) then
    for R := 0 to High(Shaped.Runs) do
    begin
      // Trecerile sosesc deja în ordine vizuală cu VisualX completat
      X := Shaped.Runs[R].VisualX;
      for G := 0 to High(Shaped.Runs[R].Glyphs) do
      begin
        EmitGlyph(Shaped.Runs[R].Glyphs[G].GlyphID,
          X + Shaped.Runs[R].Glyphs[G].OffsetX,
          Shaped.Runs[R].Glyphs[G].OffsetY);
        X := X + Shaped.Runs[R].Glyphs[G].Advance;
      end;
    end;
end;

Bugetele aparțin înregistrării de opțiuni

TPdfTextShapingOptions poartă o direcție plus trei limite: maxim de caractere, maxim de glife și maxim de treceri, cu o funcție de clasă Default care umple valori sensibile. Limitele nu sunt paranoie față de date de intrare malformate; sunt aritmetică. Modelarea expandează: un font cu substituție contextuală agresivă poate emite mai multe glife decât caractere de intrare, iar un paragraf care alternează scripturile la fiecare câteva caractere produce o trecere per comutare. Un document asamblat pentru a maximiza ambele transformă un șir modest într-o alocare mare, iar un serviciu care modelează text din PDF-uri nesigure are nevoie de o limită pe care a ales-o, nu de una pe care o impune mașina

Stabilirea direcției explicit, în loc să o lăsați pe automat, merită făcută ori de câte ori o știți deja. Automatul aplică regulile de direcție de paragraf pentru a ghici de la primul caracter puternic, ceea ce este corect pentru text liber și greșit pentru un câmp de formular a cărui direcție este o proprietate a câmpului, nu a valorii pe care cineva a bătut-o în el

Legare la execuție, nu o dependență de build

Backend-ul HarfBuzz încarcă biblioteca dinamic. Aceasta este o decizie de implementare cu consecințe reale: un singur binar rulează pe o mașină cu HarfBuzz și pe una fără, raportând capacitate redusă în al doilea caz în loc să nu pornească. Pentru o bibliotecă livrată altor dezvoltatori acesta este singurul aranjament lucrativ, deoarece nu puteți cere fiecărui consumator al unei componente PDF să achiziționeze și să coreleze versiunile unei biblioteci de modelare de care s-ar putea să nu aibă nevoie

Regula corespunzătoare pentru apelanți este să verificați. ActiveTextShaper returnează nil când platforma nu are implicit și niciunul nu a fost configurat, iar punctul de intrare de modelare raportează asta drept shaper indisponibil, nu ca eșec de modelare. Acestea sunt probleme diferite și merită mesaje diferite: una este o lacună de implementare, cealaltă este o problemă de font sau de text

Instalați o dată, înainte ca ceva să modeleze

Instalarea înlocuiește și eliberează shaper-ul anterior, deci a o apela repetat este sigur dar inutil, iar a o apela în timp ce alt fir modelează nu este deloc sigur. Faceți-o în timpul pornirii. Dacă aveți nevoie să reveniți la implicitul platformei mai târziu, transmiteți nil, ceea ce este și felul de a anula un dublur de test la finalul unui test

Odată instalat un backend, măsurarea și împachetarea se comportă la fel pe ambele platforme, deoarece consumă metricile de treceri și glife în loc să apeleze direct platforma; modelul de împachetare este descris în articolul despre măsurarea textului și împachetarea pe cuvinte. Platformele și toolchain-urile suportate pentru componentă sunt listate pe pagina de produs PDFium Delphi component