Artículo técnico

Texto vertical en japonés y chino en PDF con Delphi

Coloca 「コード」 hacia abajo por la página dibujándolo un carácter cada vez, cada llamada un em más abajo que la anterior, y tres de los cinco glifos salen mal. Los dos corchetes angulares se sientan en la columna tal y como se sientan en una línea horizontal, abriendo de lado, y la marca de vocal larga ー es un guion plano que corta la columna donde un lector japonés espera un trazo vertical corto. Solo コ y ド, los dos katakana ordinarios, lucen como los compondría un tipógrafo

Girar la línea entera noventa grados arregla los corchetes y la marca de vocal larga más o menos por accidente y rompe todo lo demás: cada kanji y kana queda ahora tumbado de lado. La escritura vertical hace tres cosas a la vez. Los caracteres permanecen erguidos, el avance corre hacia abajo por la página en lugar de a través de ella, y un puñado de caracteres cambia a dibujos distintos. Acertar las tres cosas necesita una fuente que lleve formas verticales y métricas verticales, y una fuente PDF configurada para usarlas

Diagrama de PDF Library for Delphi que compara una línea horizontal de texto rotada con el auténtico modo de escritura vertical, donde los glifos permanecen erguidos y el avance corre hacia abajo por la página
La escritura vertical compone los caracteres verticales con métricas verticales por tipografía en lugar de rotar una línea horizontal, y una columna copiada se sigue leyendo carácter por carácter

Activar la escritura vertical en PDF Library for Delphi

PDF Library for Delphi trata la escritura vertical como un modo de dibujo. SetVerticalWritingMode(1) lo activa, cada llamada DrawText posterior corre hacia abajo por la página desde el punto que le des, y SetVerticalWritingMode(0) devuelve el texto horizontal. El setter devuelve el modo vigente antes de la llamada y GetVerticalWritingMode lee el actual, así que una rutina de ayuda puede cambiar el modo y luego restablecer exactamente lo que encontró

Las dos coordenadas significan algo ligeramente distinto que en texto horizontal. El valor x nombra la línea central de la columna: cada glifo cuelga con su punto medio horizontal sobre esa línea, así que un signo de puntuación estrecho y un kanji de ancho completo quedan alineados en el mismo eje. El valor y es la cima de la primera celda de carácter, y la columna crece hacia abajo desde ahí. Con SetOrigin(1), que pone y = 0 en el borde superior de la página, la aritmética se lee como colocarías la página sobre el papel

var
  Lib: TPDFlib;
  H: Double;
begin
  Lib := TPDFlib.Create;
  try
    Lib.SetOrigin(1);
    Lib.AddTrueTypeFont('MS Mincho', 1);      // 1 = incrustar la fuente
    Lib.SetTextSize(12);
    Lib.SetVerticalWritingMode(1);            // el DrawText normal ahora corre hacia abajo
    H := Lib.GetVerticalTextHeight('MS Mincho', 12, '第三章 保守点検');
    Lib.DrawText(480, 72, '第三章 保守点検');     // la columna empieza en y = 72
    Lib.SetVerticalWritingMode(0);            // de vuelta a horizontal
    // Con Origin 1, y = 0 queda arriba, así que 72 + H es el pie de la columna;
    // el rótulo horizontal se sitúa una línea por debajo
    Lib.DrawText(456, 72 + H + 18, 'Chapter 3');
    Lib.SaveToFile('manual-ja.pdf');
  finally
    Lib.Free;
  end;
end;

GetVerticalTextHeight toma el nombre de la fuente, el tamaño y el texto, y devuelve la longitud de la columna en puntos. En el ejemplo, 72 + H es el pie de la columna y el rótulo en inglés va 18 puntos por debajo, empezando 24 puntos a la izquierda de la línea central de la columna, para que la línea latina corta quede más o menos bajo la columna. La función devuelve 0 cuando no puede abrir una fuente con ese nombre, y una maquetación construida sobre una altura cero pone el rótulo encima de la columna, así que compruébalo antes de usar el valor

¿Por qué los corchetes yacen de lado en una columna vertical?

Porque la fuente guarda dos dibujos de cada uno de esos caracteres y el horizontal es el predeterminado. Las fuentes OpenType listan las alternativas en una característica GSUB llamada vert, o vrt2 en las fuentes que traen glifos ya rotados para composición vertical. Dar forma a una línea de texto devuelve los glifos horizontales, y algo tiene que aplicar después la característica vertical, un glifo por un glifo. Los corchetes angulares, los paréntesis y la marca de vocal larga giran para discurrir a lo largo de la columna; los kana pequeños y puntuaciones como 、 y 。 se mudan a otra esquina de su cuadrado

PDF Library for Delphi lee esa característica de la fuente con la que está dibujando y cambia cada glifo por la forma vertical que la fuente nombra. Cuando una fuente lleva las dos características, vrt2 gana y vert se ignora. La especificación pretende que vrt2 sustituya a vert, y leer ambas puede entregar a un carácter dos glifos verticales distintos según qué lookup responda primero. Una fuente que no tiene ninguna característica conserva sus glifos ordinarios. Preferimos ese desenlace a una tabla de sustitución incorporada: una tabla adivinada acierta con los caracteres comunes y pasa por encima en silencio de las fuentes cuyos diseñadores trataron un carácter de otra manera

Vuelca las tablas GSUB de las copias que trae Windows y las cifras están muy lejos unas de otras: la característica vertical de MS Gothic mapea 979 glifos, Yu Gothic 2.491, SimSun 72, y Arial ninguno (los números exactos se mueven con las versiones de fuente). Una fuente latina que por casualidad contenga algún kana dibujará una columna erguida con cada corchete y marca de vocal larga en su forma horizontal, porque no hay nada en la fuente que sustituir

Diagrama de PDF Library for Delphi que muestra cómo la función vertical de la fuente suministra formas de paréntesis y de marca de sonido prolongado que se disponen a lo largo de una columna vertical mientras los caracteres ordinarios permanecen sin cambio, todo medido por GetVerticalTextHeight
La característica vertical de la propia tipografía decide qué glifos cambian de forma en una columna, y GetVerticalTextHeight mide las formas que se dibujarán realmente

La sustitución también cambia la medición. Una forma vertical puede tener su propio avance, distinto del glifo horizontal al que reemplaza, así que GetVerticalTextHeight aplica la misma sustitución antes de sumar los avances. Mide las formas horizontales y dibuja las verticales, y la columna se sale de la caja que le reservaste unos puntos por corchete

Nada de esto toca el texto en sí. Copia la columna desde un visor y recuperas 「コード」, porque cada glifo vertical se mapea al carácter que pasaste. La búsqueda y la extracción de texto ven tu cadena original, nunca las formas de presentación rotadas

Cómo acaban las métricas verticales en el PDF

Cada carácter de un tramo vertical avanza la distancia que le dan las métricas verticales de la propia fuente. Abre la salida en un inspector PDF y encontrarás una fuente Type0 con codificación Identity-V cuyo CIDFont lleva una entrada W2 para cada glifo usado: el avance vertical de la tabla vmtx de la fuente, la mitad del ancho del glifo como desplazamiento hacia la línea central de la columna, y el origen vertical. ISO 32000-1 §9.7.4.3 define esos tres números. Para una fuente sin métricas verticales no hay array W2, y el predeterminado DW2 hace avanzar cada carácter exactamente un em

El origen vertical es donde las fuentes de terceros discrepan entre sí. La tabla vhea tiene campos ascender y descender que parecen la fuente obvia, pero la convención de signo del descender no está compartida entre fuentes. Lee el campo de las copias de Windows con cualquier visor de tablas de fuentes y sale +140 en MS Gothic y +120 en Yu Gothic, contra −144 en SimSun y −500 en MingLiU, y los valores pueden cambiar con las versiones de fuente. Cualquier posición derivada de ese campo deriva una cantidad distinta para cada fuente. PDF Library for Delphi toma el origen del ascendente vertical, que las fuentes CJK nombradas arriba informan de forma consistente, y cae en 880 unidades — el predeterminado que la propia especificación PDF da a DW2 — cuando una fuente no informa nada plausible

Posicionar glifos verticales a mano tiene su propia trampa. El texto horizontal suele fijar el espaciado con números dentro de un array TJ. Bajo un modo de escritura vertical, ISO 32000-1 §9.4.4 aplica esos números a lo largo de la columna, así que una corrección calculada para espaciado horizontal empuja cada carácter más abajo en la página. Por eso un tramo vertical en el content stream es una cadena plana de códigos de glifo sin ajuste alguno, y cada gota de espaciado viene de W2

Cuando revises la salida, mira la tinta en lugar de los diccionarios. Encontrar /Identity-V en la fuente prueba que la codificación se declaró; no dice nada sobre hacia dónde corre realmente el texto. Renderizar la página y medir la caja envolvente de los píxeles dibujados sí lo dice: un tramo de siete caracteres debería salir de un carácter de ancho y siete de alto, aproximadamente

Un tramo vertical en una página horizontal

DrawVerticalText dibuja un único tramo en vertical y deja el modo de escritura en paz. Toma la posición, el nombre de la fuente, el tamaño y el texto, así que la selección de fuente actual tampoco se toca. Etiquetas de lomo, sellos, una única columna de nombres junto a una tabla: esas son las excepciones verticales dentro de un documento horizontal, y estar activando y desactivando un modo global alrededor de cada una es más estado del que el trabajo merece

Diagrama de PDF Library for Delphi de DrawVerticalText colocando una corrida vertical erguida junto a líneas horizontales ordinarias de DrawText en una página compartida sin tocar el modo de escritura global
DrawVerticalText lleva su propia fuente, tamaño y posición, de modo que una única columna vertical convive con las llamadas horizontales que la rodean
// Un tramo vertical dentro de una página por lo demás horizontal
if Lib.DrawVerticalText(520, 96, 'MS Mincho', 14, '保守点検記録') = 0 then
begin
  // La fuente no pudo abrirse o no pudo componer este texto.
  // LogLayoutProblem es una rutina tuya, no forma parte de la librería
  LogLayoutProblem('vertical label on page ' + IntToStr(Lib.SelectedPage));
end;
// El texto horizontal alrededor no se ve afectado
Lib.DrawText(72, 96, 'Maintenance inspection record');

Una página que mezcla ambas direcciones lleva la fuente dos veces en sus recursos, una con codificación horizontal y otra con Identity-V, construidas desde el mismo programa de fuente. Cada dirección conserva sus propias métricas, así que una página de libro japonés con cuerpo vertical y cabeceras corrientes horizontales, o un certificado chino con un título vertical sobre líneas de detalle horizontales, sale con ambas direcciones espaciadas correctamente

El valor de retorno es la razón por la que recurrimos a DrawVerticalText incluso en documentos mayormente verticales. En el dibujo basado en modo, un tramo que no puede componerse en vertical se dibuja a la manera horizontal ordinaria en lugar de descartarse, y DrawText informa de éxito en ambos casos. Para el cuerpo del texto ese es el mejor valor por defecto, porque una línea horizontal extravíaada se ve fácil en la prueba y un párrafo que falta no. Para una etiqueta cuya dirección es todo el punto, usa DrawVerticalText y trata un 0 como un fallo de maquetación

Maquetar columnas de derecha a izquierda

El orden de las columnas lo decides tú. El texto vertical japonés y de chino tradicional se lee columna a columna de derecha a izquierda, así que una página empieza en su margen derecho y cada columna nueva se sienta a la izquierda de la anterior. Ningún ajuste de modo de escritura puede inferirlo del texto, y lo mismo vale para el orden de las páginas en un libro encuadernado por la derecha y para dónde van las furigana, las notas al pie y los pies de figura

La rutina de abajo parte una cadena en columnas que caben en una altura fija, midiendo con GetVerticalTextHeight sobre la marcha y avanzando hacia la izquierda un em y medio por columna:

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, no una comprobación de la librería: una altura cero significa
  // que la fuente no pudo abrirse, y el bucle de abajo jamás rompería una columna
  if Lib.GetVerticalTextHeight(Face, Size, Text) <= 0 then
    Exit;
  X := Right;                                  // primera columna en el margen derecho
  Col := '';
  for I := 1 to Length(Text) do                // asume que no hay pares sustitutos
  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;                     // siguiente columna a la izquierda
      Col := Text[I];
    end
    else
      Col := Trial;
  end;
  if Col <> '' then
    Lib.DrawVerticalText(X, Top, Face, Size, Col);
end;

Volver a medir la cadena de prueba en cada carácter es más lento que sumar avances, pero mide exactamente lo que se dibujará, sustituciones incluidas. Lo que el bucle no hace es aplicar las reglas kinsoku. Un corchete de cierre o 。 en lo alto de una columna, o un corchete de apertura varado en el pie de una, es un error de composición para un lector japonés, y arreglarlo significa arrastrar o empujar un carácter a través del salto antes de dibujar. Los pares sustitutos piden el mismo cuidado: indexar Text[I] parte por la mitad un carácter fuera del Plano Multilingüe Básico, así que avanza por puntos de código si tu texto puede contener alguno

Para documentos ensamblados desde datos, el recorrido de la búsqueda de texto y la enumeración de elementos de página muestra cómo leer de vuelta lo que aterrizó en cada página, que es una forma rápida de confirmar que cada columna lleva los caracteres que tú quisiste

¿Qué fuente debería usar un documento vertical?

Una fuente CJK con métricas verticales propias y una característica vert o vrt2. Sin ellas obtienes caracteres erguidos avanzando un em cada vez y ningún cambio de forma. Cuando el texto vertical se ve sutilmente mal, examina la fuente antes de examinar el código. Un visor de tablas que liste las características GSUB de una fuente y sus tablas vhea y vmtx te dirá en un minuto si la fuente puede con el trabajo

DrawVerticalText y GetVerticalTextHeight abren una fuente instalada por su nombre a través de Windows. En builds de Lazarus para otras plataformas, la escritura vertical va por SetVerticalWritingMode y DrawText, que trabajan desde el programa de la fuente que cargaste y seleccionaste, así que se aplican las mismas formas y métricas verticales mientras esa fuente sea la cara CJK que querías usar

La incrustación sigue las reglas y los costes de siempre. Una fuente CJK completa ronda muchos megabytes, así que el subconjunto es el valor por defecto sensato para cualquier documento que salga de tu máquina. Las notas sobre la optimización de tamaño de PDF y el subsetting de fuentes cubren qué esperar, y el recorrido de incrustar fuentes ausentes en un PDF existente cubre el caso de reparación en que un documento vertical llega sin sus fuentes

La escritura CJK vertical vive en la misma API de dibujo que el resto de funciones de texto de PDF Library for Delphi, una librería PDF nativa en Pascal para Delphi, C++Builder y Lazarus. La página de producto de PDF Library for Delphi lista el conjunto completo de funciones de fuentes y texto junto a ella