La llamada que pone texto en una página PDF es directa. 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é ancho tendrá esa cadena una vez dibujada, y tampoco parte una cadena larga en varias líneas. Una sola llamada pinta una corrida de texto en una posición. Si la corrida es más ancha que la columna en la que pensaba que cabría, simplemente se pasa del borde, y nada en la llamada de dibujo se lo advierte. En cuanto quiere un párrafo en lugar de una etiqueta suelta, la pieza que falta es el ancho de una cadena en la fuente y el tamaño elegidos, medido antes de comprometerlo con 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 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 la extensión renderizada de una cadena sin dejar una sola marca en ninguna página
Por qué la medición es un class helper y no un método nuevo de TPdf
El soporte de medición llega como un class helper de Delphi para TPdf, alojado 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 estratificarlo así es la separación. El tipo TPdf central queda como está, sin ningún campo agregado y sin tocar ninguna firma existente, así que un proyecto que nunca necesita maquetación nunca carga 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 encienden. La capacidad se vuelve opcional con 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 PDF
end;
Medir sin tocar la página
La medición tiene que estar libre de efectos secundarios. Debe informar un ancho sin dejar nada atrás, porque usted la llama muchas veces mientras decide una maquetación y la página debe verse exactamente como se habría visto si nunca hubiera medido. La técnica que hace esto posible es construir un objeto de texto, preguntarle su tamaño y descartarlo antes de que llegue a adjuntarse a una página
La secuencia son cuatro llamadas de 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 ese objeto lleva. FPDFPageObj_GetBounds lee de vuelta el cuadro delimitador del objeto. FPDFPageObj_Destroy libera el objeto. Es crucial que nada en esa secuencia llame a la API de inserción en 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 manera robusta de hacerlo porque PDFium no expone un ancho de avance por glifo cómodo que usted pudiera sumar por su cuenta. Las métricas de glifos dependen del programa de fuente, de la codificación y de cómo PDFium carga la tipografía, 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 renderizada real y no una aproximación. Construir un objeto desechable y leer sus límites es la medición más confiable que la biblioteca puede dar
// La forma de MeasureText, expresada contra las llamadas PDFium verificadas.
// Se construye, se mide y se destruye un objeto de texto; ninguna página interviene.
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 salen por sustracción. El ancho es derecho menos izquierdo y el alto es superior menos inferior. Ambos se expresan en unidades de usuario PDF, donde una unidad es una setenta y dosava parte de una pulgada, el mismo espacio de coordenadas en el que usted 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 corre como lo define PDF, con Y creciendo hacia arriba, y por eso el alto es superior menos inferior y no al revés. Ese detalle importa cuando avanza un cursor hacia abajo por una columna. Mide el alto de una línea y luego lo resta de la línea base actual para encontrar la siguiente, porque bajar por la página significa moverse hacia Y más pequeñas. Si su destino es una pantalla en lugar del papel, convierte unidades de usuario a píxeles de dispositivo con la resolución de la pantalla: un valor en unidades de usuario multiplicado por la DPI y dividido entre 72 da píxeles, así que un ancho de columna fijado en puntos se puede contrastar contra una corrida medida antes de decidir dónde va el corte
Qué pasa con entradas degeneradas
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 cero en lugar de una excepción lanzada. El ancho y el alto se inicializan en cero al principio y solo se sobrescriben una vez que se ha leído con éxito un cuadro delimitador. 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
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 costo es que quien llama carga con la comprobación. Un ancho cero es un centinela, no un hecho sobre el texto, así que el código que divide por un ancho medido o que 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 calladamente 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 a mano, el ajuste de línea es un bucle voraz corto. Usted parte el párrafo en palabras, mantiene una línea actual y, por cada palabra, mide cómo quedaría la línea si le agregara esa palabra. Mientras la línea de prueba siga cabiendo en el ancho de columna, sigue agregando; cuando se desbordaría, vacía la línea actual con AddText y empieza una nueva con la palabra que no entró. La acumulación se hace enteramente con MeasureTextWidth, y lo único que llega a la página es una línea que ya confirmó 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];
// Mida 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); // vacía la línea que cabía
Y := Y - LineHeight; // Y disminuye al bajar
Line := Words[I]; // la palabra desbordada abre la siguiente
end
else
Line := Trial;
end;
if Line <> '' then
Pdf.AddText(Line, Font, FontSize, X, Y); // vacía 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 corrida medida captura eso directamente. La regla voraz, meter tantas palabras como permita la columna y cortar en la última que cabe, es la misma regla que llena el hueco entre un AddText crudo y un párrafo de verdad. 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 el helper provee
Dónde encaja esto
La medición es la capa entre generar contenido y renderizarlo, así que se combina de forma natural con el resto de un flujo de trabajo de documentos hechos desde cero. Si está ensamblando páginas y colocando texto en primer lugar, la base está en crear documentos PDF desde cero con el componente PDFium en Delphi, donde AddText y la configuración de páginas se cubren por completo. Cuando la fuente que mide importa tanto como la cadena, porque las métricas dependen de la tipografía, analizar propiedades de fuentes PDF con el componente PDFium en Delphi muestra cómo la biblioteca informa los datos de fuente que gobiernan esos cuadros delimitadores. Ambos se apoyan en el mismo binding, el PDFium Component para Delphi y Lazarus, donde el helper de medición viene junto con las APIs de documento, página y texto descritas a lo largo de este blog