기술 문서

Delphi 및 HotPDF를 사용한 PDF의 CJK 세로 텍스트

일본어 소설을 페이지에 펼쳐보면 가장 먼저 눈에 띄는 것은 텍스트가 가로줄을 가로지르는 것이 아니라 단을 따라 아래로 흐르며, 단은 페이지의 오른쪽 가장자리에서 왼쪽으로 진행된다는 점입니다. 세로 읽기에 익숙한 독자들은 가로 텍스트가 왠지 낯설고 차갑게 느껴진다고 합니다. 엔지니어링 측면에서의 문제는 거의 모든 디지털 텍스트 시스템과 마찬가지로 PDF가 왼쪽에서 오른쪽으로 진행하는 수평 기준선(baseline)을 중심으로 구축되었으며, 콘텐츠 스트림(content stream)에는 "이 단락을 아래로 작성하라"는 개념이 없다는 것입니다. 따라서 Delphi 애플리케이션이 대만, 일본 또는 한국 독자를 위해 인증서, 시, 간판 또는 전통적 형식의 법률 문서를 작성해야 하는 경우, 레이아웃을 직접 한 글자씩 아래로, 한 단씩 왼쪽으로 수동 조립해야 합니다

HotPDF는 문자별 관리를 대신 수행하는 스위치를 제공합니다. 설정한 글꼴에는 IsVertical 플래그가 포함되어 있으며, 이 기능을 켜면 한 번의 TextOut 호출로 전체 문자열을 기준선(baseline)을 따라 출력하는 대신 수직 열에 쌓아줍니다. 열의 배치, 우측에서 좌측으로 진행하는 순서, 그리고 보이지 않지만 매우 중요한 글리프 대체(glyph substitution)에 대한 내용은 이 페이지의 나머지 부분에서 다루겠습니다

단일 Delphi 프로시저를 통해 생성된, 수직 열로 이루어진 중국어, 일본어 및 한국어 텍스트가 있는 A4 단일 페이지.
하나의 Delphi 프로시저 호출로 생성된, 중국어·일본어·한국어 텍스트가 세로 단으로 배열된 A4 페이지 한 장

스위치는 SetFont에 위치합니다

세로 레이아웃은 페이지나 문서의 속성이 아닙니다. 사용자가 텍스트를 그리는 글꼴의 속성이며, SetFont의 다섯 번째 인수를 통해 해당 기능을 켭니다

// SetFont(FontName, FontStyle, Size, FontCharset, IsVertical)
// 다섯 번째 인수가 현재 폰트를 세로 모드로 전환합니다.
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, False); // 수평
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 12, DEFAULT_CHARSET, True);  // 수직

플래그가 글꼴에 포함되어 있으므로, 마지막 인수를 다르게 하여 SetFont를 다시 호출하기만 하면 가로 쓰기와 세로 쓰기를 간단히 전환할 수 있습니다. 한 페이지 내에서 상단에는 가로 제목을, 그 아래에는 세로 본문을 배치할 수 있으며 HotPDF는 페이지 단위가 아닌 글꼴 개체 단위로 두 모드를 구분하여 유지합니다. 이것이 사용자가 어떠한 특별한 모드 처리 없이도 혼합 레이아웃을 구성할 수 있는 이유입니다. 세로 SetFont를 사용한 후의 모든 TextOut은 텍스트를 아래로 쌓아 올리고, 가로 SetFont를 사용한 후의 모든 TextOut은 텍스트를 기준선(baseline)을 따라 출력하며, 가장 최근에 호출된 SetFont 설정이 우선적으로 적용됩니다

네 번째 인수는 Windows 문자 집합으로, 가로 호출 시 취하는 인수와 동일합니다. DEFAULT_CHARSET를 전달하면 시스템이 문자열별로 글리프를 확인할 수 있는데, 세로 문서에서는 종종 여러 문자가 섞이므로 이 기능이 중요합니다. 글꼴과 관련된 그 외 모든 사항은 여전히 적용됩니다. 글꼴은 빌드 컴퓨터에 설치되어 있어야 하며, Arial Unicode MS 글꼴이 없는 리더에서도 동일한 CJK 글리프가 렌더링되도록 하려면 항상 FontEmbedding := True로 설정해야 합니다

한 번의 TextOut 호출은 하나의 열입니다

세로 글꼴이 활성화된 상태에서는 TextOut을 호출해도 지정된 지점부터 문자열이 가로로 퍼지지 않습니다. 첫 번째 문자를 맨 위에 심고 나머지 문자를 똑바로 아래로 나아가며 출력하는데, 각 글리프마다 글꼴의 줄 높이(line height)만큼 전진합니다. 전달한 X값은 열의 위치를 고정하고, 전달한 Y값은 열 상단의 시작 지점을 고정합니다. 따라서 실제 단락을 배치하려면 CJK 텍스트의 열이 우측에서 좌측으로 읽히기 때문에 열당 하나의 TextOut을 발행하고 호출 사이의 X값을 좌측으로 이동해야 합니다

var
  Pdf: THotPDF;
const
  ColTop = 80;    // 모든 열이 시작하는 Y, 위쪽 기준(포인트)
  ColGap = 28;    // 열 사이의 수평 거리
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'VerticalText.pdf';
    Pdf.FontEmbedding := True;       // 이식성 있는 렌더링을 위해 CJK 서체를 임베드합니다
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := psA4;

    // 먼저 일반 쓰기 모드로 된 수평 제목.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 16, DEFAULT_CHARSET, False);
    Pdf.CurrentPage.TextOut(60, 40, 0, 'Tang poem, vertical layout');

    // 폰트를 세로 모드로 전환합니다; 이제 아래의 모든 TextOut은 세로로 쌓입니다.
    Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 18, DEFAULT_CHARSET, True);

    // 열은 오른쪽에서 왼쪽으로 진행되므로, 호출할 때마다 X가 감소합니다.
    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;

시는 원래 읽혀야 할 방식대로 읽힙니다. 즉 가장 오른쪽 단을 먼저 위에서 아래로 읽은 다음, 눈이 왼쪽의 다음 단으로 이동합니다. 문자열 자체에는 이러한 순서를 설정(encode)하는 기능이 전혀 들어있지 않습니다. 이러한 순서 설정은 호출할 때마다 줄어드는 X 좌표값을 통해 적용됩니다. 방향을 거꾸로 설정하면 시가 반대로 출력되며, 이는 가로 레이아웃을 세로 레이아웃으로 변경(porting)할 때 발생하는 가장 흔한 실수 중 하나입니다

좌표: 페이지 아래로, 오른쪽에서 왼쪽으로

서로 반대 방향으로 당기는 두 개의 축이 작용하므로 정확하게 이해할 필요가 있습니다. HotPDF는 VCL 캔버스와 같은 프레임에서, 페이지의 왼쪽 위 모서리를 기준으로 Y값이 아래로 증가하는 방식으로 포인트 단위로 측정합니다. 따라서 수직 단은 작은 Y값(용지의 맨 위 근처)에서 시작하며, 세로 쓰기 모드에서 글리프 하나 전진량씩 더 큰 Y값을 향해 문자가 내려갑니다. 사용자는 각 단마다 시작 Y값을 한 번만 설정하면 되고 아래로 내리는 작업은 컴포넌트가 처리합니다

가로 축은 사용자가 직접 조작해야 하는 축입니다. 각 열(column)은 고유한 X값을 가지며, 읽는 순서가 우측에서 좌측이므로 이어지는 열들은 점점 더 작은 X값으로 이동합니다. 가장 오른쪽 단의 X값을 선택한 다음, ColGap이 있는 예제에서와 같이 호출이 이어질 때마다 고정된 단 간격을 빼는 것이 합리적입니다. 간격은 자유롭게 선택할 수 있지만, 너무 좁으면 인접한 열과 겹치고 너무 넓으면 글이 흩어져 보입니다. 12에서 18포인트의 본문 텍스트의 경우 글꼴 크기보다 약간 큰 간격이 편안하게 읽힙니다

모양이 변경되어야 하는 글리프

일부 문자들은 단순히 가로 형태를 회전시킨 것으로는 부족하여 텍스트가 세로로 바뀔 때 반드시 다른 문자로 대체되어야 합니다. 대부분의 구현을 넘어지게 하는 문자는 커피(コーヒー)와 같은 가타카나 단어에 나타나는 장모음 막대, 일본어 장음 부호 ー(U+30FC)입니다. 가로로 그리면 기준선(baseline) 위의 짧은 대시(dash) 형태가 됩니다. 만약 똑같은 글리프를 열(column)로 쌓게 된다면 단을 평평하게 가로지르게 될 텐데 이는 잘못된 형태입니다. 세로 일본어에서 이 표시는 사이에 낀 두 문자를 잇는 세로줄 모양(vertical stroke)이 됩니다. HotPDF가 이를 고쳐 주는지는 호출에 따라 다릅니다. 세로 글꼴을 쓰는 RtLTextOut은 런을 글리프 하나씩 쌓으면서 U+30FC를 세로 막대(U+007C)로 바꿉니다. 이 페이지에서 쓰인 TextOut은 그런 대체를 하지 않습니다. 이 호출은 문자열을 글꼴의 Identity-V 인코딩으로 기록하므로, 얻는 글리프는 글꼴이 U+30FC에 매핑해 둔 것, 보통은 가로 형태입니다. TextOut에서 세로 형태를 얻으려면 THotPDF.ShapingFeatures에 sfVerticalAlternates를 추가해 글꼴 자신의 vert / vrt2 대체가 이를 공급하게 하거나, 호출 전에 직접 문자를 바꿔 넣으십시오

장모음 기호는 대부분의 소박한 구현을 깨뜨리는 사례이지만, 일반적인 문제가 어디에서 더 깊어지는지 알아두는 가치가 있습니다. 완벽한 세로 타이포그래피는 또한 라틴 문자 및 서양식 구두점을 90도로 회전시키고, 작은 가나(kana) 문자를 이동시키며 괄호와 쉼표를 세로 형태에 맞게 재배치합니다. 그리고 이에 대한 완전한 구현은 고정된 규칙이 아닌 글꼴의 OpenType 세로 기능(vertical features)에 존재합니다. HotPDF는 글꼴에 이러한 기능이 포함되어 있을 때 이를 활용할 수 있습니다. 세로 대체 기능(vert 및 vrt2 GSUB 기능)과 세로 커닝(vkrn 및 vpal GPOS 조회)은 옵트인(opt-in) 방식으로, 활성화된 글꼴에 실제로 해당 기능이 정의되어 있을 때만 수직 경로를 따라 적용됩니다. Arial Unicode MS처럼 넓은 적용 범위를 가진 단일 서체의 표의 문자 텍스트라면 일정한 줄 높이 간격만으로도 정확하고 읽기 쉬운 단(column)을 만들 수 있습니다. OpenType 기능은 장모음 기호, 작은 가나, 구두점이 세로 형태를 필요로 하는 순간부터, 그리고 미세한 세로 조판을 위해 설계된 글꼴로 옮겨 가 그 고유한 가나 위치 지정과 글리프 간 간격을 원할 때 중요해집니다

한 페이지에 문자 및 방향 혼합하기

실제 문서는 순수하게 한 가지로만 작성되는 경우가 드뭅니다. 일본어로 세로 쓰기가 적용된 페이지에 가로로 된 영어 캡션이 있거나 페이지 하단에 페이지 번호가 있거나 일본어 옆에 한국어 단락이 세로로 배치되어 있을 수도 있습니다. 세로 플래그(vertical flag)는 글꼴 수준의 스위치이므로 전체 페이지 상태를 따로 관리할 필요 없이 SetFont를 교대로 호출하여 이들을 구성할 수 있습니다. 가로 글꼴을 설정하여 런닝 헤더(running header)와 폴리오(folio)를 쓰고, 세로 글꼴을 설정하여 단을 배치하며 바닥글을 위해 가로 글꼴을 다시 설정합니다. 각 영역은 가장 최근의 SetFont 모드를 선택하므로 방향을 변경할 때마다 해당 설정만 호출해주면 됩니다

문자가 혼합될 때 고려해야 할 한 가지 세부 사항: 한·중·일 표의 문자(ideograph)는 정사각형에 가깝고 일정한 간격으로 쌓아지지만, 수직 열(column)에 임베디드된 라틴 텍스트 출력(run)은 이렇게 일정한 간격을 갖지 못합니다. 기본적으로 세로로 된 텍스트 안에 몇 개의 라틴 단어가 필요한 경우 이들을 단을 따라 내려가도록 회전시킬지 아니면 짧은 가로 삽입형으로 똑바로 세워 배치할지 신중하게 결정하고 표의 문자(ideograph)를 의미하는 세로 간격을 따르지 않고 자체 TextOut으로 해당 문자열 조각의 위치를 조정하세요. 이러한 혼합 출력을 자체적인 배치 문제로 취급하면 단의 리듬(rhythm)을 원래대로 유지할 수 있습니다

샘플에서 제품 제작 환경으로

구성 요소들은 작고 예측 가능하게 결합됩니다. SetFont에서 수직 플래그(vertical flag)를 켜고, 고정된 최상단 Y값에서 열(column)당 TextOut을 하나씩 발행한 다음 호출 간에 X값을 줄여 단이 오른쪽에서 왼쪽으로 읽히도록 설정합니다. 글꼴을 임베드하여 CJK 글리프가 독자의 장비까지 살아남도록 하고, sfVerticalAlternates를 켜서 vert / vrt2를 갖춘 글꼴이 U+30FC 장모음 기호를 포함한 세로 형태를 공급하게 하거나, 글꼴에 그런 기능이 없으면 해당 기호는 호출 전에 직접 대체하십시오. 이 시점에서 레이아웃을 실제 제작 환경에 적용하는 과정은 대개 산술적인 계산과 관련이 있습니다. 측정된 텍스트 블록의 너비에서 열의 X 위치를 도출하고, 긴 단락을 페이지 높이에 맞는 여러 열로 나누며, 가로형 머리글(headers)과 폴리오(folios)를 위한 공간을 확보하는 등의 작업이 이에 해당합니다

가로 TextOut 규칙 및 유니코드 글꼴 등록을 포함하여 이를 기반으로 하는 광범위한 텍스트 및 글꼴 표면에 대해서는 다국어 Hello World 예제 및 TextOut 샘플을 참조하십시오. 여기에 나와있는 SetFont 수직 스위치 및 TextOut 호출은 Delphi 및 C++Builder용 HotPDF 컴포넌트의 일부입니다