Artículo técnico

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

La primera factura que casi cualquier equipo renderiza con una biblioteca PDF sale mal de la misma forma: el texto de cabecera queda junto al borde inferior de la página y cada línea posterior sube hacia arriba. No hay nada roto. El espacio de usuario de PDF, según ISO 32000-1 §8.3, coloca el origen en la esquina inferior izquierda con Y creciendo hacia arriba, justo lo contrario del lienzo GDI sobre el que un desarrollador VCL lleva años dibujando. HotPDF, la biblioteca de generación PDF de losLab para Delphi y C++Builder, expone directamente ese modelo de coordenadas, así que los cinco minutos que dedique a interiorizarlo ahora ahorran una reescritura de maquetación después. Este artículo recorre las primitivas de salida que realmente necesita un generador de informes: texto posicionado, fuentes que sobreviven al despliegue, colocación de imágenes y dibujo vectorial.

Colocación de texto y origen inferior izquierdo

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 rota en grados, que es como se hacen sellos diagonales DRAFT y COPY sin maquinaria adicional. El modismo que permite seguir usando la intuición entrenada en VCL es calcular Y como altura de página menos la distancia desde la parte superior:

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 from top of Letter
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 792 - 70, 0, 'Date: 2026-06-11');
    Pdf.CurrentPage.TextOut(300, 400, 45, 'COPY');              // rotated stamp
    Pdf.AddPage;                                                // CurrentPage now points here
    Pdf.CurrentPage.SetFont('Arial', [], 10);                   // font state does not carry over
    Pdf.CurrentPage.TextOut(50, 742, 0, 'Page 2 detail rows');
    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Dos comportamientos con estado en ese listado causan la mayoría de bugs en la segunda página. AddPage vuelve a apuntar CurrentPage a la página nueva, así que cualquier referencia que haya cacheado al objeto de la página anterior ya está obsoleta para dibujar. Y la selección de fuente es por página: llame de nuevo a SetFont después de cada AddPage, o el primer TextOut de la página nueva no usará la fuente que cree. Un bucle de informe debería tratar "página nueva" y "restablecer estado de texto" como una sola unidad.

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

Los fallos de fuente son fallos de despliegue. La máquina de desarrollo tiene instalada la fuente corporativa; la cuenta de servicio Windows del host de producción no, y la salida sustituye silenciosamente. El patrón defensivo es cargar fuentes desde ficheros que controla su instalador en lugar de confiar en el directorio de fuentes del sistema, y la llamada de registro Unicode de HotPDF 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 ✓'));

Observe que TextOut acepta directamente un WideString, así que los datos de cliente que contienen cualquier cosa fuera de la página de códigos local — lo que en la práctica significa todos los datos de cliente — viajan por la misma llamada que el mobiliario ASCII del informe, siempre que la fuente seleccionada cubra los glifos. Las fuentes Unicode embebidas también requieren que el documento sea PDF 1.5 o posterior, así que tenga presente ese suelo de versión si algún otro requisito le está fijando a una versión más antigua. Para escrituras que necesitan conformación y no solo búsqueda de glifos, árabe y hebreo en particular, la canalización dedicada de derecha a izquierda se cubre en nuestro artículo sobre conformación de texto de escrituras complejas con HotPDF.

Para el caso raro en que ningún fichero de fuente pueda representar lo que necesita, marcas tipo MICR o símbolos propietarios, HotPDF soporta fuentes Type 3 mediante RegisterType3Font y AddType3Glyph, donde cada glifo es un pequeño stream de contenido que usted define. Es una herramienta de nicho, pero gana a enviar símbolos como cientos de imágenes diminutas.

Imágenes: los argumentos centrales son anchura y altura, no una esquina

HotPDF separa el registro de imágenes de su colocación. AddImage ingiere una vez un TBitmap o un TJPEGImage — descodifique antes el arte PNG a un bitmap — y devuelve un índice; ShowImage coloca ese índice tantas veces como sea necesario. La firma es la parte que conviene leer dos veces:

var
  Png: TPngImage;
  Logo: TBitmap;
  LogoIdx: Integer;
begin
  Png := TPngImage.Create;
  Logo := TBitmap.Create;
  try
    Png.LoadFromFile('brand-logo.png');
    Logo.Assign(Png);                       // decode PNG to a bitmap
    LogoIdx := Pdf.AddImage(Logo, icFlate); // lossless for flat-color art
  finally
    Logo.Free;
    Png.Free;
  end;
  // (Index, X, Y, Width, Height, Angle) — not (X1, Y1, X2, Y2)
  Pdf.CurrentPage.ShowImage(LogoIdx, 50, 700, 120, 40, 0);
end;

Los dos números después de la posición son anchura y altura, no la esquina opuesta, y el argumento final es un ángulo de rotación. El código escrito con la suposición X1/Y1/X2/Y2 produce logotipos estirados por casi toda la página, un bug obvio en la salida y desconcertante en el código. Relacionado: KeepImageAspectRatio tiene True como valor predeterminado, así que una caja desajustada encaja con bandas en lugar de distorsionar; cámbielo a False solo cuando el estiramiento sea realmente intencionado.

La separación entre registro y colocación también importa para rendimiento y tamaño: AddImage embebe una vez los datos del bitmap, y cada ShowImage con el mismo índice reutiliza ese único objeto embebido. Una tirada de 500 páginas que llama a AddImage por página para el mismo logotipo embebe el logotipo 500 veces; la misma tirada que registra una vez y reutiliza el índice lo embebe una vez. Cachee los índices en un pequeño diccionario por ruta de recurso y el problema no aparece.

El tamaño de fichero también vive aquí. El contenido fotográfico debería pasar por codificación JPEG — pase icJpeg a AddImage y establezca JpegQuality en torno a 85, ya que la propiedad vale 100 por defecto — lo que es visualmente limpio para adjuntos escaneados y fotos con una fracción del tamaño sin pérdida. Reserve PNG para arte de color plano como logotipos y gráficos, donde los artefactos de anillado de JPEG se ven y la compresión Flate ya es eficiente. Una tirada de extractos que embebe una foto por página con la configuración equivocada envía gigabytes; la misma tirada a JPEG 85 envía una décima parte sin que nadie lo note a simple vista.

Reglas, cajas y sombreados con primitivas de trazado

Las reglas de tabla y las cajas de totales no necesitan imágenes en absoluto; las primitivas vectoriales producen una salida más nítida a cualquier zoom y cuestan casi nada en tamaño de fichero. El modelo es construcción de trazado seguida de un operador de pintura:

// Horizontal rule under the table header
Pdf.CurrentPage.SetLineWidth(0.75);
Pdf.CurrentPage.MoveTo(50, 660);
Pdf.CurrentPage.LineTo(545, 660);
Pdf.CurrentPage.Stroke;

// Shaded totals box: X, Y, width, height
Pdf.CurrentPage.SetRGBFillColor(RGB(235, 235, 235));
Pdf.CurrentPage.Rectangle(395, 120, 150, 40);
Pdf.CurrentPage.Fill;

La disciplina de orden es la misma que en los streams de contenido PDF crudos: establezca el estado de pintura, construya el trazado y luego llame a Stroke o Fill. Un trazado que nunca se pinta simplemente desaparece, que es la explicación habitual cuando una regla "no aparece". SetRGBFillColor acepta un único TColor, así que las constantes VCL — clNavy, clBlack — funcionan directamente, y Rectangle sigue la misma convención de anchura y altura que la colocación de imágenes. Las hairlines merecen una cautela: anchos de línea por debajo de alrededor de medio punto quedan elegantes en pantalla y pueden desaparecer por completo en una impresora de oficina de 600 dpi, así que 0,75 pt es un suelo sensato para reglas de tabla que deben sobrevivir en papel.

Paginación contra datos reales, no datos de muestra

Las columnas numéricas exponen otro hábito que conviene construir pronto: alinee importes por su borde derecho calculando la posición X desde el límite derecho de la columna y la anchura renderizada de cada valor, en lugar de rellenar cadenas con espacios. El relleno con espacios solo alinea en fuentes monoespaciadas, y los informes financieros nunca se componen con fuentes monoespaciadas. Formatee los valores con rutinas Delphi conscientes de locale, como FormatFloat, antes de medir, para que el separador de miles que espera el locale del cliente sea el mismo cuya anchura ha medido.

El conjunto de datos de demo tiene diez filas cortas; producción tiene un cliente cuyo nombre de empresa tiene 140 caracteres y un extracto con 4.000 líneas. Un bucle de informe robusto sigue un cursor Y hacia abajo, resta la altura de cada fila y salta a una página nueva cuando el cursor cruzaría el margen inferior, recordando que "hacia abajo" significa Y decreciente en este sistema de coordenadas. Ponga la gestión de salto de página en un solo sitio, vuelva a emitir SetFont y redibuje la cabecera continua dentro de él, y desaparecerán los bugs de página de más o de menos. Si sus informes también deben cumplir requisitos de archivo o accesibilidad, las decisiones de generación tomadas aquí, fuentes embebidas, salida etiquetada, espacios de color, son exactamente lo que restringen los estándares; vea la guía HotPDF de PDF/A, PDF/X y PDF/UA antes de que la plantilla se endurezca.

FAQ

¿Por qué mi texto se renderiza en la parte inferior de la página?

El origen de PDF está en la esquina inferior izquierda con Y creciendo hacia arriba. Convierta posiciones relativas a la parte superior con PageHeight - Offset, o diseñe desde el principio el código de maquetación alrededor del origen inferior izquierdo.

¿Por qué la fuente es incorrecta en la página 2 pero correcta en la página 1?

La selección de fuente no se arrastra entre páginas, y AddPage cambia CurrentPage a la página nueva. Llame a SetFont después de cada AddPage antes del primer TextOut.

¿Cómo mantengo un tamaño de fichero razonable con muchas fotos embebidas?

Pase icJpeg a AddImage y establezca JpegQuality cerca de 85 para contenido fotográfico; reserve icFlate sin pérdida para logotipos y arte de líneas de color plano. Registre cada imagen distinta una vez con AddImage y reutilice el índice.

Referencia de producto

Todas las llamadas de este artículo se entregan con HotPDF Component para Delphi y C++Builder, que documenta la API completa de texto, fuentes, imágenes y dibujo junto a las funciones de formularios, cifrado y firma tratadas en otros artículos de este blog.