La llamada que coloca texto en una página de PDF es directa. Le da a AddText una cadena, una fuente, un tamaño y una posición, y aparecen los glifos. Lo que no hace es decirle cuánto de ancha será esa cadena una vez dibujada, y no divide una cadena larga en varias líneas. Una sola llamada pinta una tirada de texto en una posición. Si la tirada es más ancha que la columna en la que pretendía que encajara, simplemente se sale por el borde, y nada en la llamada de dibujo se lo advierte. En el momento en que quiere 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, medido antes de comprometerla a la página
Este es el problema clásico de maquetación. Para ajustar un párrafo dentro de una columna hay que saber, palabra por palabra, cuánto espacio horizontal ocupará cada línea candidata, y hay que saberlo antes de dibujar nada. El ajuste de línea (word wrap) es un bucle de medición envuelto alrededor de una llamada de dibujo, y un binding que solo dibuja le da la segunda mitad. El soporte de medición de texto del componente PDFium cierra esa brecha con dos funciones, MeasureText y MeasureTextWidth, que informan de la extensión renderizada de una cadena sin dejar ninguna marca en página alguna
Por qué la medición es un class helper y no un método nuevo en TPdf
El soporte de medición llega como un class helper de Delphi para TPdf, viviendo en su propia unidad, en lugar de como métodos nuevos atornillados a la clase TPdf. Un class helper es una característica del lenguaje que permite adjuntar métodos a un tipo existente desde fuera de su declaración. Una vez que la unidad está en el ámbito, los métodos nuevos se llaman exactamente como si pertenecieran a la clase, así que un método del helper se lee como Pdf.MeasureTextWidth(...), sin ningún objeto aparte que construir o pasar de un lado a otro
La razón para estructurarlo así es la separación. El tipo TPdf básico permanece tal cual, sin ningún campo añadido y sin tocar ninguna firma existente, así que un proyecto que nunca necesita maquetación nunca carga con el código de medición. Un proyecto que sí lo necesita añade una unidad a una cláusula uses y los métodos se activan. La capacidad se vuelve opcional a la granularidad de una sola unidad, que es la forma más limpia de extender un tipo que no le pertenece o que no quiere perturbar
uses
PDFium, FPdfView, FPdfEdit,
FPdfMeasure; // la unidad del helper; trae MeasureText al ámbito de TPdf
// Con la unidad en el ámbito, los métodos se leen como miembros de TPdf:
var
W, H: Double;
begin
Pdf.MeasureText('Subtotal', 'Helvetica', 11, W, H);
// W y H son ahora el ancho y el alto renderizados en unidades de usuario de PDF
end;
Medir sin tocar la página
La medición tiene que estar libre de efectos secundarios. Debe informar de un ancho sin dejar nada atrás, porque se la llama muchas veces mientras se decide una maquetación y la página debe verse exactamente como se habría visto si nunca se hubiera medido nada. La técnica que hace esto posible es construir un objeto de texto, preguntarle su tamaño y desecharlo antes de que llegue a adjuntarse a una página
La secuencia son cuatro llamadas a PDFium. FPDFPageObj_NewTextObj crea un objeto de texto contra el documento, dados el nombre de fuente y el tamaño. FPDFText_SetText fija la cadena que lleva ese objeto. FPDFPageObj_GetBounds lee de vuelta el cuadro delimitador del objeto. FPDFPageObj_Destroy libera el objeto. Fundamentalmente, 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, así que el documento queda sin cambios cuando la función retorna. Es una sonda desechable cuya única salida son los cuatro números de su cuadro delimitador
Esta es la forma robusta de hacerlo porque PDFium no expone un ancho de avance por glifo cómodo que usted mismo pudiera sumar. Las métricas de glifo dependen del programa de fuente, de la codificación y de cómo PDFium carga el tipo, y no hay ninguna llamada pública que le entregue el avance de cada carácter de una cadena. El cuadro delimitador de un objeto de texto real, en cambio, lo calcula la misma maquinaria que dispondría los glifos para dibujarlos, así que refleja la extensión realmente renderizada en lugar de una aproximación. Construir un objeto desechable y leer sus límites es la medición más fiable que la biblioteca puede ofrecer
// La forma de MeasureText, expresada sobre las llamadas verificadas de PDFium.
// Se construye un objeto de texto, se mide y se destruye; no interviene ninguna página.
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); // sonda descartada, página intacta
end;
end;
Coordenadas y unidades del resultado
El cuadro delimitador vuelve como cuatro bordes, izquierdo, inferior, derecho y superior, y las dos dimensiones se obtienen por resta. El ancho es derecho menos izquierdo y el alto es superior menos inferior. Ambos se expresan en unidades de usuario de PDF, donde una unidad es una setenta y dosava parte de una pulgada, el mismo espacio de coordenadas en el que se posiciona el texto en la página. No hay ninguna unidad de dispositivo oculta ni ningún píxel involucrado en esta etapa. Un ancho de 36 significa media pulgada de página, sea cual sea la resolución de renderizado final
El eje vertical se comporta como PDF lo define, con Y creciendo hacia arriba, razón por la cual el alto es superior menos inferior y no al revés. Ese detalle importa cuando se avanza un cursor hacia abajo por una columna. Se mide el alto de una línea y después se resta de la línea base actual para encontrar la siguiente, porque bajar por la página significa moverse hacia una Y menor. Si su destino es una pantalla en lugar de papel, convierta las unidades de usuario a píxeles de dispositivo con la resolución de la pantalla: un valor en unidades de usuario multiplicado por el DPI y dividido entre 72 da píxeles, así que un ancho de columna fijado en puntos se puede contrastar con una tirada medida antes de decidir dónde va el salto
Qué ocurre con una entrada degenerada
Las funciones están escritas para fallar en silencio. 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 una excepción lanzada. El ancho y el alto se inicializan a cero al principio y solo se sobrescriben una vez que se ha leído de vuelta un cuadro delimitador con éxito. Una cadena vacía, un documento ausente, una fuente que la biblioteca no puede resolver en un objeto, cada uno de estos casos devuelve cero en lugar de lanzar una excepción
Esa elección mantiene simple un bucle de medición, porque un bucle que recorre miles de palabras no es el lugar para manejar excepciones en cada iteración. El coste es que quien llama carga con la comprobación. Un ancho de cero es un centinela, no un hecho sobre el texto, así 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 queda claro; ignórelo y una entrada degenerada se convierte en silencio en una maquetación con una columna de glifos superpuestos
Un ajuste de línea voraz construido sobre la medición
Con una función de ancho en la mano, el ajuste de línea es un breve bucle voraz. Se divide el párrafo en palabras, se mantiene una línea actual, y para cada palabra se mide cómo quedaría la línea si se le añadiera esa palabra. Mientras la línea de prueba siga cabiendo en el ancho de columna, se sigue añadiendo; cuando desbordaría, se vuelca la línea actual con AddText y se empieza una nueva con la palabra que no cupo. La acumulación se hace enteramente con MeasureTextWidth, y lo único que llega a la página es una línea que ya se ha confirmado que cabe
procedure WrapParagraph(Pdf: TPdf; const Para, Font: WString;
FontSize: Single; X, TopY, ColumnWidth, LineHeight: Double);
var
Words: TArray<string>;
Line, Trial: WideString;
I: Integer;
Y: Double;
begin
Words := string(Para).Split([' ']);
Line := '';
Y := TopY;
for I := 0 to High(Words) do
begin
if Line = '' then
Trial := Words[I]
else
Trial := Line + ' ' + Words[I];
// Mide la línea candidata antes de dibujar nada.
if (Line <> '') and (Pdf.MeasureTextWidth(Trial, Font, FontSize) > ColumnWidth) then
begin
Pdf.AddText(Line, Font, FontSize, X, Y); // vuelca la línea que cupo
Y := Y - LineHeight; // Y decrece al bajar
Line := Words[I]; // la palabra que desborda empieza la siguiente línea
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // vuelca la línea final
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 tirada medida captura eso directamente. La regla voraz, encajar tantas palabras como permita la columna y cortar en la última que quepa, es la misma regla que llena el hueco 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 sí lo es, y eso es exactamente lo que ofrece el helper
Dónde encaja esto
La medición es la capa entre generar contenido y renderizarlo, así que combina de forma natural con el resto de un flujo de trabajo de documentos desde cero. Si está ensamblando páginas y colocando texto desde el principio, la base está en crear documentos PDF desde cero con el componente PDFium en Delphi, donde AddText y la configuración de página se cubren por completo. Cuando la fuente que está midiendo importa tanto como la cadena, porque las métricas dependen del tipo, analizar propiedades de fuente de PDF con el componente PDFium en Delphi muestra cómo informa la biblioteca de la información de fuente que impulsa esos cuadros delimitadores. Ambos se construyen sobre el mismo binding, el PDFium Component para Delphi y Lazarus, donde el helper de medición se incluye junto a las API de documento, página y texto descritas en todo este blog