Artículo técnico

HotPDF TextOut en Delphi: tamaño, estilo y rotación

Una cadena dibujada en (72, 72) aterriza una pulgada por debajo del borde superior de la página, lo que descoloca a cualquiera que aprendiera PDF por el lado del content stream, donde y = 72 está una pulgada por encima del borde inferior. La segunda sorpresa aparece unos cientos de líneas más tarde. Un informe maquetado para US Letter vuelve con una primera página de 612 por 792 puntos y todas las siguientes a 595 por 842, porque la línea que fijaba el tamaño de papel se ejecutó exactamente una vez

Los dos efectos tienen la misma raíz. El texto en un PDF es una corta ristra de cambios de estado (fuente y tamaño, espaciado de caracteres, espaciado de palabras, una matriz de texto para posición y rotación) seguida de un operador show-text, y una llamada de texto de alto nivel es un envoltorio fino sobre esa secuencia. La llamada lee el estado. Nunca lo reinicia por usted, y una página nueva arranca desde un conjunto fijo de defaults en lugar de desde lo que la página anterior dejó puesto

Dónde coloca TextOut el (X, Y) en la página

HotPDF dibuja texto con THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), al que se llega como Pdf.CurrentPage.TextOut. X e Y se miden desde la esquina superior izquierda de la página con Y creciendo hacia abajo, y con la Resolution por defecto de 72 una unidad es un punto, 1/72 de pulgada. La página convierte cada Y al espacio de usuario de PDF, que corre de abajo arriba, restándoselo a la altura de la página. El ángulo va en grados y, como el texto es un WideString, el Unicode pasa directamente. Una segunda sobrecarga recibe un PWORD más una longitud para código que ya maneja buffers UTF-16

La geometría de la página también viene fija. BeginDoc añade la primera página, y cada página creada por BeginDoc o AddPage arranca como A4, 595 por 842 puntos, con Arial 12 como fuente actual. US Letter (612 por 792) está a una asignación de distancia vía CurrentPage.Size := Letter, pero esa asignación pertenece a la página, así que hay que repetirla en cada página nueva

Por defecto, la Y que usted pasa es la parte superior del texto, y la baseline queda una altura de ascendente por debajo. TextFromBaseLine(True) cambia la página a posicionamiento por baseline. Actívelo para cualquier cosa que mezcle tamaños en una misma línea. En modo borde superior, un código de divisa de 9 puntos junto a un importe de 14 puntos tiene su parte superior alineada con la del importe, así que se ve visiblemente alto; en modo baseline las dos corridas se sientan en la misma línea, como las pondría un tipógrafo. El último bloque del ejemplo siguiente hace exactamente eso

El tamaño y el estilo viven en SetFont

TextOut no tiene parámetro de tamaño ni de estilo. SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical) lleva todo eso, con los dos últimos argumentos tomando por defecto ANSI_CHARSET y texto horizontal, y lo que fija sigue en vigor para cada TextOut posterior hasta el próximo SetFont. Mucha confusión del primer día se remonta a eso. Una línea sale en negrita porque tres llamadas antes algo puso [fsBold] y nadie lo limpió

var
  W: Single;
begin
  Pdf.BeginDoc;                                     // primera página: A4, Arial 12
  Pdf.CurrentPage.TextFromBaseLine(True);           // cada Y de aquí abajo es una baseline

  Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
  Pdf.CurrentPage.TextOut(72, 72, 0, 'Quarterly Report');        // 24 pt regular

  Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
  Pdf.CurrentPage.TextOut(72, 104, 0, 'Revenue');                // 12 pt negrita

  Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
  Pdf.CurrentPage.TextOut(72, 120, 0, 'figures in thousands');   // 11 pt cursiva

  Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
  Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY');             // los estilos se combinan

  // Tamaños mezclados en una misma baseline: medir mientras la fuente de 14 pt está activa
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
  Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR');            // hueco de 3 pt
  ...
end;

El argumento de estilo es un conjunto TFontStyles ordinario, así que [] es normal y [fsBold, fsItalic] es negrita cursiva. fsUnderline y fsStrikeOut se dibujan como barras rellenas finas bajo o a través de la corrida, y siguen a la corrida cuando esta se rota. El tamaño va en puntos, la misma unidad que las coordenadas, lo que deja la aritmética vertical sencilla: texto de cuerpo de 11 puntos con un paso de 16 puntos deja unos cinco puntos de aire entre líneas. No hay avance de línea automático. Cada Y la calcula usted, lo que es tedioso para un párrafo y exactamente correcto para un formulario cuyos campos se sientan en posiciones fijas

GetWideTextWidth mide con la fuente que esté activa en el momento de la llamada, así que en el último bloque la medición tiene que ocurrir mientras la cara de 14 puntos en negrita sigue puesta. Muévala por debajo del segundo SetFont y el sufijo de 9 puntos aterriza encima de los últimos dígitos del importe

El nombre de la fuente se resuelve contra las fuentes instaladas en la máquina que construye el PDF, y una cara que resuelve en el escritorio de un desarrollador puede faltar en un servidor de builds, de modo que la cara del archivo terminado depende de qué máquina lo produjo. La cobertura de scripts es la otra mitad. Una cara solo latina a la que se le pida dibujar cirílico o CJK produce cajas de glifo faltante, y eso es una propiedad del archivo de fuentes, no de la llamada. Cuando una sola cadena mezcla scripts que ninguna cara única cubre, TextOutWithFallback toma los mismos cuatro argumentos, cambia de cara corrida a corrida para los caracteres que la fuente principal no tiene y restaura la fuente original al terminar

Página TextOut de HotPDF mostrando Arial, Times New Roman y Courier New renderizadas con estilos normal, negrita y cursiva a lo largo de varios conjuntos de caracteres

¿Cómo rota el texto el argumento de ángulo?

Un ángulo distinto de cero gira la corrida en sentido antihorario esos grados alrededor de su ancla, y el ancla es la esquina superior izquierda del texto en el marco propio del texto. El ancla no se mueve. Solo cambia la dirección en la que marchan los glifos, así que la coordenada que colocaba una etiqueta horizontal también coloca a su gemela rotada. Las corridas rotadas conservan esa ancla superior izquierda incluso cuando la página está en modo baseline; TextFromBaseLine desplaza solo las corridas horizontales

Diagrama de la geometría de rotación de TextOut de HotPDF: corridas a 0, 45 y 90 grados pivotando en sentido antihorario alrededor de un ancla fija, con ejemplos de llamadas TextOut de Delphi para etiquetas de lomo, marcas de agua y encabezados de columna inclinados
Un ángulo distinto de cero gira la secuencia en sentido antihorario sobre su anclaje inmóvil, la esquina superior izquierda del texto, y las líneas base compartidas avanzan X exactamente como las líneas horizontales avanzan Y
var
  W, AxisMidY: Single;
begin
  AxisMidY := 400;                                // punto medio del eje Y del gráfico

  // Una corrida a 90 grados se lee de abajo arriba y crece hacia arriba desde Y,
  // así que centrarla en el eje significa empezar medio ancho por debajo del punto medio
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
  W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
  Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');

  // Una marca de agua en diagonal
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
  Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');

  // Cabeceras de columna inclinadas 60 grados: misma Y, X escalonada
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
  Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;

Noventa grados cubre etiquetas de ejes y títulos de lomo. A 90 grados los glifos se yerguen a la derecha de la X del ancla, con una línea de alto, y la corrida trepa hacia el borde superior de la página a lo ancho de sí misma. Por eso centrar suma media anchura a Y en lugar de restarla, y por eso una etiqueta anclada en el punto medio se sienta íntegramente por encima. Sesenta grados va bien para cabeceras de columna, dejando que una etiqueta larga se alce sobre una columna estrecha sin derramarse en su vecina, y 45 grados es el ángulo típico de marca de agua. Las corridas que comparten baseline reciben la misma Y y una X escalonada, exactamente como las líneas horizontales apiladas comparten X y escalonan Y

¿Por qué la segunda página sale como A4 en Arial 12?

AddPage devuelve el índice de la página nueva y la vuelve actual, y la página nueva arranca desde los defaults: A4, Arial 12, posicionamiento por borde superior, espaciados de caracteres y palabras a cero. Nada de la página anterior se arrastra. Ese diseño mantiene cada página autocontenida, ya que el content stream de una página PDF empieza de todos modos con un estado de gráficos fresco, pero significa que cualquier código de maquetación que configure la página una sola vez al principio del documento cambiará discretamente de tamaño de papel y de tipografía en la página dos. La solución es meter todos los ajustes por página en una única rutina y llamarla tras BeginDoc y tras cada AddPage

HotPDF: Diagrama de página US Letter mostrando la aritmética de la posición de línea con un margen izquierdo, una línea de inicio superior y un Leading fijo avanzando hacia el límite del margen inferior, junto al bucle de maquetación con constantes con nombre y su guarda manual de AddPage y reset de SetFont
Los anclajes nombrados convierten una columna de números mágicos en aritmética: avanza la Y en curso en Leading cada línea y vigila tú mismo el margen inferior con AddPage
procedure StartReportPage(Pdf: THotPDF);
begin
  Pdf.CurrentPage.Size := Letter;               // una página nueva empieza como A4
  Pdf.CurrentPage.TextFromBaseLine(True);       // una página nueva empieza en modo borde superior
  Pdf.CurrentPage.SetFont('Arial', [], 11);     // una página nueva empieza en Arial 12
end;

procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
  LeftMargin   = 72;   // 1 pulgada
  TopLine      = 72;   // primera baseline, 1 pulgada hacia abajo
  BottomMargin = 72;
  Leading      = 16;
var
  I: Integer;
  Y: Single;
begin
  StartReportPage(Pdf);
  Y := TopLine;
  for I := 0 to Lines.Count - 1 do
  begin
    if Y > Pdf.CurrentPage.Height - BottomMargin then   // 720 en Letter
    begin
      Pdf.AddPage;
      StartReportPage(Pdf);
      Y := TopLine;
    end;
    Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
    Y := Y + Leading;
  end;
end;

No hay ningún flow layout por debajo de TextOut, y una Y pasada del borde inferior de la página sigue siendo una coordenada válida en lo que respecta al content stream: el texto se escribe, simplemente cae fuera del media box y ningún visor lo muestra. Así que el bucle comprueba Y antes de cada línea y abre página nueva por su cuenta. Leer el límite desde CurrentPage.Height en lugar de clavar 720 mantiene el bucle correcto si alguien cambia luego el helper de vuelta a A4, donde el mismo margen de una pulgada pone el límite en 770

La comprobación más rápida es la que haría el diálogo de impresión de un lector. Abra la salida, vaya a la página dos y mire las propiedades del documento: una página Letter reporta 8.5 por 11 pulgadas, una A4 unos 8.27 por 11.69. Si la primera página y la segunda discrepan, algún ajuste por página vive fuera del helper

El espaciado de caracteres y de palabras también es estado de página

A veces la cadena está bien y la anchura está mal: una cabecera que debe cubrir una regla fija, un código que se lee mejor con dígitos más espaciados, una línea de etiquetas cuyos huecos piden abrirse. PDF tiene dos parámetros de estado de texto para esto en ISO 32000-1: el espaciado de caracteres Tc (§9.3.2), añadido tras cada glifo, y el espaciado de palabras Tw (§9.3.3), añadido en cada espacio. HotPDF los fija con SetCharacterSpacing(Spacing: Single) y SetWordSpacing(Spacing: Single), en puntos con la resolución por defecto, y como la fuente se mantienen en vigor hasta que se cambien

HotPDF: El espaciado de caracteres Tc reparte espacio extra uniformemente tras cada glifo de SUMMARY frente al espaciado de palabras Tw que lo acumula solo en los caracteres de espacio, conectado al ciclo asignar, dibujar, restablecer que evita que el estado de espaciado escape a párrafos posteriores
Tc reparte su ajuste entre todos los glifos mientras que Tw actúa solo sobre el carácter de espacio, y restablecer en la línea siguiente mantiene limpio el estado de dibujo

El espaciado de palabras tiene una trampa escrita en la especificación. §9.3.3 aplica Tw solo al código de carácter de un byte 32, y explícitamente no a un byte 32 dentro de un código multi-byte, así que por sí solo el operador no hace nada para texto escrito con códigos de glifo de dos bytes, que es como se escriben normalmente las fuentes Unicode incrustadas. HotPDF escribe las corridas Unicode de esa manera, y por ese camino aplica el espaciado de palabras como ajustes de posición explícitos tras cada glifo de espacio, de modo que SetWordSpacing ensancha los huecos por cualquiera de las dos vías. El disparador sigue siendo el espacio sencillo, U+0020. Una corrida de chino o japonés sin espacios ASCII no recibe espacio extra alguno, y un espacio ideográfico (U+3000) se trata como un glifo ordinario

El espaciado de caracteres es la herramienta correcta cuando una cabecera debe alcanzar una anchura objetivo, porque reparte el cambio de manera uniforme en lugar de acumularlo en los espacios. GetWideTextWidth cuenta el espaciado de caracteres y de palabras en vigor, así que mida con ambos a cero y luego derive el espaciado:

const
  Heading = 'SUMMARY';
  Target  = 200;        // ancho de la regla que la cabecera debe cubrir, en puntos
var
  Natural, Tc: Single;
begin
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // medir sin espaciado residual
  Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);

  // Tc va detrás de cada glifo, pero solo los huecos entre glifos son visibles
  Tc := (Target - Natural) / (Length(Heading) - 1);

  Pdf.CurrentPage.SetCharacterSpacing(Tc);      // los valores negativos comprimen
  Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // reset antes del cuerpo de texto

  Pdf.CurrentPage.SetWordSpacing(6);
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
  Pdf.CurrentPage.SetWordSpacing(0);
end;

Las líneas de reset importan más que las líneas de asignación. Deje un espaciado de caracteres de 4 puntos en pie tras una cabecera y cada párrafo por debajo hereda el estiramiento, que se lee como una rareza difusa que sobrevive a una prueba de lectura rápida y luego aparece en la copia impresa. Fije el valor, dibuje la única corrida que lo necesita y póngalo a cero en la línea siguiente, así ningún código posterior tiene que saber lo que hizo una sección anterior. La misma página ofrece SetHorizontalScaling (Tz, 100 por defecto) para comprimir una corrida sin cambiar su altura, y SetTextRenderingMode con valores como trStroke para texto perfilado, ambos persistiendo exactamente igual

Página TextOut de HotPDF comparando el escalado horizontal del texto, el espaciado de caracteres, el espaciado de palabras, y los modos de renderizado relleno frente a trazo

Si esta es su primera página de salida, el ejemplo Hello World de HotPDF cubre la configuración del documento y el guardado. Para texto que tiene que llenar una columna de borde a borde, la justificación completa de texto PDF tiene su propio recorrido, y el CJK vertical se cubre en vertical text layout en HotPDF. TextOut, SetFont, las llamadas de espaciado y los ajustes de tamaño de página de arriba vienen todos con el HotPDF Delphi Component para Delphi y C++Builder