Artigo Técnico

Texto Vertical em Japonês e Chinês em PDF com Delphi

Coloque 「コード」 descendo a página desenhando um caractere por vez, cada chamada um em mais abaixo que a anterior, e três dos cinco glifos saem errados. Os dois colchetes de canto ficam na coluna como ficam em uma linha horizontal, abrindo de lado, e a marca de vogal longa ー é um traço reto cortando a coluna onde um leitor japonês espera um pequeno traço vertical. Só コ e ド, os dois katakana comuns, ficam como um diagramador os composaria

Rotacionar a linha inteira em noventa graus conserta os colchetes e a marca de vogal longa mais ou menos por acidente e quebra todo o resto: cada kanji e kana agora está deitado de lado. A escrita vertical faz três coisas ao mesmo tempo. Os caracteres ficam em pé, o avanço corre para baixo na página em vez de atravessá-la, e um punhado de caracteres troca para desenhos diferentes. Acertar as três exige uma fonte que traga formas verticais e métricas verticais, e uma fonte PDF configurada para usá-las

Diagrama do PDF Library for Delphi comparando uma linha horizontal de texto rotacionada com o verdadeiro modo de escrita vertical, em que os glifos permanecem eretos e o avanço corre para baixo na página
A escrita vertical posiciona caracteres em pé com métricas verticais por fonte em vez de rotacionar uma linha horizontal, e uma coluna copiada ainda se lê caractere por caractere

Ativando a escrita vertical no PDF Library for Delphi

O PDF Library for Delphi trata a escrita vertical como um modo de desenho. SetVerticalWritingMode(1) liga o modo, toda chamada DrawText depois disso corre para baixo na página a partir do ponto que você der, e SetVerticalWritingMode(0) traz o texto horizontal de volta. O setter retorna o modo que vigorava antes da chamada e o GetVerticalWritingMode lê o atual, então uma rotina auxiliar pode trocar o modo e depois repor exatamente o que encontrou

As duas coordenadas significam algo ligeiramente diferente do texto horizontal. O valor x nomeia a linha central da coluna: cada glifo pende com o seu meio horizontal sobre essa linha, então uma marca de pontuação estreita e um kanji de largura total ficam alinhados no mesmo eixo. O valor y é o topo da primeira célula de caractere, e a coluna cresce para baixo a partir dali. Com SetOrigin(1), que põe y = 0 na borda superior da página, a aritmética se lê como você deitaria a página no papel

var
  Lib: TPDFlib;
  H: Double;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('MS Mincho', 1);      // 1 = incorpora a face
    Lib.SetTextSize(12);
    Lib.SetVerticalWritingMode(1);            // o DrawText comum agora corre para baixo
    H := Lib.GetVerticalTextHeight('MS Mincho', 12, '第三章 保守点検');
    Lib.DrawText(480, 72, '第三章 保守点検');     // a coluna começa em y = 72
    Lib.SetVerticalWritingMode(0);            // volta ao horizontal
    // Origin 1 põe y = 0 no topo, então 72 + H é o pé da coluna;
    // a legenda horizontal fica uma linha abaixo dela
    Lib.DrawText(456, 72 + H + 18, 'Chapter 3');
    Lib.SaveToFile('manual-ja.pdf');
  finally
    Lib.Free;
  end;
end;

O GetVerticalTextHeight recebe o nome da face, o tamanho e o texto, e retorna o comprimento da coluna em pontos. No exemplo, 72 + H é o pé da coluna e a legenda em inglês vai 18 pontos abaixo dele, começando 24 pontos à esquerda da linha central da coluna para que a linha latina curta fique aproximadamente sob a coluna. A função retorna 0 quando não consegue abrir uma face com esse nome, e um layout construído sobre altura zero põe a legenda em cima da coluna, então teste antes de usar o valor

Por que os colchetes ficam de lado em uma coluna vertical?

Porque a fonte guarda dois desenhos de cada caractere desse tipo e o horizontal é o padrão. Faces OpenType listam as alternativas em uma feature GSUB chamada vert, ou vrt2 em faces que vêm com glifos já rotacionados para diagramação vertical. Dar shape a uma linha de texto retorna os glifos horizontais, e algo precisa aplicar a feature vertical depois, um glifo por um glifo. Colchetes de canto, parênteses e a marca de vogal longa viram para correr ao longo da coluna; kana pequenos e pontuação como 、 e 。 se movem para um canto diferente do seu quadrado

O PDF Library for Delphi lê essa feature da face com que está desenhando e troca cada glifo pela forma vertical que a fonte nomeia. Quando uma face carrega as duas features, o vrt2 vence e o vert é ignorado. A especificação pretende que o vrt2 substitua o vert, e ler ambos pode entregar a um caractere dois glifos verticais diferentes dependendo de qual lookup responde primeiro. Uma face que não tem nenhuma das features mantém os seus glifos comuns. Preferimos esse desfecho a uma tabela de substituição embutida: uma tabela chutada acerta os caracteres comuns e sobrescreve silenciosamente as faces cujos designers trataram um caractere de forma diferente

Despeje as tabelas GSUB das cópias que vêm com o Windows e as contagens ficam bem distantes: a feature vertical do MS Gothic mapeia 979 glifos, a do Yu Gothic 2.491, a do SimSun 72, e a do Arial nenhuma (os números exatos mudam com as versões da fonte). Uma face latina que por acaso contenha alguns kana vai desenhar uma coluna em pé com todo colchete e marca de vogal longa na forma horizontal, porque não há nada na fonte para substituir

Diagrama do PDF Library for Delphi mostrando como o recurso vertical da fonte fornece formas de parêntese e marca de som prolongado que se deitam ao longo de uma coluna vertical, enquanto caracteres comuns permanecem inalterados, tudo medido por GetVerticalTextHeight
O recurso vertical da própria fonte decide quais glifos mudam de forma em uma coluna, e GetVerticalTextHeight mede as formas que realmente serão desenhadas

A substituição também muda a medição. Uma forma vertical pode ter o seu próprio avanço, diferente do glifo horizontal que substitui, então o GetVerticalTextHeight aplica a mesma substituição antes de somar os avanços. Meça as formas horizontais e desenhe as verticais, e a coluna ultrapassa a caixa que você reservou para ela em alguns pontos por colchete

Nada disso toca o texto em si. Copie a coluna de um visualizador e você recebe 「コード」 de volta, porque cada glifo vertical mapeia para o caractere que você passou. Busca e extração de texto veem a sua string de origem, nunca as formas de apresentação rotacionadas

Como as métricas verticais chegam ao PDF

Cada caractere em uma execução vertical avança pela distância que as próprias métricas verticais da fonte lhe dão. Abra a saída em um inspetor de PDF e você vai encontrar uma fonte Type0 com codificação Identity-V cujo CIDFont carrega uma entrada W2 para cada glifo usado: o avanço vertical da tabela vmtx da face, metade da largura do glifo como offset para a linha central da coluna, e a origem vertical. A ISO 32000-1 §9.7.4.3 define esses três números. Para uma face sem métricas verticais não há array W2, e o padrão DW2 avança cada caractere em exatamente um em

A origem vertical é onde fontes de terceiros discordam entre si. A tabela vhea tem campos ascender e descender que parecem a fonte óbvia, mas a convenção de sinal do descender não é compartilhada entre faces. Leia o campo das cópias do Windows com qualquer visualizador de tabelas de fonte e ele sai +140 no MS Gothic e +120 no Yu Gothic, contra −144 no SimSun e −500 no MingLiU, e os valores podem mudar com as versões da fonte. Qualquer posição derivada desse campo deriva por quantidades diferentes para cada face. O PDF Library for Delphi pega a origem do ascender vertical, que as faces CJK nomeadas acima reportam de forma consistente, e cai para 880 unidades — o padrão que a própria especificação PDF dá ao DW2 — quando uma face não reporta nada plausível

Posicionar glifos verticais à mão tem a sua própria armadilha. Texto horizontal frequentemente ajusta espaçamento com números dentro de um array TJ. Sob um modo de escrita vertical, a ISO 32000-1 §9.4.4 aplica esses números ao longo da coluna, então uma correção calculada para espaçamento horizontal empurra cada caractere mais para baixo na página. É por isso que uma execução vertical no content stream é uma string simples de códigos de glifo sem ajuste nenhum, e cada bit de espaçamento vem do W2

Ao conferir a saída, olhe a tinta em vez dos dicionários. Encontrar /Identity-V na fonte prova que a codificação foi declarada; não diz nada sobre para onde o texto realmente corre. Renderizar a página e medir o bounding box dos pixels desenhados diz: uma execução de sete caracteres deve sair com cerca de um caractere de largura e sete de altura

Uma execução vertical em uma página horizontal

O DrawVerticalText desenha uma única execução vertical e deixa o modo de escrita em paz. Ele recebe a posição, o nome da fonte, o tamanho e o texto, então a seleção de fonte atual também fica intocada. Etiquetas de lombada, selos, uma única coluna de nomes ao lado de uma tabela: essas são as exceções verticais dentro de um documento horizontal, e ligar e desligar um modo global ao redor de cada uma é mais estado do que o trabalho merece

Diagrama do PDF Library for Delphi do DrawVerticalText colocando uma execução vertical ereta ao lado de linhas horizontais comuns de DrawText em uma página compartilhada sem tocar o modo de escrita global
DrawVerticalText carrega a própria fonte, tamanho e posição, então uma única coluna vertical coexiste com as chamadas horizontais ao redor
// Uma execução vertical dentro de uma página que de resto é horizontal
if Lib.DrawVerticalText(520, 96, 'MS Mincho', 14, '保守点検記録') = 0 then
begin
  // A face não pôde ser aberta ou não pôde definir esse texto.
  // LogLayoutProblem é rotina sua, não faz parte da biblioteca
  LogLayoutProblem('vertical label on page ' + IntToStr(Lib.SelectedPage));
end;
// O texto horizontal ao redor não é afetado
Lib.DrawText(72, 96, 'Maintenance inspection record');

Uma página que mistura as duas direções carrega a face duas vezes nos seus recursos, uma com codificação horizontal e outra com Identity-V, construídas do mesmo programa de fonte. Cada direção mantém as próprias métricas, então uma página de livro japonês com corpo vertical e títulos correntes horizontais, ou um certificado chinês com título vertical sobre linhas de detalhes horizontais, sai com as duas direções espaçadas corretamente

O valor de retorno é a razão de recorrermos ao DrawVerticalText mesmo em documentos que são majoritariamente verticais. No desenho baseado em modo, uma execução que não pode ser posta verticalmente é desenhada da forma horizontal comum em vez de ser descartada, e o DrawText reporta sucesso nos dois casos. Para corpo de texto, esse é o melhor padrão, porque uma linha horizontal perdida é fácil de avistar na prova e um parágrafo ausente não é. Para uma etiqueta cuja direção é todo o ponto, use o DrawVerticalText e trate um 0 como falha de layout

Diagramando colunas da direita para a esquerda

A ordem das colunas é você quem decide. Texto vertical em japonês e chinês tradicional se lê coluna a coluna da direita para a esquerda, então uma página começa na margem direita e cada nova coluna fica à esquerda da anterior. Nenhuma configuração de modo de escrita infere isso a partir do texto, e o mesmo vale para a ordem das páginas em um livro encadernado à direita e para onde pertencem furigana, notas de rodapé e legendas de figuras

A rotina abaixo quebra uma string em colunas que cabem em uma altura fixa, medindo com o GetVerticalTextHeight conforme avança e andando para a esquerda um em e meio por coluna:

procedure DrawColumns(Lib: TPDFlib; const Face: WideString; Size: Double;
  const Text: WideString; Right, Top, BoxH: Double);
var
  Col, Trial: WideString;
  I: Integer;
  X: Double;
begin
  // Guarda sugerida, não é checagem da biblioteca: altura zero significa que a face
  // não pôde ser aberta, e o loop abaixo nunca quebraria uma coluna
  if Lib.GetVerticalTextHeight(Face, Size, Text) <= 0 then
    Exit;
  X := Right;                                  // primeira coluna na margem direita
  Col := '';
  for I := 1 to Length(Text) do                // assume que não há surrogate pairs
  begin
    Trial := Col + Text[I];
    if (Col <> '') and (Lib.GetVerticalTextHeight(Face, Size, Trial) > BoxH) then
    begin
      Lib.DrawVerticalText(X, Top, Face, Size, Col);
      X := X - Size * 1.5;                     // próxima coluna à esquerda
      Col := Text[I];
    end
    else
      Col := Trial;
  end;
  if Col <> '' then
    Lib.DrawVerticalText(X, Top, Face, Size, Col);
end;

Remedir a string de teste a cada caractere é mais lento que somar avanços, mas mede exatamente o que será desenhado, substituições incluídas. O que o loop não faz é aplicar regras de kinsoku. Um colchete de fechamento ou 。 no topo de uma coluna, ou um colchete de abertura abandonado no pé de uma, é um erro de diagramação para um leitor japonês, e corrigir significa puxar ou empurrar um caractere através da quebra antes de desenhar. Surrogate pairs pedem o mesmo cuidado: indexar Text[I] parte um caractere fora do Basic Multilingual Plane ao meio, então avance por code point se o seu texto pode conter um

Para documentos montados a partir de dados, o passo a passo de busca de texto e enumeração de elementos de página mostra como ler de volta o que caiu em cada página, o que é um jeito rápido de confirmar que toda coluna carrega os caracteres que você pretendia

Qual fonte um documento vertical deve usar?

Uma face CJK com métricas verticais próprias e uma feature vert ou vrt2. Sem elas você ganha caracteres em pé avançando um em por vez e nenhuma mudança de forma. Quando o texto vertical parece sutilmente errado, examine a fonte antes de examinar o código. Um visualizador de tabelas que liste as features GSUB de uma face e as tabelas vhea e vmtx dela diz em um minuto se a face pode fazer o trabalho

O DrawVerticalText e o GetVerticalTextHeight abrem uma face instalada pelo nome via Windows. Em builds Lazarus para outras plataformas, a escrita vertical passa pelo SetVerticalWritingMode e pelo DrawText, que trabalham a partir do programa da fonte que você carregou e selecionou, então as mesmas formas verticais e métricas se aplicam contanto que essa fonte seja a face CJK que você pretendia usar

A incorporação segue as regras e os custos de sempre. Uma face CJK completa vai a muitos megabytes, então subsetting é o padrão sensato para qualquer documento que deixa a sua máquina. As notas sobre otimização de tamanho de arquivo PDF e subsetting de fontes cobrem o que esperar, e o passo a passo de incorporar fontes ausentes a um PDF existente cobre o caso de reparo em que um documento vertical chega sem as suas fontes

A escrita CJK vertical vive na mesma API de desenho que o restante das funções de texto do PDF Library for Delphi, uma biblioteca PDF Pascal nativa para Delphi, C++Builder e Lazarus. A página do produto PDF Library for Delphi lista o conjunto completo de recursos de fonte e texto ao lado dela