Technical Article

RtLTextOut в HotPDF: PDF текст от дясно на ляво в Delphi

Изпратете арабското изречение يوضح ملف PDF هذا към стандартния TextOut и получената страница ще бъде грешна по два начина едновременно. Думите се изписват от ляво на дясно вместо от дясно на ляво, а буквите стоят отделно в своите изолирани форми вместо да се съединяват в свързани думи. Не възникват грешки. Кодът на Delphi се компилира, файлът се отваря, а проверяващият, който чете арабски, ви казва, че резултатът е неизползваем. Решението е едно извикване, а не смяна на библиотека: HotPDF насочва текста от дясно на ляво през отделен метод, RtLTextOut, който се справя с пренареждането, което стандартният TextOut не прави. Четири неща в този метод определят дали изходният резултат е неизползваем: какво прави той с низа, как неговият аргумент charset избира писмеността, промяната на ниво документ, която прави като страничен ефект, и подготовката на шрифта, която трябва да се извърши първо.

Защо текстът от дясно на ляво се нуждае от собствено извикване

Потокът от съдържание в PDF не съхранява редактируем текст. Той съхранява глифове на фиксирани позиции, което означава, че генериращият потока трябва сам да реши в какъв ред да се подредят тези глифове. На екрана операционната система прави това вместо вас: поставете арабски текст в TEdit и текстовият стек на ОС го пренарежда и свързва, преди изобщо да видите пиксел. Точно затова низът изглежда перфектно във вашата форма и се чупи в PDF файла. Десктопът е свършил работата безшумно, и в момента, в който пишете свой собствен поток от съдържание, работата се връща на ваша страна.

TextOut се доверява на подадените данни. То изчертава кодовите точки в реда, в който ги предавате, от ляво на дясно, което е правилно за латиница, кирилица и CJK (китайски, японски, корейски) и грешно за арабски и иврит. RtLTextOut е извикването, което първо пренарежда реда в визуален ред от дясно на ляво, след което го изчертава. HotPDF умишлено разделя двата метода, вместо да отгатва посоката от символите, така че изборът кой от тях да се извика определя поведението на писмеността. По-дълбоката механика на двупосочното пренареждане и контекстното свързване на арабски език са отделна тема, разгледана в статията за оформяне на арабски и RTL текст с HotPDF; тук практическата цел е по-тясна. Използвайте RtLTextOut за последователности от дясно на ляво, използвайте TextOut за всичко останало и никога не пренасочвайте едното през другото.

Диаграма на това как RtLTextOut пренарежда смесен арабски и латински ред във визуален ред от дясно на ляво, преди да го изчертае в PDF
RtLTextOut пренарежда всеки ред във визуален ред преди изчертаване: пасажите от дясно на ляво запазват своята последователност, докато вградените латински думи и цифри се четат от ляво на дясно вътре в реда.

Аргументът charset определя писмеността

Това, което указва на RtLTextOut дали подрежда арабски или иврит, не е самият метод, а шрифтът. SetFont приема Windows charset как свой четвърти аргумент, и тази стойност пренася правилата на писмеността в извикването от дясно на ляво: 178 избира арабски, 177 избира иврит. Настройте кодирането на символите (charset), след което изчертайте, и двата реда по-долу ще се покажат в правилния ред за четене без допълнителни настройки.

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

Два детайла за тези координати лесно могат да бъдат изпуснати. Позицията, която предавате, все още е началото на пасажа в собствената координатна система на страницата, измерена от долния ляв ъгъл с Y, нарастващ нагоре â€?същото начало, което използва всеки TextOut; RtLTextOut променя реда на глифовете, а не отправната точка на страницата. И както при всяко извикване за изчертаване, SetFont трябва да е първо и да се повтаря след всяко AddPage, тъй като текущият шрифт не се запазва след прекъсване на страницата. Ако забравите повторението, втората страница се връща към активния дотогава шрифт, което за арабски обикновено означава празни правоъгълници.

Не обръща текст, който вече сте обърнали ръчно

Единствената грешка, която отнема най-много време за отстраняване тук, е подаването на низ към RtLTextOut, който вече сте обърнали ръчно. Разработчиците стигат до този метод, след като първият им опит с обикновен TextOut е излязъл наобратно, и често временно решение е да обърнат символите в кода преди изчертаването. RtLTextOut обръща текста вътрешно сам, така че предварително обърнатият низ се обръща втори път и се връща в първоначалното си грешно състояние. Подавайте текста в неговия логически ред â€?реда, в който бихте го въвели и прочели на глас â€?и оставете извикването да свърши работата по пренареждането.

Капанът е по-неприятен от обикновено обръщане, тъй като двойно обърнатият низ може да изглежда правилен за някоя тестова фраза, изцяло на арабски, и след това да се счупи в момента, в който редът съдържа латинска дума или число. В линия от дясно на ляво тези вградени пасажи трябва да се четат от ляво на дясно, а ръчното обръщане разрушава това влагане, докато случаят само с арабски език случайно оцелява. Така бъгът преминава успешно първия ви бърз тест и се появява по-късно в реална фактура с номер на сметка в нея. Премахнете всяко ръчно обръщане в момента, в който преминете към RtLTextOut.

Страничният ефект върху Direction, който си струва да знаете

Извикването на RtLTextOut променя нещо повече от реда, който изчертавате. То също така превключва предпочитанието за посока на четене на документа на „оÑ?дясно на лявоâ€? същото, което иначе бихте задали сами чрез свойството Direction. Това задаване добавя vpDirection към ViewerPreferences на документа, което казва на четеца как да подреди разтворите от две страници и от коя страна започва оформлението с две срещуположни страници. Когато целият документ е на арабски или иврит, това е точно каквото искате, и го получавате безплатно.

Струва си да знаете за това именно защото е невидимо на единична страница. Ако документът е предимно от ляво на дясно с един блок от дясно на ляво, първото извикване на RtLTextOut пак ще промени предпочитанието за целия файл и нищо в единичната тествана страница няма да го покаже. Симптомът се появява седмици по-късно, когато някой отпечата двустранна брошура и разтворите излязат огледално. Ако това не е целта ви, върнете стойността на Direction изрично след пасажа от дясно на ляво:

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

За документ, който действително се чете от дясно на ляво, не го променяйте. Целта е да знаете, че извикването има ефект върху целия документ, за да не се окажете изненадани при печат на брошура.

Регистрирайте шрифта, който разпространявате, а не този, който се надявате да е инсталиран

Никакво пренареждане няма значение, ако шрифтът няма глифове за изчертаване. Класическият провал е отчет, който се изобразява безупречно на компютъра на разработчика, където случайно присъства Arial Unicode MS, но излиза като редове от празни кутийки на сървъра на клиента, където Windows тихомълком е заменил шрифта с такъв без никакво покритие на арабски език. Решението е да спрете да се доверявате на инсталираните системни шрифтове и да регистрирате такъв, който доставяте заедно с приложението.

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

Две ограничения съпътстват регистрацията. Шрифт, добавен чрез RegisterUnicodeTTF, се вгражда, а вградената Unicode поддръжка на HotPDF изисква документът да е на PDF 1.5 или по-нов; това е проблем само ако някоя последваща система изисква PDF 1.4, но когато това стане, грешката е тиха. Другото ограничение е по-скоро юридическо, отколкото техническо: TrueType файловете съдържат битове за разрешение за вграждане, и шрифт, който изглежда добре на екран, може да е лицензиран по начин, който забранява разпространението му в документи на клиенти. Потвърдете лиценза преди да го вградите, а не след като получите оплакване.

Пълен конзолен пример

Сглобявайки всички части заедно, ето самостоятелна програма, която записва една страница с ред на арабски, ред на иврит и смесен ред, съдържащ латинско име на продукт. Всеки блок настройва своя charset, след което изчертава в логически ред.

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.

Стартирайте я и отворете резултата. Редовете на арабски и иврит се четат от дясно на ляво, буквите се свързват там, където писмеността ги съединява, а на последния ред лексемата HotPDF седи от ляво на дясно в арабския пасаж, което е правилният резултат според спецификацията, макар и да изненадва всеки, който вижда двупосочно оформление за първи път. Този последен момент си струва да се включи в критериите ви за приемане, преди роден говорител да прегледа резултата, тъй като четенето на вградения пасаж в „грешнатаâ€?посока спрямо заобикалящата писменост е нещото, което най-често се докладва като бъг, без да е такъв.

Проверка на резултата

Страница, която изглежда правилно, не е същото като страница, която е правилна, така че я проверете по начина, по който ще го направи следваща по веригата система. Копирайте текста обратно от четеца и сравнете кодовите точки с оригиналния низ; правилният визуален ред с разбъркан логически ред е реален вид грешка. Стартирайте търсене в документа за дума, която виждате на страницата. След това отворете файла на машина, която няма вашите шрифтове за разработка â€?това е най-вероятният начин да откриете тиха замяна. Нищо от това не замества четенето на истински документ от роден говорител, което улавя проблеми, каквито синтетичен тестов низ не би могъл, така че планирайте този преглед, преди форматът да бъде доставен.

RtLTextOut се справя с двупосочното пренареждане и контекстното свързване на арабски език, което обхваща по-голямата част от работата с отчети и документи от дясно на ляво. Границите на възможностите му, писменостите, които се нуждаят от повече от пренареждане и свързване (като индийските езикови фамилии), и незадължителните OpenType функции, преминаващи през замяна на единични глифове, са описани заедно с подробностите за покритието на глифовете и оформянето в придружаващата статия за оформяне на арабски и RTL текст с HotPDF.

Извикванията на RtLTextOut, SetFont и RegisterUnicodeTTF, показани тук, са част от HotPDF Component за Delphi и C++Builder.