Abre un libro guardado por LibreOffice Calc y una columna escrita como hidden="false" puede volver oculta, aunque no hay nada malo en el fichero. Bajo xsd:boolean, el tipo detrás de ese atributo, true, false, 1 y 0 son todos legales, con o sin blancos alrededor; HotXLS para Delphi lee todas esas grafías dondequiera que un schema XLSX declare un boolean
La mitad de xsd:boolean que Excel nunca escribe
W3C XML Schema Part 2 §3.2.2 define boolean con cuatro literales legales, true, false, 1 y 0, y fija su faceta whiteSpace en collapse, lo que significa que customHeight=" 1 " es tan válido como customHeight="1". La misma sección nombra true y false como las formas canónicas. SpreadsheetML no hace nada por estrechar eso: sml.xsd declara customHeight, hidden, collapsed, showGridLines, wrapText, quotePrefix y docenas de sus vecinos como xsd:boolean plano, la mayoría con un valor por defecto explícito de false y unos pocos, como showGridLines, con por defecto true. Excel, por su lado, escribe solo los dígitos no canónicos. LibreOffice y algunos otros productores escriben las palabras, así que un paquete de Calc lleva <sheetFormatPr defaultRowHeight="12.8" customHeight="true"/> donde Excel habría escrito customHeight="1"
Esa asimetría es por lo que las comparaciones de un dígito sobreviven años en los lectores OOXML. Construye un corpus de pruebas con salida de Excel y todo boolean que veas jamás será 0 o 1. La comparación pasa todas las pruebas. Entonces un fichero de Calc llega a un usuario. Mete al menos unos pocos paquetes guardados por LibreOffice en cualquier corpus XLSX que alimente pruebas de lectores — su grafía de los booleanos es uno de varios sitios donde los dos productores difieren, la misma brecha que aparece en fórmulas y reglas cuando mueves libros entre Excel y LibreOffice vía ODS. Un detalle más del datatype importa para el diseño del parser: los literales distinguen mayúsculas, así que TRUE no es un xsd:boolean válido en absoluto, un punto que vuelve cuando decides lo indulgente que debe ser tu propio lector
Lo que falla en una comparación de un solo carácter
Un lector que compara un atributo contra un único dígito falla en una dirección o en la otra, y la dirección depende del valor por defecto que la comparación codifica sin decirlo. La tabla lista las formas que aparecen en código OOXML escrito a mano, qué hace cada una con las grafías de palabras y algunos atributos donde el daño se ve. Deja fuera showDropDown, cuyo problema es su nombre y no su grafía: el valor por defecto del schema es false, pero un showDropDown="1" en una validación de lista oculta la flecha de la celda, así que TXLSXDataValidation.ShowDropDown expone el significado de cara al usuario (True muestra la flecha) y guarda el flag OOXML invertido
| Prueba ingenua | Por defecto implícito | Lectura errónea | Dónde se ve |
|---|---|---|---|
Value = '1' | false | true se vuelve false | sheetFormatPr customHeight, el <v> de una celda t="b" |
Value <> '0' | true si está presente | false se vuelve true | columna hidden, definedName hidden, alignment wrapText |
Value = '0' | true | false se ignora y el defecto sigue activo | sheetView showGridLines, conmutadores de permisos de sheetProtection |
Copy(Raw, P + 5, 1) = '1' | false | true se recorta a t | bordes de la tabla de datos ChartML, smooth de series |
La fila de en medio produce el daño más visible, porque un hidden="false" en un elemento <col> es algo perfectamente normal que un productor escribe cuando anota el ancho de una columna visible, y un lector que trate cualquier texto distinto de cero como activo oculta esa columna al cargar. HotXLS pasa cada uno de estos atributos por una única conversión xsd:boolean que recorta los blancos alrededor, acepta los dígitos y las palabras, y devuelve el valor por defecto del propio atributo para cualquier otra cosa. Esa última rama refleja una decisión de diseño que vale la pena enunciar: un valor como yes es un fichero malformado, pero negarse a abrir un libro por un flag de visibilidad malo castigaría al usuario por el error del productor, así que el lector mantiene un fallback fijo por atributo y sigue adelante
Leer una hoja estilo LibreOffice con HotXLS
Los flags de nivel de hoja afloran como propiedades corrientes en TXLSXWorksheet: CustomHeight, ThickTop y ThickBottom vienen de <sheetFormatPr>, mientras que ColHidden[], RowHidden[], RowThickTop[] y RowThickBottom[] toman números de columna y fila base uno. Una celda booleana llega como variante Boolean en TXLSXCell.Value, sea cual sea la grafía que usó el elemento <v>. La rutina de abajo imprime todos para la primera hoja, y la lista posterior da la salida esperada para markup estilo Calc
uses
SysUtils, Variants, lxHandleX;
procedure DumpSheetBooleans(const FileName: string);
var
Book: TXLSXWorkbook;
Sheet: TXLSXWorksheet;
Flag: Variant;
Col, Row: Integer;
begin
Book := TXLSXWorkbook.Create;
try
if Book.Open(FileName) <> 1 then
raise Exception.CreateFmt('Cannot open %s', [FileName]);
if Book.Sheets.Count = 0 then
raise Exception.Create('Workbook has no sheets');
Sheet := Book.Sheets[1]; // el indexador de Sheets es base uno
Writeln('Default row height is custom: ', Sheet.CustomHeight);
Writeln('Rows default to thick top / bottom: ',
Sheet.ThickTop, ' / ', Sheet.ThickBottom);
for Col := 1 to 8 do
if Sheet.ColHidden[Col] then
Writeln('Column ', Col, ' is hidden');
for Row := 1 to 20 do
if Sheet.RowHidden[Row] or Sheet.RowThickTop[Row] or
Sheet.RowThickBottom[Row] then
Writeln('Row ', Row, ': hidden=', Sheet.RowHidden[Row],
' thickTop=', Sheet.RowThickTop[Row],
' thickBot=', Sheet.RowThickBottom[Row]);
Flag := Sheet.Cells[2, 1].Value;
if VarType(Flag) = varBoolean then
Writeln('A2 holds the boolean ', Boolean(Flag));
finally
Book.Free;
end;
end;
<sheetFormatPr defaultRowHeight="12.8" customHeight="true"/>:CustomHeightimprime TRUE<col min="3" max="3" width="20" hidden="false"/>: la columna 3 no aparece en la lista<row r="5" thickBot="true">: la fila 5 reportathickBot=TRUE<c r="A2" t="b"><v>true</v></c>: A2 guarda TRUE
La pista de fila es donde el schema en sí se pone incómodo. Una idea, dos grafías — CT_SheetFormatPr nombra sus atributos thickTop y thickBottom, mientras que CT_Row usa thickTop y thickBot, así que un lector que busque thickBottom en una fila no encuentra nada. HotXLS escribe cada forma de vuelta en su propio elemento, y las pistas sobreviven a un guardado junto con el resto del paquete, igual que las partes intactas en un round trip XLSX sin pérdidas
¿Cómo debe leer un gráfico un elemento sin val?
Un conmutador ChartML sin atributo val está activado. DrawingML Charts declara esos conmutadores con CT_Boolean en dml-chart.xsd, cuyo val es un xsd:boolean opcional con valor por defecto true, así que <c:plotVisOnly/> y <c:showHorzBorder/> significan lo mismo que escribir val="1". Ese valor por defecto es el inverso de la mayoría de los atributos de SpreadsheetML, y rompe el atajo habitual de leer el XML de gráficos como texto: encuentra el elemento por nombre y busca hacia delante el siguiente val=". Cuando el elemento omite val, esa búsqueda corre más allá del final del start tag y pide prestado el valor del elemento que venga después en la parte, quizá un val="0" dos hermanos más abajo. Cuando no hay ninguna coincidencia, PosEx devuelve 0, y el código que corta en P + 5 acaba leyendo desde el carácter 5 de la parte, que es la l de <?xml. Ninguno de los dos fallos lanza nada; la propiedad del gráfico simplemente discrepa del fichero
Las comillas son la otra mitad del problema. XML 1.0 permite delimitar el valor de un atributo con comillas dobles o simples (la producción AttValue de §2.3), así que <c:majorTickMark val='out'/> es tan legítimo como la forma con comillas dobles, y una búsqueda de val=" lo salta por completo. HotXLS conserva el ChartML original para poder editar gráficos de Excel sin perder el markup que no modela, y los conmutadores leídos de ese texto se buscan solo dentro del start tag del propio elemento, aceptando cualquiera de los dos estilos de comillas y leyendo un val ausente como el valor por defecto de CT_Boolean. Las propiedades públicas del gráfico reflejan esa lectura
procedure DumpChartSwitches(Sheet: TXLSXWorksheet);
const
TickNames: array[TXLSXChartTickMark] of string =
('none', 'in', 'out', 'cross');
var
I: Integer;
Chart: TXLSXChart;
begin
for I := 0 to Sheet.Charts.Count - 1 do // Charts es base cero
begin
Chart := Sheet.Charts[I];
Writeln(Chart.Name, ': plotVisOnly=', Chart.PlotVisibleOnly,
' dTable showHorzBorder=', Chart.ShowDataTableHorzBorder);
if Chart.ValueAxis <> nil then
Writeln(' value axis majorTickMark=',
TickNames[Chart.ValueAxis.MajorTickMark]);
if Chart.SeriesCount > 0 then
Writeln(' first series smooth=', Chart.Series[0].Smooth);
end;
end;
Esperado para una parte de gráfico que contenga <c:plotVisOnly/> seguida más tarde de un elemento con val="0", una tabla de datos con <c:showHorzBorder/><c:showVertBorder val="0"/>, y un eje de valores con <c:majorTickMark val='out'/>: PlotVisibleOnly y ShowDataTableHorzBorder son ambos TRUE, y la marca de tick imprime out
Un lector xsd:boolean acotado para tu propio código OOXML
Cuando posprocesas un paquete tú mismo, por ejemplo una parte a medida que añade tu aplicación o un vistazo rápido al XML de gráficos en un paso de build, aplican las mismas dos reglas: convertir con el espacio léxico completo y no dejar nunca que la búsqueda de un atributo salga del start tag del elemento al que pertenece. Las funciones de abajo hacen ambas cosas. ChartSwitch espera que TagStart apunte al < del elemento y aplica el valor por defecto de CT_Boolean cuando falta val
uses
SysUtils, StrUtils;
function TryParseXsdBoolean(const Literal: string; out Value: Boolean): Boolean;
var
S: string;
begin
S := Trim(Literal); // whiteSpace=collapse: el relleno es legal
Result := True;
if (S = '1') or (S = 'true') then
Value := True
else if (S = '0') or (S = 'false') then
Value := False
else
Result := False; // TRUE, yes, on: no es xsd:boolean
end;
function ReadXsdBoolean(const Literal: string; Default: Boolean): Boolean;
begin
if not TryParseXsdBoolean(Literal, Result) then
Result := Default;
end;
function AttrInStartTag(const Xml: string; TagStart: Integer;
const AttrName: string; out Value: string): Boolean;
var
TagEnd, P, Q: Integer;
Quote: Char;
begin
Result := False;
Value := '';
TagEnd := PosEx('>', Xml, TagStart);
if TagEnd = 0 then
Exit;
// el nombre debe abrir un token, no cerrar el nombre de otro atributo
P := PosEx(AttrName + '=', Xml, TagStart);
while (P > 0) and (P < TagEnd) and
not CharInSet(Xml[P - 1], [' ', #9, #10, #13]) do
P := PosEx(AttrName + '=', Xml, P + 1);
if (P = 0) or (P >= TagEnd) then
Exit;
Q := P + Length(AttrName) + 1;
Quote := Xml[Q];
if (Quote <> '"') and (Quote <> '''') then
Exit;
P := PosEx(Quote, Xml, Q + 1);
if (P = 0) or (P > TagEnd) then
Exit;
Value := Copy(Xml, Q + 1, P - Q - 1);
Result := True;
end;
function ChartSwitch(const Xml: string; TagStart: Integer): Boolean;
var
V: string;
begin
if not AttrInStartTag(Xml, TagStart, 'val', V) then
Exit(True); // CT_Boolean: un val ausente significa true
Result := ReadXsdBoolean(V, True);
end;
La rama estricta de TryParseXsdBoolean sigue el datatype al pie de la letra, y ahí tu lector y HotXLS se separan un poco: la conversión de HotXLS compara las palabras sin fijarse en las mayúsculas, así que TRUE se lee como true. Preferimos aceptar una grafía que el schema prohíbe antes que apagar un flag porque a algún productor se le ocurrió escribirlo en mayúsculas, ya que ningún productor escribe TRUE para significar false; un validador, en cambio, debería quedarse con la rama estricta y reportar el valor. El scanner de texto toma además dos atajos que son seguros para los conmutadores de ChartML y erróneos para XML en general. Un > es legal dentro del valor de un atributo, así que pararse en el primero puede truncar un tag cuyos valores lo contengan, y XML permite blancos alrededor del =, que la búsqueda de AttrName + '=' no ve. Los valores de los conmutadores ChartML son tokens cortos sin ningún > dentro, y saltarse los blancos alrededor del igual es una extensión de dos líneas, pero para cualquier cosa más allá de un puñado de conmutadores conocidos, un lector XML que te entregue los atributos de un elemento cada vez elimina los dos problemas, y es lo que HotXLS usa para las partes de hojas de trabajo
Cuando un libro de Calc carga con columnas ocultas o altos de fila distintos que su gemelo de Excel, descomprime ambos y mira cómo están escritos los flags; hidden="false" frente a hidden="0" suele explicar la diferencia en un minuto. Mapear ambas grafías, con relleno o desnudas, sobre un único conjunto de propiedades de hoja, celda y gráfico es el trabajo que el componente de hojas de cálculo HotXLS para Delphi hace en cada carga