El PDF exportado coloca cada límite de columna media letra a la izquierda de donde Excel lo dibuja, y cada celda con ajuste de línea ahora corta en otro punto. 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 excepciones, los anchos guardados hacen round-trip 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 conteo 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 dpi, y da la conversión de un ancho guardado de vuelta a píxeles como una expresión con truncamiento sobre el MDW. Para Calibri 11, que es lo que Excel entrega como estilo Normal, el MDW mide 7 píxeles. Pase el ancho por defecto de 8.43 unidades por la fórmula de la especificación con MDW 7 y obtiene exactamente 64 píxeles, que son 48 puntos a 96 dpi. Esos son los números que el propio Excel reporta, así que sirven como comprobación útil: si su conversión reproduce 8.43 unidades en 64 píxeles, la aritmética está bien y solo la entrada MDW puede seguir estando mal
const
// Max digit width (MDW) de la fuente de cuerpo por defecto en píxeles a 96 dpi.
// Calibri 11 mide 7 px, lo que reproduce los anchos de píxel exactos
// que Excel guarda (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 dpi -> puntos
if Result < MinimumColumnWidth then
Result := MinimumColumnWidth;
end;
HotXLS mantiene esa aritmética en exactamente una función, en la unidad lxPagination, así que hay un único lugar donde la regla puede estar mal. El + 5 es el relleno que Excel añade por líneas de cuadrícula y márgenes de celda, el * 0.75 convierte píxeles a 96 dpi en puntos PostScript, y el piso en MinimumColumnWidth existe para que una columna patológicamente estrecha aún deje una franja donde el renderizador pueda dibujar un borde. El punto de entrada público ColumnWidthToPoints conserva su vieja firma de un argumento y reenvía un MDW medido a esta función, que es lo que permitió aterrizar el cambio de comportamiento 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, por eso se lee como un bug de renderizado y no como un bug de unidades. El MDW es un factor sobre el ancho, no un offset. Empuje el MDW de 7 a 8 y la columna por defecto de 8.43 unidades pasa de 64 píxeles a 72, un salto de 8 píxeles u 6 puntos en una sola columna. Diez columnas de eso y el borde derecho de la tabla se movió casi una pulgada. Los libros que tropiezan con esto son completamente ordinarios: cualquier cosa generada por una herramienta de reportes 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 del MDW, algo que vale la pena recordar cuando construye plantillas de reportes con celdas combinadas. El ajuste por reducción compara el ancho de texto medido contra el mismo ancho de columna, así que un MDW equivocado también cambia qué celdas se reducen y cuánto. La misma familia de confusión de unidades aparece en los anclajes de dibujo, donde la geometría de imágenes y el escalado EMU tienen su propia cadena de conversión que equivocar
Cómo mide HotXLS el MDW en tiempo de ejecución
HotXLS resuelve el MDW desde el propio libro en lugar de asumir una constante, y dos procedimientos 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; primero se restablece a Calibri 11, así que un libro sin tabla de fuentes no puede heredar estado añejo 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 libro de la hoja de cálculo.
// Las hojas clásicas sin tabla de fuentes conservan el default 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 procedimiento, PaginationMeasureMdW, le pide a GDI la extensión del carácter único '0' mediante GetTextExtentPoint32W sobre un canvas de mapa de bits compartido fuera de pantalla, recae en tmAveCharWidth de GetTextMetricsW cuando la llamada de extensión falla, y recae en DefaultMDW cuando no hay nada disponible. Su caché es una sola ranura con clave (name, size), lo que suena tosco hasta que mira 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 faltante?
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 BIFF clásicas son el caso común: los formatos antiguos no llevan un pool de fuentes XLSX al que fonts[0] pueda referirse, así que el guard de tipo sale temprano y el 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 regresar la salida del 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 equipo Windows con la fuente instalada. En un servicio o un agente de build sin interfaz, las métricas de texto de GDI generalmente aún se resuelven, pero una fuente que no está instalada en esa máquina es sustituida por el font mapper y usted mide el sustituto. Nunca falla estridentemente; devuelve un número plausible para el tipo de letra equivocado. Si las exportaciones del lado servidor deben coincidir con una referencia de escritorio, instale en el equipo de exportación las fuentes que nombran sus plantillas, o fije la fuente Normal antes de invocar la ruta de exportación PDF de hojas de cálculo
var
Book: TXLSXWorkbook;
Exporter: TXLSPDFExport;
begin
Book := TXLSXWorkbook.Create;
Exporter := TXLSPDFExport.Create;
try
Book.Open('quarterly-report.xlsx');
// Fije la fuente Normal para que el MDW medido en este equipo sea el
// contra el 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 crasheó 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, tiene que cachearse, y cachear dentro de una pasada de render es donde este trabajo sacó sangre. El ciclo de ajuste por reducción baja el tamaño de fuente en incrementos de 0.5 pt y vuelve a medir tras cada paso, así que una celda puede llamar PaginationMeasureTextWidth una docena de veces con la misma cadena, y el ajuste de línea la llama de nuevo por cada línea candidata. Un memo con clave de nombre de fuente, tamaño y texto colapsa eso en una llamada GDI por cadena distinta, almacenado en un TStringList como pares nombre/valor
La otra caché añadida junto a ella no fue tan ordenada. La pasada de render 5 resuelve el pool de fuentes por celda mediante FontIndex, y su memo usaba arreglos dinámicos paralelos con un FontMemoCount mantenido a mano. La primera versión olvidó llamar ResetFontMemo al inicio de cada página, así que el conteo siguió subiendo entre páginas mientras los arreglos no, y el código escribió más allá del final de todos ellos. En Win32 eso garabateó silenciosamente en el heap contiguo y terminó; en Win64 lanzó una violación de acceso al escribir en 0x538 de inmediato. La lección generalizable: una caché respaldada por arreglos y guardada 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 faltante creciendo, y los arreglos paralelos no
Comprobar su propia conversión
Usted no necesita el componente para verificar nada de esto. Tome un libro cuya fuente Normal no sea Calibri 11, lea un ancho de <col width="..."/>, y páselo dos veces por la fórmula de la especificación, una con MDW 7 y otra con el MDW que su renderizador realmente mide para esa fuente; si las respuestas difieren y su salida coincide con la primera, encontró la deriva. La geometría de columnas es una de esas partes de un motor de hojas de cálculo que o es invisible o es lo único que todos notan, y acertar significa tratar la fuente Normal como una entrada de la maquetación y no como un detalle de estilo. Si construye 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 del MDW, el modelo de paginación y la pipeline PDF detrás de un solo conjunto de clases VCL