La llamada que pone texto en una página PDF es sencilla. 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 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 serie de texto en una posición. Si la serie es más ancha que la columna en la que pretendía que encajara, simplemente se pasa 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 faltante es el ancho de una cadena en la fuente y el tamaño elegidos, medido antes de plasmarlo en la página
Este es el problema clásico 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 enlace que solo dibuja le da la segunda mitad. El soporte de medición de texto en el PDFium Component cierra esa brecha con dos funciones, MeasureText y MeasureTextWidth, que informan de la extensión renderizada de una cadena sin poner una marca en ninguna página
Por qué la medición es un ayudante de clase (class helper), no un nuevo método en TPdf
El soporte de medición llega como un ayudante de clase de Delphi para TPdf, viviendo en su propia unidad, en lugar de como nuevos métodos atornillados en la clase TPdf. Un ayudante de clase 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á dentro del ámbito, 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 ningún objeto separado para construir o pasar de un lado a otro
La razón de colocarlo en capas de esta manera es la separación. El tipo básico TPdf se mantiene como está, sin que se añada ningún campo y sin tocar ninguna firma existente, por lo que un proyecto que nunca necesita diseño nunca carga con 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 (opt-in) en la granularidad de una sola unidad, que es la forma más limpia de extender un tipo que no es suyo o que no desea perturbar
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 informar de un ancho sin dejar nada atrás, porque usted la llama muchas veces mientras decide un diseño y la página debe verse exactamente como lo habría hecho si nunca hubiera medido en absoluto. La técnica que hace esto posible es construir un objeto de texto, preguntarle por su tamaño y desecharlo antes de que alguna vez se adjunte a una página
La secuencia son cuatro llamadas a PDFium. FPDFPageObj_NewTextObj crea un objeto de texto contra el documento, dados el nombre y tamaño de la fuente. FPDFText_SetText establece la cadena que lleva ese objeto. FPDFPageObj_GetBounds lee la caja delimitadora del objeto. FPDFPageObj_Destroy libera el objeto. Crucialmente, nada en esa secuencia llama a la API de inserción de páginas. El objeto se crea, se consulta y se destruye de forma aislada, por lo que el documento no cambia cuando la función regresa. Es una sonda desechable cuya única salida son los cuatro números de su caja delimitadora
Esta es la forma robusta de hacerlo porque PDFium no expone un ancho de avance por glifo conveniente que usted pueda sumar por su cuenta. Las métricas de los glifos dependen del programa de fuente, de la codificación y de cómo PDFium carga la cara, y no hay ninguna llamada pública que le entregue el avance de cada carácter en una cadena. La caja delimitadora de un objeto de texto real, por otro lado, es calculada por la misma maquinaria que dispondría los glifos para dibujar, 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 medida más fiable 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
La caja delimitadora regresa como cuatro bordes, izquierdo, inferior, derecho y superior, y las dos dimensiones resultan de la resta. El ancho es derecho menos izquierdo y la altura es superior menos inferior. Ambas 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 usted posiciona texto en la página. No hay ninguna unidad de dispositivo oculta ni ningún píxel implicado en esta etapa. Un ancho de 36 significa media pulgada de página, cualquiera que sea la resolución de renderizado final
El eje vertical discurre de la forma que lo define el PDF, con Y aumentando hacia arriba, que es la razón por la que la altura es superior menos inferior en lugar de a la inversa. Ese detalle importa cuando usted hace avanzar un cursor hacia abajo en una columna. Usted mide la altura de una línea, y luego se la resta a la línea base actual para encontrar la siguiente, porque moverse hacia abajo en la página significa moverse hacia Y menores. Si su destino es una pantalla en lugar de papel, convierte las unidades de usuario a píxeles del dispositivo con la resolución de pantalla: un valor en unidades de usuario multiplicado por los DPI y dividido entre 72 da píxeles, por lo que un ancho de columna que establezca en puntos puede compararse con una serie medida antes de que usted decida dónde va la ruptura
Qué sucede con una entrada degenerada
Las funciones están escritas para fallar silenciosamente. Si no hay un documento abierto, o si no se puede crear el objeto de texto, el resultado es una extensión cero en lugar de una excepción levantada. El ancho y la altura se inicializan a cero en la parte superior y solo se sobrescriben una vez que se ha vuelto a leer exitosamente una caja delimitadora. Una cadena vacía, un documento faltante, una fuente que la biblioteca no puede resolver en un objeto, cada uno de ellos devuelve cero en lugar de lanzar 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 llamador carga con 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 queda 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 codicioso construido sobre la medición
Con una función de ancho en la mano, el ajuste de línea es un breve bucle codicioso (greedy). Usted divide el párrafo en palabras, mantiene una línea actual, y por cada palabra mide cuál sería la línea si anexara esa palabra. Mientras la línea de prueba siga encajando en el ancho de la columna, usted sigue añadiendo; cuando se desborde, usted descarga la línea actual con AddText y comienza una nueva con la palabra que no encajó. La acumulación se hace por completo con MeasureTextWidth, y la única cosa que alguna vez alcanza la página es una línea de la 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 serie medida captura eso directamente. La regla codiciosa, encajar tantas palabras como permita la columna y romper 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 sí lo es, y eso es exactamente lo que el ayudante proporciona
Dónde encaja esto
La medición es la capa que se encuentra entre la generación de contenido y su renderización, por lo que se empareja naturalmente con el resto del flujo de trabajo de un documento desde cero. Si usted está ensamblando páginas y colocando texto en primer lugar, el trabajo de base está en creación de documentos PDF desde cero con el componente PDFium en Delphi, donde AddText y la configuración de página se tratan por completo. Cuando la fuente que está midiendo importa tanto como la cadena, porque las métricas dependen de la cara de la fuente, el análisis de las propiedades de fuentes PDF con el componente PDFium en Delphi muestra cómo la biblioteca informa de la información de la fuente que dirige esas cajas delimitadoras. Ambos se construyen sobre el mismo enlace, el PDFium Component para Delphi y Lazarus, donde el ayudante de medición se distribuye junto a las API de documento, página y texto descritas a lo largo de este blog