Teknisk artikel

Pluggbar textformning: Uniscribe och HarfBuzz i Delphi

Textformning i PDFium-komponenten går genom ett installerbart objekt. ConfigureTextShaper installerar formaren som varje formningsingångspunkt leder genom, ersättande och frigörande vadhelst som fanns där; ActiveTextShaper returnerar den installerade och skapar plattformsstandarden vid första användningen; ActiveTextShaperName rapporterar vilken backend som är aktiv; ClearTextShaper tappar installationen och låter standarden skapas igen. På Windows är standarden TPdfUniscribeTextShaper. Under Free Pascal finns TPdfHarfBuzzTextShaper, som binder libharfbuzz vid körtid så att ett saknat bibliotek är ett rapporterat tillstånd i stället för ett laddfel

Arkitektur för pluggbar textformning i PDFium Delphi-komponenten: ConfigureTextShaper, ActiveTextShaper och ClearTextShaper hanterar en installerad backend, Uniscribe på Windows och ett vid körtid bundet HarfBuzz under Free Pascal
Varje formningsanrop leds genom det enda installerade formningsobjektet, med en plattformsstandard på varje mål

Ett gränssnitt, två backends som delar arbetet helt olika. Att förstå den asymmetrin är det som hindrar det portabla spåret från att producera text som är formad korrekt och placerad fel

Varför är Windows-backend en klass och den portabla tre delar?

För Uniscribe är fyra API:er som låtsas vara ett. ScriptItemize segmenterar en sträng per skript och löser tvåriktade nivåer; ScriptShape mappar tecken till glyfer; ScriptPlace beräknar framåtlöpningar och offset; ScriptLayout lägger de resulterande körningarna i visuell ordning. En backend byggd på den har därför ingenting kvar att lägga till, vilket är varför Windows-formaren är en enda klass med en enda metod

HarfBuzz täcker de mellersta två. Den formar och placerar en körning vars riktning och skript anroparen redan har beslutat, och den har ingen åsikt om hur ett stycke delas i körningar eller i vilken ordning de körningarna uppträder. Så den portabla backenden tillhandahåller resten: den tvåriktade algoritmen löser inbäddningsnivåer, HarfBuzz Unicode-funktioner segmenterar texten per skript och körningarna läggs ut i den visuella ordning som UAX #9-regel L2 producerar. Den tvåriktade hälften är tillräckligt omfattande för att vara sin egen enhet, beskriven i artikeln om UAX #9-inbäddningsnivåer

Jämförelse av formningspipeline för PDF-text: Uniscribe tillhandahåller ScriptItemize, ScriptShape, ScriptPlace och ScriptLayout inuti en klass, medan HarfBuzz täcker bara formning och placering kring komponentens egna UAX #9-steg
Uniscribe täcker alla fyra steg; det portabla spåret måste själv tillhandahålla segmentering och visuell ordning

Formaren löser inte teckensnitt, och det är med flit

Uniscribe läser teckensnittsbinären ur en GDI device context. Det finns ingen portabel motsvarighet till det, och att hitta på en inuti en formningsenhet skulle betyda att besluta, på alla applikationers vägnar, huruvida teckensnitt kommer från fontconfig, från CoreText, från en applikationsfontmapp eller från en databas. Så HarfBuzz-backenden tar en resolver: ett callback som mappar ett teckensnittsnamn till TrueType- eller OpenType-bytena. Att returnera False fäller formningsbegäran på samma sätt som ett oläsbart GDI-teckensnitt fäller den på Windows

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

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Din policy: fontconfig, CoreText, en appfontmapp, en databas
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Ägandet övergår till enheten; anropa en gång vid uppstarten,
  // innan något formar text
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // På Delphi skapas plattformsstandarden (Uniscribe) på begäran,
  // så ingen installation behövs alls
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Att hålla teckensnittsfinning utanför formaren har en andra fördel som visar sig i servrar: samma process kan forma med en inbäddad fontuppsättning som inte har något att göra med vad som är installerat på maskinen, vilket är vad du vill ha när utmatningen måste vara byte-reproducerbar mellan värdar. Komponenten exponerar också en värdsystemfontleverantör för fallen där du faktiskt vill ha installerade teckensnitt, tas upp i artikeln om systemfontleverantören

Resultatposten är backend-neutral, och kluster är orsaken

Båda backends fyller samma TPdfShapedText: källtexten, teckensnittsnamnet, storleken, teckensnittsbytena, en array av körningar, den totala bredden, glyfantalet och det logiska teckenantalet. Varje TPdfShapedRun bär sitt stycke i källtexten, sin visuella X-position, sin bredd, sin tvåriktade nivå och en höger-till-vänster-flagga, plus sina glyfer. Varje TPdfShapedGlyph bär en glyfidentifierare, en framåtlöpning, X- och Y-offset och klustret den tillhör som en start och en längd i källtexten

De klusterfälten är det som gör posten användbar snarare än blott informativ. Formning är inte en en-till-en-mappning: en devanagari stavelse blir en glyf ur fyra tecken, en arabisk ligatur slår ihop två, och ett enskilt tecken kan producera flera diakritiska märken. Utan klusterstycken kan du inte placera en textmarkör, träff testa ett klick eller markera ett urval, eftersom du inte kan säga vilka tecken en glyf tillhör. Med dem är aritmetiken lokal och samma kod fungerar för båda backends

Glyfklusterstycken i TPdfShapedText: en devanagari-stavelseglyf ur fyra tecken, en arabisk ligatur ur två, och en bas plus diakritiskt märke ur ett tecken, vart och ett mappat tillbaka via ClusterStart och ClusterLength
Klusterstycken mappar varje glyf tillbaka till sina källtecken så att textmarkörer, träfftester och urval fungerar
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
      // Körningar anländer redan i visuell ordning med VisualX ifyllt
      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;

Budgetar hör hemma i alternativposten

TPdfTextShapingOptions bär en riktning plus tre tak: maximalt antal tecken, maximalt antal glyfer och maximalt antal körningar, med en klassfunktion Default som fyller förnuftiga värden. Taken är inte paranoia om felformerad indata; de är aritmetik. Formning expanderar: ett teckensnitt med aggressiv kontextuell substitution kan avge fler glyfer än indatatecken, och ett stycke som växlar skript vart några tecken producerar en körning per växling. Ett dokument monterat för att maximera båda gör en blygsam sträng till en stor allokering, och en tjänst som formar text ur obetrodda PDF:er behöver en gräns den valde i stället för en gräns maskinen påtvingar

Att ställa in riktningen uttryckligen i stället för att lämna den på automatisk är värt att göra närhelst du redan vet den. Automatisk tillämpar styckriktningsreglerna för att gissa från första starka tecknet, vilket är rätt för fri text och fel för ett formulärfält vars riktning är en egenskap hos fältet snarare än hos värdet någon skrev in i det

Bindning vid körtid, inte ett byggberoende

HarfBuzz-backenden laddar biblioteket dynamiskt. Det är ett distributionsbeslut med verkliga konsekvenser: en binärfil körs på en maskin med HarfBuzz och på en maskin utan den, med rapporterad reducerad förmåga i det andra fallet i stället för att misslyckas starta. För ett bibliotek levererat till andra utvecklare är det det enda fungerande upplägget, eftersom du inte kan kräva att varje konsument av en PDF-komponent skaffar och versionsmatchar ett formningsbibliotek de kanske inte behöver

Motsvarande regel för anropare är att kontrollera. ActiveTextShaper returnerar nil när plattformen inte har någon standard och ingen konfigurerades, och formningsingångspunkten rapporterar det som en otillgänglig formare i stället för som ett formningsfel. De är olika problem och förtjänar olika meddelanden: den ena är ett distributionsgap, den andra ett teckensnitts- eller textproblem

Installera en gång, innan något formar

Installationen ersätter och frigör den tidigare formaren, så att anropa den upprepade gånger är säkert men poänglöst, och att anropa den medan en annan tråd formar är inte säkert alls. Gör det under uppstarten. Om du behöver falla tillbaka till plattformsstandarden senare, skicka nil, vilket också är hur du ogör ett testdubbel i slutet av ett test

När en backend väl är installerad beter sig mätning och radbrytning likadant på båda plattformarna, eftersom de konsumerar körnings- och glyfmåtten i stället för att anropa plattformen direkt; radbrytningsmodellen beskrivs i artikeln om textmätning och ordlindning. Stödda plattformar och verktygskedjor för komponenten listas på produktsidan för PDFium Delphi component