El PDF exportado pone cada límite de columna medio carácter a la izquierda de donde Excel lo dibuja, y ahora cada celda con ajuste de línea corta en un sitio distinto. El ancho de columna de Excel no se mide en caracteres ni en puntos. Se mide en unidades de Max Digit Width (MDW) de la fuente Normal del libro, y HotXLS mide esa fuente con GDI antes de cada construcción de paginación. El modo de fallo es silencioso: nada lanza una excepción, los anchos almacenados hacen ida y vuelta byte a byte, y la geometría sigue desviada unos puntos porcentuales por columna hasta que la deriva acumulada empuja una tabla de una página a dos
¿En qué unidad se mide el ancho de columna de Excel?
Un ancho de columna en una hoja de cálculo es un recuento de caracteres de dígito de la fuente Normal del libro, no una medida absoluta. ECMA-376 §18.3.1.13 define el atributo width de <col> en términos del Maximum Digit Width de esa fuente a 96 ppp, y da la conversión de un ancho almacenado de vuelta a píxeles como una expresión de truncamiento sobre la MDW. Para Calibri 11, que es lo que Excel entrega como estilo Normal, la MDW mide 7 píxeles. Pasa el ancho por defecto de 8.43 unidades por la fórmula de la especificación con MDW 7 y obtienes exactamente 64 píxeles, que son 48 puntos a 96 ppp. Esos son los números que el propio Excel reporta, así que son una comprobación útil: si tu conversión reproduce 8.43 unidades a 64 píxeles, la aritmética está bien y solo la entrada de MDW puede seguir estando mal
const
// Maximum digit width (MDW) de la fuente de cuerpo por defecto, en píxeles a 96 ppp.
// Calibri 11 mide 7 px, lo que reproduce los anchos en píxeles exactos
// que Excel almacena (8.43 unidades -> 64 px -> 48 pt).
DefaultMDW = 7;
MinimumColumnWidth = 24.0;
function ColumnWidthToPointsMdW(Value: Double; MdW: Integer): Double;
var
Pixels: Integer;
begin
if Value <= 0 then
Value := 8.43;
if MdW <= 0 then
MdW := DefaultMDW;
Pixels := Trunc(((256 * Value + Trunc(128 / MdW)) / 256) * MdW) + 5;
Result := Pixels * 0.75; // píxeles a 96 ppp -> puntos
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS guarda esa aritmética en exactamente una función, en la unidad lxPagination, así que hay un único sitio donde la regla puede estar mal. El + 5 es el relleno que Excel añade para líneas de cuadrícula y márgenes de celda, el * 0.75 convierte píxeles de 96 ppp a puntos PostScript, y el suelo en MinimumColumnWidth existe para que una columna patológicamente estrecha aún deje una franja en la que el renderizador pueda dibujar un borde. El punto de entrada público ColumnWidthToPoints conserva su antigua firma de un argumento y reenvía una MDW medida a esta función, que es lo que permitió que el cambio de comportamiento aterrizara sin tocar ni un solo sitio de llamada
Por qué una fuente Normal que no es Calibri mueve cada límite
La deriva es multiplicativa, y por eso se lee como un bug de renderizado y no de unidades. La MDW es un factor sobre el ancho, no un desplazamiento. Sube la MDW de 7 a 8 y la columna por defecto de 8.43 unidades pasa de 64 a 72 píxeles, un salto de 8 píxeles o 6 puntos en una columna. Diez columnas de eso y el borde derecho de la tabla se ha movido casi una pulgada. Los libros que tropiezan con esto son completamente ordinarios: cualquier cosa generada por una herramienta de informes que estampa Arial o Segoe UI en el estilo Normal, cualquier cosa guardada desde una plantilla de exportación de ERP, cualquier cosa que un cliente re-estilizó una vez y olvidó
Dos sistemas de maquetación relacionados heredan el error en lugar de causarlo. Las regiones combinadas suman los anchos en puntos de sus columnas miembro, así que una combinación que cabía en una página en Excel puede desbordarse tras la deriva de MDW, lo que conviene recordar cuando construyes plantillas de informes con celdas combinadas. Shrink-to-fit compara el ancho de texto medido contra el mismo ancho de columna, así que una MDW equivocada también cambia qué celdas se encogen y cuánto. La misma familia de confusión de unidades aparece en anclas de dibujo, donde la geometría de imágenes y el escalado EMU tiene su propia cadena de conversión que equivocar
Cómo mide HotXLS la MDW en tiempo de ejecución
HotXLS resuelve la MDW desde el propio libro en lugar de asumir una constante, y dos procedures hacen el trabajo. PaginationApplyNormalFont lee la fuente del estilo Normal del libro y corre al inicio de la construcción de paginación, antes de calcular cualquier geometría de columna; se restablece a Calibri 11 primero, así que un libro sin tabla de fuentes no puede heredar estado rancio de una construcción anterior. La fuente del estilo Normal es fonts[0] en styles.xml, expuesta por el componente como Workbook.Fonts[0]
// Lee fonts[0] (la fuente del estilo Normal) del workbook de la hoja.
// Las hojas clásicas sin tabla de fuentes conservan el valor por defecto Calibri 11.
procedure PaginationApplyNormalFont(Worksheet: TObject);
var
Sh: TXLSXWorksheet;
Fnt: TXLSXFont;
begin
PaginationNormalFontName := 'Calibri';
PaginationNormalFontSize := 11;
if not (Worksheet is TXLSXWorksheet) then
Exit;
Sh := TXLSXWorksheet(Worksheet);
if (Sh.Workbook = nil) or (Sh.Workbook.Fonts.Count < 1) then
Exit;
Fnt := Sh.Workbook.Fonts[0];
if Fnt.Name <> '' then
PaginationNormalFontName := Fnt.Name;
if Fnt.Size > 0 then
PaginationNormalFontSize := Fnt.Size;
end;
El segundo procedure, PaginationMeasureMdW, le pide a GDI la extensión del único carácter '0' a través de GetTextExtentPoint32W sobre un canvas de bitmap fuera de pantalla compartido, recurre a tmAveCharWidth de GetTextMetricsW cuando la llamada de extensión falla, y recurre a DefaultMDW cuando no hay nada disponible. Su caché es de una sola ranura con clave (nombre, tamaño), lo que suena tosco hasta que miras el patrón de acceso: una construcción de paginación pide la misma fuente Normal en cada columna de cada página, así que una ranura tiene una tasa de aciertos casi perfecta y cuesta tres comparaciones por llamada
¿Qué pasa sin tabla de fuentes, sin GUI o con una fuente ausente?
HotXLS decae a la constante Calibri 11 en todos los casos en que la fuente Normal real no puede determinarse, y lo hace en silencio por diseño. Las hojas de cálculo BIFF clásicas son el caso común: los formatos heredados no llevan ninguna bolsa de fuentes XLSX a la que fonts[0] pueda referirse, así que la guarda de tipo sale temprano y la MDW por defecto de 7 se mantiene. Eso no es un arreglo, es el comportamiento anterior preservado deliberadamente, de modo que añadir medición a la ruta XLSX no pudiera provocar una regresión en la salida de formato clásico
La dependencia de GDI es la advertencia honesta. La medición corre contra un contexto de dispositivo de Windows, así que la ruta asume un anfitrión Windows con la fuente instalada. En un servicio o un agente de compilación sin cabeza, las métricas de texto de GDI generalmente aún se resuelven, pero una fuente que no está instalada en esa máquina la sustituye el font mapper y mides el sustituto. Nunca falla a gritos; devuelve un número plausible para la tipografía equivocada. Si las exportaciones del lado servidor deben coincidir con una referencia de escritorio, instala en el host de exportación las fuentes que nombran tus plantillas, o fija la fuente Normal antes de invocar la ruta de exportación PDF de la hoja
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fija la fuente Normal para que la MDW medida en este host sea la
// contra la que se diseñó la maquetación, no un sustituto del font mapper.
if Book.Fonts.Count > 0 then
begin
Book.Fonts[0].Name := 'Calibri';
Book.Fonts[0].Size := 11;
end;
Exporter.UseWorksheetPageSetup := True;
Exporter.SaveAsPDF(Book, 'quarterly-report.pdf');
finally
Exporter.Free;
Book.Free;
end;
end;
Cachés de medición, y la que se estrelló en Win64
Una vez que la medición de texto es un viaje de ida y vuelta a GDI en lugar de una multiplicación, hay que meterla en caché, y hacer caché dentro de una pasada de render es donde este trabajo sacó sangre. El bucle shrink-to-fit reduce el tamaño de fuente en incrementos de 0.5 pt y vuelve a medir tras cada paso, así que una sola celda puede llamar a PaginationMeasureTextWidth una docena de veces con la misma cadena, y el ajuste de línea la llama otra vez por línea candidata. Una memo con clave por nombre de fuente, tamaño y texto colapsa eso a una llamada GDI por cadena distinta, almacenada en un TStringList como pares nombre/valor
La otra caché añadida junto a ella no estaba tan ordenada. La pasada de render 5 resuelve la bolsa de fuentes por celda mediante FontIndex, y su memo usaba arrays dinámicos paralelos con un FontMemoCount mantenido a mano. La primera versión olvidó llamar a ResetFontMemo al inicio de cada página, así que el conteo siguió subiendo entre páginas mientras los arrays no, y el código escribió más allá del final de todos ellos. En Win32 eso garabateó en silencio en el heap adyacente y terminó; en Win64 lanzó una access violation en una escritura a 0x538 inmediatamente. La lección generalizable: una caché respaldada por arrays mantenida en una variable a nivel de unidad debe restablecerse en la entrada de cada pasada que la use, porque una lista de cadenas o un diccionario perdona un restablecimiento ausente creciendo, y los arrays paralelos no
Comprobar tu propia conversión
No necesitas el componente para verificar nada de esto. Toma un libro cuya fuente Normal no sea Calibri 11, lee un ancho de <col width="..."/> y pásalo por la fórmula de la especificación dos veces, una con MDW 7 y otra con la MDW que tu renderizador realmente mide para esa fuente; si las respuestas difieren y tu salida coincide con la primera, has encontrado la deriva. La geometría de columnas es una de esas partes de un motor de hoja de cálculo que o es invisible o es lo único que alguien nota, y acertarla significa tratar la fuente Normal como una entrada de la maquetación y no como un detalle de estilo. Si construyes aplicaciones Delphi o C++Builder que leen, escriben, renderizan e imprimen libros de Excel sin Office instalado, el componente Excel HotXLS para Delphi maneja la medición de MDW, el modelo de paginación y la tubería PDF detrás de un único conjunto de clases VCL