Un libro de Excel puede llevar imágenes EMF y WMF, y la forma convencional de dibujar una es entregarle el stream de bytes al reproductor de metarchivos del sistema operativo. Es una decisión que merece mirarse de frente: un metarchivo es un stream de comandos serializado para una API de gráficos, y reproducirlo significa dejar que un fichero que llegó por correo maneje el controlador de gráficos. HotXLS toma la otra ruta. XLSDecodeVectorScene analiza el metarchivo ella misma, valida la cabecera, cada tamaño de registro, el total de registros declarado y la colocación exacta del registro de fin de fichero, rechaza los registros escape sin más, y devuelve una TXLSVectorScene de comandos de dibujo primitivos que los backends Canvas y SVG reproducen con su propio código. En ningún punto interviene reproducción por el controlador
El intercambio es cobertura por contención. Una whitelist de comandos orientada a rectángulos no reproducirá todo metarchivo que un diseñador pueda crear, así que la escena informa de cuántos registros de dibujo no pudo representar y quien llama decide qué hacer con ello. Para un proceso servidor que renderiza documentos que no creó, ese intercambio está bien planteado
Por qué la reproducción de metarchivos encaja mal con entrada no confiable?
Porque el formato no es una imagen, es un programa. Un stream de registros EMF manipula una pila de estados de contexto de dispositivo, aloja y selecciona objetos de una tabla de handles, y puede llevar registros escape cuya carga se pasa a un controlador de dispositivo. Reproducirlo ejercita rutas de la pila gráfica de la plataforma escritas suponiendo que el metarchivo venía de una aplicación cooperante en la misma máquina. Cuando la entrada es un adjunto de hoja de cálculo, ese supuesto desaparece, y nada de lo que se cuide dentro de la biblioteca de hojas de cálculo ayuda porque la biblioteca no es el componente que analiza
Es el mismo razonamiento que gobierna la capa de contenedor. Un libro es un archivo ZIP, y HotXLS valida su central directory en lugar de fiarse de los offsets declarados, como se describe en el artículo de validación del end-of-central-directory ZIP. Las cargas de metarchivo son la siguiente capa del mismo problema
Qué comprueba el decodificador antes de dibujar nada
La validación es estructural y ocurre por adelantado, porque un parser que empieza a dibujar y valida sobre la marcha ya ha actuado sobre datos que no ha verificado. La cabecera debe coincidir estrictamente, no plausiblemente. Cada registro debe declarar un tamaño que quepa en el buffer restante y sea bastante grande para sus campos fijos. El recuento de registros que declara la cabecera debe coincidir con los registros realmente presentes. El registro de fin de fichero debe estar exactamente donde termina el stream, no meramente en algún sitio cercano, lo que cierra el truco de la basura final que esconde una segunda carga detrás de una imagen válida
Más allá de la estructura, el decodificador es fail-closed en la semántica. Los registros escape se rechazan, no se saltan. Un registro que cambia de estado y que el decodificador no modela provoca que la decodificación falle en lugar de ignorarse, porque ignorar un cambio de estado significa que cada comando de dibujo posterior se ejecuta en un estado que el fichero no pidió, y el resultado es una imagen equivocada de una forma que nadie puede predecir. Los registros de dibujo fuera del conjunto de comandos soportado son otra cosa: esos se cuentan y se saltan, porque una forma ausente es un hueco visible y reportable, no una corrupción silenciosa
Los presupuestos son parte del contrato del formato
Los formatos vectoriales tienen su propia versión de la bomba de descompresión. Unos pocos kilobytes de registros pueden declarar polilíneas con cientos de millones de puntos, o una imagen cuyas dimensiones declaradas multiplican hasta terabytes. Los límites, por tanto, tienen que ser constantes explícitas y no lo que la máquina aguante por casualidad
// De lxVectorScene: el presupuesto de decodificación, declarado y no implícito
XL_VECTOR_MAX_RECORDS = 1000000;
XL_VECTOR_MAX_HANDLES = 4096;
XL_VECTOR_MAX_DC_DEPTH = 32;
XL_VECTOR_MAX_COMMANDS = 100000;
XL_VECTOR_MAX_POINTS_PER_RECORD = 100000;
XL_VECTOR_MAX_TOTAL_POINTS = 2000000;
XL_VECTOR_MAX_TEXT_CHARS = 4096;
XL_VECTOR_MAX_TOTAL_TEXT_CHARS = 1000000;
XL_VECTOR_MAX_IMAGE_SIDE = 8192;
XL_VECTOR_MAX_IMAGE_PIXELS = 32 * 1024 * 1024;
XL_VECTOR_MAX_IMAGE_BYTES = 64 * 1024 * 1024;
XL_VECTOR_MAX_COORD = 1000000000;
Dos de ellos merecen una nota. El tope de profundidad de contexto de dispositivo de 32 existe porque los registros SaveDC y RestoreDC se anidan, y un stream desequilibrado puede apilar para siempre; 32 es generoso para metarchivos reales y barato de aplicar. El tope de coordenadas existe porque las coordenadas alimentan una transformación, y un valor cerca de los límites del rango de enteros produce un resultado transformado que es infinito o desborda, tras lo cual todo cómputo de bounding box aguas abajo es un sinsentido. Acotar coordenadas en el parseo es mucho más fácil de razonar que defender a cada consumidor de la geometría
Usar la escena
El decodificador devuelve un objeto que es vuestro, un recuento de comandos, un tamaño nominal y un recuento de registros de dibujo que optó por no representar
uses
lxVectorScene;
var
Scene: TXLSVectorScene;
Error: WideString;
I: Integer;
begin
// Data contiene la carga bruta de imagen tomada del libro
if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
begin
// Rechazada: cabecera, límites, totales, colocación del EOF o un presupuesto
LogReject('metafile rejected: ' + Error);
Exit;
end;
try
if Scene.SkippedDrawRecords > 0 then
LogWarning(Format('%d drawing records outside the safe subset',
[Scene.SkippedDrawRecords]));
for I := 0 to Scene.Count - 1 do
case Scene.Commands[I].Kind of
xlsvcRectangle: DrawRect(Scene.Commands[I]);
xlsvcEllipse: DrawEllipse(Scene.Commands[I]);
xlsvcPolyline,
xlsvcPolygon,
xlsvcBezier: DrawPath(Scene.Commands[I]);
xlsvcText: DrawText(Scene.Commands[I]);
xlsvcImage: DrawImage(Scene.Commands[I]);
end;
finally
Scene.Free;
end;
end;
El registro de comando lleva todo lo que un backend necesita y nada que exija un dispositivo: presencia, color, anchura y estilo de pluma; presencia y color de brocha; la geometría; y para el texto la cadena, nombre de fuente, tamaño, estilos y alineación. Eso es lo que hace que la misma escena sea utilizable tanto por el renderizador de canvas en pantalla como por el escritor SVG, y es la razón por la que la ruta vectorial no diverge entre previsualización y exportación. El renderizado en pantalla de contenido de hojas de cálculo en general se cubre en el artículo de renderizado con grid VCL personalizado
Rechazar una imagen no daña el libro
Una propiedad importante de este diseño es que una decodificación rechazada afecta solo al renderizado. La carga original se queda en el modelo, así que un libro que se abre y se guarda de nuevo saca sus imágenes de metarchivo byte a byte, quisiera o no el decodificador seguro dibujarlas. La ruta rasterizada acotada existente sigue también disponible como fallback. Dicho de otro modo, el parser estricto condiciona lo que se ejecuta, no lo que se conserva, que es la distinción que permite que un cambio motivado por seguridad se publique sin convertirse en un cambio de pérdida de datos
El manejo de objetos de dibujo en general, incluidas las partes del modelo de objetos que sobreviven intactas a los viajes de ida y vuelta, se cubre en el artículo de gráficos, imágenes y dibujos
Dónde deja esto a un despliegue servidor
Si renderizáis libros subidos por usuarios en un servicio, la posición práctica ahora es defendible: las imágenes de metarchivo las analiza código que podéis auditar, las acotan constantes que podéis leer, y nunca se entregan a un controlador de gráficos. La advertencia honesta es la cobertura. Los metarchivos complejos producidos por herramientas de dibujo tocarán el contador de registros saltados, y la respuesta a eso es mostrar el contador en lugar de ensanchar la whitelist en silencio. Una imagen que se renderiza parcialmente y lo dice es una conversación de soporte; una imagen que se renderiza mal y no dice nada es un informe de error de un cliente
HotXLS maneja XLS, XLSX, ODS y CSV de forma nativa en Delphi y C++Builder sin Excel instalado, y la misma filosofía de parseo acotado atraviesa sus capas de contenedor, fórmulas y dibujos. Los detalles de formato y seguridad están en la página de producto de HotXLS Delphi spreadsheet component