Technischer Artikel

PDFlibPas: Silbentrennung & Textspalten in Delphi

PDFlibPas lässt eine gespeicherte Textpassage mit DrawTextFlowColumns durch ein bis 64 gleich breite Spalten fließen und bricht Zeilen mit begrenzter, sprachbewusster Silbentrennung um, sobald SetTextFlowLanguage und SetTextFlowHyphenation aufgerufen werden. Neun Sprachen werden unterstützt, und die Sprache kann aus dem /Lang-Wert des Dokument-Catalogs übernommen statt pro Flow gesetzt werden

Beide Funktionen existieren aus demselben Grund: In einer schmalen Spalte hört naiver Zeilenumbruch auf, wie Satzgestaltung auszusehen, und beginnt, wie ein Bugreport auszusehen

Warum zerfällt Blocksatz in schmalen Spalten?

Weil Blocksatz überschüssigen Platz in die Wortzwischenräume einer Zeile verteilt, und wie viel übrig bleibt, hängt davon ab, was hineinpasst. Bei einem breiten Satzspiegel ist der Überschuss klein, und das Auge bemerkt ihn nie. Halbiert man die Breite, drückt ein einzelnes langes Wort, das nicht mehr passt, in die nächste Zeile und lässt seine Vorgänger den gesamten Platz aufnehmen. Drei solcher Zeilen hintereinander erzeugen die vertikalen weißen Kanäle, die Typografen Flüsse nennen und die Leser als schwer verfolgbaren Text erleben, ohne zu wissen, warum

Silbentrennung behebt die Ursache statt des Symptoms, indem sie einen Umbruch innerhalb des Wortes zulässt. Deutsche und niederländische Komposita machen das unverzichtbar: Ein 24 Zeichen langes Substantiv in einer 60-Millimeter-Spalte hat ohne Trennstelle kein gutes Ergebnis. Englisch verträgt das Fehlen besser, weshalb englischsprachig entwickelte Produkte häufig Layout-Code ausliefern, der beim ersten deutschen Kunden zusammenbricht

Welche Sprachen, und woher kommt die Sprache?

Die Silbentrennung deckt Englisch, Deutsch, Niederländisch, Französisch, Spanisch, Italienisch, Portugiesisch, Russisch und Türkisch ab. Sie kann pro Flow explizit mit SetTextFlowLanguage gesetzt werden oder aus dem /Lang-Eintrag des Dokument-Catalogs übernommen werden, also aus dem Wert, den ein getaggtes und barrierefreies Dokument ohnehin schon trägt

Diese Vererbung sollte man nutzen, statt sie zu überschreiben. Ein Dokument, das seine Sprache im Catalog erklärt, teilt Screenreadern, Suchindizierern und der Silbentrennung dieselbe Tatsache aus einer einzigen Quelle mit, und genau dort sollte eine Tatsache leben. Wer bereits getaggte Ausgabe erzeugt, wie in automatischem Tagging für barrierefreie PDFs beschrieben, hat den Sprach-Eintrag bereits gesetzt, und der Flow kann ihm einfach folgen

var
  Lib: TPDFlib;
  Flow, Drawn: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('Georgia', 1);
    Lib.SetTextSize(10.5);

    Flow := Lib.NewTextFlow(ArticleBody);
    try
      Lib.SetTextFlowLanguage(Flow, 'de');
      // Aktivieren, mindestens 3 Zeichen vor dem Umbruch, 3 danach
      Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);
      Lib.SetTextFlowMinLines(Flow, 2);   // nie eine einzelne Zeile isolieren lassen

      repeat
        // Drei Spalten über einen 480-pt-Bereich, 18-pt-Stege, ausgeglichen
        Drawn := Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);
        if (Drawn = 0) or (Lib.TextFlowFinished(Flow) = 1) then
          Break;
        Lib.NewPage;
      until False;
    finally
      Lib.ReleaseTextFlow(Flow);
    end;

    Lib.SaveToFile('newsletter.pdf');
  finally
    Lib.Free;
  end;
end;

MinPrefix und MinSuffix sind Typografie, keine Validierung

Die beiden Ganzzahlen nach dem Aktivierungs-Flag legen fest, wie viele Zeichen mindestens vor und nach einem Umbruch stehen bleiben müssen. Drei und drei ist eine konservative Vorgabe, die die meisten Hausstile akzeptieren. Zwei und zwei erzeugt mehr Umbruchmöglichkeiten und deutlich hässlichere Ergebnisse, weil ein zweibuchstabiges Fragment am Zeilenende wie ein Tippfehler wirkt

Erhöhen Sie die Mindestwerte bei großer Schrift, wo jedes Fragment optisch auffällt, und senken Sie sie nur, wenn die Spalte tatsächlich schmal ist und Sie entschieden haben, dass ein enger Satzspiegel wichtiger ist als ein sauberer. Das ist eine Entscheidung des Hausstils, keine technische, und genau deshalb ist es ein Parameter statt einer Konstante

Was bedeutet "ausgeglichen" hier eigentlich?

Der Parameter Balance ändert das Verhalten nur am Ende einer Textpassage. Ist Balancing aktiviert, werden die Spalten auf eine exakt gleiche Zeilenzahl gekürzt, wenn der gesamte verbleibende Text in den Bereich passt, was verhindert, dass eine letzte Seite zwei volle Spalten und eine dritte mit nur einer einsamen Zeile zeigt. Passt die Passage nicht hinein, behält jede Spalte ihre volle Höhe, sodass die Seite so viel Text wie möglich trägt und der Rest auf der nächsten Seite fortgesetzt wird

Diese Asymmetrie ist die richtige Vorgabe für fortlaufende Dokumente. Balancing mitten in einem fließenden Artikel würde auf jeder Seite vertikalen Platz für einen kosmetischen Effekt verschwenden, den niemand sieht, da die Spalten ohnehin voll sind. Am Ende bemerkt das Auge es tatsächlich, und genau dort greift es

Der Zeilenumbruch misst ganze Wörter

Der Umbruch-Algorithmus misst vollständige Wörter, statt Zeichenbreiten aufzusummieren, und reserviert eine begrenzte Suche für überlange Tokens, die überhaupt nicht in eine Zeile passen, etwa eine URL oder eine Belegnummer. Das hält den Normalfall schnell und den pathologischen Fall begrenzt, statt umgekehrt

Weiche Trennzeichen und automatische Trennstriche werden nur dann gezeichnet, wenn der von ihnen markierte Umbruch tatsächlich der gewählte ist. Das klingt selbstverständlich und ist ein klassischer Fehler: Eine naive Implementierung schreibt das Trennzeichen bereits beim Messen, und wandert der Umbruch weiter, bleibt der Trennstrich mitten in einem Wort zurück. Nichts wirkt mehr nach einer kaputten Textengine als ein verirrter Trennstrich mitten im Wort

var
  Lib: TPDFlib;
  Flow, Needed: Integer;
begin
  // Das Layout entscheiden, bevor irgendetwas gezeichnet wird
  Flow := Lib.NewTextFlow(ArticleBody);
  try
    Lib.SetTextFlowLanguage(Flow, 'fr');
    Lib.SetTextFlowHyphenation(Flow, 1, 3, 3);

    // Zeilen, die der Rest der Passage bei einer Spaltenbreite braucht
    Needed := Lib.MeasureTextFlow(Flow, 148);
    if Needed > 3 * LinesPerColumn then
      UseTwoPageSpread
    else
      UseSinglePage;

    Lib.DrawTextFlowColumns(Flow, 72, 720, 480, 620, 3, 18, 1);

    if Lib.TextFlowFinished(Flow) <> 1 then
      CarryOver(Lib.GetTextFlowRemaining(Flow));
  finally
    Lib.ReleaseTextFlow(Flow);
  end;
end;

Die Schrifteinstellungen über alle Boxen hinweg identisch halten

Eine Regel gilt für jedes flow-basierte Layout, und sie ist es wert, unmissverständlich ausgesprochen zu werden: DrawTextFlow, DrawTextFlowColumns und MeasureTextFlow brechen Zeilen alle mit der Schriftart um, die im Moment ihres Aufrufs ausgewählt ist. Ändern Sie Schriftart oder Größe zwischen zwei Boxen desselben Flows, oder beginnen Sie eine neue Seite, ohne die Schrift neu auszuwählen, bricht die zweite Box anders um als das, was für die erste gemessen wurde

Das Symptom ist zermürbend, gerade weil es zeitweilig wirkt: Text, der auf Seite eins passt, läuft auf Seite zwei über, oder eine gemessene Zeilenzahl widerspricht dem, was gezeichnet wurde. Wählen Sie die Schriftart einmal vor der Schleife aus, wählen Sie sie nach jedem NewPage erneut aus, und der Flow verhält sich korrekt. Tauchen gemischte Schriftsysteme in derselben Passage auf, gilt die in automatischem Font-Fallback für CJK- und Emoji-Text beschriebene Auflösung sowohl für Messung als auch für das Zeichnen, sodass die Breiten auch über die Fallback-Läufe hinweg konsistent bleiben

Bei Berichtslayouts, in denen der Flow nur ein Element unter Kopfzeilen, Fußzeilen und datengetriebenen Blöcken ist, lassen sich die in der Dataset-Report-Engine beschriebenen Kompositionsmuster sauber mit Spalten-Flows kombinieren: erst messen, dann das feste Inventar platzieren, dann dem Flow den verbleibenden Bereich überlassen

PDFlibPas ist eine Delphi-, C++Builder- und Lazarus-PDF-Bibliothek, und der vollständige TextFlow-Lebenszyklus, Erzeugung, Zeichnen, Messung, Inspektion, Rücksetzung und Freigabe, steht ebenso über die DLL- und ActiveX-Schnittstellen zur Verfügung. Die vollständige Dokumentation steht auf der PDFlibPas Delphi PDF-Bibliothek-Seite