Artículo técnico

Informes PDF Delphi con HotPDF: TextOut, fuentes e imágenes

Generar un informe se reduce a colocar tres cosas en una página y lograr que se pongan de acuerdo sobre dónde van: texto en coordenadas conocidas, fuentes que se representan igual en el servidor que en su escritorio, e imágenes dimensionadas para encajar. Todo lo demás que hace una biblioteca de informes se organiza alrededor de esas tres. HotPDF, la biblioteca de generación de PDF de losLab para Delphi y C++Builder, le ofrece cada una como una llamada directa sobre el objeto de página, y la única fricción real es el sistema de coordenadas de debajo, que va justo al revés que el lienzo VCL al que está acostumbrado. Deje resuelta esa orientación desde el principio y el resto del trabajo de maquetación dejará de pelearse con usted

Colocación de texto y el origen en la esquina inferior izquierda

El primer informe de casi todo el mundo sale del revés. El título aterriza cerca del borde inferior y cada línea siguiente trepa hacia arriba. No hay ninguna avería. El espacio de usuario del PDF, definido en ISO 32000-1 §8.3, sitúa el origen en la esquina inferior izquierda con la Y creciendo hacia arriba, que es la imagen especular del lienzo GDI donde la Y crece hacia abajo desde la esquina superior izquierda. Cinco minutos dedicados a hacer las paces con eso le ahorran una maquetación que si no tendría que reescribir en cuanto los números dejen de cuadrar

Diagrama de HotPDF que contrapone el origen de coordenadas VCL en la esquina superior izquierda con el origen PDF en la inferior izquierda, donde TextOut coloca un título a 50 puntos del borde superior de una página Letter en Y 792 menos 50
El espacio de usuario del PDF es el espejo del lienzo VCL, así que un título a 50pt del borde superior de una página Letter es TextOut(50, 792 - 50, 0, 'INVOICE') y esa misma conversión mantiene intuitiva cada coordenada del informe

La llamada central del objeto de página es TextOut(X, Y, Angle, Text). X e Y sitúan el texto en puntos desde la esquina inferior izquierda, y Angle lo gira en grados, que es como se dibuja un sello diagonal de DRAFT o COPY sin ningún soporte especial. El truco que permite que la intuición entrenada en VCL siga sirviendo es expresar la Y como la altura de página menos la distancia que quiere desde arriba:

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice-0001.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 792 - 50, 0, 'INVOICE');       // 50pt desde arriba en Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // sello girado
    Pdf.AddPage;                                                // CurrentPage ya apunta aquí
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // el estado de fuente no se hereda
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Los dos comportamientos con estado de ese listado son responsables de la mayoría de los fallos que solo asoman en la segunda página. AddPage reapunta CurrentPage a la página que acaba de crear, así que una referencia de página que guardó antes ya no dibuja donde espera. La selección de fuente también es por página y no por documento. Si se salta el SetFont después de un AddPage, el primer TextOut de la página nueva recae en el valor predeterminado con que arrancó la página, no en la fuente de titular en negrita que fijó tres páginas atrás. La costumbre segura es tratar «empezar página nueva» y «restablecer el estado de texto» como un único paso inseparable dentro del bucle del informe

Fuentes que existen en el servidor, no solo en su escritorio

La mayoría de los problemas de fuentes son en realidad problemas de despliegue disfrazados. Su máquina de desarrollo tiene instalada la fuente corporativa, así que el informe se ve bien en su pantalla y se publica. El equipo de producción ejecuta el trabajo bajo una cuenta de servicio que nunca ha tenido instalada esa fuente, el renderizador sustituye en silencio por algo que sí encuentra, y lo primero que se sabe del asunto es un cliente preguntando por qué ha cambiado el membrete. La salida consiste en dejar de fiarse del directorio de fuentes del sistema operativo y cargar la fuente desde un archivo que su instalador deja en disco. La llamada de registro Unicode de HotPDF recibe una ruta y hace exactamente eso:

Diagrama de un problema de despliegue de fuentes en PDF con Delphi: el servidor de producción sustituye en silencio una fuente ausente, mientras que RegisterUnicodeTTF carga el TTF desde un archivo desplegado y lo incrusta en el PDF
Fiarse del directorio de fuentes del sistema operativo se rompe cuando la cuenta de servicio de producción no tiene la fuente, mientras que cargar el TTF desde un archivo desplegado incrusta los glifos y todos los equipos representan igual
Pdf.RegisterUnicodeTTF('C:\ProgramData\MyApp\Fonts\NotoSans.ttf');
Pdf.CurrentPage.SetFont('NotoSans', [], 12);
Pdf.CurrentPage.TextOut(50, 700, 0, WideString('Łódź - Ünïcode test ✓'));

TextOut acepta un WideString directamente, algo que importa más de lo que parece al principio. Un nombre de cliente con acento, una calle alemana, una ciudad polaca: no son casos límite, son el contenido normal de una tabla de clientes, y pasan por la misma llamada que las etiquetas ASCII que escribe a mano, siempre que la fuente registrada contenga de verdad los glifos. Con las fuentes incrustadas viaja una restricción de versión: el documento tiene que ser PDF 1.5 o posterior, así que si algún requisito ajeno le ata a una versión anterior, eso es lo que se romperá sin avisar. Las escrituras de derecha a izquierda como el árabe y el hebreo necesitan un modelado real y no una búsqueda directa de glifos, y eso tiene su propia cadena; consulte nuestro artículo sobre modelado de texto de escrituras complejas con HotPDF

Cuando ninguna fuente instalada puede expresar lo que necesita, piense en caracteres MICR de un cheque o en un juego de símbolos propietario, las fuentes Type 3 cubren el hueco. Cada glifo se define como un pequeño flujo de contenido mediante RegisterType3Font y AddType3Glyph. Es un rincón especializado de la API al que echará mano rara vez, pero resulta mucho más limpio que esparcir cientos de mapas de bits diminutos por una página

Imágenes: los argumentos centrales son un ancho y un alto, no una esquina

El manejo de imágenes se divide en dos pasos, y mantenerlos separados es justo lo importante. AddImage recibe un TBitmap o un TJPEGImage, lo incrusta una vez y devuelve un índice. El arte en PNG hay que decodificarlo a un bitmap antes de llegar ahí. Después, ShowImage dibuja ese índice donde quiera y tantas veces como quiera. El orden de los argumentos de ShowImage es el único punto en el que conviene frenar y leer:

Diagrama de la cadena de imágenes de HotPDF donde AddImage incrusta el bitmap una sola vez y devuelve un índice, ShowImage lo coloca por ancho y alto, y el orden de argumentos no es un par de esquinas
AddImage incrusta los píxeles una vez y cada llamada a ShowImage reutiliza ese índice, y los argumentos centrales son un ancho y un alto en lugar de las coordenadas de la esquina opuesta
var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decodifica el PNG a un bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // sin pérdida para arte de color plano
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle): no (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Los dos números que siguen a la posición son un ancho y un alto. No son las coordenadas de la esquina opuesta, y el último argumento es un ángulo de rotación en grados. Lea la firma como una caja X1/Y1/X2/Y2 y un logotipo de 120 por 40 colocado en (50, 700) se estirará desde ahí hasta (120, 40), desparramándose por casi toda la página. La salida deja el error a la vista mientras el código fuente parece del todo razonable, que es lo que hace que le cueste una tarde. KeepImageAspectRatio vale True de forma predeterminada, así que una caja con proporciones erróneas encaja la imagen con bandas en vez de deformarla; póngala en False solo cuando de verdad quiera estirar

La separación entre registrar y colocar rinde en tiradas largas. Como AddImage incrusta los píxeles una sola vez y cada ShowImage con ese índice apunta al mismo objeto incrustado, el sitio donde llame a AddImage decide el tamaño del archivo. Llámelo dentro del bucle de páginas de un extracto de 500 páginas y el mismo logotipo se incrustará 500 veces. Llámelo una vez antes del bucle, guarde el índice, y el logotipo se almacena una única vez. Basta con un pequeño diccionario indexado por la ruta del recurso para asegurar que cada imagen distinta se registra exactamente una vez

La elección de códec es la otra palanca de tamaño. El contenido fotográfico, adjuntos escaneados y similares, va en JPEG: pase icJpeg a AddImage y baje JpegQuality a unos 85, ya que la propiedad arranca en 100 y la diferencia a 85 es invisible sobre una página impresa. El arte de color plano, como logotipos, gráficos y dibujos lineales, va en icFlate, donde la compresión sin pérdida ya resulta compacta y JPEG emborronaría con un halo visible los bordes duros. Una tirada de extractos que meta una foto de máxima calidad en cada página puede hincharse hasta los gigabytes; el mismo contenido a JPEG 85 se queda en torno a una décima parte, y ningún lector nota la diferencia

Filetes, cajas y sombreados con primitivas de trazado

La línea horizontal bajo la cabecera de una tabla y la caja gris tras una cifra de totales no necesitan ser imágenes. Dibújelas como vectores y se mantendrán nítidas a cualquier zoom, imprimirán con precisión y añadirán casi nada al archivo. HotPDF sigue el mismo modelo que usan los flujos de contenido PDF en bruto: construya un trazado y luego llame a un operador que lo pinte

// Filete horizontal bajo la cabecera de la tabla
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Caja sombreada de totales: X, Y, ancho, alto
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

El orden no es opcional: fije el estado de pintura, construya el trazado y llame después a Stroke o a Fill. Un trazado que construye pero nunca pinta no aporta nada a la página, que es casi siempre la explicación cuando un filete «no aparece». SetRGBFillColor recibe un único TColor, así que las constantes VCL de siempre como clNavy y clBlack encajan directamente, y Rectangle usa los mismos argumentos de ancho y alto que la colocación de imágenes en vez de dos esquinas. Una advertencia sobre las líneas finas: cualquier cosa por debajo de aproximadamente medio punto puede quedar elegante en un monitor y luego desaparecer en una impresora de oficina de 600 ppp, así que 0,75pt es un suelo razonable para cualquier filete que tenga que sobrevivir a la impresión

Paginación contra datos reales, no contra datos de ejemplo

Un detalle que conviene acertar antes de que cuaje la maquetación: las columnas numéricas deben alinearse por su borde derecho, y la forma de conseguirlo es medir el ancho representado de cada valor y colocarlo hacia atrás desde el límite de la columna, no rellenar la cadena con espacios por delante. El relleno con espacios solo cuadra en una fuente monoespaciada, y nadie compone un informe financiero en monoespaciada. Pase antes los valores por las rutinas de Delphi sensibles a la configuración regional, como FormatFloat, para que el separador de miles cuyo ancho mide sea el mismo que mostrará de verdad la configuración regional del cliente

El peligro de la paginación es que se escribe contra el conjunto de datos de demostración, donde diez filas cortas caben en una página y el bucle nunca tiene que saltar. Producción le entrega un cliente cuya razón social ocupa 140 caracteres y un extracto con 4.000 líneas de detalle, y ahora el bucle tiene que saltar bien cada vez. El patrón que aguanta es un único cursor de Y que se desplaza hacia abajo conforme resta la altura de cada fila, y una comprobación que abre página nueva en el momento en que el cursor cruzaría el margen inferior. Hacia abajo significa aquí Y decreciente, que es el único punto donde el origen inferior izquierdo sigue resultando contraintuitivo. Meta todo eso en una sola rutina que además vuelva a emitir SetFont y redibuje la cabecera corrida en la página nueva, y los fallos de una página de más nunca cogerán terreno. Cuando esos mismos informes tienen además que cumplir reglas de archivado o accesibilidad, las decisiones que toma justo aquí, qué fuentes incrusta, si la salida va etiquetada, qué espacios de color usa, son las que vigilan esas normas; conviene leer la guía de PDF/A, PDF/X y PDF/UA con HotPDF antes de que la plantilla se endurezca

Todas las llamadas mostradas aquí, la colocación de texto, el registro de fuentes, la incrustación de imágenes y el dibujo de trazados, se distribuyen en el HotPDF Delphi Component para Delphi y C++Builder, cuya referencia documenta la API de salida completa junto con las funciones de formularios, cifrado y firma que la acompañan