Fachartikel

Vollständiger Blocksatz für PDF-Text in Delphi mit HotPDF

Vollständiger Blocksatz (Full Justification) ist das Layout, bei dem eine Textspalte sowohl am linken als auch am rechten Rand bündig abschließt – der Look, den Sie von einem gedruckten Buch oder einem formellen Bericht erwarten. Er ist leicht zu beschreiben und überraschend leicht falsch zu machen, da die Antwort auf die Frage "Wo kommt der zusätzliche Platz hin?" für Englisch nicht dieselbe ist wie für Japanisch, und da die naive Methode zur Messung jeder Zeile eine schnelle Seite in eine langsame verwandelt. HotPDF bietet Ihnen eine skriptabhängige (script-aware) Blocksatzfunktion durch einen einzigen Box-Layout-Aufruf, und unter diesem Aufruf verbirgt sich eine klassische Leistungsoptimierung (Performance Fix), die es wert ist, für sich genommen verstanden zu werden.

Dieser Artikel geht beides durch. Erstens die typografische Regel, die entscheidet, wie der Leerraum (Slack) für Skripte mit Wortlücken (Word Gaps) gegenüber Skripten ohne diese verteilt wird. Zweitens die Anpassung der Messung (Measurement Change), die die Kosten für den Blocksatz pro Seite um etwa das Achtzigfache gesenkt hat, ohne sichtbaren Unterschied in der Ausgabe. Beides ist wichtig, wenn Sie Dokumente in großen Mengen generieren und möchten, dass sie sich wie echter Schriftsatz (Typesetting) lesen lassen, anstatt wie auf Länge gezogene Festbreitenschrift (Monospaced Output).

Was vollständiger Blocksatz eigentlich erfordert

Eine Textzeile, die in ihrer natürlichen Breite gezeichnet wird, erreicht fast nie den rechten Rand ihrer Spalte. Es bleibt immer ein Rest, der Leerraum (Slack), zwischen dem Ende der letzten Glyphe und der Spaltengrenze. Linksbündigkeit belässt diesen Leerraum auf der rechten Seite. Rechtsbündigkeit verschiebt ihn nach links. Zentrierung teilt ihn auf. Vollständiger Blocksatz entfernt ihn, indem die Zeile selbst verbreert wird, bis beide Ränder die Box berühren, und die einzige ehrliche Methode dazu besteht darin, die Glyphen von innen heraus auseinanderzuschieben.

Die Regel, die guten Blocksatz von schlechtem trennt, lautet: Wo platziert man den Leerraum? Ein Skript, das Wörter mit Leerzeichen dazwischen schreibt, wie Englisch und der Rest der lateinischen Sprachfamilie, weist an jedem Leerzeichen zwischen den Wörtern (Inter-word Space) natürliche Nähte (Seams) auf. Das Verbreitern dieser Abstände ist für das Auge unsichtbar, da die Leser bereits akzeptieren, dass Wortlücken variieren. Ein Skript, das ohne Wortlücken schreibt, wie chinesische Han-Zeichen, japanisches Kana oder koreanisches Hangul, hat keine solchen Nähte. Dort muss der Leerraum gleichmäßig zwischen benachbarten Glyphen verteilt werden, was dem Prinzip entspricht, das japanische Setzer "kintou-waritsuke", gleichmäßige Abstände, nennen. Wenn man einer CJK-Zeile eine Streckung der Wortlücken im lateinischen Stil verpasst oder den gesamten Leerraum an die einzige Stelle quetscht, an der eine CJK-Zeile zufällig ein Leerzeichen enthält, entstehen die Gießbäche (Rivers) und Lücken, die eine Amateurausgabe kennzeichnen.

Wie HotPDF entscheidet, wo der Platz hinkommt

HotPDF trifft diese Entscheidung pro Lücke, nicht pro Zeile. Wenn es eine Zeile im Blocksatz ausrichtet, durchläuft es jedes benachbarte Glyphenpaar und fragt, ob sich dazwischen eine dehnbare Grenze (Stretchable Boundary) befindet. Eine Grenze ist dehnbar, wenn sich auf einer der beiden Seiten ein Leerzeichen oder ein Tabulator befindet (der lateinische Fall), oder wenn sich auf beiden Seiten umbruchfähige CJK-Zeichen befinden (der Fall des gleichmäßigen Abstands). Es zählt diese Grenzen, teilt den Leerraum der Zeile gleichmäßig auf sie auf und fügt diesen Anteil zu jeder qualifizierenden Lücke hinzu.

Die Konsequenz ergibt sich auf natürliche Weise. Eine englische Zeile hat dehnbare Grenzen nur an ihren Wortzwischenräumen, sodass der gesamte Leerraum dort landet und die Wörter auseinandergezogen werden, während die Buchstaben innerhalb jedes Wortes ihren natürlichen Abstand beibehalten. Eine Han- oder Kana-Zeile hat eine dehnbare Grenze zwischen fast jedem Glyphenpaar, sodass sich der Leerraum gleichmäßig über die gesamte Zeile verteilt, genau der gleichmäßige Glyphenabstand (Inter-glyph Spacing), den diese Skripte verlangen. Eine Zeile, die ein einziges langes lateinisches Wort ohne internen Abstand ist, hat überhaupt keine dehnbare Grenze, also belässt HotPDF sie in ihrer natürlichen Breite, anstatt das Wort Buchstabe für Buchstabe auseinanderzureißen. Dieselbe Logik bewältigt gemischte lateinische und CJK-Textabschnitte (Runs) in einer Zeile ohne Sonderfallbehandlung (Special-Casing), da die Entscheidung lokal für jede Grenze getroffen wird.

Eine Grenze wird überall bewusst ausgeschlossen. Die Position nach der letzten Glyphe einer Zeile wird niemals als Lücke behandelt, da eine Dehnung an dieser Stelle lediglich wieder einen Rest auf der rechten Seite einführen würde, was das Gegenteil von Blocksatz ist.

Warum die letzte Zeile in Ruhe gelassen wird

Die letzte Zeile eines Absatzes ist besonders, und sie falsch zu machen, ist der häufigste Fehler beim Blocksatz. Die letzte Zeile eines Absatzes ist meistens kurz, oft nur wenige Wörter, und sie auf die volle Spaltenbreite zu strecken, zieht diese Wörter über die Seite zu einer spärlichen (sparse), zerrissenen Reihe. Die korrekte Typografie belässt die letzte Zeile in ihrer natürlichen Breite, linksbündig ausgerichtet.

HotPDF erkennt die abschließende Zeile (Trailing Line) anhand der Position. Während es den Text in Zeilen umbricht, weiß es, wann die gerade abgetrennte Zeile das Ende des bereitgestellten Strings erreicht. Diese letzte Zeile wird mit einer einfachen Linksausrichtung ausgegeben und behält ihre natürliche Breite. Jede Zeile davor wird an beiden Rändern im Blocksatz ausgerichtet. Harte Zeilenumbrüche (Hard Line Breaks), die Sie in den Text schreiben, werden wie geschrieben berücksichtigt, sodass eine absichtlich kurze Zeile ebenfalls nie gedehnt wird. Der Leser sieht einen sauberen rechteckigen Textblock, dessen letzte Zeile natürlich endet, was das Auge erwartet.

Die Messkosten, die den Blocksatz langsam machten

Um eine Zeile im Blocksatz auszurichten, müssen Sie deren exakte Breite kennen, und Sie müssen den Vorschub (Advance) jeder Glyphe kennen, damit Sie den zusätzlichen Platz präzise platzieren können. Die erste Implementierung ermittelte diese Zahlen auf dem naheliegenden Weg. Sie maß die gesamte Zeile mit einer vollständigen Unicode-Breitenabfrage, maß dann Präfix nach Präfix, um den Vorschub jeder Glyphe durch Differenzbildung (Differencing) zurückzugewinnen. Für eine Zeile mit N Glyphen bedeutet das N+1 Aufrufe der Mess-Engine, und jeder Aufruf ist ein vollständiger GDI-Roundtrip, bei dem das Betriebssystem aufgefordert wird, Text zu formen (shape) und zu messen und die Antwort zurückzugeben.

Pro Zeile klingt das günstig. Für eine ganze Seite ist es das nicht. Nehmen Sie eine dichte A4-Seite mit Fließtext, ungefähr fünfundvierzig Zeilen zu je etwa achtzig Zeichen. Bei N+1 Roundtrips pro Zeile sind das etwa 81 Roundtrips für jede Zeile und ungefähr 3.645 für die Seite. Bei fast allen davon wurde Text neu gemessen, den sich die Engine wenige Momente zuvor bereits angesehen hatte. Bei einem Batch-Job, der Tausende von Seiten produziert, dominiert dieser Overhead (Mehraufwand) die Layout-Zeit, und jeder Roundtrip überschreitet die Grenze zwischen Ihrem Prozess und dem Grafik-Subsystem.

Ein Aufruf statt N plus eins

Die Fehlerbehebung (Fix) ist die Art von Änderung, die klein aussieht und sich stark auszahlt. GDI kann bereits in einer einzigen Abfrage die Gesamtbreite eines Strings und die Position jeder Glyphe zurückgeben. HotPDF macht dies durch GetWideCharAdvances zugänglich, das ein Array mit dem natürlichen Vorschub jeder Glyphe (inklusive Kerning) füllt und die Gesamtbreite zurückgibt – in einem Aufruf statt in N+1. Die Blocksatzroutine, intern _HPDFEmitJustifiedWideLine, fordert alle Vorschübe einmalig an, berechnet den Leerraum (Slack), verteilt ihn auf die dehnbaren Grenzen und gibt die Zeile aus.

Für dieselbe A4-Seite sinkt die Messung pro Zeile von etwa 81 Roundtrips auf einen, sodass die Seite von ungefähr 3.645 Roundtrips auf etwa 45 abfällt, was fast einer achtzigfachen Reduzierung entspricht. Die Ausgabe ist bytegenau identisch, da sich an der Messung nichts geändert hat, außer wie oft sie angefordert wird. Dieselbe GDI-Engine, dieselben Font-Metriken (Font Metrics), dasselbe Kerning liefern dieselben Zahlen. Nur die Anzahl der Roundtrips ist gesunken. Wenn eine Messung bereits korrekt ist, besteht die richtige Optimierung (Optimization) darin, sie nicht mehr wiederholt anzufordern, anstatt sie zu approximieren.

Wie die Zeile auf die Seite gelangt

Sobald der Leerraum aufgeteilt ist, gibt HotPDF die Zeile mit ExtTextOut und einem Array für den Glyphen-Vorschub (per-glyph advance array), dem Dx-Array, aus. Jeder Eintrag ist die Entfernung vom Ursprung einer Glyphe zur nächsten, was dem natürlichen Vorschub dieser Glyphe zuzüglich ihres Anteils am Leerraum entspricht, sofern eine dehnbare Grenze folgt. Dies lässt sich direkt auf das PDF-Bildgebungsmodell (PDF Imaging Model) übertragen. Positionierter Text wird mit dem TJ-Operator geschrieben, einem Array, das Glyphenläufe (Glyph Runs) mit expliziten horizontalen Anpassungen verschränkt, und die Dx-Werte werden genau zu diesen Anpassungen. Aus diesem Grund landet der zusätzliche Platz zwischen den Glyphen an präzisen Sub-Point-Positionen, anstatt mit Füllzeichen (Padding Characters) vorgetäuscht zu werden, und deshalb wird eine im Blocksatz formatierte HotPDF-Zeile korrekt gemessen, wenn ein nachgeschaltetes Werkzeug (Downstream Tool) sie wieder einliest.

Sie rufen ExtTextOut für Absätze im Blocksatz nicht selbst auf. Der Einstiegspunkt ist WideTextOutBox, was einen Unicode-String in einer Box umbricht und die von Ihnen gewünschte Ausrichtung anwendet. Es zerlegt den Text in Zeilen, die in die Boxbreite passen, platziert jede Zeile entlang der Boxhöhe und gibt die Anzahl der Zeichen zurück, die es einpassen konnte, bevor der vertikale Platz ausging. Die Ausrichtung wird durch das Justification-Enum gewählt.

type
  THPDFJustificationType = (jtLeft, jtCenter, jtRight, jtJustify);

Die ersten drei sind die selbsterklärenden linksbündigen, zentrierten und rechtsbündigen Ausrichtungen. Die vierte, jtJustify, ist der hier beschriebene vollständige zweiseitige Blocksatz, und dies ist der Wert, den WideTextOutBox liest, um die skriptabhängigen Abstände einzuschalten.

Einen Absatz in der Praxis im Blocksatz formatieren

Ein vollständiges Beispiel erstellt ein Dokument, legt eine Schriftart fest und gießt einen Absatz im vollständigen Blocksatz in eine Box. Derselbe Code richtet lateinischen und CJK-Text ohne Änderung eines Flags im Blocksatz aus, da sich die Skriptabhängigkeit unterhalb der API befindet.

uses
  HPDFDoc;

procedure JustifyParagraph;
var
  Pdf: THotPDF;
  Body: WideString;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Justified.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', 11);

    Body :=
      'Vollständiger Blocksatz verteilt den Leerraum (Slack) auf jede gefüllte Zeile, ' +
      'sodass beide Ränder bündig zur Spalte abschließen, während die letzte ' +
      'Zeile ihre natürliche Breite beibehält. Bei Skripten mit Wortlücken ' +
      'landet der Leerraum zwischen den Wörtern; bei Skripten ohne sie ' +
      'wird er gleichmäßig zwischen den Glyphen verteilt.';

    // X, Y, Zeilenabstand (LineSpacing), BoxBreite (BoxWidth), BoxHöhe (BoxHeight), Text, Ausrichtung (Align)
    Pdf.CurrentPage.WideTextOutBox(72, 72, 4, 380, 240, Body, jtJustify);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Um denselben Block linksbündig, zentriert oder rechtsbündig zu zeichnen, ändern Sie nur das letzte Argument auf jtLeft, jtCenter oder jtRight. Der Umbruch (Wrapping), die Zeilenplatzierung und der Rückgabewert bleiben gleich. Die gemessene Breite, die alle vier Pfade steuert, stammt von GetWideTextWidth, der Unicode-fähigen Breitenabfrage, die einen WideString dort korrekt misst, wo die ältere byteweise Messung alles jenseits von Latin-1 falsch bemessen würde, was von vornherein dafür sorgt, dass die Box CJK- und Surrogate-Pair-Text an der richtigen Stelle umbricht.

Blocksatz ist eine Schicht (Layer) eines größeren Text-Shaping-Stacks. Wenn eine Zeile Skripte enthält, die ihre Glyphen neu ordnen oder verbinden, setzen die hier getroffenen Abstandsentscheidungen auf der Arbeit auf, die in unserem Artikel über Complex-Script-Text-Shaping beschrieben ist, und wenn eine Schriftart typografische Varianten mitbringt, die Sie auswählen möchten, sehen Sie sich an, wie Sie stilistische OpenType GSUB-Alternativen steuern. All dies wird mit der HotPDF Component für Delphi und C++Builder ausgeliefert, zusammen mit den umfassenderen Text-, Layout- und Dokument-APIs, die in diesem Blog behandelt werden.