Columnas en WXTABLE y XLSX

Figura
Figura. Tabla con 3 columnas de 60px en WXTABLE

Para ilustrar como se configuran las columnas en XLSX haremos una tabla en WXTABLE con 3 columnas, como se observa en Figura. Tienen un ancho de 60px cada una. Puede ver como exportamos esa tabla, incluyendo selectores de filas y columnas y rejilla, en formato HTML con wxtable-export.html. También exportamos esa tabla en formato WXTABLE, generándose un archivo TXT que puede consultar con detalle en este enlace columns3.txt.

No hay atributos ("attributes"). Y la tabla "values" contiene 3 columnas y 11 filas, donde la primera fila es la de los títulos de columnas. En WXTABLE podemos modificar los títulos de las columnas con texto, aunque por defecto aparacen numeradas. No hay valores en las celdas indicándose con una cadena vacía "".

{
    "sheets":{
        "Tab1":{
            "index":0,
            "attributes":{},
            "styles":{"1,1:10,3;0,1:0,3":"width:60px;"},
            "values":[
                ["1","2","3"],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""],
                ["","",""]
            ]}
    },
    "images":[]
}

Veamos la entrada "styles":{"1,1:10,3;0,1:0,3":"width:60px;"}. Se trata de la aplicación de 60px para las tres columnas. La clave "1,1:10,3;0,1:0,3" es una lista de rangos de celdas donde se aplica el estilo CSS "width:60px;". La lista de rangos se separa con punto y coma (";"), obteniéndose estos dos rangos:

  • 1,1:10,3
  • 0,1:0,3

En WXTABLE una referencia a una celda es row,col, teniendo en cuenta que la fila cero es la de selectores de columnas y la columna cero es la de selectores de fila. De esa forma 0,1 es la columna uno y 0,3 es la columna tres, siendo 0,1:0,3 el rango de esas tres columnas. Con 0,0 se referencia la tabla. Estas referencias son posibles para definir atributos y estilos. Por otro lado el rango 1,1:10,3 referencia todas las celdas. Cuando referenciamos en fórmulas no puede incluirse filas o columnas cero. Además en una referencia a una celda se usan paréntesis de la forma [R1C2] que apunta a la celda 1,2 con una referencia absoluta (ya comentamos esto un tema anterior).

Conociendo esto vemos que el estilo CSS "width:60px;" se aplica tanto a las columnas como a todas las celdas. Esto es así porque WXTABLE aplica los estilos con el mecanismo de herencia de CSS. Podemos aplicar un ancho a una columna y sobrescribirlo en alguna celda de esa columna con otro ancho mayor, con lo que toda la columna se adapta al mayor de los anchos definidos en sus celdas (elementos <td>) y columna (elemento <col>).

Figura
Figura. Excel con 3 columnas de 60px

Exportamos a XLSX generándose el archivo columns3.xlsx que puede abrirlo en GoogleSheet o bien Excel web, apareciendo como se muestra en la Figura.

Se observan tres primeras columnas con un ancho menor que el resto. Son las que configuramos a 60px. Excel se abre con un total de 16384 columnas tituladas desde la "A" hasta la "XFD", pues usa letras para los títulos. De la cuarta columna en adelante tienen un mayor ancho, 96px, valor que es el que por defecto tiene una columna en WXTABLE. Se incluyen más de las 10 filas que exportamos. En GoogleSheet hay 1000 filas disponibles y en Excel creo que hay muchas más. En cambio en WXTABLE sólo disponemos de las filas y columnas que necesitemos, pudiéndose agregar más filas o columnas en cualquier momento.

Información de anchos de columnas en el XML de la hoja

El archivo xl/worksheets/sheet1.xml es un XML que contiene la información de la única hoja:

<worksheet xmlns="http://schemas.openxmlformats.org/spreadsheetml/2006/main"
xmlns:r="http://schemas.openxmlformats.org/officeDocument/2006/relationships">
    <dimension ref="A1:C10"/>
    <sheetViews>
        <sheetView workbookViewId="0" zoomScaleSheetLayoutView="100" zoomScaleNormal="100" tabSelected="1"></sheetView>
    </sheetViews>
    <sheetFormatPr customHeight="1" defaultRowHeight="15" defaultColWidth="10.6640625"/>
    <cols count="3">
        <col min="1" max="1" hidden="0" customWidth="1" width="6.6640625"/>
        <col min="2" max="2" hidden="0" customWidth="1" width="6.6640625"/>
        <col min="3" max="3" hidden="0" customWidth="1" width="6.6640625"/>
    </cols>
    <sheetData>
        <row r="1" hidden="0">
            <c r="A1"/>
            <c r="B1"/>
            <c r="C1"/>
        </row>
    </sheetData>
</worksheet>

Se observa el elemento <sheetFormatPr> que informa del formato predeterminado de la hoja. Se incluye la altura de fila predeterminada defaultRowHeight con 15 puntos. Y el ancho de columna predeterminado defaultColWidth con 10.6640625 caracteres. Son los que se traducen de los 96px que tiene como ancho predeterminado una columna en WXTABLE. Esto quiere decir que en esos píxeles caben casi once caracteres usando la fuente predeterminada.

Se incluye el elemento <cols> que contiene la información de las columnas. Ponemos tantos elementos <col> como columnas tengamos en nuestro WXTABLE. Se observa el ancho 6.6640625 caracteres que son la traducción de los 60px. En el siguiente apartado explicaré cómo calcular esos anchos en caracteres. Los atributos min y max dicen a que columnas se aplica. Aunque en nuestro exportador no agrupamos varias columnas, siendo siempre min = max. El atributo customWidth con valor uno nos dice que se debe aplicar este ancho dado por el usuario.

Calcular el ancho de columnas en XLSX

El ancho de una columna se define en función del número de caracteres que caben en las celdas de esa columna. Y ese número depende de la fuente, su tamaño y de rellenos (padding) izquierdo y derecho. Para probar esto hice una tabla en WXTABLE con columnas desde 40px hasta 130px con incrementos de 10px. La fuente es Arial 12pt. Exporté esa tabla a XLSX en GoogleSheet y Excel, enfrentando gráficamente las tres versiones para observar si se respetaban los anchos. Este es el resultado final consiguiendo que los XLSX tuviesen los anchos esperados:

Figura
Figura. Anchos de columnas en WXTABLE, GoogleSheet y Excel

Puede acceder a columns.txt (WXTABLE) cuyo contenido puede importar en el Gestor tablas. Por otro lado puede descargar el archivo columns.xlsx (XLSX), que se debe abrir en GoogleSheet o Excel web. En lo que sigue intentaré explicar como se consigue esto.

En el apartado 18.3.1.13 Column Width & Formatting de OOX, página 1610, se expone lo relacionado con el elemento <col> que establece el ancho y formato de columna. El atributo width de ese elemento establece el ancho de columna medido como el número de caracteres del ancho máximo de dígitos de los números 0, 1, 2,…, 9 como se muestra en la fuente del estilo normal. Hay 4 píxeles de relleno de margen (dos en cada lado), más un relleno de 1 píxel para las líneas de cuadrícula.

Es muy importante entender que el ancho de columna se establece en un número de caracteres que caben en esa columna. No se usan otras medidas como puntos (pt), sino la cantidad de dígitos númericos que caben en la columna, usando como medida el dígito de mayor ancho. En cambio otras medidas como el alto de la fila se especifica en puntos.

La fuente del estilo normal viene a ser la fuente y tamaño predeterminado en una hoja de cálculo inicial. En GoogleSheet es Arial 10pt. Mientras que en Excel es Calibri de 11pt.

La fuente predeterminada en WXTABLE es Arial con 1em (16px, 12pt). Esta fuente y tamaño es la que se adjudica al XLSX en el primer elemento <font> del archivo xl/styles.xml. De esa forma todas las celdas de la hoja de cálculo tendrán por defecto una fuente Arial de 12pt.

<styleSheet xmlns="...">
    <numFmts count="0"></numFmts>
    <fonts count="1">
        <font>
            <name val="Arial"/>
            <sz val="12"/>
            <color rgb="FF000000"/>
        </font>
    </fonts>
    ...
</styleSheet>

Con esa fuente y tamaño hemos de determinar el dígito de mayor ancho entre los dígitos "0, 1, 2, ..., 9". Este cálculo hemos de hacerlo al iniciar el módulo xlsx.js. Para ello utilizamos el siguiente código para determinar el ancho del dígito de mayor ancho, maximumDigitWidth:

const maximumDigitWidth = (function(){
    let div = document.createElement("div");
    div.style.fontFamily = configIni.fontName;
    div.style.fontSize = `${configIni.fontSize}pt`;
    div.style.display = "inline-block";
    let max = 0;
    for (let digit of [..."0123456789"]){
        div.textContent = digit;
        document.body.appendChild(div);
        max = Math.max(div.offsetWidth, max);
    }
    div.remove();
    return max;
})();

El objeto configIni es una variable global que contiene la configuración inicial del módulo exportador XLSX, con algunos valores iniciales predeterminados en WXTABLE, entre ellos la fuente Arial y el tamaño de 12pt. Iteramos por los dígitos incluyéndolos en un elemento HTML para obtener el ancho de ese elemento y, por tanto, del dígito de mayor ancho. Al final obtendremos los píxeles del dígito más ancho.

El tamaño predeterminado Arial 12pt no puede modificarse en ejecución. Como dijimos antes, se especifica en el objeto de configuración configIni que tiene este aspecto. Las medidas se expresan en puntos ("pt"). Para este tema sólo nos interesa las entradas fontName y fontSize:

const configIni = {
    colWidth: 72,
    minWidthCol: 24,
    rowHeight: 15,
    fontSize: 12,
    fontName: "Arial",
    fontFamily: 2,
    dataExtend: 1,
    checkView: "list",
    radioView: "list",
    lang: "es",
    rn: "\n",
    tab: 4
};

En una ejecución obtengo un ancho de dígito de 9px para la fuente Arial de 12pt y de 7px para Calibri de 11pt. Este último valor coincide con el ejemplo que se incluye en OOX.

En la página 1611 y 1612 de OOX dice que para traducir desde píxeles a ancho de columna en caracteres se debe usar el cálculo Truncate((({pixels} - 5) / {Maximum Digit Width}) * 100 + 0.5) / 100. Y por otro lado dice que el ancho de columna medido como el número de caracteres del dígito de mayor ancho se calcula con Truncate([{Number Of Characters} * {Maximum Digit Width} + {5 pixel padding}] / {Maximum Digit Width} * 256) / 256. Pues entonces aplicaremos estas dos fórmulas para pasar los anchos de columnas en píxeles que tenemos en WXTABLE para incluir en los elementos <col>.

Sin embargo haremos en pequeño cambio, donde en las fórmulas dice que usemos un pixel padding de 5px usaremos uno de 7px. En un apartado siguiente explicaremos este motivo.

const pixelPadding = 7;
function getWidthColFile(pixels=0){
    let width = Math.trunc(((pixels-pixelPadding)/maximumDigitWidth)*100+0.5)/100;
    width = Math.trunc(((width*maximumDigitWidth+pixelPadding)/maximumDigitWidth)*256)/256;
    return width;
}

Con esto el elemento <worksheet> incluido en el archivo xl/worksheets/sheet1.xml quedaría así (abreviamos con puntos suspensivos algunas cosas que ahora no interesan):

<worksheet xmlns="..." xmlns:r="...">
    <dimension ref="A1:J10"/>
    <sheetViews>
        <sheetView workbookViewId="0" ...></sheetView>
    </sheetViews>
    <sheetFormatPr customHeight="1" defaultRowHeight="15" defaultColWidth="10.6640625"/>
    <cols count="10">
        <col min="1" max="1" hidden="0" customWidth="1" width="4.4453125"/>
        <col min="2" max="2" hidden="0" customWidth="1" width="5.5546875"/>
        <col min="3" max="3" hidden="0" customWidth="1" width="6.6640625"/>
        <col min="4" max="4" hidden="0" customWidth="1" width="7.77734375"/>
        <col min="5" max="5" hidden="0" customWidth="1" width="8.88671875"/>
        <col min="6" max="6" hidden="0" customWidth="1" width="9.99609375"/>
        <col min="7" max="7" hidden="0" customWidth="1" width="11.10546875"/>
        <col min="8" max="8" hidden="0" customWidth="1" width="12.21484375"/>
        <col min="9" max="9" hidden="0" customWidth="1" width="13.3359375"/>
        <col min="10" max="10" hidden="0" customWidth="1" width="14.4453125"/>
    </cols>
    ...
</worksheet>

En la Figura 2 comprobamos visualmente que la hoja de cálculo de GoogleSheet y Excel tienen las columnas con el mismo ancho que la tabla de WXTABLE. Comprobemos en el archivo XLSX generado columns.xlsx por ejemplo la tercera columna "C", que debe tener un ancho de 60px. En el archivo XML le pasamos un ancho de 6.6640625 caracteres.

Figura
Figura. Ancho de columna en Excel

En la Figura vemos que en Excel tiene esos 60 píxeles.

Figura
Figura. Ancho de columna en GoogleSheet

En la Figura vemos que en GoogleSheet tiene 59 píxeles, pues siempre muestra un píxel menos, pero efectivamente tiene 60 píxeles.

Relleno de celdas en XLSX (pixel padding)

En Excel existe la posibilidad de ver y editar el ancho de columna tanto en píxeles como en número de caracteres. Veamos esto.

Figura
Figura. Ancho de columna en Excel en número de caracteres

En la Figura vemos que la columna "C" de 60px ocupa 5.89 caracteres. Ese número sale del cálculo (60px - 7px) / 9px = 5.89. Se trata de restar 7px del pixel padding y dividir entre 9px que es el ancho del dígito de mayor ancho (maximum digit width). Recuerde que esa hoja de cálculo se generó con una fuente predeterminada Arial de 12pt.

XLSX no contempla la edición del padding o relleno del contenido de una celda, tal como lo conocemos en CSS. Sin embargo si aplica relleno de una forma variable dependiendo del tamaño de la fuente, a pesar de que las fórmulas que vimos más arriba extraidas de OOX hablan de un pixel padding que parece constante de 5px, que son 2px por cada lado derecho e izquierdo y 1px para la línea de la rejilla.

Figura
Figura. Pixel padding en Excel

En la Figura puede ver una captura de pantalla de la hoja de cálculo paddings.xlsx ejecutada en Excel. Ponemos la letra "U" mayúscula con las fuentes Calibri y Arial. Hacemos una captura de pantalla para observar en una aplicación de imagen como MS Paint cuántos píxeles hay en el relleno izquierdo.

Figura
Figura. Pixel padding en Excel

Convirtiendo la imagen a blanco y negro, ampliando lo máximo posible y usando la cuadrícula, se observa en la Figura que con un tamaño de 10pt Excel usa un relleno izquierdo de 2px. En este caso el padding pixel sería 2+2+1=5px tal como se expone en OOX.

Mientras que con 12pt Excel usa un relleno de 3px, pasando a 3+3+1=7px el valor del padding pixel, valor que es el que usamos en la función getWidthColFile() para obtener el ancho de las columnas, pues se establece como fuente predeterminada Arial 12pt.

El valor del relleno no es editable y además se incrementa con el tamaño de la fuente. Ni siquiera Excel y GoogleSheet usan el mismo criterio. No sabría decir a que fórmula obedece pues no he encontrado más documentación. Estos son los valores de los rellenos analizando gráficamente los píxeles en Excel y GoogleSheet para diversos tamaños de fuente:

Tamañopadding Excelpadding GoogleSheet
823
923
1023
1123
1234
1444
1644
1844
2054
2465
2675
2875
3696
48137
722010

Para complicar más las cosas como veremos a continuación, EXCEL impone el relleno por ambos lados derecho e izquierdo. Pero GoogleSheet sólo lo hace por la izquierda.

Ancho de columnas, rellenos y tamaños de fuentes

Figura
Figura. Columnas de 96px con tres tamaños de fuente en WXTABLE

Creo que es importante volver a repetir que los anchos de las columnas se configuran con la fuente predeterminada y no con la fuente que aparecen en las celdas. En la Figura tenemos un ejemplo en WXTABLE con tres columnas configuradas a 96px. La fuente predeterminada es Arial 16px que equivale a 12pt. Las celdas de la primera fila tiene fuente Arial (la predeterminada) y tamaño 12pt (el predeterminado), 16pt y 20pt.

El cálculo del ancho de las tres columnas es de 10.66 caracteres que caben en los 96px con una fuente Arial 12pt:

<cols count="3">
    <col min="1" max="1" hidden="0" customWidth="1" width="10.6640625"/>
    <col min="2" max="2" hidden="0" customWidth="1" width="10.6640625"/>
    <col min="3" max="3" hidden="0" customWidth="1" width="10.6640625"/>
</cols>

En la primera celda caben eso 10 caracteres y algo más, tal como se declara en el primer elemento <col>. Pero en la segunda apenas caben 8 y en la tercera sólo caben 6 a pesar de que sus elementos <col> tienen el mismo valor para las tres.

Figura
Figura. Columnas de 96px con tres tamaños de fuente en GoogleSheet

En la Figura puede ver el resultado exportado a GoogleSheet (puede descargarlo en columns-fonts.xlsx). El ancho de columnas sigue siendo de 96px. Ahora entra en juego el asunto de rellenos que vimos en el apartado anterior. WXTABLE permite incrementar el relleno con la propiedad padding de CSS, pero en ese ejemplo se usa el relleno por defecto que es cero. En la celda de la izquierda con 12pt GoogleSheet usa 4px por cada lado, aunque no se aprecia mucho la diferencia con WXTABLE. Pero en la celda del centro con fuente de 16pt ya no caben los 8 caracteres. Además se observa que GoogleSheet impone el relleno izquierdo, pero no el derecho. En la celda de la derecha también se observa que hay más relleno por la izquierda y no impone el mismo relleno por la derecha, resultando así que caben los seis caracteres.

Figura
Figura. Columnas de 96px con tres tamaños de fuente en EXCEL

La Figura es el resultado en EXCEL, con columnas de 96px también. Aquí además entra en juego el efecto de que cuando EXCEL considera que un número no cabe con holgura en una celda lo presenta en formato exponencial. Observe además que EXCEL no auto-ajusta el alto de las filas al contenido, tal como si hace GoogleSheet y WXTABLE.

Figura
Figura. Columnas de 96px con texto en EXCEL

Si abrimos el EXCEL web y modificamos el formato de las tres celdas a texto y volvemos a escribir los números que quepan sin que se produzca un salto de línea, se obtiene el resultado de la Figura. La primera celda es similar a GoogleSheet. En la segunda celda EXCEL impone el relleno derecho también, algo que no hace GoogleSheet, por lo que solo caben siete caracteres. Y en la tercera celda ya no caben seis caracteres pues EXCEL usa un relleno mayor que GoogleSheet. Observe de paso que cuando se modifica una celda se auto-ajusta la altura de la fila.