Technický článek

RtLTextOut v HotPDF: Text zprava doleva pro PDF v Delphi

Odešlete arabskou větu يوضح ملف PDF هذا do běžné funkce TextOut a stránka, která se vám vrátí, bude špatná hned dvěma způsoby najednou. Slova poběží zleva doprava místo zprava doleva a písmena budou odděleně ve svých izolovaných formách, místo aby se spojila do slov. K žádné chybě ale nedojde. Kód v Delphi se zkompiluje, soubor se otevře a recenzent, který umí číst arabsky, vám sdělí, že výstup je nepoužitelný. Nápravou je jedno volání, nikoli výměna knihovny: HotPDF směruje text psaný zprava doleva přes samostatnou metodu RtLTextOut, která řeší uspořádání, což běžná TextOut nezvládne. Tato stránka je pracovní referencí pro tuto metodu: její signatura a parametry, argument charset, který vybírá písmo, vedlejší efekt na úrovni dokumentu, nastavení písma, které musí přijít jako první, a chyby, které se nejčastěji dostanou na podporu, včetně jejich řešení

Signatura a parametry

procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: WideString); overload;
procedure RtLTextOut(X, Y: Single; angle: Extended;
  Text: PWORD; TextLength: Integer); overload;

Parametry X a Y ukotvují běh textu ve vlastním souřadnicovém systému stránky, měřeno od levého dolního rohu, přičemž Y roste nahoru – to je stejný počátek, který používá každé volání TextOut; RtLTextOut mění pořadí glyfů, nikoli bod, od kterého stránka měří. Parametr angle otáčí základní linii (baseline) naprosto stejně jako v TextOut, takže hodnota 0 nakreslí vodorovnou čáru. Parametr Text představuje řetězec v logickém pořadí, tedy v pořadí, ve kterém byste jej psali. Druhé přetížení (overload) přijímá stejná data UTF-16 jako hrubý buffer PWORD s explicitním počtem kódových jednotek, což je tvar, který se má použít, pokud text přichází z rozhraní API a nikoli jako řetězec Delphi. U starších verzí Delphi, které ještě neměly rozlišení přetížení pro tyto typy, je řetězcová podoba k dispozici pod názvem RtLTextOutStr se stejným seznamem parametrů

Rozdělení práce mezi tato dvě výstupní volání je striktní. TextOut kreslí kódové body (codepoints) v pořadí, ve kterém mu je předáte, což je správně pro latinku, cyrilici a CJK, ale špatně pro arabštinu a hebrejštinu. RtLTextOut naopak každý řádek nejprve přeřadí do vizuálního uspořádání zprava doleva a teprve poté kreslí, přičemž uvnitř řádku ponechává vložená latinská slova a číslice čitelné zleva doprava. HotPDF udržuje obě metody záměrně oddělené, místo aby odhadovalo směr ze znaků, takže volba, kterou metodu zavoláte, je volbou chování požadovaného skriptu. Používejte RtLTextOut pro zápisy zprava doleva a TextOut pro všechno ostatní. Proč vůbec existuje přeřazování pořadí (reordering), co vlastně dělá Unicode Bidirectional Algorithm a arabské kontextové spojování a kde končí možnosti tvarování textu (shaping) v HotPDF, to je předmětem doprovodného článku Tvarování arabského a RTL textu pomocí HotPDF; vše, co najdete níže, je pouze praktické nastavení

Diagram of how RtLTextOut reorders a mixed Arabic and Latin line into visual right-to-left order before drawing it into a PDF
RtLTextOut přeřadí každý řádek do vizuálního pořadí před jeho vykreslením: text zprava doleva si zachovává svoji posloupnost, zatímco vložená latinská slova a číslice se v rámci řádku čtou zleva doprava.

Argument charset rozhoduje o skriptu

To, co metodě RtLTextOut říká, zda má sázet arabštinu nebo hebrejštinu, není samotná metoda, ale písmo. SetFont přijímá jako čtvrtý argument znakovou sadu Windows (charset) a tato hodnota přenáší pravidla skriptu do volání pro směr zprava doleva: 178 vybírá arabštinu, 177 hebrejštinu. Nastavte znakovou sadu a pak kreslete; následující dva řádky vyjdou ve správném pořadí čtení bez další nutné konfigurace

// 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 זה');

Na jeden detail ohledně pořadí se ale snadno zapomíná: volání SetFont musí předcházet a musí se opakovat po každém AddPage, protože aktuální písmo, včetně znakové sady, nepřetrvá zlom stránky (page break). Pokud opakování vynecháte, druhá stránka spadne k jakémukoli písmu, které bylo zrovna aktivní, což pro arabštinu obvykle znamená prázdné čtverečky

Metoda nepřevrátí text, který jste už obrátili

Chybou, která spolkne nejvíce času při ladění (debugging), je dodání řetězce, který jste již ručně převrátili, do RtLTextOut. Lidé sahají po této metodě poté, co první pokus s obyčejnou funkcí TextOut nevyšel a text se zobrazil obráceně, takže častým dočasným řešením je převrátit znaky v kódu ještě před kreslením. RtLTextOut však interně převrací text samo, takže dříve obrácený řetězec je přehozen podruhé a ocitne se přesně tam, kde začal. Předávejte text v logickém pořadí (tak, jak byste ho napsali a nahlas přečetli) a nechte samotné volání udělat změnu pořadí

Tato past je zrádnější než běžné převrácení, protože dvakrát převrácený řetězec může vypadat správně u celoarabské testovací věty a okamžitě se rozbít v momentě, kdy řádek obsahuje latinské slovo nebo číslo. Uvnitř řádku psaného zprava doleva by se vložené latinské části měly číst zleva doprava a ruční přehození toto zanoření zničí, zatímco čistě arabský případ to může přežít. Takže chyba projde prvním smoke testem a objeví se až později na reálné faktuře obsahující číslo účtu. Jakmile přejdete na RtLTextOut, odstraňte každý ruční převrat textu

Vedlejší účinek u parametru Direction, který stojí za to znát

Zavolání RtLTextOut ovlivní více než jen řádek, který právě kreslíte. Také přepne preferenci směru čtení dokumentu na zprava doleva, tedy udělá přesně to, co byste jinak museli nastavit prostřednictvím vlastnosti Direction. Tato vlastnost přidá vpDirection do nastavení ViewerPreferences dokumentu, což prohlížeči říká, jak má uspořádat zobrazení dvou stránek vedle sebe a na které straně rozložení (spread) má začít. U dokumentu, který je čistě v arabštině nebo hebrejštině, je to přesně to, co chcete, a získáte to tak zdarma

Stojí ale za to o tom vědět, protože na jedné stránce to není vidět. Pokud se jedná o dokument, který je z většiny psaný zleva doprava a obsahuje jen jeden blok textu psaný zprava doleva, první volání RtLTextOut stejně změní preferenci celého souboru, a žádný důkaz na jedné stránce to neprozradí. Symptom se objeví až o týdny později, když někdo vytiskne oboustrannou brožuru a stránky vyjdou zrcadlově. Pokud toto chování nechcete, nastavte po dokončení bloku psaného zprava doleva Direction explicitně zpět:

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

U dokumentu, který se skutečně čte zprava doleva, do tohoto nastavení nesahejte. Záměrem je vědět, že toto volání má vliv na celý dokument, takže se nikdy nestane, že by vás po tisku překvapila brožura naruby

Registrujte písmo, které dodáváte, ne to, u kterého doufáte, že je nainstalované

Změna uspořádání textu k ničemu není, pokud písmo nemá glyfy, které by mohl vykreslit. Klasickým selháním je report, který se bezchybně vykreslí na počítači vývojáře, kde je náhodou Arial Unicode MS k dispozici, a následně se zobrazí jako řady prázdných obdélníků na zákazníkově serveru, kde Windows tiše nahradila písmo jiným, bez pokrytí arabštiny. Záchranou je přestat věřit systémovým písmům, která mohou a nemusí být nainstalována, a místo toho zaregistrovat písmo, které dodáváte s aplikací

// 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 هذا');

Tato registrace ovšem s sebou nese dvě omezení. Písmo vložené přes RegisterUnicodeTTF je do souboru embedováno (vloženo) a způsob zpracování vloženého Unicode v HotPDF vyžaduje PDF verze 1.5 nebo vyšší; to se stává problémem pouze v případě, když se něco v následném zpracování trvá na starším formátu PDF 1.4. V takovém případě ale k chybě dochází skrytě. Druhým omezením jsou licenční práva: soubory TrueType obsahují práva (embedding-permission bits) specifikující, zda mohou být vkládána. Písmo, které na monitoru vypadá hezky, může být licencováno takovým způsobem, že zakazuje jeho odesílání vloženého do zákaznických dokumentů. Zkontrolujte licenci dříve, než font začnete vkládat (embedovat), ne až po přijetí stížnosti

Kompletní příklad v konzoli

Spojením výše popsaných kroků získáme kompletní program, který vytvoří stránku s arabským řádkem, hebrejským řádkem a smíšeným řádkem nesoucím latinský název produktu. Každý blok má nastavený svůj charset (znakovou sadu) a text je odeslán do metody v logickém uspořádání (pořadí)

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.

Spusťte program a otevřete výsledek. Arabské a hebrejské řádky se čtou zprava doleva, písmena se spojují v místech, kde to určuje skript, a na posledním řádku sedí označení (token) HotPDF ukotvené zleva doprava uprostřed arabského běhu. Toto zanoření (nesting) je správným bidirekcionálním výsledkem a nikoliv chybou, ačkoli jej jako chybu často nahlašují recenzenti, kteří takovou konfiguraci vidí poprvé. Odkazovaný článek o tvarování textu výše vysvětluje, proč pravidla Unicode právě toto zanoření vyžadují a jak zformulovat kritéria přijetí tak, abyste tento problém už nikdy nemuseli s klienty řešit

Běžné chyby a jejich opravy

Každá níže zmíněná chyba se již reálně objevila na vláknech technické podpory a vždy vycházela ze skutečností popsaných v předchozích odstavcích

  • Výstupní text se čte pozpátku nebo jsou slova ve smíšeném řádku neuspořádaná — pravděpodobně jste manuálně řetězec otočili dříve, než jste jej předali funkci, většinou po předchozích pokusech s klasickým TextOut. Smažte všechna manuální obracení textu a pošlete data do funkce v logickém uspořádání. RtLTextOut provádí změnu uspořádání interně
  • Písmena jsou vytištěna rozpojená, a to každé v oddělené formě — předali jste text klasické funkci TextOut nebo jste zapomněli přes SetFont nakonfigurovat charset směrovaný na zprava doleva. Vždy sázejte tyto texty skrze RtLTextOut s předáním hodnoty 178 pro arabštinu či 177 pro hebrejštinu jakožto čtvrtého argumentu ve volání SetFont
  • Na počítači klienta (customer's machine) jsou vykresleny pouze prázdné čtverečky — operační systém Windows nemohl najít vhodný font, takže nahradil požadované písmo jiným, jež ale arabské ani hebrejské znaky nepodporuje. Přestaňte se spoléhat na to, co uživatel má nebo nemá na svém systému. Skrze RegisterUnicodeTTF zaregistrujte font, jež do instalace své aplikace přibalíte, a odvolejte se na něj následně funkcí SetFont
  • Druhá stránka se vykresluje nesprávným písmem — vaše navolené písmo (current font) nepřežije přesun funkcí AddPage na další list. Dbejte na zopakování volání SetFont a to včetně správného nastavení hodnoty pro charset
  • Zkušební oboustranný duplex tisk vytvořil pouze převrácený (zrcadlový) booklet z textu vedeného převážně formou LTR — úplně to první vyvolání RtLTextOut převrátilo kvůli svému vedlejšímu chování vlastnost dokumentu jménem Direction. Proto ihned poté, kdy dokončíte sázení zprava doleva, navraťte stav příkazem Pdf.Direction := LeftToRight
  • Embedovaný text v Unicode vykazuje neočekávanou (či dokonce prázdnou) sníženou funkčnost pro některé starší navazující (downstream) postupy — objevilo se něco, co vás omezuje ve workflow tím, že vynucuje dodržet formát PDF verze 1.4, jenže to odporuje požadavku nástroje pro zpracování embedded Unicode v knihovně HotPDF požadující ve výchozím nastavení minimálně formát 1.5 nebo vyšší. Proto nezapomínejte povolit formát s vyšším verzováním (raise the document version), případně toto umělé omezení zcela odstraňte (remove downstream constraint)

Dříve, než váš formát zašlete klientům nebo uvolníte formou aktualizace do produkce (ships), ujistěte se důkladným otestováním v reálném prohlížeči dokumentů. Překopírujte text z dokumentu jinam, pusťte vevnitř zabudovaný vyhledávač, otestujte takový hotový soubor někde na cizím zařízení postrádajícím instalované development fonty a nezapomeňte poslat finální dokument nativně mluvícímu uživateli pro závěrečné prověření čitelnosti. Abyste získali představu, na co vše se přesně zaměřit v testování, pro jaká slova si tvořit zkušební sestavy dat (test-string corpus) i pro nalezení tabulky zobrazující pokrytí, nahlédněte do již dříve zmiňovaného doprovodného článku zabývajícím se tímto: Tvarování arabského a RTL textu pomocí HotPDF

Ukázková rozhraní a volání (jako je RtLTextOut, SetFont či RegisterUnicodeTTF) zobrazená výše jsou součástí komponenty HotPDF Component pro Delphi a C++Builder