Um livro Excel pode transportar imagens EMF e WMF, e a forma convencional de desenhar uma é entregar o stream de bytes ao reprodutor de metaficheiros do sistema operativo. Essa é uma decisão que vale a pena olhar diretamente: um metaficheiro é um stream de comandos serializado para uma API de gráficos, e reproduzi-lo significa deixar um ficheiro que chegou por email comandar o driver gráfico. O HotXLS toma o outro caminho. O XLSDecodeVectorScene analisa ele próprio o metaficheiro, valida o cabeçalho, cada tamanho de registo, o total de registos declarado e a colocação exata do registo de fim de ficheiro, recusa registos escape de imediato, e devolve uma TXLSVectorScene de comandos de desenho primitivos que os backends Canvas e SVG reproduzem através do seu próprio código. Nenhuma reprodução por driver está envolvida em ponto algum
A troca é cobertura por contenção. Uma whitelist de comandos orientada a retângulos não reproduzirá todos os metaficheiros que um designer pode criar, pelo que a cena reporta quantos registos de desenho não conseguiu representar e o chamador decide o que fazer com isso. Para um processo de servidor que renderiza documentos que não criou, essa troca está do lado certo
Porque é que a reprodução de metaficheiros serve mal a entrada não confiável?
Porque o formato não é uma imagem, é um programa. Um stream de registos EMF manipula uma pilha de estado de contexto de dispositivo, aloca e seleciona objetos de uma tabela de handles, e pode transportar registos escape cujo conteúdo é passado a um driver de dispositivo. Reproduzi-lo exercita caminhos na pilha de gráficos da plataforma que foram escritos partindo do pressuposto de que o metaficheiro veio de uma aplicação cooperante na mesma máquina. Quando a entrada é um anexo de folha de cálculo, esse pressuposto desaparece, e nenhum cuidado dentro da biblioteca de folhas de cálculo ajuda porque a biblioteca não é o componente que faz a análise
Este é o mesmo raciocínio que rege a camada de contentores. Um livro é um arquivo ZIP, e o HotXLS valida o seu diretório central em vez de confiar nos desvios declarados, como descrito no artigo sobre validação do end-of-central-directory ZIP. Os conteúdos de metaficheiros são a camada seguinte do mesmo problema
O que o descodificador verifica antes de desenhar alguma coisa
A validação é estrutural e acontece logo no início, porque um parser que começa a desenhar e valida ao mesmo tempo já atuou sobre dados que não verificou. O cabeçalho tem de corresponder estritamente e não apenas plausivelmente. Cada registo tem de declarar um tamanho que caiba dentro do buffer restante e seja suficientemente grande para os seus campos fixos. A contagem de registos que o cabeçalho declara tem de corresponder aos registos realmente presentes. O registo de fim de ficheiro tem de estar exatamente onde o stream termina, não meramente algures perto, o que fecha o truque do lixo final que esconde uma segunda carga atrás de uma imagem válida
Para além da estrutura, o descodificador falha em modo fechado na semântica. Registos escape são recusados, não saltados. Um registo que mude o estado e que o descodificador não modele faz a descodificação falhar em vez de ser ignorado, porque ignorar uma mudança de estado significa que cada comando de desenho subsequente é executado num estado que o ficheiro não pediu, e o resultado é uma imagem errada de um modo que ninguém pode prever. Registos de desenho fora do conjunto de comandos suportado são outra história: esses são contados e saltados, porque uma forma em falta é um buraco visível e reportável em vez de uma corrupção silenciosa
Os orçamentos fazem parte do contrato do formato
Os formatos vetoriais têm a sua própria versão da bomba de descompressão. Alguns quilobytes de registos podem declarar polilinhas com centenas de milhões de pontos, ou uma imagem cujas dimensões declaradas multiplicam para terabytes. Os limites portanto têm de ser constantes explícitas em vez do que a máquina por acaso sobreviva
// De lxVectorScene: o orçamento de descodificação, enunciado em vez de 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;
Duas destas merecem uma nota. O limite de profundidade de contexto de dispositivo de 32 existe porque os registos SaveDC e RestoreDC aninham-se, e um stream desequilibrado pode empilhar para sempre; 32 é generoso para metaficheiros reais e barato de impor. O limite de coordenadas existe porque as coordenadas alimentam uma transformação, e um valor perto dos limites do intervalo de inteiros produz um resultado transformado que é infinito ou dá a volta, depois do que todos os cálculos de bounding box a jusante são disparates. Limitar as coordenadas em tempo de análise é muito mais fácil de raciocinar do que defender todos os consumidores da geometria
Usar a cena
O descodificador entrega um objeto que é seu, uma contagem de comandos, um tamanho nominal, e uma contagem de registos de desenho que escolheu não representar
uses
lxVectorScene;
var
Scene: TXLSVectorScene;
Error: WideString;
I: Integer;
begin
// Data guarda o conteúdo bruto da imagem retirado do livro
if not XLSDecodeVectorScene(Data, xlsvfEmf, Scene, Error) then
begin
// Recusado: cabeçalho, limites, totais, colocação do EOF ou um orçamento
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;
O registo de comando transporta tudo o que um backend precisa e nada que exija um dispositivo: presença de caneta, cor, largura e estilo; presença e cor de pincel; a geometria; e para texto a string, nome de fonte, tamanho, estilos e alinhamento. É isso que torna a mesma cena utilizável tanto pelo renderizador de canvas no ecrã como pelo escritor SVG, e é por isso que o caminho vetorial não diverge entre pré-visualização e exportação. A renderização no ecrã de conteúdo de folhas de cálculo em geral está coberta no artigo sobre renderização de grelha VCL personalizada
Rejeitar uma imagem não danifica o livro
Uma propriedade importante deste design é que uma descodificação recusada afeta apenas a renderização. O conteúdo original fica no modelo, pelo que um livro que é aberto e gravado de novo transporta as suas imagens de metaficheiro para fora byte a byte, quer o descodificador seguro as consiga desenhar quer não. O caminho raster limitado existente também continua disponível como recurso. Por outras palavras, o parser estrito condiciona o que é executado, não o que é preservado, que é a distinção que permite uma alteração motivada por segurança ser lançada sem se transformar numa alteração de perda de dados
O tratamento de objetos de desenho em geral, incluindo as partes do modelo de objetos que sobrevivem intactas às viagens de ida e volta, está coberto no artigo sobre gráficos, imagens e desenhos
Onde isto deixa uma implantação em servidor
Se renderiza livros carregados por utilizadores num serviço, a posição prática agora é defensável: as imagens de metaficheiro são analisadas por código que pode auditar, limitadas por constantes que pode ler, e nunca entregues a um driver gráfico. A ressalva honesta é a cobertura. Metaficheiros complexos produzidos por ferramentas de desenho vão ativar o contador de registos saltados, e a resposta é evidenciar o contador em vez de alargar a whitelist em silêncio. Uma imagem que renderiza parcialmente e o diz é uma conversa de suporte; uma imagem que renderiza mal e não diz nada é um relatório de bug de um cliente
O HotXLS trata XLS, XLSX, ODS e CSV nativamente em Delphi e C++Builder sem Excel instalado, e a mesma filosofia de análise limitada atravessa as suas camadas de contentor, fórmulas e desenhos. Os detalhes de formato e segurança estão listados na página de produto do HotXLS Delphi spreadsheet component