Техническа статия

CJK вертикален текст в PDF с Delphi и HotPDF

Поставете японски роман на страницата и първото нещо, което забелязвате, е че текстът върви надолу по колоната, а не по линията, и че колоните напредват от десния край на листа към левия. Читател, израснал с това, намира хоризонталния текст за леко клиничен. Инженерният проблем е, че PDF, като почти всяка цифрова текстова система, е изграден около хоризонтална базова линия (baseline), която расте отляво надясно, и потокът от съдържание (content stream) няма понятие за „напиши този абзац надолу вместо това“. Така че, когато едно Delphi приложение трябва да създаде сертификат, стихотворение, табела или правен документ в традиционен формат за тайвански, японски или корейски читател, оформлението трябва да бъде сглобено на ръка: един знак под следващия, една колона вляво от предишната

HotPDF ви дава превключвател (switch), който извършва счетоводството знак по знак вместо вас. Шрифтът, който задавате, носи флаг IsVertical и след като той е включен, едно-единствено извикване на TextOut подрежда (stacks) цял низ във вертикална колона, вместо да го пуска по базова линия. Разположението на колоната, редът от-дясно-наляво и една тихо важна замяна на глиф са това, през което преминава остатъчната част от тази страница

A4 PDF page produced by HotPDF showing Chinese, Japanese, and Korean text stacked into vertical columns that read from right to left
Една-единствена страница A4 с китайски, японски и корейски текст във вертикални колони, генерирана от една Delphi процедура.

Превключвателят живее в SetFont

Вертикалното оформление не е свойство на страницата или на документа. Това е свойство на шрифта, с който чертаете, и го включвате с петия аргумент на SetFont:

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// The 5th argument flips the current font into vertical mode.
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // horizontal
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // vertical

Тъй като флагът се носи от шрифта, вие превключвате между хоризонтално и вертикално писане просто като извикате SetFont отново с различен последен аргумент. Една страница може да съдържа хоризонтално заглавие най-отгоре и вертикален основен текст под него, и HotPDF държи двата режима разделени за обект на шрифт, а не за страница. Това е, което прави смесените оформления възможни без никакво специално управление на състоянието (mode handling) от ваша страна: всяко TextOut след вертикален SetFont се подрежда вертикално, всяко TextOut след хоризонтален такъв върви по базовата линия и последният SetFont печели

Четвъртият аргумент е наборът от знаци (character set) на Windows, същият, който приема и хоризонтално извикване. Подаването на DEFAULT_CHARSET позволява на системата да разрешава глифове за всеки низ, което има значение тук, защото един вертикален документ често смесва скриптове. Всичко останало относно шрифта все още важи: той трябва да бъде инсталиран на машината за изграждане (build machine) и почти винаги ще искате FontEmbedding := True, така че файлът да рендира същите CJK глифове на четец, който никога не е имал Arial Unicode MS

Едно извикване на TextOut е една колона

С активен вертикален шрифт извикването на TextOut вече не разпространява низа си настрани от дадената точка. То засажда първия знак в горната част и отвежда останалите право надолу, напредвайки с височината на реда на шрифта за всеки глиф. X, който подавате, фиксира колоната; Y, който подавате, фиксира къде започва върхът на колоната. За да разположите истински пасаж (passage), вие следователно издавате по едно TextOut за колона и стъпвате с X наляво между извикванията, защото CJK колоните се четат от дясно на ляво

var
  Pdf: THotPDF;
const
  ColTop = 760;   // y of the first glyph in every column (points)
  ColGap = 28;    // horizontal distance between columns
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // embed the CJK face for portable rendering
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // A horizontal heading first, in the ordinary writing mode.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 800, 0, 'Tang poem, vertical layout');

    // Switch the font into vertical mode; every TextOut below now stacks.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // Columns advance right to left, so X decreases with each call.
    Pdf.CurrentPage.TextOut(520, ColTop, 0, '床前明月光');
    Pdf.CurrentPage.TextOut(520 - ColGap,     ColTop, 0, '疑是地上霜');
    Pdf.CurrentPage.TextOut(520 - ColGap * 2, ColTop, 0, '舉頭望明月');
    Pdf.CurrentPage.TextOut(520 - ColGap * 3, ColTop, 0, '低頭思故鄉');

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

Стихотворението се чете, както трябва: най-дясната колона първо, отгоре надолу, след това окото скача наляво към следващата колона. Нищо в самия низ не кодира този ред. Вие го кодирате в координатите X, намалявайки ги извикване по извикване. Обърнете посоката наобратно и стихът излиза обърнат, което е най-честата единична грешка при портване на хоризонтално оформление към вертикално

Координати: надолу по страницата, от дясно на ляво

Две оси са в играта и те дърпат в противоположни посоки, така че си струва да бъдем прецизни. HotPDF измерва от долния ляв ъгъл на страницата, като Y се увеличава нагоре, в пунктове. Една вертикална колона следователно започва при високо Y (близо до горния край на листа) и знаците се спускат оттам, тъй като HotPDF изважда височина на реда за всеки от тях. Вие задавате това начално Y веднъж на колона и компонентът се справя със спускането

Хоризонталната ос е тази, която управлявате ръчно. Всяка колона седи на свое собствено X, а последователните колони стъпват към по-малки стойности на X, защото редът на четене е от дясно на ляво. Разумен ритъм е да изберете X на най-дясната колона, след което да извадите фиксирана празнина (column gap) за всяко последващо извикване, както прави примерът с ColGap. Празнината избирате вие; твърде тясна и съседните колони се докосват, твърде широка и блокът изглежда рядък. За основен текст при 12 до 18 пункта, празнина малко по-голяма от размера на шрифта се чете комфортно

Глифове, които трябва да променят формата си

Няколко знака не са просто завъртени копия на своите хоризонтални същности; те трябва да бъдат заместени, когато текстът стане вертикален. Този, с който HotPDF се справя вместо вас, е японският знак за удължен звук (U+30FC) – лентата за дълга гласна, която се появява в катакана думи като コーヒー. Начертан хоризонтално, това е късо тире на базовата линия. Ако подредите същия този глиф в колона, той би лежал напречно на колоната, което е грешно: във вертикален японски знакът става вертикална черта, свързваща двата знака, между които седи. HotPDF открива U+30FC във вертикалния път и го рендира като вертикална лента (U+007C), така че знакът за дълга гласна да сочи в правилната посока без никаква работа от ваша страна

Тази единствена замяна покрива случая, който чупи повечето наивни имплементации, но си струва да знаете къде общият проблем навлиза по-дълбоко. Пълната вертикална типография също така завърта латинските букви и западната пунктуация на деветдесет градуса, измества малката кана (small kana) и препозиционира скобите и запетаите в техните вертикални форми, и пълната имплементация на това живее във вертикалните OpenType характеристики (features) на даден шрифт, а не във фиксирани правила. HotPDF може да се възползва от тези характеристики, когато шрифтът ги носи: вертикалните алтернативи (GSUB характеристиките vert и vrt2) и вертикалният кърнинг (GPOS lookups vkrn и vpal) са по избор (opt-in), прилагани по вертикалния път само когато активният шрифт действително ги дефинира. За смесен CJK текст в един шрифт с широко покритие като Arial Unicode MS, вградената обработка на U+30FC плюс равномерната стъпка за височина на реда са достатъчни за създаване на правилни, четими колони; OpenType характеристиките имат значение, когато преминете към шрифт, проектиран за фино вертикално набиране, и искате неговото нативно позициониране на кана и разстояние между глифовете

Смесване на скриптове и ориентации на една страница

Истинските документи рядко са чисти. Вертикална японска страница може да носи хоризонтален английски надпис, номер на страница по долния ръб или блок от корейски, набран вертикално до японския. Тъй като вертикалният флаг е превключвател на ниво шрифт, вие композирате тези неща чрез редуване на извиквания на SetFont, а не чрез управление на някакво състояние на цялата страница. Задайте хоризонтален шрифт, напишете бягащото заглавие (running header) и фолиото, задайте вертикален шрифт, разположете колоните, задайте хоризонтален шрифт отново за долен колонтитул. Всеки регион придобива режима на най-скорошния SetFont, така че единствената необходима дисциплина е да го извикате винаги, когато променяте посоката

Един детайл, за който трябва да планирате, когато скриптовете се смесват: китайските, японските и корейските идеограми са близки до квадрат и се подреждат на равномерна стъпка, но латинските рънове (runs), вложени във вертикална колона, нямат такова равномерно напредване (advance). Ако имате нужда от няколко латински думи вътре в иначе вертикален текст, решете умишлено дали те трябва да бъдат завъртени, за да вървят надолу по колоната, или да бъдат набрани изправени като къса хоризонтална вложка, и позиционирайте този фрагмент със собствен TextOut, вместо да го оставяте да се вози на вертикалната стъпка, предназначена за идеограми. Третирането на смесения рън (mixed run) като собствен проблем за позициониране запазва ритъма на колоната непокътнат

От пример към продукция

Парчетата са малки и се композират предсказуемо. Включете вертикалния флаг в SetFont, издайте по едно TextOut за колона от фиксирано горно Y и намалете X в различните извиквания, така че колоните да се четат от дясно на ляво. Вградете шрифта, така че CJK глифовете да преживеят пътуването до машината на читателя, и оставете компонента да се справи с U+30FC знака за дълга гласна и, където шрифтът ги поддържа, вертикалните OpenType характеристики. Оттам нататък вкарването на оформлението в продукция (productionizing) е предимно аритметика: извличане на X позициите на колоните от измерена ширина на блока, разбиване на дълги пасажи в колони, които се побират във височината на страницата, и запазване на място за хоризонтални заглавки (headers) и фолиа

За по-широката повърхност на текста и шрифтовете, върху която това надгражда, включително хоризонталните конвенции на TextOut и регистрацията на Unicode шрифтове, вижте многоезичния пример Hello World и примера за TextOut. Вертикалният превключвател в SetFont и извикванията на TextOut, показани тук, са част от компонента HotPDF за Delphi и C++Builder