Una primera línea escrita desde el punto de vista de la especificación cae en y = 770. El razonamiento es sólido sobre el papel: ISO 32000-1 pone el origen del espacio de usuario en la esquina inferior izquierda, una página A4 mide 842 puntos de alto, así que 770 queda una pulgada por debajo del borde superior. Ejecuta ese programa contra un componente que mide desde arriba y el saludo cae una pulgada por encima del borde inferior, con el resto de la página vacío por encima. Añade una segunda página, y la fuente elegida con tanto cuidado vuelve a ser Arial de 12 puntos. Las dos sorpresas vienen de las mismas dos decisiones que todo programa PDF toma en su primera página: qué fuente está activa, y desde qué esquina cuentan los números
El programa HotPDF más pequeño en Delphi
El Hello World de HotPDF es un programa de consola con cuatro llamadas en un orden fijo: establecer propiedades del documento, BeginDoc, dibujar con SetFont y TextOut, y luego EndDoc. Todo lo demás es Delphi corriente. Aquí está, escribiendo una página que saluda al lector en once idiomas:
program HelloWorld;
{$APPTYPE CONSOLE}
uses
Winapi.Windows, // DEFAULT_CHARSET; en Delphi anterior a XE2 usa Windows a secas
HPDFDoc; // unit principal de HotPDF
procedure CreateHelloWorld(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
// Ajustes de documento: todos van antes de BeginDoc
Pdf.FileName := FileName;
Pdf.AutoLaunch := False; // por defecto es True: abre el visor tras EndDoc
Pdf.Compression := cmFlateDecode; // por defecto es cmNone
Pdf.FontEmbedding := True; // ya es el valor por defecto, se indica para el lector
Pdf.EnableFontSubsetting := True; // incrusta solo los glifos realmente dibujados
Pdf.BeginDoc; // crea la página 1 (A4)
// El 4.º argumento es ANSI_CHARSET por defecto; pasa DEFAULT_CHARSET explícitamente
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
// X, Y en puntos desde la esquina SUPERIOR IZQUIERDA; Y es el borde superior de la línea
Pdf.CurrentPage.TextOut(72, 72, 0, 'Hello, Delphi PDF world!'); // inglés
Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!'); // español
Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!'); // alemán
Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!'); // francés
Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!'); // italiano
Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!'); // portugués
Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // Russian
Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!'); // Japanese
Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!'); // Turkish
Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界'); // Chinese
Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!'); // Korean
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
begin
CreateHelloWorld('HelloWorld.pdf');
Writeln('Wrote HelloWorld.pdf');
end.

THotPDF.Create(nil) emparejado con try/finally ... Free es el patrón de propiedad habitual de Delphi, y producir un PDF no lo cambia. Las líneas marchan hacia abajo desde Y = 72, separadas 30 puntos, lo que deja un interlineado cómodo para texto de 14 puntos. Ejecútalo y HelloWorld.pdf aparece en el directorio de trabajo: una página, contenido comprimido, la fuente incrustada como subconjunto
Las propiedades del documento van antes de BeginDoc
BeginDoc arranca el documento y crea su primera página, así que cualquier cosa que moldee el archivo entero tiene que asignarse antes. HotPDF lo obliga para algunas propiedades en lugar de dejarlo a la convención. Asigna Compression después de BeginDoc y el setter lanza una excepción con el mensaje Cannot set Compression value - document in progress.; AutoLaunch está guardado igual. Que sea ruidoso es bueno aquí. Pon todos los ajustes de documento en un solo bloque encima de BeginDoc y jamás verás ese mensaje
Dos de los valores por defecto merecen una segunda mirada. Compression arranca como cmNone, así que un programa que nunca lo toca escribe content streams sin comprimir; para una página de texto es un mero desperdicio, para un informe de cientos de páginas es lo primero que cualquiera nota del tamaño del archivo. AutoLaunch arranca como True, lo que significa que una herramienta de consola abre el PDF terminado en el visor asociado en cuanto EndDoc devuelve. Eso es agradable en tu escritorio y está mal dentro de un servicio, un trabajo programado o un agente de build. Para esos entornos, ponlo a False en código; para ejecuciones de CI donde no puedes cambiar el código, la variable de entorno HOTPDF_SUPPRESS_AUTO_LAUNCH puesta a 1 también desactiva la apertura
El manejo de fuentes es donde tomamos partido. FontEmbedding es True por defecto, así que la fuente viaja con el archivo y renderiza en una máquina que jamás tuvo Arial Unicode MS instalada. EnableFontSubsetting, en cambio, está apagado por defecto para mantener las fuentes completas disponibles para flujos de edición. Para un saludo, una factura o un informe generados, activa el subsetting. Arial Unicode MS cubre una enorme gama de sistemas de escritura, e incrustar la fuente entera para mostrar once líneas cortas haría el archivo megas más grande sin beneficio visible. Déjalo apagado solo cuando alguien aguas abajo vaya a editar el texto y necesite glifos que no dibujaste
¿Dónde está (0, 0) en una página HotPDF?
HotPDF mide las coordenadas de página desde la esquina superior izquierda con la Y creciendo hacia abajo, el mismo marco que usa un TCanvas de VCL. Con la Resolution por defecto de 72, una unidad es un punto, 1/72 de pulgada. El archivo PDF en sí sigue almacenando coordenadas de abajo hacia arriba, como exige ISO 32000-1 §8.3.2.3; HotPDF convierte cada Y que pasas restándosela a la altura de la página cuando escribe el content stream
La aritmética tras el ejemplo de apertura es corta. La página por defecto que BeginDoc y AddPage crean es A4, 595 por 842 puntos. Una línea pensada para quedar a una pulgada del inicio es Y = 72 en términos HotPDF y y = 770 en términos PDF crudos, porque 842 − 72 = 770. Pasa 770 a TextOut y el texto va 770 puntos hacia abajo desde el inicio, o sea 72 puntos por encima del borde inferior
Hay una segunda trampa, más pequeña, en la misma llamada. Por defecto la Y que pasas es el borde superior del texto, y HotPDF baja lo que mide el texto antes de colocar la línea base. El código portado desde GDI suele esperar exactamente eso. El código portado desde otra librería PDF, o desde tu propio escritor de content streams, suele esperar que la Y sea la línea base, y cada línea sale una altura de texto más abajo de lo planeado. THPDFPage.TextFromBaseLine(True) cambia la página actual a posicionamiento por línea base:
// Puente para coordenadas que vienen de una fuente de abajo hacia arriba (un layout PDF,
// un diagrama de especificación, otra librería). Helper escrito para este artículo,
// no forma parte de HotPDF. Válido con la Resolution por defecto de 72
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
Result := Page.Height - PdfY;
end;
procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
Pdf.CurrentPage.SetFont('Arial', [], 12);
// La Y ahora nombra la línea base, como colocan glifos los operadores de texto del propio PDF
Pdf.CurrentPage.TextFromBaseLine(True);
// Línea base 770 puntos por encima del borde inferior, o sea Y = 72 desde arriba
Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
'Baseline one inch below the top edge');
end;
Resolution es el otro ajuste que cambia lo que significan los números. Pon THotPDF.Resolution a 96 y las coordenadas X/Y, los márgenes y los resultados de TextWidth se expresan en 1/96 de pulgada, lo que viene bien cuando la maquetación se diseñó en píxeles de pantalla. El Width y el Height de página siguen informando puntos, así que el código de maquetación que mezcle límites de página con coordenadas de dibujo debería leer CurrentPage.UserWidth y CurrentPage.UserHeight, que vuelven en las mismas unidades que X e Y. Con 72 las dos parejas coinciden. Si prefieres pensar en milímetros, la propiedad MeasurementUnit (muPoints por defecto, con muMM, muInches, muCentimeters, muTwips y muPixels disponibles) es el interruptor más limpio. Elige una convención por programa. Nunca las apiles
¿Por qué la página dos imprime en Arial de 12 puntos?
Porque cada página nueva arranca de un estado conocido. THotPDF.AddPage vuelve actual la página nueva, la dimensiona A4 y llama a SetFont('Arial', [], 12) sobre ella, manteniendo el tamaño en 12 puntos sea cual sea la Resolution. BeginDoc crea la página uno por el mismo camino, razón por la que el Hello World fija su fuente justo después de BeginDoc y no antes. La consecuencia para salidas multipágina es fácil de perder: el SetFont que llamaste en la página uno no se arrastra. Las líneas latinas de la página dos siguen viéndose bien en Arial, solo que más pequeñas. Las líneas CJK son la delación, porque Arial no tiene glifos para ellas y el lector muestra cajas vacías o nada en absoluto
Una fuente de arranque predecible es el diseño correcto para un objeto página: el código de dibujo de la página cinco no depende en silencio de lo que la página cuatro dejara por casualidad. El coste es que la configuración de página hay que repetirla, y la forma más limpia de repetirla es un único procedimiento por el que pasa toda página, incluida la primera:
uses
Winapi.Windows, Vcl.Graphics, HPDFDoc;
// Tu propio helper: un único sitio que sabe lo que necesita una página nueva
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
if not IsFirst then
Pdf.AddPage; // reinicia la fuente a Arial 12
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;
procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
TopMargin = 72;
Leading = 30;
LinesPerPage = 20;
var
I, Row: Integer;
begin
PreparePage(Pdf, True); // la página 1 ya existe tras BeginDoc
Row := 0;
for I := 0 to High(Lines) do
begin
if Row = LinesPerPage then
begin
PreparePage(Pdf, False);
Row := 0;
end;
Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
Inc(Row);
end;
// Un titular en negrita necesita su propio SetFont, y luego vuelve la fuente del cuerpo
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;
Si tus páginas además cambian a posicionamiento por línea base con TextFromBaseLine, pon esa llamada también en PreparePage, junto a la fuente. Mantener cada ajuste por página en un solo procedimiento significa que un requisito nuevo, un color o un tamaño distinto se añade una vez y llega a todas las páginas
Una fuente para muchos sistemas de escritura, y dónde se queda corta
Un único SetFont puede llevar latín, cirílico, japonés y chino en una página porque Arial Unicode MS empaqueta glifos de una gama muy amplia de sistemas de escritura en una sola fuente. El cuarto argumento de SetFont es el juego de caracteres de Windows, y su valor por defecto es ANSI_CHARSET, que ata la selección de fuente a la página de códigos occidental. Pasar DEFAULT_CHARSET (valor 1) deja que Windows elija la fuente por nombre, que es lo que una página multilingüe quiere
La fuente tiene que existir en la máquina que construye el PDF. HotPDF incrusta la fuente a la que Windows resuelva el nombre, y cuando falta Arial Unicode MS, Windows sustituye otra fuente sin queja. La salida incrusta entonces la sustituta, y las líneas en sistemas de escritura que esa fuente no cubre salen como cajas vacías. Un servidor de build es el lugar habitual donde esto aparece, porque nadie instaló allí las fuentes de Office. Comprueba la lista de fuentes incrustadas del primer archivo que produzca una máquina nueva antes de fiarle una ejecución por lotes
Cuando ninguna fuente sola cubre todos los strings, TextOutWithFallback toma los mismos argumentos que TextOut, resuelve una fuente para cada carácter, dibuja cada tramo con la fuente que pueda renderizarlo y restaura tu fuente original después. Los sistemas de escritura de derecha a izquierda como el árabe y el hebreo necesitan más que cobertura de glifos: necesitan shaping y orden de tramo invertido, que es el territorio de la función RtLTextOut, normalmente emparejada con un archivo TrueType cargado mediante RegisterUnicodeTTF
Desde aquí los pasos naturales siguientes son el tamaño, el estilo y la rotación del texto, y colocar varios bloques deliberadamente en una página, lo que el ejemplo TextOut recorre. Reglas, cajas e imágenes usan el mismo objeto CurrentPage y las mismas coordenadas superiores izquierdas, así que todo lo de arriba sobre orígenes y configuración por página les aplica sin cambios
La página de HotPDF Delphi Component cubre el resto de la API de dibujo para Delphi y C++Builder, desde formas e imágenes hasta formularios, una vez que este Hello World te funciona limpio con tus propias fuentes