Teknisk artikkel

Pluggbar tekstshaping: Uniscribe og HarfBuzz i Delphi

Tekstshaping i PDFium-komponenten går gjennom ett installerbart objekt. ConfigureTextShaper installerer shaperen som hvert shaping-inngangspunkt ruter gjennom, og erstatter og frigjør det som var der; ActiveTextShaper returnerer den installerte og oppretter plattformstandarden ved første bruk; ActiveTextShaperName rapporterer hvilken backend som er levende; ClearTextShaper dropper installasjonen og lar standarden opprettes igjen. På Windows er standarden TPdfUniscribeTextShaper. Under Free Pascal finnes TPdfHarfBuzzTextShaper, som binder libharfbuzz ved kjøretid slik at et manglende bibliotek er en rapportert tilstand snarere enn en lastefeil

Pluggbar tekstshaping-arkitektur i PDFium Delphi-komponenten: ConfigureTextShaper, ActiveTextShaper og ClearTextShaper administrerer én installert backend, Uniscribe på Windows og en runtime-bundet HarfBuzz under Free Pascal
Hvert shaping-kall ruter gjennom det enkelte installerte shaper-objektet, med en plattformstandard på hvert mål

Ét grensesnitt, to backends som deler arbeidet helt forskjellig. Å forstå den asymmetrien er det som stopper den portable veien fra å produsere tekst som er shapet korrekt og posisjonert feil

Hvorfor er Windows-backenden én klasse og den portable tre deler?

Fordi Uniscribe er fire API-er som later som de er én. ScriptItemize segmenterer en streng etter skript og løser bidireksjonelle nivåer; ScriptShape mapper tegn til glyfer; ScriptPlace beregner fremstøt og forskyvninger; ScriptLayout legger de resulterende runene i visuell rekkefølge. En backend bygget på den har derfor ingenting igjen å tilføye, noe som er hvorfor Windows-shaperen er én enkelt klasse med én enkelt metode

HarfBuzz dekker de to midterste. Den shaper og plasserer en run hvis retning og skript kalleren allerede har avgjort, og den har ingen mening om hvordan et avsnitt deler seg i runer eller hvilken rekkefølge de runene opptrer i. Så den portable backenden leverer resten: den bidireksjonelle algoritmen løser innbyggingsnivåer, HarfBuzz Unicode-funksjonene segmenterer teksten etter skript, og runene legges i den visuelle rekkefølgen UAX #9 regel L2 produserer. Den bidireksjonelle halvdelen er omfattende nok til å være sin egen enhet, beskrevet i artikkelen om UAX #9-innbyggingsnivåer

Shaping-pipeline-sammenligning for PDF-tekst: Uniscribe leverer ScriptItemize, ScriptShape, ScriptPlace og ScriptLayout inne i én klasse, mens HarfBuzz bare dekker shaping og plassering rundt komponentens egne UAX #9-stadier
Uniscribe dekker alle fire stadier; den portable veien må selv levere itemisering og visuell rekkefølge

Shaperen løser ikke fonter, og det er bevisst

Uniscribe leser fontbinæren ut av en GDI device context. Det finnes ingen portabel ekvivalent til det, og å finne på én inne i en shaping-enhet ville bety å avgjøre, på vegne av enhver applikasjon, om fonter kommer fra fontconfig, fra CoreText, fra en applikasjonsfontmappe eller fra en database. Så HarfBuzz-backenden tar en resolver: en callback som mapper et fontnavn til TrueType- eller OpenType-bytene. Å returnere False feiler shaping-forespørselen på samme måte som en uleselig GDI-font feiler 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 app-fontmappe, en database
  Result := FLookup.TryGetValue(LowerCase(FontName), Path);
  if Result then
    FontData := TFile.ReadAllBytes(Path);
end;

procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
  // Eierskapet går til enheten; kall én gang under oppstart,
  // før noe shaper tekst
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // På Delphi opprettes plattformstandarden (Uniscribe) ved behov,
  // så ingen installasjon trengs i det hele tatt
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

Å holde fontoppdagelse utenfor shaperen har en annen fordel som viser seg i servere: den samme prosessen kan shape med et innebygd fontsett som ikke har noe med det som er installert på maskinen å gjøre, noe som er det du vil ha når output må være byte-reproduserbar på tvers av verter. Komponenten eksponerer også en vertssystemfontleverandør for tilfellene der du faktisk vil ha installerte fonter, dekket i artikkelen om systemfontleverandøren

Resultatrecorden er backend-nøytral, og klynger er grunnen

Begge backends fyller den samme TPdfShapedText: kildeteksten, fontnavnet, størrelsen, fontbytene, en array av runer, totalbredden, glyfantallet og det logiske tegnantallet. Hver TPdfShapedRun bærer sitt spenn i kildeteksten, sin visuelle X-posisjon, sin bredde, sitt bidireksjonelle nivå og et høyre-til-venstre-flagg, pluss sine glyfer. Hver TPdfShapedGlyph bærer en glyfidentifikator, et fremstøt, X- og Y-forskyvninger, og klyngen den tilhører som en start og en lengde i kildeteksten

De klyngefeltene er det som gjør recorden brukbar snarere enn bare informativ. Shaping er ikke en én-til-én-mapping: en devanagaristavelse blir én glyf fra fire tegn, en arabisk ligatur fletter to, og et enkelt tegn kan produsere flere merker. Uten klyngespenn kan du ikke plassere en caret, treffteste et klikk, eller utheve et utvalg, fordi du ikke kan si hvilke tegn en glyf tilhører. Med dem er aritmetikken lokal, og den samme koden fungerer for begge backends

Glyfklyngespenn i TPdfShapedText: én devanagaristavelse-glyf fra fire tegn, en arabisk ligatur fra to, og en base pluss merke fra ett tegn, hvert mappet tilbake gjennom ClusterStart og ClusterLength
Klyngespenn mapper hver glyf tilbake til sine kildetegn slik at carets, trefftester og utvalg fungerer
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
      // Runer ankommer allerede i visuell rekkefølge med VisualX fylt ut
      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;

Budsjetter hører hjemme i opsjonsrecorden

TPdfTextShapingOptions bærer en retning pluss tre tak: maksimum tegn, maksimum glyfer og maksimum runer, med en Default-klassefunksjon som fyller fornuftige verdier. Takene er ikke paranoia om feilformet input; de er aritmetikk. Shaping ekspanderer: en font med aggressiv kontekstuell substitusjon kan sende ut flere glyfer enn inngangstegn, og et avsnitt som veksler skript hvert par tegn produserer en run per veksling. Et dokument satt sammen for å maksimere begge gjør en beskjeden streng til en stor allokering, og en tjeneste som shaper tekst fra utiltrudte PDF-er trenger en grense den valgte snarere enn en grense maskinen pålegger

Å sette retningen eksplisitt snarere enn å la den stå på automatisk, er verdt å gjøre når som helst du allerede vet den. Automatisk anvender avsnittsretningreglene for å gjette fra det første sterke tegnet, noe som er riktig for fri tekst og feil for et skjemafelt hvis retning er en egenskap ved feltet snarere enn ved verdien noen tastet inn i det

Runtime-binding, ikke en byggavhengighet

HarfBuzz-backenden laster biblioteket dynamisk. Det er en utrullingsavgjørelse med reelle konsekvenser: én binærfil kjører på en maskin med HarfBuzz og på en maskin uten den, og rapporterer redusert evne i det andre tilfellet i stedet for å feile å starte. For et bibliotek sendt til andre utviklere er det den eneste fungerbare ordningen, fordi du ikke kan kreve at hver konsument av en PDF-komponent skal skaffe og versjonsmatche et shaping-bibliotek de kanskje ikke trenger

Den tilsvarende regelen for kallere er å sjekke. ActiveTextShaper returnerer nil når plattformen ikke har noen standard og ingen ble konfigurert, og shaping-inngangspunktet rapporterer det som en utilgjengelig shaper snarere enn som en shaping-feil. Det er forskjellige problemer og fortjener forskjellige meldinger: den ene er et utrullingshull, den andre er et font- eller tekstproblem

Installer én gang, før noe shaper

Installasjon erstatter og frigjør den forrige shaperen, så å kalle den gjentatte ganger er trygt men nytteløst, og å kalle den mens en annen tråd shaper, er ikke trygt i det hele tatt. Gjør det under oppstart. Hvis du trenger å falle tilbake til plattformstandarden senere, send nil, noe som også er hvordan du angrer en testdobbel på slutten av en test

Når en backend er installert, oppfører måling og ombryting seg likt på begge plattformer, siden de konsumerer run- og glyfmetrikkene snarere enn å kalle plattformen direkte; ombrytingsmodellen er beskrevet i artikkelen om tekstmåling og ordombryting. Støttede plattformer og verktøykjeder for komponenten er oppført på produktsiden for PDFium Delphi component