Text-Shaping in der PDFium-Komponente läuft durch ein einziges installierbares Objekt. ConfigureTextShaper installiert den Shaper, durch den jeder Shaping-Einstiegspunkt läuft, und ersetzt und gibt frei, was dort war; ActiveTextShaper gibt den installierten zurück und erzeugt beim ersten Gebrauch den Plattform-Default; ActiveTextShaperName meldet, welches Backend live ist; ClearTextShaper wirft die Installation weg und lässt den Default erneut erzeugen. Auf Windows ist der Default TPdfUniscribeTextShaper. Unter Free Pascal gibt es TPdfHarfBuzzTextShaper, das libharfbuzz zur Laufzeit bindet, sodass eine fehlende Bibliothek ein gemeldeter Zustand ist statt eines Ladefehlers
Ein Interface, zwei Backends, die die Arbeit völlig unterschiedlich teilen. Diese Asymmetrie zu verstehen ist das, was den portablen Pfad daran hindert, Text zu produzieren, der korrekt geshaped und falsch positioniert ist
Warum ist das Windows-Backend eine Klasse und das portable drei Teile?
Weil Uniscribe vier APIs sind, die so tun, als wären sie eine. ScriptItemize segmentiert eine Zeichenkette nach Skript und löst bidirektionale Ebenen auf; ScriptShape mappt Zeichen auf Glyphen; ScriptPlace berechnet Vorschübe und Offsets; ScriptLayout bringt die entstehenden Runs in visuelle Reihenfolge. Ein darauf gebautes Backend hat also nichts mehr hinzuzufügen, weshalb der Windows-Shaper eine einzige Klasse mit einer einzigen Methode ist
HarfBuzz deckt die mittleren beiden ab. Es shaped und platziert einen Run, dessen Richtung und Skript der Aufrufer bereits entschieden hat, und es hat keine Meinung dazu, wie ein Absatz in Runs zerfällt oder in welcher Reihenfolge diese Runs erscheinen. Das portable Backend liefert also den Rest: Der bidirektionale Algorithmus löst Embedding-Ebenen auf, die HarfBuzz-Unicode-Funktionen segmentieren den Text nach Skript, und die Runs werden in die visuelle Reihenfolge gebracht, die die UAX-#9-Regel L2 produziert. Die bidirektionale Hälfte ist substanzreich genug, um eine eigene Unit zu sein, beschrieben in dem Artikel zu UAX-#9-Embedding-Ebenen
Der Shaper löst keine Schriften auf, und das ist Absicht
Uniscribe liest die Schrift-Binärdatei aus einem GDI-Gerätekontext. Dafür gibt es kein portables Äquivalent, und eines innerhalb einer Shaping-Unit zu erfinden hieße, im Namen jeder Anwendung zu entscheiden, ob Schriften aus fontconfig kommen, aus CoreText, aus einem Anwendungs-Schriftordner oder aus einer Datenbank. Das HarfBuzz-Backend nimmt deshalb einen Resolver: einen Callback, der einen Schriftnamen auf die TrueType- oder OpenType-Bytes mappt. False zurückzugeben scheitert die Shaping-Anfrage auf dieselbe Weise, wie es auf Windows eine unlesbare GDI-Schrift tut
uses
FPdfTextShaping
{$IFDEF FPC}
, FPdfTextShapingHb
{$ENDIF}
;
function TFontCatalogue.Resolve(const FontName: WideString;
out FontData: TBytes): Boolean;
var
Path: string;
begin
// Ihre Richtlinie: fontconfig, CoreText, ein App-Schriftordner, eine Datenbank
Result := FLookup.TryGetValue(LowerCase(FontName), Path);
if Result then
FontData := TFile.ReadAllBytes(Path);
end;
procedure InstallShaper(Catalogue: TFontCatalogue);
begin
{$IFDEF FPC}
// Das Eigentum geht an die Unit über; einmal beim Start aufrufen,
// bevor irgendetwas Text shaped
ConfigureTextShaper(TPdfHarfBuzzTextShaper.Create(Catalogue.Resolve));
{$ENDIF}
// Auf Delphi wird der Plattform-Default (Uniscribe) bei Bedarf erzeugt,
// also ist überhaupt keine Installation nötig
LogInfo('shaping backend: ' + ActiveTextShaperName);
end;
Die Schriftsuche außerhalb des Shapers zu halten hat einen zweiten Nutzen, der in Servern auftaucht: Derselbe Prozess kann mit einem eingebetteten Schriftsatz shapen, der nichts mit dem zu tun hat, was auf der Maschine installiert ist, was Sie wollen, wenn die Ausgabe über Hosts hinweg byte-reproduzierbar sein muss. Die Komponente legt außerdem einen Host-System-Schrift-Provider offen für die Fälle, in denen Sie installierte Schriften wollen, behandelt in dem Artikel zum System-Schrift-Provider
Der Ergebnisdatensatz ist backend-neutral, und Cluster sind der Grund
Beide Backends füllen dasselbe TPdfShapedText: den Quelltext, Schriftnamen, Größe, Schrift-Bytes, ein Array von Runs, die Gesamtbreite, die Glyphenzahl und die logische Zeichenanzahl. Jeder TPdfShapedRun trägt seine Spanne im Quelltext, seine visuelle X-Position, seine Breite, seine bidirektionale Ebene und ein Right-to-Left-Flag, dazu seine Glyphen. Jede TPdfShapedGlyph trägt eine Glyphen-Kennung, einen Vorschub, X- und Y-Offsets und den Cluster, zu dem sie gehört, als Start und Länge im Quelltext
Diese Cluster-Felder machen den Datensatz brauchbar statt bloß informativ. Shaping ist keine Eins-zu-Eins-Abbildung: Eine Devanagari-Silbe wird eine Glyphe aus vier Zeichen, eine arabische Ligatur verschmilzt zwei, und ein einzelnes Zeichen kann mehrere Marks produzieren. Ohne Cluster-Spannen können Sie kein Caret setzen, einen Klick per Hit-Test auflösen oder eine Auswahl hervorheben, denn Sie können nicht sagen, zu welchen Zeichen eine Glyphe gehört. Mit ihnen ist die Arithmetik lokal, und derselbe Code funktioniert für beide 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 kommen bereits in visueller Reihenfolge mit gefülltem VisualX an
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 gehören in den Optionsdatensatz
TPdfTextShapingOptions trägt eine Richtung plus drei Obergrenzen: maximale Zeichen, maximale Glyphen und maximale Runs, mit einer Default-Klassenfunktion, die vernünftige Werte füllt. Die Grenzen sind keine Paranoia über fehlerhafte Eingaben; sie sind Arithmetik. Shaping expandiert: Eine Schrift mit aggressiver kontextueller Substitution kann mehr Glyphen emittieren als Eingabezeichen, und ein Absatz, der alle paar Zeichen das Skript wechselt, produziert einen Run pro Wechsel. Ein Dokument, das konstruiert ist, beides zu maximieren, macht aus einer bescheidenen Zeichenkette eine große Allokation, und ein Dienst, der Text aus nicht vertrauenswürdigen PDFs shaped, braucht eine Grenze, die er gewählt hat, nicht eine, die die Maschine auferlegt
Die Richtung explizit zu setzen, statt sie auf automatisch zu lassen, lohnt sich, wann immer Sie sie bereits kennen. Automatisch wendet die Absatzrichtungsregeln an, um aus dem ersten starken Zeichen zu raten, was für freien Text richtig und für ein Formularfeld falsch ist, dessen Richtung eine Eigenschaft des Felds ist, nicht des Werts, den jemand hineintippte
Laufzeitbindung, keine Build-Abhängigkeit
Das HarfBuzz-Backend lädt die Bibliothek dynamisch. Das ist eine Deployment-Entscheidung mit realen Konsequenzen: Eine Binärdatei läuft auf einer Maschine mit HarfBuzz und auf einer ohne, und meldet im zweiten Fall reduzierte Fähigkeiten, statt am Start zu scheitern. Für eine Bibliothek, die an andere Entwickler ausgeliefert wird, ist das die einzige praktikable Anordnung, denn man kann nicht von jedem Konsumenten einer PDF-Komponente verlangen, eine Shaping-Bibliothek zu beschaffen und versionengleich zu halten, die er vielleicht gar nicht braucht
Die entsprechende Regel für Aufrufer ist zu prüfen. ActiveTextShaper gibt nil zurück, wenn die Plattform keinen Default hat und keiner konfiguriert wurde, und der Shaping-Einstiegspunkt meldet das als nicht verfügbaren Shaper statt als Shaping-Fehler. Das sind verschiedene Probleme und verdienen verschiedene Meldungen: das eine ist eine Deployment-Lücke, das andere ein Schrift- oder Textproblem
Einmal installieren, bevor irgendetwas shaped
Die Installation ersetzt und gibt den vorherigen Shaper frei, also ist wiederholtes Aufrufen sicher aber sinnlos, und Aufrufen, während ein anderer Thread shaped, ist überhaupt nicht sicher. Tun Sie es beim Start. Wenn Sie später auf den Plattform-Default zurückfallen müssen, übergeben Sie nil, was auch der Weg ist, ein Test-Double am Ende eines Tests zurückzunehmen
Sobald ein Backend installiert ist, verhalten sich Messung und Umbruch auf beiden Plattformen gleich, denn sie konsumieren die Run- und Glyphen-Metriken, statt die Plattform direkt aufzurufen; das Umbruchmodell beschreibt der Artikel zu Textmessung und Wortumbruch. Unterstützte Plattformen und Toolchains für die Komponente listet die Produktseite der PDFium Delphi-Komponente