Technisch artikel

Plugbare tekstshaping: Uniscribe en HarfBuzz in Delphi

Tekstshaping in de PDFium-component verloopt door één instelbaar object. ConfigureTextShaper installeert de shaper waardoor elk shaping-toegangspunt routeert, en vervangt en geeft vrij wat er stond; ActiveTextShaper geeft de geïnstalleerde terug en maakt de platformstandaard bij eerste gebruik aan; ActiveTextShaperName meldt welke backend actief is; ClearTextShaper laat de installatie vallen en laat de standaard opnieuw worden aangemaakt. Onder Windows is de standaard TPdfUniscribeTextShaper. Onder Free Pascal bestaat TPdfHarfBuzzTextShaper, die libharfbuzz tijdens runtime bindt zodat een ontbrekende bibliotheek een gemelde toestand is in plaats van een lafstoring

Architectuur van plugbare tekstshaping in de PDFium Delphi-component: ConfigureTextShaper, ActiveTextShaper en ClearTextShaper beheren één geïnstalleerde backend, Uniscribe onder Windows en een runtime-gebonden HarfBuzz onder Free Pascal
Elke shaping-aanroep routeert door het ene geïnstalleerde shaperobject, met een platformstandaard per doel

Eén interface, twee backends die het werk volkomen anders verdelen. Die asymmetrie begrijpen is wat verhindert dat het draagbare pad tekst oplevert die correct gevormd en verkeerd gepositioneerd is

Waarom is de Windows-backend één klasse en de draagbare drie stukken?

Omdat Uniscribe vier API's is die doen alsof ze er één zijn. ScriptItemize segmenteert een string per schrift en resolveert bidirectionele niveaus; ScriptShape mapt tekens op glyphs; ScriptPlace berekent advances en offsets; ScriptLayout zet de resulterende runs in visuele volgorde. Een backend daarop gebouwd heeft dus niets meer toe te voegen, en daarom is de Windows-shaper één klasse met één methode

HarfBuzz dekt de middelste twee. Die vormt en plaatst een run waarvan de aanroeper de richting en het schrift al heeft beslist, en die heeft geen mening over hoe een alinea in runs splitst of in welke volgorde die runs verschijnen. De draagbare backend levert dus de rest: het bidirectionele algoritme resolveert embeddingniveaus, de HarfBuzz-Unicodefuncties segmenteren de tekst per schrift, en de runs worden gelegd in de visuele volgorde die regel L2 van UAX #9 oplevert. De bidirectionele helft is substantieel genoeg voor een eigen unit, beschreven in het artikel over UAX #9 embeddingniveaus

Vergelijking van shaping-pijplijnen voor PDF-tekst: Uniscribe levert ScriptItemize, ScriptShape, ScriptPlace en ScriptLayout in één klasse, terwijl HarfBuzz alleen shaping en plaatsing dekt rond de eigen UAX #9-fasen van de component
Uniscribe dekt alle vier de fasen; het draagbare pad moet itemisatie en visuele volgorde zelf leveren

De shaper resolveert geen fonts, en dat is opzettelijk

Uniscribe leest het fontbinair bestand uit een GDI-device context. Er is geen draagbaar equivalent van, en er één verzinnen binnen een shaping-unit zou betekenen dat namens elke applicatie wordt beslist of fonts uit fontconfig komen, uit CoreText, uit een fontmap van de applicatie of uit een database. Daarom neemt de HarfBuzz-backend een resolver: een callback die een fontnaam mapt op de TrueType- of OpenType-bytes. False teruggeven laat het shaping-verzoek falen op dezelfde manier waarop een onleesbaar GDI-font dat onder Windows laat falen

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

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Uw beleid: fontconfig, CoreText, een fontmap van de app, een database
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Eigendom gaat naar de unit; roep één keer aan tijdens de opstart,
  // voordat er ook maar iets tekst vormt
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // Onder Delphi wordt de platformstandaard (Uniscribe) op verzoek aangemaakt,
  // dus helemaal geen installatie nodig
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Fontontdekking buiten de shaper houden heeft een tweede voordeel dat zich in servers toont: hetzelfde proces kan vormen met een ingebedde fontset die niets te maken heeft met wat op de machine is geïnstalleerd, en dat is wat u wilt wanneer de uitvoer byte-reproduceerbaar moet zijn over hosts heen. De component stelt ook een fontprovider van het hostsysteem bloot voor de gevallen waarin u wél geïnstalleerde fonts wilt, behandeld in het artikel over de systeemfontprovider

Het resultaatrecord is backend-neutraal, en clusters zijn de reden

Beide backends vullen dezelfde TPdfShapedText in: de brontekst, fontnaam, grootte, fontbytes, een array van runs, de totale breedte, het glyphaantal en het logische tekenaantal. Elke TPdfShapedRun draagt zijn reikwijdte in de brontekst, zijn visuele X-positie, zijn breedte, zijn bidirectioneel niveau en een vlag rechts-naar-links, plus zijn glyphs. Elke TPdfShapedGlyph draagt een glyph-identificator, een advance, X- en Y-offsets en de cluster waartoe die behoort als een begin en een lengte in de brontekst

Die clustervelden zijn wat het record bruikbaar maakt in plaats van slechts informatief. Shaping is geen één-op-één-mapping: een Devanagari-lettergreep wordt één glyph uit vier tekens, een Arabische ligatuur voegt er twee samen, en één teken kan verschillende diakritische tekens opleveren. Zonder clusterspans kunt u geen caret plaatsen, een klik hit-testen of een selectie markeren, want u kunt niet zeggen bij welke tekens een glyph hoort. Met die velden is de rekenkunde lokaal en werkt dezelfde code voor beide backends

Glyph-clusterspans in TPdfShapedText: één Devanagari-lettergreepglyph uit vier tekens, een Arabische ligatuur uit twee, en een basis plus diakriticum uit één teken, elk terug gemapt via ClusterStart en ClusterLength
Clusterspans mappen elke glyph terug op zijn brontekens zodat carets, hit-tests en selecties werken
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
      // Runs komen al binnen in visuele volgorde met VisualX ingevuld
      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;

Budgets horen in het optierecord

TPdfTextShapingOptions draagt een richting mee plus drie caps: maximaal aantal tekens, maximaal aantal glyphs en maximaal aantal runs, met een klassenfunctie Default die zinnige waarden invult. De caps zijn geen paranoïde over misvormde input; het is rekenkunde. Shaping zet uit: een font met agressieve contextuele substitutie kan meer glyphs uitzenden dan invoertekens, en een alinea die om de paar tekens van schrift wisselt levert een run per wisseling op. Een document dat zo is opgebouwd om beide te maximaliseren maakt van een bescheiden string een grote toewijzing, en een dienst die tekst uit onbetrouwbare PDFs vormt heeft een limiet nodig die die zelf koos in plaats van een limiet die de machine oplegt

De richting expliciet instellen in plaats van die op automatisch te laten is het doen waard zodra u die al weet. Automatisch past de regels voor alineabrichting toe om te gokken vanuit het eerste sterke teken, wat juist is voor vrije tekst en fout voor een formulierveld waarvan de richting een eigenschap van het veld is in plaats van van de waarde die iemand erin typte

Binding tijdens runtime, geen buildafhankelijkheid

De HarfBuzz-backend laadt de bibliotheek dynamisch. Dat is een uitrolbeslissing met echte gevolgen: één binary draait op een machine met HarfBuzz en op een machine zonder, en meldt in het tweede geval verminderde capaciteit in plaats van niet op te starten. Voor een bibliotheek die aan andere ontwikkelaars wordt geleverd is dat de enige werkbare opzet, want u kunt niet van elke verbruiker van een PDF-component eisen dat die een shaping-bibliotheek aanschaft en op versie matcht die die misschien niet nodig heeft

De bijbehorende regel voor aanroepers is controleren. ActiveTextShaper geeft nil terug wanneer het platform geen standaard heeft en er geen is ingesteld, en het shaping-toegangspunt meldt dat als een niet-beschikbare shaper in plaats van als een shaping-fout. Dat zijn verschillende problemen en ze verdienen verschillende meldingen: de ene is een uitrolgat, de andere is een font- of tekstprobleem

Installeer één keer, voordat er iets vormt

De installatie vervangt de vorige shaper en geeft die vrij, dus herhaald aanroepen is veilig maar zinloos, en aanroepen terwijl een andere thread aan het vormen is, is helemaal niet veilig. Doe het tijdens de opstart. Moet u later terugvallen op de platformstandaard, geef dan nil door, en dat is ook hoe u een testdubbel aan het eind van een test ongedaan maakt

Zodra een backend is geïnstalleerd, gedragen meten en afbreken zich hetzelfde op beide platforms, omdat ze de run- en glyphmetrieken verbruiken in plaats van het platform rechtstreeks aan te roepen; het afbreekmodel wordt beschreven in het artikel over tekstmeting en woordafbreking. Ondersteunde platforms en toolchains voor de component staan op de productpagina van de PDFium Delphi component