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 saber qué sistema de coordenadas habla cada llamada: las llamadas de dibujo usan la misma orientación superior izquierda que el lienzo VCL al que está acostumbrado, mientras que el fichero PDF de debajo va al revés. 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 superior izquierda

A los desarrolladores que conocen el formato PDF a menudo les sale del revés su primer informe con HotPDF. Restan cada Y de la altura de página, 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, sí sitúa el origen en la esquina inferior izquierda con la Y creciendo hacia arriba, la imagen especular del lienzo GDI, pero las llamadas de dibujo de página de HotPDF lo ocultan: miden desde la esquina superior izquierda con la Y creciendo hacia abajo, exactamente igual que el lienzo VCL, y THPDFPage.YProjection voltea cada coordenada al espacio de usuario conforme se escribe el flujo de contenido. Cinco minutos dedicados a confirmar en qué marco está le ahorran una maquetación que si no tendría que reescribir en cuanto los números dejen de cuadrar

Diagrama de HotPDF que muestra el lienzo VCL y la página HotPDF compartiendo un origen arriba a la izquierda, donde TextOut coloca un título a 50 puntos del borde superior de una página Letter en Y 50 y HotPDF lo escribe al espacio de usuario PDF de abajo hacia arriba como 792 menos 50
HotPDF conserva la orientación de VCL, así que un título a 50pt del borde superior de una página Letter es TextOut(50, 50, 0, 'INVOICE'), y el volteo al espacio de usuario PDF, 792 - 50, ocurre dentro del componente

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 superior izquierda, donde la Y marca el borde superior del texto salvo que llame a TextFromBaseLine(True) para posicionar líneas base, y Angle lo gira en grados, que es como se dibuja un sello diagonal de DRAFT o COPY sin ningún soporte especial. La intuición entrenada en VCL funciona por tanto sin cambios, porque la Y es sencillamente la distancia desde la parte superior de la página:

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, 50, 0, 'INVOICE');             // 50pt desde el borde superior
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 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, 50, 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, 132);
Pdf.CurrentPage.LineTo(545, 132);
Pdf.CurrentPage.Stroke;

// Caja sombreada de totales: X, Y de esquina superior izquierda, luego ancho y alto
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 632, 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 suma 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 creciente, igual que en el lienzo VCL; solo las coordenadas que lee de vuelta de un fichero terminado, como las posiciones de extracción de texto, llegan en espacio de usuario PDF y cuentan al revés. 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