La primera factura que casi cualquier equipo renderiza con una biblioteca PDF sale mal de la misma manera: el texto del encabezado queda junto al borde inferior de la página, y cada línea siguiente sube. 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 aumentando hacia arriba, exactamente lo opuesto al lienzo GDI sobre el que un desarrollador VCL ha dibujado durante años. HotPDF, la biblioteca de generación PDF de losLab para Delphi y C++Builder, expone ese modelo de coordenadas directamente, por lo que los cinco minutos que inviertan en interiorizarlo ahora ahorran una reescritura de layout después. Este artículo recorre las primitivas de salida que un generador de reportes realmente necesita: 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 página es TextOut(X, Y, Angle, Text). X e Y ubican 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 mantiene funcionando la intuición entrenada por VCL es calcular Y como altura de página menos la distancia 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 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 los bugs de la página dos. AddPage redirige CurrentPage a la página nueva, por lo que cualquier referencia que hayan guardado al objeto de la página anterior queda obsoleta para dibujar. Y la selección de fuente es por página: llamen SetFont otra vez después de cada AddPage, o el primer TextOut de la nueva página no usará la fuente que ustedes creen. Un bucle de reporte debe tratar "nueva página" y "restablecer estado de texto" como una sola unidad.
Fuentes que existen en el servidor, no sólo en su escritorio
Las fallas de fuentes son fallas de despliegue. La máquina de desarrollo tiene instalada la fuente corporativa; la cuenta de servicio de Windows en el host de producción no, y la salida sustituye en silencio. El patrón defensivo es cargar fuentes desde archivos que controla su instalador en lugar de confiar en el directorio de fuentes del sistema operativo, 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 ✓'));
Noten que TextOut toma un WideString directamente, 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 texto ASCII fijo del reporte, 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 tengan presente ese piso de versión si otro requisito los fija a una versión más antigua. Para escrituras que necesitan shaping y no sólo búsqueda de glifos, árabe y hebreo en particular, la canalización dedicada de derecha a izquierda se cubre en nuestro artículo sobre composición de texto de escrituras complejas con HotPDF.
Para el caso poco común en que ningún archivo de fuente puede representar lo que necesitan, marcas tipo MICR o símbolos propietarios, HotPDF admite fuentes Type 3 mediante RegisterType3Font y AddType3Glyph, donde cada glifo es un pequeño stream de contenido que ustedes definen. Es una herramienta de nicho, pero supera enviar símbolos como cientos de imágenes diminutas.
Imágenes: los argumentos intermedios son ancho y alto, no una esquina
HotPDF separa registro de imagen y colocación. AddImage ingiere una TBitmap o TJPEGImage una vez, decodifiquen primero la ilustración 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 ancho y alto, no una esquina opuesta, y el argumento final es un ángulo de rotación. El código escrito bajo una 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 fuente. Relacionado: KeepImageAspectRatio tiene True como valor predeterminado, por lo que una caja desajustada agrega bandas en lugar de distorsionar; configúrenlo en False sólo cuando el estiramiento sea realmente intencional.
La separación entre registro y colocación también importa para rendimiento y tamaño: AddImage embebe los datos del bitmap una vez, y cada ShowImage con el mismo índice reutiliza ese único objeto embebido. Una corrida de estados de cuenta de 500 páginas que llama AddImage por página para el mismo logotipo embebe el logotipo 500 veces; la misma corrida que registra una vez y reutiliza el índice lo embebe una vez. Guarden los índices en un diccionario pequeño por ruta de activo y el problema no aparece.
El tamaño de archivo también vive aquí. El contenido fotográfico debe pasar por codificación JPEG: pasen icJpeg a AddImage y configuren JpegQuality alrededor de 85, ya que la propiedad tiene 100 como valor predeterminado, lo que resulta visualmente limpio para adjuntos escaneados y fotos con una fracción del tamaño sin pérdida. Mantengan PNG para gráficos de color plano como logotipos y diagramas, donde los artefactos de anillado de JPEG son visibles y la compresión Flate ya es eficiente. Una corrida de estados de cuenta que embebe una foto por página con configuraciones equivocadas envía gigabytes; la misma corrida con JPEG 85 envía una décima parte sin que nadie lo note a simple vista.
Reglas, cajas y sombreado con primitivas de ruta
Las líneas de tabla y las cajas de totales no necesitan imágenes en absoluto; las primitivas vectoriales producen salida más nítida en cualquier zoom y cuestan casi nada en tamaño de archivo. El modelo es construcción de ruta seguida por 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: establezcan el estado de pintura, construyan la ruta y luego llamen Stroke o Fill. Una ruta que nunca se pinta simplemente desaparece, que es la explicación usual cuando una línea "no aparece". SetRGBFillColor toma un único TColor, así que las constantes VCL, clNavy, clBlack, funcionan directamente, y Rectangle sigue la misma convención de ancho y alto que la colocación de imágenes. Las hairlines merecen una advertencia: anchos de línea por debajo de aproximadamente medio punto se ven elegantes en pantalla y pueden desaparecer por completo en una impresora de oficina de 600 dpi, así que 0.75pt es un piso sensato para líneas de tabla que deben sobrevivir al papel.
Paginación contra datos reales, no datos de muestra
Las columnas numéricas exponen otro hábito que conviene construir temprano: alinear importes por su borde derecho calculando la posición X desde el límite derecho de la columna y el ancho renderizado de cada valor, en lugar de rellenar cadenas con espacios. El relleno con espacios sólo alinea en fuentes monoespaciadas, y los reportes financieros nunca se componen en fuentes monoespaciadas. Formateen valores mediante rutinas de Delphi sensibles a la configuración regional, como FormatFloat, antes de medir, para que el separador de miles que espera la localidad del cliente sea el mismo cuyo ancho midieron.
El conjunto de datos demo tiene diez filas cortas; producción tiene un cliente cuyo nombre de empresa mide 140 caracteres y un estado de cuenta con 4,000 renglones. Un bucle de reporte robusto rastrea un cursor Y hacia abajo, restando 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. Pongan el manejo del salto de página en un solo lugar, vuelvan a emitir SetFont y redibujen el encabezado continuo dentro de él, y desaparecen los bugs de una página de más o de menos. Si sus reportes también deben satisfacer requisitos de archivo o accesibilidad, las decisiones de generación tomadas aquí, fuentes embebidas, salida etiquetada, espacios de color, son exactamente lo que los estándares restringen; vean la guía de HotPDF para PDF/A, PDF/X y PDF/UA antes de que la plantilla quede fija.
FAQ
¿Por qué mi texto se renderiza en la parte inferior de la página?
El origen de PDF es la esquina inferior izquierda con Y aumentando hacia arriba. Conviertan posiciones relativas al borde superior con PageHeight - Offset, o diseñen su código de layout alrededor del origen inferior izquierdo desde el principio.
¿Por qué la fuente está mal en la página 2 pero correcta en la página 1?
La selección de fuente no se conserva entre páginas, y AddPage cambia CurrentPage a la página nueva. Llamen SetFont después de cada AddPage antes del primer TextOut.
¿Cómo mantengo razonable el tamaño del archivo con muchas fotos embebidas?
Pasen icJpeg a AddImage y configuren JpegQuality cerca de 85 para contenido fotográfico; reserven icFlate sin pérdida para logotipos de color plano y line art. Registren cada imagen distinta una vez con AddImage y reutilicen el índice.
Referencia de producto
Cada llamada de este artículo se incluye con HotPDF Component para Delphi y C++Builder, que documenta la API completa de texto, fuentes, imágenes y dibujo junto con las funciones de formularios, cifrado y firma cubiertas en otros artículos de este blog.