La llamada que coloca texto en una página PDF es sencilla. Usted le da a AddText una cadena, una fuente, un tamaño y una posición, y los glifos aparecen. Lo que no hace es decirle qué tan ancha será esa cadena una vez que se dibuje, y no divide una cadena larga en varias líneas. Una sola llamada pinta una secuencia de texto en una posición. Si la secuencia es más ancha que la columna en la que pretendía que encajara, simplemente se sale del borde, y nada en la llamada de dibujo se lo advierte. En el momento en que desea un párrafo en lugar de una sola etiqueta, la pieza que falta es el ancho de una cadena en la fuente y el tamaño elegidos, medida antes de enviarla a la página
Este es el clásico problema de diseño. Para ajustar un párrafo en una columna, tiene que saber, palabra por palabra, cuánto espacio horizontal ocupará cada línea candidata, y tiene que saberlo antes de dibujar nada. El ajuste de línea es un bucle de medición que envuelve una llamada de dibujo, y un binding que solo dibuja le da la segunda mitad. El soporte de medición de texto en el componente PDFium cierra esa brecha con dos funciones, MeasureText y MeasureTextWidth, que reportan la extensión renderizada de una cadena sin poner una marca en ninguna página
Por qué la medición es un class helper y no un nuevo método en TPdf
El soporte de medición llega como un class helper de Delphi para TPdf, que vive en su propia unidad, en lugar de como nuevos métodos incorporados en la clase TPdf. Un class helper es una característica del lenguaje que le permite adjuntar métodos a un tipo existente desde fuera de su declaración. Una vez que la unidad está en el alcance, los nuevos métodos se llaman exactamente como si pertenecieran a la clase, por lo que un método auxiliar se lee como Pdf.MeasureTextWidth(...) sin un objeto separado que construir o pasar de un lado a otro
La razón para estructurarlo en capas de esta manera es la separación. El tipo central TPdf se mantiene como está, sin campos agregados y sin tocar ninguna firma existente, por lo que un proyecto que nunca necesita diseño nunca lleva el código de medición. Un proyecto que sí lo necesita agrega una unidad a una cláusula uses y los métodos se iluminan. La capacidad se vuelve opcional en la granularidad de una sola unidad, que es la forma más limpia de extender un tipo que no posee o que no desea alterar
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // the helper unit; brings MeasureText into scope on TPdf
// With the unit in scope the methods read as members of TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W and H are now the rendered width and height in PDF user units
end;
Medición sin tocar la página
La medición tiene que estar libre de efectos secundarios. Debe reportar un ancho sin dejar nada atrás, porque usted lo llama muchas veces mientras decide un diseño y la página debe verse exactamente como se vería si nunca hubiera medido en absoluto. La técnica que hace esto posible es construir un objeto de texto, preguntarle su tamaño y descartarlo antes de que se adjunte a una página
La secuencia es de cuatro llamadas a PDFium. FPDFPageObj_NewTextObj crea un objeto de texto contra el documento, dado el nombre y tamaño de la fuente. FPDFText_SetText establece la cadena que lleva ese objeto. FPDFPageObj_GetBounds lee el bounding box del objeto. FPDFPageObj_Destroy libera el objeto. Crucialmente, nada en esa secuencia llama a la API de inserción de página. El objeto se crea, se consulta y se destruye de forma aislada, por lo que el documento no cambia cuando la función retorna. Es una sonda desechable cuya única salida son los cuatro números de su bounding box
Esta es la forma robusta de hacerlo porque PDFium no expone un ancho de avance por glifo conveniente que usted podría sumar por su cuenta. Las métricas de los glifos dependen del programa de la fuente, de la codificación y de cómo PDFium carga la familia tipográfica, y no hay ninguna llamada pública que le entregue el avance de cada carácter en una cadena. El bounding box de un objeto de texto real, por otro lado, es calculado por la misma maquinaria que colocaría los glifos para el dibujo, por lo que refleja la extensión renderizada real en lugar de una aproximación. Construir un objeto desechable y leer sus límites es la medición más confiable que la biblioteca puede dar
// The shape of MeasureText, expressed against the verified PDFium calls.
// A text object is built, measured, and destroyed; no page is involved.
procedure TPdfMeasureHelper.MeasureText(const Text, Font: WString;
FontSize: Single; out Width, Height: Double);
var
TextObject: FPDF_PAGEOBJECT;
L, B, R, T: Single;
begin
Width := 0;
Height := 0;
if Self.Document = nil then
Exit;
TextObject := FPDFPageObj_NewTextObj(Self.Document,
FPDF_BYTESTRING(AnsiString(Font)), FontSize);
if TextObject = nil then
Exit;
try
if FPDFText_SetText(TextObject, FPDF_WIDESTRING(WideString(Text))) = 0 then
Exit;
if FPDFPageObj_GetBounds(TextObject, L, B, R, T) <> 0 then
begin
Width := R - L;
Height := T - B;
end;
finally
FPDFPageObj_Destroy(TextObject); // probe discarded, page untouched
end;
end;
Coordenadas y unidades del resultado
El bounding box regresa como cuatro bordes, izquierdo, inferior, derecho y superior, y las dos dimensiones se obtienen por resta. El ancho es el derecho menos el izquierdo y el alto es el superior menos el inferior. Ambos se expresan en unidades de usuario PDF, donde una unidad es un setenta y dosavo de pulgada, el mismo espacio de coordenadas en el que usted posiciona el texto en la página. No hay una unidad de dispositivo oculta ni ningún píxel involucrado en esta etapa. Un ancho de 36 significa media pulgada de página, independientemente de la resolución final de renderizado
El eje vertical se ejecuta de la forma en que PDF lo define, con la Y aumentando hacia arriba, razón por la cual el alto es el superior menos el inferior en lugar de a la inversa. Ese detalle importa cuando usted avanza un cursor hacia abajo en una columna. Usted mide el alto de una línea, luego lo resta de la línea base actual para encontrar la siguiente, porque moverse hacia abajo en la página significa moverse hacia una Y más pequeña. Si su destino es una pantalla en lugar de papel, usted convierte las unidades de usuario en píxeles del dispositivo con la resolución de pantalla: un valor en unidades de usuario multiplicado por los DPI y dividido por 72 da píxeles, por lo que un ancho de columna que establezca en puntos puede compararse con una secuencia medida antes de decidir dónde va el salto de línea
Qué sucede con una entrada degenerada
Las funciones están escritas para fallar silenciosamente. Si no hay ningún documento abierto, o si el objeto de texto no se puede crear, el resultado es una extensión de cero en lugar de lanzar una excepción. El ancho y el alto se inicializan en cero en la parte superior y solo se sobrescriben una vez que el bounding box se ha leído correctamente. Una cadena vacía, un documento faltante, una fuente que la biblioteca no puede resolver en un objeto, cada uno de estos retorna cero en lugar de arrojar un error
Esa elección mantiene simple un bucle de medición, porque un bucle que se ejecuta sobre miles de palabras no es el lugar para el manejo de excepciones en cada iteración. El costo es que el invocador lleva la comprobación. Un ancho cero es un centinela, no un hecho sobre el texto, por lo que el código que divide por un ancho medido o asume un valor positivo tiene que protegerse contra el cero antes de confiar en él. Trate el cero como "no se pudo medir" y el contrato es claro; ignórelo y una entrada degenerada se convierte silenciosamente en un diseño con una columna de glifos superpuestos
Un ajuste de línea voraz basado en la medición
Con una función de ancho a la mano, el ajuste de línea es un breve bucle voraz. Usted divide el párrafo en palabras, mantiene una línea actual, y para cada palabra mide cuál sería la línea si le agregara esa palabra. Mientras la línea de prueba aún encaja en el ancho de la columna, usted sigue agregando; cuando se desborde, vacía la línea actual con AddText y comienza una nueva con la palabra que no encajó. La acumulación se realiza completamente con MeasureTextWidth, y lo único que llega a la página es una línea que usted ya ha confirmado que encaja
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<WideString>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := WideString(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Measure the candidate line before drawing anything.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the line that fit
Y := Y - LineHeight; // Y decreases going down
Line := Words[I]; // overflowing word starts next line
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(X, Y, Font, FontSize, Line); // flush the final line
end;
El bucle mide la línea de prueba en lugar de medir cada palabra y sumar, porque el ancho de una línea no es la suma de los anchos de sus palabras. Los espacios entre palabras contribuyen, y una secuencia medida captura eso directamente. La regla voraz, encajar tantas palabras como permita la columna y saltar en la última que encaje, es la misma regla que llena el vacío entre un AddText en bruto y un párrafo real. La llamada de dibujo nunca fue la parte difícil. La medición que tiene que precederla lo es, y eso es exactamente lo que proporciona el helper
Dónde encaja esto
La medición es la capa entre la generación de contenido y su renderizado, por lo que se combina naturalmente con el resto de un flujo de trabajo de documentos desde cero. Si usted está ensamblando páginas y colocando texto en primer lugar, el trabajo preliminar está en la creación de documentos PDF desde cero con el componente PDFium en Delphi, donde se cubren en su totalidad AddText y la configuración de la página. Cuando la fuente que está midiendo importa tanto como la cadena, porque las métricas dependen de la familia tipográfica, el análisis de las propiedades de fuentes PDF con el componente PDFium en Delphi muestra cómo la biblioteca reporta la información de la fuente que impulsa esos bounding boxes. Ambos se construyen sobre el mismo binding, el Componente PDFium para Delphi y Lazarus, donde el helper de medición se envía junto con las API de documento, página y texto descritas en este blog