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
É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
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
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