Artículo técnico

Delphi HotPDF Hello World: tu primer programa PDF

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

Diagrama del flujo del programa Hello World de HotPDF en cuatro llamadas en Delphi, con el origen de página superior izquierdo y dos trampas de configuración señaladas
Crear, comenzar, dibujar, terminar es toda la columna vertebral, y el origen superior izquierdo explica por qué las líneas posteriores usan valores Y mayores

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.

PDF Hello World multilingüe producido por el componente Delphi HotPDF en Delphi

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