Artículo técnico

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

Una cadena dibujada en (72, 72) cae una pulgada por debajo del borde superior de la página, lo que sorprende a cualquiera que aprendió PDF desde el lado del content stream, donde y = 72 queda una pulgada por encima del borde inferior. La segunda sorpresa aparece unas cuantas líneas más tarde: un reporte maquetado para US Letter regresa con una primera página de 612 por 792 puntos y todas las páginas 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 tira corta 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 wrapper delgado sobre esa secuencia. La llamada lee el estado. Nunca lo resetea por usted, y una página nueva arranca desde un set fijo de valores predeterminados en lugar de arrancar desde donde quedó la página anterior

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

HotPDF dibuja texto con THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), que se alcanza 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 el Resolution predeterminado 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 hacia arriba, restándola de la altura de la página. El ángulo va en grados, y como el texto es un WideString, Unicode pasa directo. Una segunda sobrecarga toma un PWORD más una longitud para código que ya tiene buffers UTF-16

La geometría de la página también viene fija. BeginDoc agrega la primera página, y cada página creada por BeginDoc o por 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 el borde superior del texto, y la baseline queda un ascent de la fuente 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 moneda de 9 puntos junto a un monto de 14 puntos alinea su borde superior con el del monto, así que se ve visiblemente alto; en modo baseline ambas tiras se sientan en la misma línea, como las acomodarí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 ANSI_CHARSET y texto horizontal por defecto, y lo que establece sigue vigente para cada TextOut posterior hasta el próximo SetFont. La mayor parte de la confusión del primer día se remonta a eso. Una línea sale en negrita porque tres llamadas antes algo estableció [fsBold] y nada lo limpió

var
  W: Single;
begin
  Pdf.BeginDoc;                                     // primera página: A4, Arial 12
  Pdf.CurrentPage.TextFromBaseLine(True);           // cada Y de aquí en adelante 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: mida 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 set TFontStyles corriente, así que [] es normal y [fsBold, fsItalic] es negrita cursiva. fsUnderline y fsStrikeOut se dibujan como barras delgadas rellenas bajo o a través de la tira, y siguen a la tira cuando rota. El tamaño va en puntos, la misma unidad que las coordenadas, lo que mantiene simple la aritmética vertical: texto de cuerpo de 11 puntos con paso de 16 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 al momento de la llamada, así que en el último bloque la medición tiene que ocurrir mientras la fuente de 14 puntos en negrita sigue establecida. Muévala debajo del segundo SetFont y el sufijo de 9 puntos cae encima de los últimos dígitos del monto

El nombre de la fuente se resuelve contra las fuentes instaladas en la máquina que construye el PDF, y una fuente que se resuelve en el escritorio de desarrollo puede estar ausente en un servidor de build, así que la fuente del archivo final depende de qué máquina lo produjo. La cobertura de escrituras es la otra mitad. Una fuente solo latina a la que se le pide dibujar cirílico o CJK produce cajas de glifos faltantes, y eso es una propiedad del archivo de fuente, no de la llamada. Cuando una sola cadena mezcla escrituras que ninguna fuente cubre entera, TextOutWithFallback toma los mismos cuatro argumentos, cambia de fuente tira por tira para los caracteres que la fuente primaria no tiene, y restaura la fuente original al terminar

Página TextOut de HotPDF mostrando Arial, Times New Roman y Courier New renderizados con estilos regular, negrita y cursiva a través de varios conjuntos de caracteres

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

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

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

  // Una tira a 90 grados se lee de abajo hacia arriba y crece hacia arriba desde Y,
  // así que centrarla en el eje significa arrancar la mitad de su ancho por debajo de la mitad
  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');

  // Encabezados de columna inclinados 60 grados: misma Y, saltos en X
  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 eje y títulos de lomo. A 90 grados los glifos quedan a la derecha de la X del ancla, con una altura de línea de ancho, y la tira sube hacia el tope de la página por su propio ancho. Por eso centrar suma la mitad del ancho a Y en lugar de restarlo, y por eso una etiqueta anclada en la propia mitad queda por completo por encima. Sesenta grados sirve para encabezados de columna, permitiendo que una etiqueta larga se pare sobre una columna angosta sin derramarse en su vecina, y 45 grados es el ángulo usual de marca de agua. Las tiras que comparten baseline reciben la misma Y y saltos en X, exactamente como las líneas horizontales apiladas comparten una X y saltan en 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 valores predeterminados: A4, Arial 12, posicionamiento por borde superior, espaciado de caracteres y de palabras en cero. Nada de la página anterior se arrastra. Ese diseño mantiene cada página autocontenida — el content stream de una página PDF arranca con un estado de gráficos fresco de todos modos — pero implica que cualquier código de maquetación que configure la página una sola vez al inicio del documento cambiará en silencio el tamaño de papel y la tipografía en la página dos. La solución es poner cada ajuste por página en una sola rutina y llamarla después de BeginDoc y después de cada AddPage

HotPDF: diagrama de página US Letter que muestra la aritmética de posiciones de línea con un margen izquierdo, una línea de inicio superior y un Leading fijo que avanza hacia un límite de margen inferior, junto al bucle de diseño con constantes nombradas y su guarda manual de AddPage y reinicio de SetFont
Los anclajes con nombre convierten una columna de números mágicos en aritmética: avance la Y en curso en Leading por línea y custodie usted mismo el margen inferior con AddPage
procedure StartReportPage(Pdf: THotPDF);
begin
  Pdf.CurrentPage.Size := Letter;               // una página nueva arranca como A4
  Pdf.CurrentPage.TextFromBaseLine(True);       // una página nueva arranca en modo borde superior
  Pdf.CurrentPage.SetFont('Arial', [], 11);     // una página nueva arranca en Arial 12
end;

procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
  LeftMargin   = 72;   // 1 pulgada
  TopLine      = 72;   // primera baseline, 1 pulgada 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 layout de flujo debajo de TextOut, y una Y más allá del pie 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. Por eso el bucle chequea Y antes de cada línea y abre página nueva por su cuenta. Leer el límite desde CurrentPage.Height en lugar de hard-codear el 720 mantiene el bucle correcto si alguien cambia el helper de vuelta a A4, donde el mismo margen de una pulgada pone el límite en 770

La verificación más rápida es la que haría el diálogo de impresión de un visor. 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 no coinciden, 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 el ancho está mal: un encabezado que debe abarcar una regla fija, un código que se lee mejor con dígitos más holgados, una línea de etiquetas cuyos huecos necesitan abrirse. PDF tiene dos parámetros de estado de texto para esto en ISO 32000-1, el espaciado de caracteres Tc (§9.3.2), agregado después de cada glifo, y el espaciado de palabras Tw (§9.3.3), agregado en cada espacio. HotPDF los establece con SetCharacterSpacing(Spacing: Single) y SetWordSpacing(Spacing: Single), en puntos con la resolución predeterminada, y como la fuente se mantienen vigentes 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, cableado al ciclo de asignar, dibujar y reiniciar que evita que el estado de espaciado se filtre a párrafos posteriores
Tc distribuye su ajuste por cada glifo mientras que Tw actúa solo en 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 normalmente se escribe texto en fuentes Unicode embebidas. HotPDF escribe las tiras Unicode de esa manera, y en ese camino aplica el espaciado de palabras como ajustes de posición explícitos después de cada glifo de espacio, así que SetWordSpacing abre los huecos de cualquier forma. El disparador sigue siendo el espacio simple, U+0020. Una tira de chino o japonés sin espacios ASCII no recibe nada de espacio extra, y un espacio ideográfico (U+3000) se trata como un glifo ordinario

El espaciado de caracteres es la herramienta correcta cuando un encabezado debe alcanzar un ancho objetivo, porque reparte el cambio uniformemente en lugar de acumularlo en los espacios. GetWideTextWidth cuenta el espaciado de caracteres y de palabras vigente, así que mida con ambos en cero y luego derive el espaciado:

const
  Heading = 'SUMMARY';
  Target  = 200;        // ancho de la regla que el encabezado debe abarcar, 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 despué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 aprietan
  Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // resetear antes del texto de cuerpo

  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 activo después de un encabezado y cada párrafo debajo hereda el estiramiento, que se lee como una incorrección vaga que sobrevive a una prueba de lectura rápida y luego aparece en la copia impresa. Asigne el valor, dibuje la única tira que lo necesita y póngalo en cero en la línea siguiente, así ningún código posterior tiene que saber qué hizo una sección anterior. La misma página ofrece SetHorizontalScaling (Tz, 100 por defecto) para condensar una tira sin cambiar su altura, y SetTextRenderingMode con valores como trStroke para texto con contorno, y ambos persisten exactamente de la misma manera

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

Si esta es su primera página de salida, el ejemplo Hola Mundo 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 para texto PDF tiene su propio recorrido, y el CJK de arriba hacia abajo está cubierto en layout de texto vertical en HotPDF. TextOut, SetFont, las llamadas de espaciado y los ajustes de tamaño de página de arriba vienen todos con el Componente HotPDF de Delphi para Delphi y C++Builder