Technischer Artikel

RtLTextOut in HotPDF: Von-rechts-nach-links-PDF-Text in Delphi

Wenn Sie den arabischen Satz يوضح ملف PDF هذا an ein einfaches TextOut senden, ist die zurückgegebene Seite gleich in zweierlei Hinsicht fehlerhaft. Die Wörter verlaufen von links nach rechts statt von rechts nach links und die Buchstaben stehen in ihrer isolierten Form einzeln, statt sich zu zusammenhängenden Wörtern zu verbinden. Es treten keine Fehler auf. Das Delphi wird kompiliert, die Datei wird geöffnet und ein Prüfer, der Arabisch lesen kann, teilt Ihnen mit, dass die Ausgabe unbrauchbar ist. Die Fehlerbehebung ist ein einziger Aufruf, kein Bibliothekswechsel: HotPDF leitet von-rechts-nach-links-Text durch eine separate Methode, RtLTextOut, die die Neuordnung handhabt, die mit einem einfachen TextOut nicht möglich ist. Vier Dinge bei dieser Methode entscheiden darüber, ob die Ausgabe brauchbar ist: was sie mit der Zeichenfolge macht, wie ihr Zeichensatzargument die Schrift auswählt, welche Änderung auf Dokumentebene als Nebeneffekt vorgenommen wird und welche Schriftarteneinrichtung vorher erfolgen muss

Warum eine Von-rechts-nach-links-Lese- bzw. Schreibrichtung einen eigenen Aufruf erfordert

In einem PDF-Inhaltsstream wird kein bearbeitbarer Text gespeichert. Es werden Glyphen an festen Positionen gespeichert. Das bedeutet, dass derjenige, der den Stream ausgibt, entscheiden muss, in welcher Reihenfolge diese Glyphen dargestellt werden. Auf dem Bildschirm hat das Betriebssystem das für Sie erledigt: Fügen Sie Arabisch in eine TEdit-Komponente ein und der Text-Stack des Betriebssystems ordnet es neu an und verbindet es, bevor Sie überhaupt ein Pixel sehen. Genau deshalb sieht der String in Ihrem Formular perfekt aus, aber bricht im PDF zusammen. Der Desktop hat die Arbeit im Hintergrund erledigt, und in dem Moment, in dem Sie Ihren eigenen Inhaltsstream schreiben, liegt die Arbeit wieder bei Ihnen

TextOut nimmt Sie beim Wort. Es zeichnet die Codepoints in der Reihenfolge, in der Sie sie übergeben, von links nach rechts, was für Latein, Kyrillisch und CJK richtig und für Arabisch und Hebräisch falsch ist. RtLTextOut ist der Aufruf, der die Zeile zuerst in die visuelle Von-rechts-nach-links-Reihenfolge umordnet und dann zeichnet. HotPDF hält die beiden Methoden absichtlich getrennt, anstatt die Richtung anhand der Zeichen zu erraten. Die Entscheidung, welche aufgerufen wird, bestimmt also das Schriftverhalten, das Sie erhalten. Die tieferen Mechanismen der bidirektionalen Neuordnung und der kontextuellen arabischen Verbindung sind ein eigenes Thema, das im Artikel Arabische und RTL-Textformung mit HotPDF behandelt wird; hier ist der praktische Punkt enger gefasst. Verwenden Sie RtLTextOut für von-rechts-nach-links-Läufe, verwenden Sie TextOut für alles andere und leiten Sie niemals den einen durch den anderen

Diagramm, wie RtLTextOut eine gemischte arabische und lateinische Zeile in eine visuelle Von-rechts-nach-links-Reihenfolge bringt, bevor sie in eine PDF-Datei gezeichnet wird
RtLTextOut ordnet jede Zeile vor dem Zeichnen in die visuelle Reihenfolge um: Von-rechts-nach-links-Läufe behalten ihre Reihenfolge bei, während eingebettete lateinische Wörter und Ziffern innerhalb der Zeile von links nach rechts gelesen werden.

Das Charset-Argument entscheidet über die Schrift

Was RtLTextOut mitteilt, ob das Layout Arabisch oder Hebräisch ist, ist nicht die Methode, sondern die Schriftart. SetFont akzeptiert einen Windows-Zeichensatz als viertes Argument, und dieser Wert trägt die Schriftregeln in den von-rechts-nach-links-Aufruf ein: 178 wählt Arabisch, 177 wählt Hebräisch. Legen Sie den Zeichensatz fest und zeichnen Sie dann. Die beiden folgenden Zeilen werden ohne weitere Konfiguration in der korrekten Lesereihenfolge ausgegeben

// Arabic: charset 178 tells RtLTextOut to apply Arabic rules
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

// Hebrew: charset 177 switches the rules to Hebrew
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
Pdf.CurrentPage.RtLTextOut(400, 660, 0, 'קובץ PDF זה');

Zwei Details bei diesen Koordinaten können leicht übersehen werden. Die Position, die Sie übergeben, ist immer noch der Start des Laufs im eigenen Koordinatensystem der Seite, gemessen von der unteren linken Ecke mit Y nach oben ansteigend, derselbe Ursprung, den jedes TextOut verwendet. RtLTextOut ändert die Glyphenreihenfolge, nicht, von wo die Seite misst. Und wie bei jedem Zeichenaufruf muss SetFont zuerst kommen und nach jedem AddPage wiederholt werden, da die aktuelle Schriftart einen Seitenumbruch nicht übersteht. Wenn Sie die Wiederholung vergessen, fällt die zweite Seite auf die gerade aktive Schriftart zurück, was bei Arabisch normalerweise leere Kästchen bedeutet

Es kehrt Text, den Sie bereits gekehrt haben, nicht um

Der einzige Fehler, der hier die meiste Debugging-Zeit verschlingt, ist, dass RtLTextOut eine Zeichenfolge zugeführt wird, die Sie bereits per Hand gespiegelt haben. Die Leute greifen auf diese Methode zurück, nachdem ein erster Versuch mit reinem TextOut rückwärts herausgekommen ist, und ein übliches Provisorium besteht darin, die Zeichen im Code vor dem Zeichnen umzukehren. RtLTextOut kehrt intern von sich aus um, sodass eine vorab umgekehrte Zeichenfolge ein zweites Mal umgekehrt wird und genau da landet, wo sie angefangen hat. Übergeben Sie den Text in logischer Reihenfolge, der Reihenfolge, in der Sie ihn tippen und laut vorlesen würden, und lassen Sie den Aufruf die Neuordnung vornehmen

Die Falle ist unangenehmer als ein reiner Spiegel, da eine doppelt invertierte Zeichenfolge bei einem rein arabischen Testausdruck richtig aussehen kann, aber sofort fehlerhaft ist, sobald eine Zeile ein lateinisches Wort oder eine Zahl enthält. Innerhalb einer Von-rechts-nach-links-Zeile sollten diese eingebetteten Läufe von links nach rechts gelesen werden, und die manuelle Umkehrung zerstört diese Verschachtelung, während der rein arabische Fall sie zufällig überlebt. Der Fehler besteht also Ihren ersten Test problemlos und taucht später bei einer echten Rechnung mit einer Kontonummer wieder auf. Entfernen Sie jede manuelle Umkehrung in dem Moment, in dem Sie zu RtLTextOut wechseln

Der Direction-Nebeneffekt, den Sie kennen sollten

Ein Aufruf von RtLTextOut ändert mehr als nur die Zeile, die Sie zeichnen. Es ändert auch die Leserichtungspräferenz des Dokuments in „Von-rechts-nach-links“, dieselbe Einstellung, die Sie sonst selbst über die Eigenschaft Direction vornehmen würden. Dieser Setter fügt den ViewerPreferences des Dokuments vpDirection hinzu, was einem Betrachter mitteilt, wie Doppelseiten angeordnet werden sollen und auf welcher Seite ein Layout mit gegenüberliegenden Seiten beginnt. Wenn das gesamte Dokument auf Arabisch oder Hebräisch verfasst ist, ist dies genau das, was Sie möchten, und Sie erhalten es kostenlos

Es ist genau deshalb wissenswert, weil es auf einer einzelnen Seite unsichtbar ist. Wenn das Dokument größtenteils von links nach rechts verläuft und nur einen Block von rechts nach links enthält, ändert der erste RtLTextOut-Aufruf dennoch die Voreinstellung der gesamten Datei, und in Ihrem einseitigen Probedruck wird dies nicht angezeigt. Das Symptom tritt Wochen später auf, wenn jemand eine Duplex-Broschüre druckt und die Doppelseiten gespiegelt herauskommen. Wenn Sie das nicht möchten, setzen Sie Direction nach dem Von-rechts-nach-links-Lauf ausdrücklich wieder zurück:

// RtLTextOut already set the document direction to RightToLeft;
// restore left-to-right if the document is predominantly LTR
Pdf.Direction := LeftToRight;

Bei einem Dokument, das wirklich von rechts nach links gelesen wird, belassen Sie es so. Der Punkt ist, zu wissen, dass der Aufruf auswirkungen auf das gesamte Dokument hat, damit die Broschürenüberraschung niemals passiert

Registrieren Sie die Schriftart, die Sie mitliefern, nicht diejenige, von der Sie hoffen, dass sie installiert ist

Die Neuordnung ist unerheblich, wenn die Schriftart keine zu zeichnenden Glyphen hat. Der klassische Fehler ist ein Bericht, der auf dem Computer des Entwicklers makellos gerendert wird, wo zufällig Arial Unicode MS vorhanden ist, und als eine Reihe leerer Kästchen auf dem Server eines Kunden herauskommt, wo Windows unbemerkt eine Schriftart ersetzt hat, die überhaupt keine arabische Abdeckung hat. Die Lösung besteht darin, den installierten Systemschriftarten nicht mehr zu vertrauen und eine zu registrieren, die Sie mit der Anwendung ausliefern

// Ship a known Arabic font and register it before drawing
Pdf.RegisterUnicodeTTF('C:\Fonts\NotoSansArabic.ttf');
Pdf.CurrentPage.SetFont('NotoSansArabic', [], 12, 178);
Pdf.CurrentPage.RtLTextOut(400, 700, 0, 'يوضح ملف PDF هذا');

Zwei Einschränkungen gehen mit der Registrierung einher. Eine über RegisterUnicodeTTF importierte Schriftart wird eingebettet, und HotPDFs Handhabung für eingebettetes Unicode erfordert das Dokument in PDF 1.5 oder höher. Das ist nur dann problematisch, wenn etwas im nachgeschalteten Prozess PDF 1.4 fordert, aber wenn das der Fall ist, erfolgt der Fehler ohne Meldung. Die andere ist eher juristischer als technischer Natur: TrueType-Dateien enthalten Bits für die Einbettungserlaubnis, und eine Schriftart, die auf dem Bildschirm gut aussieht, kann so lizenziert sein, dass es verboten ist, sie in Kundendokumenten auszuliefern. Bestätigen Sie die Lizenz vor der Einbettung, nicht erst nach einer Beschwerde

Ein vollständiges Konsolenbeispiel

Fügen wir die Einzelteile zusammen. Hier ist ein in sich geschlossenes Programm, das eine Seite mit einer arabischen Zeile, einer hebräischen Zeile und einer gemischten Zeile schreibt, die einen lateinischen Produktnamen enthält. Jeder Block legt seinen Zeichensatz fest und zeichnet dann in logischer Reihenfolge

program RtLTextOutDemo;

{$APPTYPE CONSOLE}

uses
  HPDFDoc;   // HotPDF main unit

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'RtLTextOut.pdf';
    Pdf.BeginDoc;

    // A Latin heading goes through the ordinary TextOut path
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(40, 780, 0, 'Right-to-left text with HotPDF');

    // Arabic: charset 178, logical order, RtLTextOut does the reordering
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 720, 0,
      'يوضح ملف PDF هذا كيفية التعامل مع النص العربي.');

    // Hebrew: charset 177
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 177);
    Pdf.CurrentPage.RtLTextOut(400, 680, 0,
      'קובץ PDF זה מדגים טקסט עברי הזורם מימין לשמאל.');

    // Mixed line: the embedded Latin word still reads left to right
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, 178);
    Pdf.CurrentPage.RtLTextOut(400, 640, 0,
      'مرحبا بالعالم! تم إنشاؤه بواسطة HotPDF');

    Pdf.EndDoc;
    Writeln('Wrote RtLTextOut.pdf');
  finally
    Pdf.Free;
  end;
end.

Führen Sie es aus und öffnen Sie das Ergebnis. Die arabischen und hebräischen Zeilen werden von rechts nach links gelesen, die Buchstaben verbinden sich dort, wo das Skript sie verbindet, und in der letzten Zeile steht das Token HotPDF von links nach rechts im arabischen Text. Das ist das spezzifikationsgemäße Ergebnis, auch wenn es jeden überrascht, der zum ersten Mal ein bidirektionales Layout sieht. Der letzte Punkt sollte unbedingt in Ihre Akzeptanzkriterien aufgenommen werden, bevor ein Muttersprachler die Ausgabe überprüft, da der eingebettete Text, der in die im Verhältnis zur umgebenden Schrift „falsche“ Richtung gelesen wird, am häufigsten als Fehler gemeldet wird, obwohl es gar keiner ist

Überprüfung der Ausgabe

Eine Seite, die richtig aussieht, ist nicht dasselbe wie eine Seite, die richtig ist. Überprüfen Sie sie also so, wie es ein nachgeschaltetes System tun würde. Kopieren Sie den Text aus dem Viewer zurück und vergleichen Sie die Codepoints mit Ihrem Quellstring; die korrekte visuelle Reihenfolge bei einer verschlüsselten logischen Reihenfolge ist ein echter Fehlermodus. Führen Sie die dokumentspezifische Suche des Viewers nach einem Wort durch, das Sie auf der Seite sehen können. Öffnen Sie die Datei dann auf einem Rechner, der nicht über Ihre Entwicklungsschriftarten verfügt, also dem Rechner, bei dem am ehesten eine stille Ersetzung auftritt. Nichts davon ersetzt das Lesen eines echten Dokuments durch einen Muttersprachler, wodurch Probleme aufgedeckt werden, die mit keinem synthetischen Teststring aufgespürt werden können. Tragen Sie diese Überprüfung also in den Kalender ein, bevor das Format ausgeliefert wird

RtLTextOut verarbeitet bidirektionale Neuordnung und arabische kontextuelle Verknüpfung, was den Großteil der Von-rechts-nach-links-Berichts- und Dokumentarbeit abdeckt. Wo es nicht mehr weitergeht, Schriften, die mehr als nur Neuanordnung und Zusammenfügen erfordern, wie die indischen Familien, und die optionalen OpenType-Funktionen, die auf Einzelglyphensubstitution beruhen, werden neben den Details zur Glyphenabdeckung und Textformung im begleitenden Artikel zu Arabische und RTL-Textformung mit HotPDF beschrieben

Die hier gezeigten Aufrufe RtLTextOut, SetFont und RegisterUnicodeTTF sind Teil der HotPDF-Komponente für Delphi und C++Builder