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

Вертикальный текст CJK в PDF с помощью Delphi и HotPDF

Поместите на страницу японский роман, и первое, что вы заметите, это то, что текст идет вниз по столбцу, а не поперек строки, и что столбцы продвигаются от правого края листа к левому. Читатель, выросший на этом, находит горизонтальный текст немного стерильным. Инженерная проблема заключается в том, что PDF, как и почти любая система цифрового текста, был построен вокруг горизонтальной базовой линии, которая растет слева направо, и поток контента не имеет понятия "вместо этого напишите этот абзац вниз". Поэтому, когда приложению Delphi необходимо создать сертификат, стихотворение, вывеску или юридический документ в традиционном формате для тайваньского, японского или корейского читателя, макет приходится собирать вручную: один символ под другим, один столбец слева от предыдущего

HotPDF дает вам переключатель, который выполняет за вас посимвольную бухгалтерию. Заданный вами шрифт имеет флаг IsVertical, и как только он включен, один вызов TextOut складывает всю строку в вертикальный столбец, а не пускает ее вдоль базовой линии. Размещение столбцов, порядок справа налево и одна незаметно важная замена глифа — это то, над чем работает остальная часть этой страницы

Страница PDF формата A4, созданная с помощью HotPDF, на которой китайский, японский и корейский текст сложены в вертикальные столбцы, читаемые справа налево
Одна страница 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 сохраняет эти два режима отдельно для каждого объекта шрифта, а не для каждой страницы. Именно это делает возможными смешанные макеты без какой-либо специальной обработки режимов с вашей стороны: каждый TextOut после вертикального SetFont складывается в стек, каждый TextOut после горизонтального идет вдоль базовой линии, и побеждает последний SetFont

Четвертый аргумент — это набор символов Windows, тот же самый, который принимает горизонтальный вызов. Передача DEFAULT_CHARSET позволяет системе разрешать глифы для каждой строки, что важно в данном случае, поскольку в вертикальном документе часто смешиваются алфавиты. Все остальное, что касается шрифта, по-прежнему применяется: он должен быть установлен на машине сборки, и вам почти всегда нужно FontEmbedding := True, чтобы файл отображал те же глифы CJK на программе для чтения, в которой никогда не было Arial Unicode MS

Один вызов TextOut — это один столбец

При активном вертикальном шрифте вызов TextOut больше не распространяет свою строку вбок от заданной точки. Он помещает первый символ наверх и пускает остальные прямо вниз, продвигаясь на высоту строки шрифта для каждого глифа. Передаваемый вами X фиксирует столбец; передаваемый вами Y фиксирует, где начинается верх столбца. Поэтому для верстки настоящего отрывка вы выдаете один 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 самого правого столбца, а затем вычитать фиксированный интервал между столбцами для каждого последующего вызова, как это делает пример с ColGap. Выбор интервала остается за вами; слишком плотно, и соседние столбцы соприкасаются, слишком свободно, и блок выглядит разреженным. Для основного текста от 12 до 18 пунктов интервал чуть больше размера шрифта читается комфортно

Глифы, которые должны изменить форму

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

Эта единственная замена покрывает случай, который ломает большинство наивных реализаций, но стоит знать, где общая проблема кроется глубже. Полная вертикальная типографика также поворачивает латинские буквы и западную пунктуацию на девяносто градусов, сдвигает малую кану и изменяет положение скобок и запятых на их вертикальные формы, и полная реализация этого живет в вертикальных функциях OpenType шрифта, а не в фиксированных правилах. HotPDF может задействовать эти функции, когда они есть в шрифте: вертикальные альтернативы (функции GSUB vert и vrt2) и вертикальный кернинг (поиски GPOS vkrn и vpal) включаются по желанию, применяясь вдоль вертикального пути, только когда активный шрифт фактически определяет их. Для смешанного текста CJK в одном шрифте с широким покрытием, таком как Arial Unicode MS, встроенной обработки U+30FC плюс равномерного шага высоты строки достаточно для создания правильных, читаемых столбцов; функции OpenType имеют значение, когда вы переходите на шрифт, предназначенный для тонкой вертикальной верстки, и вам нужно его собственное позиционирование каны и интервалы между глифами

Смешивание алфавитов и ориентаций на одной странице

Настоящие документы редко бывают чистыми. Вертикальная японская страница может содержать горизонтальную английскую подпись, номер страницы внизу или блок корейского текста, расположенный вертикально рядом с японским. Поскольку вертикальный флаг — это переключатель на уровне шрифта, вы комбинируете их, чередуя вызовы SetFont, а не управляя каким-либо состоянием в масштабах всей страницы. Задайте горизонтальный шрифт, напишите колонтитул и номер страницы, задайте вертикальный шрифт, разместите столбцы, снова задайте горизонтальный шрифт для нижнего колонтитула. Каждая область принимает режим самого последнего SetFont, поэтому единственная дисциплина, которая требуется — это вызывать его всякий раз, когда вы меняете направление

Одна деталь, которую следует планировать при смешивании алфавитов: китайские, японские и корейские иероглифы близки к квадратным и складываются в стопку с равномерным шагом, но латинские блоки, встроенные в вертикальный столбец, не имеют такого равномерного продвижения. Если вам нужно несколько латинских слов внутри текста, который в остальном вертикальный, преднамеренно решите, следует ли их повернуть, чтобы они шли вниз по столбцу, или установить вертикально в виде короткой горизонтальной вставки, и разместите этот фрагмент с помощью его собственного TextOut, а не позволяйте ему ехать на вертикальном шаге, предназначенном для иероглифов. Отношение к смешанному блоку текста как к отдельной проблеме размещения сохраняет ритм столбца нетронутым

От примера к рабочей среде

Части невелики, и они комбинируются предсказуемо. Включите вертикальный флаг в SetFont, выполните один TextOut для каждого столбца из фиксированного верхнего Y и уменьшайте X при каждом вызове, чтобы столбцы читались справа налево. Встройте шрифт, чтобы глифы CJK пережили путешествие на машину читателя, и позвольте компоненту обрабатывать знак долгой гласной U+30FC и, там, где шрифт поддерживает их, вертикальные функции OpenType. Оттуда запуск макета в производство — это в основном арифметика: получение позиций X столбца из измеренной ширины блока, разбиение длинных отрывков на столбцы, которые соответствуют высоте страницы, и резервирование места для горизонтальных колонтитулов и номеров страниц

Чтобы узнать больше о тексте и шрифтах, на которых это строится, включая соглашения о горизонтальном TextOut и регистрацию шрифтов Unicode, см. многоязычный пример Hello World и пример TextOut. Вертикальный переключатель SetFont и вызовы TextOut, показанные здесь, являются частью компонента HotPDF для Delphi и C++Builder