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
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:
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:
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