Teknisk artikel

Pluggbar tekstshaping: Uniscribe og HarfBuzz i Delphi

Tekstshaping i PDFium-komponenten går gennem ét installérbart objekt. ConfigureTextShaper installerer shaperen, som hvert shaping-entry point router gennem, og erstatter og frigør, hvad end der var dér; ActiveTextShaper returnerer den installerede og skaber platformstandard ved første brug; ActiveTextShaperName rapporterer, hvilken backend der er live; ClearTextShaper dropper installationen og lader standarden blive skabt igen. På Windows er standarden TPdfUniscribeTextShaper. Under Free Pascal findes TPdfHarfBuzzTextShaper, som binder libharfbuzz på kørselstidspunktet, så et manglende bibliotek er en rapporteret tilstand snarere end en indlæsningsfejl

Pluggbar tekstshaping-arkitektur i PDFium Delphi-komponenten: ConfigureTextShaper, ActiveTextShaper og ClearTextShaper administrerer én installeret backend, Uniscribe på Windows og et runtime-bundet HarfBuzz under Free Pascal
Hvert shaping-kald router gennem det eneste installerede shaper-objekt med en platformstandard på hvert mål

Ét interface, to backends, der deler arbejdet fuldstændig forskelligt. At forstå den asymmetri er det, der forhindrer den portable vej i at producere tekst, der er shapet korrekt og positioneret forkert

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

Fordi Uniscribe er fire API'er, der udgiver sig for at være én. ScriptItemize segmenterer en streng efter script og opløser bidirektionelle niveauer; ScriptShape mapper tegn til glyphs; ScriptPlace beregner advances og offsets; ScriptLayout sætter de resulterende runs i visuel orden. En backend bygget på den har derfor intet tilbage at tilføje, hvilket er grunden til, at Windows-shaperen er én klasse med én metode

HarfBuzz dækker de to midterste. Den shaper og placerer et run, hvis retning og script kalderen allerede har besluttet, og den har ingen mening om, hvordan et afsnit splittes i runs, eller i hvilken orden de runs optræder. Så den portable backend leverer resten: den bidirektionelle algoritme opløser embedding-niveauer, HarfBuzz Unicode-funktioner segmenterer teksten efter script, og runsene lægges i den visuelle orden, som UAX #9-reglen L2 producerer. Den bidirektionelle halvdel er tilstrækkelig stor til at være sin egen unit, beskrevet i artiklen om UAX #9 embedding-niveauer

Shaping-pipeline-sammenligning til PDF-tekst: Uniscribe leverer ScriptItemize, ScriptShape, ScriptPlace og ScriptLayout inde i én klasse, mens HarfBuzz kun dækker shaping og placering omkring komponentens egne UAX #9-trin
Uniscribe dækker alle fire trin; den portable vej må selv levere itemization og visuel orden

Shaperen opløser ikke fonte, og det er bevidst

Uniscribe læser fontbinæren ud af en GDI device context. Der findes ingen portabel ækvivalent til det, og at opfinde en inde i en shaping-unit ville betyde at afgøre på vegne af enhver applikation, om fonte kommer fra fontconfig, fra CoreText, fra en applikations fontmappe eller fra en database. Så HarfBuzz-backenden tager en resolver: et callback, der mapper et fontnavn til TrueType- eller OpenType-bytene. At returnere False får shaping-anmodningen til at fejle på samme måde, som en ulæselig GDI-font fejler den på Windows

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

function TFontCatalogue.Resolve(const FontName: WideString;
  out FontData: TBytes): Boolean;
var
  Path: string;
begin
  // Din politik: 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}
  // Ejerskabet går til uniten; kald én gang under opstart,
  // før noget shaper tekst
  ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
  // På Delphi skabes platformstandard (Uniscribe) på anmodning,
  // så ingen installation behøves overhovedet
  LogInfo('shaping backend: ' + ActiveTextShaperName);
end;

At holde fontopdagelse uden for shaperen har en anden gevinst, der viser sig på servere: samme proces kan shape med et indlejret fontsæt, der intet har at gøre med, hvad der er installeret på maskinen, hvilket er det, du vil have, når output skal være byte-reproducerbart på tværs af værter. Komponenten eksponerer også en værtssystem-fontudbyder til de tilfælde, hvor du reelt vil have installerede fonte, dækket i artiklen om systemfontudbyderen

Resultatrecorden er backend-neutral, og clusters er grunden

Begge backends fylder samme TPdfShapedText: kildeteksten, fontnavnet, størrelsen, fontbytes, et array af runs, totalbredden, glyph-antallet og det logiske tegnantal. Hvert TPdfShapedRun bærer sit span i kildeteksten, sin visuelle X-position, sin bredde, sit bidirektionelle niveau og et højre-mod-venstre-flag plus sine glyphs. Hvert TPdfShapedGlyph bærer en glyph-identifikator, en advance, X- og Y-offsets og det cluster, det tilhører, som en start og en længde i kildeteksten

De cluster-felter er det, der gør recorden brugbar snarere end blot informativ. Shaping er ikke en en-til-en-mapping: en devanagari-stavelse bliver én glyph fra fire tegn, en arabisk ligatur fletter to, og et enkelt tegn kan producere flere mærker. Uden cluster-spans kan du ikke placere en caret, hit-teste et klik eller fremhæve en markering, for du kan ikke sige, hvilke tegn en glyph tilhører. Med dem er aritmetikken lokal, og samme kode virker for begge backends

Glyph cluster-spans i TPdfShapedText: ét devanagari-stavelse-glyph fra fire tegn, en arabisk ligatur fra to, og en base plus mærke fra ét tegn, hvert mappet tilbage gennem ClusterStart og ClusterLength
Cluster-spans mapper hver glyph tilbage til sine kildetegn, så carets, hit tests og markeringer virker
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 ankommer allerede i visuel orden med VisualX udfyldt
      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;

Budgetter hører hjemme i options-recorden

TPdfTextShapingOptions bærer en retning plus tre tag: maksimum tegn, maksimum glyphs og maksimum runs, med en Default class function, der fylder fornuftige værdier. Taggene er ikke paranoia omkring misdannet input; de er aritmetik. Shaping ekspanderer: en font med aggressiv kontekstuel substitution kan udsende flere glyphs end inputtegn, og et afsnit, der skifter script hvert par tegn, producerer ét run pr. skift. Et dokument samlet for at maksimere begge gør en beskeden streng til en stor allokering, og en tjeneste, der shaper tekst fra upålidelige PDF-filer, behøver en grænse, den valgte, frem for en grænse, maskinen pålægger

At sætte retningen eksplicit frem for at lade den stå på automatisk er værd at gøre, når du allerede kender den. Automatisk anvender afsnitsretningens regler til at gætte ud fra det første stærke tegn, hvilket er rigtigt for fri tekst og forkert for et formularfelt, hvis retning er en egenskab ved feltet snarere end ved den værdi, nogen tastede ind i det

Binding på kørselstidspunktet, ikke en build-afhængighed

HarfBuzz-backenden indlæser biblioteket dynamisk. Det er en udrulningsbeslutning med reelle konsekvenser: én binær kører på en maskine med HarfBuzz og på en maskine uden, og rapporterer reduceret evne i det andet tilfælde i stedet for at fejle at starte. For et bibliotek udsendt til andre udviklere er det den eneste arbejdbare ordning, for du kan ikke kræve, at hver forbruger af en PDF-komponent anskaffer og version-matcher et shaping-bibliotek, de muligvis ikke behøver

Den tilsvarende regel for kaldere er at tjekke. ActiveTextShaper returnerer nil, når platformen ikke har nogen standard, og ingen blev konfigureret, og shaping-entry pointet rapporterer det som en utilgængelig shaper snarere end som en shaping-fejl. Det er forskellige problemer og fortjener forskellige meddelelser: den ene er et udrulningshul, den anden er et font- eller tekstproblem

Installér én gang, før noget shaper

Installation erstatter og frigør den forrige shaper, så at kalde den gentagne gange er sikkert men meningsløst, og at kalde den, mens en anden tråd shaper, er slet ikke sikkert. Gør det under opstart. Hvis du behøver at falde tilbage til platformstandard senere, send nil, hvilket også er sådan, du annullerer en testdobbelt i slutningen af en test

Når en backend er installeret, opfører måling og wrapping sig ens på begge platforme, da de indtager run- og glyph-metrics i stedet for at kalde platformen direkte; wrapping-modellen er beskrevet i artiklen om tekstmåling og word wrap. Understøttede platforme og toolchains til komponenten er opført på produktsiden PDFium Delphi component