Una cadena dibujada en (72, 72) aterriza una pulgada por debajo del borde superior de la página, lo que descoloca a cualquiera que aprendiera PDF por el lado del content stream, donde y = 72 está una pulgada por encima del borde inferior. La segunda sorpresa aparece unos cientos de líneas más tarde. Un informe maquetado para US Letter vuelve con una primera página de 612 por 792 puntos y todas las siguientes a 595 por 842, porque la línea que fijaba el tamaño de papel se ejecutó exactamente una vez
Los dos efectos tienen la misma raíz. El texto en un PDF es una corta ristra de cambios de estado (fuente y tamaño, espaciado de caracteres, espaciado de palabras, una matriz de texto para posición y rotación) seguida de un operador show-text, y una llamada de texto de alto nivel es un envoltorio fino sobre esa secuencia. La llamada lee el estado. Nunca lo reinicia por usted, y una página nueva arranca desde un conjunto fijo de defaults en lugar de desde lo que la página anterior dejó puesto
Dónde coloca TextOut el (X, Y) en la página
HotPDF dibuja texto con THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString), al que se llega como Pdf.CurrentPage.TextOut. X e Y se miden desde la esquina superior izquierda de la página con Y creciendo hacia abajo, y con la Resolution por defecto de 72 una unidad es un punto, 1/72 de pulgada. La página convierte cada Y al espacio de usuario de PDF, que corre de abajo arriba, restándoselo a la altura de la página. El ángulo va en grados y, como el texto es un WideString, el Unicode pasa directamente. Una segunda sobrecarga recibe un PWORD más una longitud para código que ya maneja buffers UTF-16
La geometría de la página también viene fija. BeginDoc añade la primera página, y cada página creada por BeginDoc o AddPage arranca como A4, 595 por 842 puntos, con Arial 12 como fuente actual. US Letter (612 por 792) está a una asignación de distancia vía CurrentPage.Size := Letter, pero esa asignación pertenece a la página, así que hay que repetirla en cada página nueva
Por defecto, la Y que usted pasa es la parte superior del texto, y la baseline queda una altura de ascendente por debajo. TextFromBaseLine(True) cambia la página a posicionamiento por baseline. Actívelo para cualquier cosa que mezcle tamaños en una misma línea. En modo borde superior, un código de divisa de 9 puntos junto a un importe de 14 puntos tiene su parte superior alineada con la del importe, así que se ve visiblemente alto; en modo baseline las dos corridas se sientan en la misma línea, como las pondría un tipógrafo. El último bloque del ejemplo siguiente hace exactamente eso
El tamaño y el estilo viven en SetFont
TextOut no tiene parámetro de tamaño ni de estilo. SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical) lleva todo eso, con los dos últimos argumentos tomando por defecto ANSI_CHARSET y texto horizontal, y lo que fija sigue en vigor para cada TextOut posterior hasta el próximo SetFont. Mucha confusión del primer día se remonta a eso. Una línea sale en negrita porque tres llamadas antes algo puso [fsBold] y nadie lo limpió
var
W: Single;
begin
Pdf.BeginDoc; // primera página: A4, Arial 12
Pdf.CurrentPage.TextFromBaseLine(True); // cada Y de aquí abajo es una baseline
Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Quarterly Report'); // 24 pt regular
Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
Pdf.CurrentPage.TextOut(72, 104, 0, 'Revenue'); // 12 pt negrita
Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
Pdf.CurrentPage.TextOut(72, 120, 0, 'figures in thousands'); // 11 pt cursiva
Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY'); // los estilos se combinan
// Tamaños mezclados en una misma baseline: medir mientras la fuente de 14 pt está activa
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR'); // hueco de 3 pt
...
end;
El argumento de estilo es un conjunto TFontStyles ordinario, así que [] es normal y [fsBold, fsItalic] es negrita cursiva. fsUnderline y fsStrikeOut se dibujan como barras rellenas finas bajo o a través de la corrida, y siguen a la corrida cuando esta se rota. El tamaño va en puntos, la misma unidad que las coordenadas, lo que deja la aritmética vertical sencilla: texto de cuerpo de 11 puntos con un paso de 16 puntos deja unos cinco puntos de aire entre líneas. No hay avance de línea automático. Cada Y la calcula usted, lo que es tedioso para un párrafo y exactamente correcto para un formulario cuyos campos se sientan en posiciones fijas
GetWideTextWidth mide con la fuente que esté activa en el momento de la llamada, así que en el último bloque la medición tiene que ocurrir mientras la cara de 14 puntos en negrita sigue puesta. Muévala por debajo del segundo SetFont y el sufijo de 9 puntos aterriza encima de los últimos dígitos del importe
El nombre de la fuente se resuelve contra las fuentes instaladas en la máquina que construye el PDF, y una cara que resuelve en el escritorio de un desarrollador puede faltar en un servidor de builds, de modo que la cara del archivo terminado depende de qué máquina lo produjo. La cobertura de scripts es la otra mitad. Una cara solo latina a la que se le pida dibujar cirílico o CJK produce cajas de glifo faltante, y eso es una propiedad del archivo de fuentes, no de la llamada. Cuando una sola cadena mezcla scripts que ninguna cara única cubre, TextOutWithFallback toma los mismos cuatro argumentos, cambia de cara corrida a corrida para los caracteres que la fuente principal no tiene y restaura la fuente original al terminar
¿Cómo rota el texto el argumento de ángulo?
Un ángulo distinto de cero gira la corrida en sentido antihorario esos grados alrededor de su ancla, y el ancla es la esquina superior izquierda del texto en el marco propio del texto. El ancla no se mueve. Solo cambia la dirección en la que marchan los glifos, así que la coordenada que colocaba una etiqueta horizontal también coloca a su gemela rotada. Las corridas rotadas conservan esa ancla superior izquierda incluso cuando la página está en modo baseline; TextFromBaseLine desplaza solo las corridas horizontales
var
W, AxisMidY: Single;
begin
AxisMidY := 400; // punto medio del eje Y del gráfico
// Una corrida a 90 grados se lee de abajo arriba y crece hacia arriba desde Y,
// así que centrarla en el eje significa empezar medio ancho por debajo del punto medio
Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');
// Una marca de agua en diagonal
Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');
// Cabeceras de columna inclinadas 60 grados: misma Y, X escalonada
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;
Noventa grados cubre etiquetas de ejes y títulos de lomo. A 90 grados los glifos se yerguen a la derecha de la X del ancla, con una línea de alto, y la corrida trepa hacia el borde superior de la página a lo ancho de sí misma. Por eso centrar suma media anchura a Y en lugar de restarla, y por eso una etiqueta anclada en el punto medio se sienta íntegramente por encima. Sesenta grados va bien para cabeceras de columna, dejando que una etiqueta larga se alce sobre una columna estrecha sin derramarse en su vecina, y 45 grados es el ángulo típico de marca de agua. Las corridas que comparten baseline reciben la misma Y y una X escalonada, exactamente como las líneas horizontales apiladas comparten X y escalonan Y
¿Por qué la segunda página sale como A4 en Arial 12?
AddPage devuelve el índice de la página nueva y la vuelve actual, y la página nueva arranca desde los defaults: A4, Arial 12, posicionamiento por borde superior, espaciados de caracteres y palabras a cero. Nada de la página anterior se arrastra. Ese diseño mantiene cada página autocontenida, ya que el content stream de una página PDF empieza de todos modos con un estado de gráficos fresco, pero significa que cualquier código de maquetación que configure la página una sola vez al principio del documento cambiará discretamente de tamaño de papel y de tipografía en la página dos. La solución es meter todos los ajustes por página en una única rutina y llamarla tras BeginDoc y tras cada AddPage
procedure StartReportPage(Pdf: THotPDF);
begin
Pdf.CurrentPage.Size := Letter; // una página nueva empieza como A4
Pdf.CurrentPage.TextFromBaseLine(True); // una página nueva empieza en modo borde superior
Pdf.CurrentPage.SetFont('Arial', [], 11); // una página nueva empieza en Arial 12
end;
procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
LeftMargin = 72; // 1 pulgada
TopLine = 72; // primera baseline, 1 pulgada hacia abajo
BottomMargin = 72;
Leading = 16;
var
I: Integer;
Y: Single;
begin
StartReportPage(Pdf);
Y := TopLine;
for I := 0 to Lines.Count - 1 do
begin
if Y > Pdf.CurrentPage.Height - BottomMargin then // 720 en Letter
begin
Pdf.AddPage;
StartReportPage(Pdf);
Y := TopLine;
end;
Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
Y := Y + Leading;
end;
end;
No hay ningún flow layout por debajo de TextOut, y una Y pasada del borde inferior de la página sigue siendo una coordenada válida en lo que respecta al content stream: el texto se escribe, simplemente cae fuera del media box y ningún visor lo muestra. Así que el bucle comprueba Y antes de cada línea y abre página nueva por su cuenta. Leer el límite desde CurrentPage.Height en lugar de clavar 720 mantiene el bucle correcto si alguien cambia luego el helper de vuelta a A4, donde el mismo margen de una pulgada pone el límite en 770
La comprobación más rápida es la que haría el diálogo de impresión de un lector. Abra la salida, vaya a la página dos y mire las propiedades del documento: una página Letter reporta 8.5 por 11 pulgadas, una A4 unos 8.27 por 11.69. Si la primera página y la segunda discrepan, algún ajuste por página vive fuera del helper
El espaciado de caracteres y de palabras también es estado de página
A veces la cadena está bien y la anchura está mal: una cabecera que debe cubrir una regla fija, un código que se lee mejor con dígitos más espaciados, una línea de etiquetas cuyos huecos piden abrirse. PDF tiene dos parámetros de estado de texto para esto en ISO 32000-1: el espaciado de caracteres Tc (§9.3.2), añadido tras cada glifo, y el espaciado de palabras Tw (§9.3.3), añadido en cada espacio. HotPDF los fija con SetCharacterSpacing(Spacing: Single) y SetWordSpacing(Spacing: Single), en puntos con la resolución por defecto, y como la fuente se mantienen en vigor hasta que se cambien
El espaciado de palabras tiene una trampa escrita en la especificación. §9.3.3 aplica Tw solo al código de carácter de un byte 32, y explícitamente no a un byte 32 dentro de un código multi-byte, así que por sí solo el operador no hace nada para texto escrito con códigos de glifo de dos bytes, que es como se escriben normalmente las fuentes Unicode incrustadas. HotPDF escribe las corridas Unicode de esa manera, y por ese camino aplica el espaciado de palabras como ajustes de posición explícitos tras cada glifo de espacio, de modo que SetWordSpacing ensancha los huecos por cualquiera de las dos vías. El disparador sigue siendo el espacio sencillo, U+0020. Una corrida de chino o japonés sin espacios ASCII no recibe espacio extra alguno, y un espacio ideográfico (U+3000) se trata como un glifo ordinario
El espaciado de caracteres es la herramienta correcta cuando una cabecera debe alcanzar una anchura objetivo, porque reparte el cambio de manera uniforme en lugar de acumularlo en los espacios. GetWideTextWidth cuenta el espaciado de caracteres y de palabras en vigor, así que mida con ambos a cero y luego derive el espaciado:
const
Heading = 'SUMMARY';
Target = 200; // ancho de la regla que la cabecera debe cubrir, en puntos
var
Natural, Tc: Single;
begin
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.SetCharacterSpacing(0); // medir sin espaciado residual
Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);
// Tc va detrás de cada glifo, pero solo los huecos entre glifos son visibles
Tc := (Target - Natural) / (Length(Heading) - 1);
Pdf.CurrentPage.SetCharacterSpacing(Tc); // los valores negativos comprimen
Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
Pdf.CurrentPage.SetCharacterSpacing(0); // reset antes del cuerpo de texto
Pdf.CurrentPage.SetWordSpacing(6);
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
Pdf.CurrentPage.SetWordSpacing(0);
end;
Las líneas de reset importan más que las líneas de asignación. Deje un espaciado de caracteres de 4 puntos en pie tras una cabecera y cada párrafo por debajo hereda el estiramiento, que se lee como una rareza difusa que sobrevive a una prueba de lectura rápida y luego aparece en la copia impresa. Fije el valor, dibuje la única corrida que lo necesita y póngalo a cero en la línea siguiente, así ningún código posterior tiene que saber lo que hizo una sección anterior. La misma página ofrece SetHorizontalScaling (Tz, 100 por defecto) para comprimir una corrida sin cambiar su altura, y SetTextRenderingMode con valores como trStroke para texto perfilado, ambos persistiendo exactamente igual
Si esta es su primera página de salida, el ejemplo Hello World de HotPDF cubre la configuración del documento y el guardado. Para texto que tiene que llenar una columna de borde a borde, la justificación completa de texto PDF tiene su propio recorrido, y el CJK vertical se cubre en vertical text layout en HotPDF. TextOut, SetFont, las llamadas de espaciado y los ajustes de tamaño de página de arriba vienen todos con el HotPDF Delphi Component para Delphi y C++Builder