Artigo Técnico

Geometria de imagens no HotXLS em Delphi: EMU, cm e escala

Você coloca um logotipo de 600×400 pixels no cabeçalho de uma nota fiscal gerada, ele parece correto no monitor de desenvolvimento de 96 DPI e, uma semana depois, um cliente em um notebook de alta densidade de pixels reclama que a impressão saiu do tamanho de um selo. Os pixels não mudaram. O que mudou foi a suposição de que uma contagem de pixels equivale a um tamanho físico, e em OOXML isso não equivale. Uma imagem de planilha carrega suas dimensões em EMU e, até você pensar em EMU, ou nas unidades do mundo real que se mapeiam de forma limpa para ele, o layout fica à mercê do DPI que a máquina de renderização resolver assumir

HotXLS é um componente VCL nativo de planilhas para Delphi e C++Builder que lê e grava XLS e XLSX sem Excel ou qualquer dependência de COM. A partir da v2.91.0, o objeto de imagem XLSX deixa de exigir que você faça a aritmética de unidades na mão: além do EMU bruto, ele expõe largura e altura em centímetros, polegadas e pontos, e ainda um método Scale que redimensiona por porcentagem com bloqueio opcional da proporção. Este artigo explica o que o EMU realmente é, por que o DrawingML o escolheu e como usar a nova superfície de geometria para posicionar imagens por tamanho físico em vez de por uma contagem de pixels na qual você não pode confiar

O que é um EMU e por que o DrawingML trabalha com essa unidade

EMU significa English Metric Unit, e é a unidade base de comprimento do DrawingML, a camada de desenho compartilhada por toda a família Office Open XML (ECMA-376, Parte 1, §20). Um EMU é definido de modo que existam exatamente 914400 EMU por polegada e 360000 EMU por centímetro. Essas duas constantes são o motivo inteiro de a unidade existir. 914400 é divisível por 2, 3, 4, 5, 6, 8, 9, 10, 12 e muito mais; sua fatoração é 26 × 32 × 52 × 127. Como 1 polegada = 2,54 cm exatamente, escolher uma unidade divisível tanto por 360000 quanto por uma fração limpa de 914400 permite que o formato expresse polegadas, centímetros e pontos como inteiros, sem arredondamento na fronteira da unidade. Onde um "1,27 cm" em ponto flutuante derivaria, o EMU armazena 457200 e permanece exato

A outra unidade que importa aqui é o ponto. Um ponto tipográfico é 1/72 de polegada, então existem 12700 EMU por ponto (914400 / 72). Pontos são o jeito que o Excel pensa internamente sobre alturas de linha, tamanhos de fonte e margens, e por isso expor a geometria da imagem em pontos é útil quando você quer que uma imagem se alinhe às métricas do texto, não a uma régua impressa. O HotXLS codifica todas as quatro relações como constantes de unidade na biblioteca:

const
  XlsxEmuPerInch  = 914400;  // 1 inch
  XlsxEmuPerCm    = 360000;  // 1 centimetre
  XlsxEmuPerPoint = 12700;   // 1 point (1/72 inch)
  XlsxEmuPerPixel = 9525;    // 1 pixel at 96 DPI (914400 / 96)

Essa última linha é o núcleo do bug do tamanho de selo. Um pixel só tem tamanho físico depois que você fixa um DPI, e 9525 EMU é o tamanho de um pixel especificamente a 96 DPI. O DPI padrão de renderização do Excel é 96, então uma imagem de 100 pixels cai em 100 × 9525 = 952500 EMU, cerca de 2,54 cm em uma configuração padrão, mas nada no arquivo garante que o consumidor use 96. Escreva em unidades reais e essa ambiguidade desaparece: 4 cm são 4 cm, seja a tela 96 ou 220 DPI

A superfície de geometria de TXLSXImage na prática

Uma imagem incorporada no HotXLS é um TXLSXImage. O armazenamento canônico dela usa dois campos inteiros, WidthEMU e HeightEMU, ancorados em um Row e Col baseados em 1, a célula superior esquerda da qual a imagem parte. As propriedades em unidade real são visões calculadas sobre esses campos EMU, não estado separado. Ler WidthCM divide o EMU por 360000, e gravá-lo multiplica e arredonda de volta. Então cada dimensão que você define é apenas uma grafia diferente do mesmo valor EMU subjacente:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - a fonte inteira da verdade

Você adiciona uma imagem com AddImage(ARow, ACol, AData, AFormat), passando os bytes codificados brutos e um TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif ou xlsxImageBmp); ele retorna o índice baseado em zero da coleção Images da worksheet. Também existe AddImageFromFile(ARow, ACol, AFileName), que infere o formato pela extensão do arquivo. Observe a base do índice: AddImage retorna base zero e Images[] é base zero, o que contrasta de propósito com a grade Cells[Row, Col] baseada em 1, então não assuma que os dois combinam

var
  Sheet: TXLSXWorksheet;
  Img: TXLSXImage;
  Idx: Integer;
begin
  Sheet := Workbook.Sheets.Add('Images');

  // Anchor a PNG at row 3, column 2; AddImage returns a 0-based index.
  Idx := Sheet.AddImage(3, 2, LogoBytes, xlsxImagePng);

  Img := Sheet.Images[Idx];
  Img.WidthCM := 4.0;    // 4 cm wide  -> 1440000 EMU
  Img.HeightCM := 3.0;   // 3 cm tall  -> 1080000 EMU

  // Same geometry, read back in other units.
  // Img.WidthPt  is now 113.39 pt, Img.WidthInch is 1.5748 in.
end;

Uma imagem recém-criada usa como padrão 100×100 pixels, isto é, 952500 EMU quadrados, algo perto de uma caixa de 2,54 cm a 96 DPI. Esse padrão existe para a imagem ficar visível mesmo se você esquecer de dimensioná-la, mas, para qualquer layout real, você deve definir um tamanho físico explícito em vez de confiar no padrão derivado de pixels

Escala e a opção de manter proporção

Quando você quer redimensionar em relação às dimensões atuais em vez de mirar um alvo absoluto, por exemplo, reduzir uma imagem de gráfico para 60% do tamanho em que foi importada, use Scale:

procedure Scale(APercent: Double; AKeepAspect: Boolean = True);

APercent é uma porcentagem em que 100 significa sem mudança, 150 aumenta pela metade e 50 reduz pela metade. Com AKeepAspect no padrão True, largura e altura são multiplicadas pelo mesmo fator, então as proporções se mantêm e uma imagem de 4×3 cm vira 6×4,5 cm depois de Scale(150). Passe False e só a largura escala, a altura fica exatamente como estava. Essa assimetria é intencional: quando você quer esticar um eixo de forma independente, a ferramenta certa são os setters explícitos WidthCM e HeightCM, e o ramo sem proporção de Scale existe para o caso mais estreito de ajustar apenas a largura. É fácil ler Scale(150, False) como "esticar os dois livremente" e ter surpresa, então recorra aos setters quando realmente quiser duas dimensões independentes

Img.WidthCM := 4.0;
Img.HeightCM := 3.0;

Img.Scale(150);          // aspect locked: now 6.0 x 4.5 cm
Img.Scale(100);          // no-op, returns immediately

Img.Scale(50, False);    // width only: 3.0 cm wide, height unchanged at 4.5 cm

Um comportamento pequeno que vale conhecer: Scale(100) corta caminho e retorna sem tocar em nenhum dos campos, então é seguro chamá-lo incondicionalmente em um loop em que a porcentagem possa ser 100. E, como a geometria é armazenada como EMU inteiro, todo setter arredonda. A ida e volta por centímetros fracionários pode, portanto, derivar por uma fração de um EMU, muito abaixo de qualquer coisa visível, mas útil de saber se você algum dia afirmar igualdade exata em um teste. Para controle pixel a pixel, defina WidthEMU e HeightEMU diretamente e pule a conversão de unidade por completo

Como ler a geometria de volta

A coleção de imagens é consultável, o que importa quando você carrega uma pasta de trabalho existente e precisa inspecionar ou ajustar o que já está lá, em vez do que acabou de adicionar. Images.Count enumera cada imagem da planilha, Images[i] as indexa base zero, e FindAt(ARow, ACol) retorna a imagem ancorada em uma célula específica, ou nil se não houver nenhuma. Também existe IndexOfCell para o índice em vez do objeto, e DeleteAt / DeleteInRange para remoção

var
  i: Integer;
  Img: TXLSXImage;
begin
  for i := 0 to Sheet.Images.Count - 1 do
  begin
    Img := Sheet.Images[i];
    Writeln(Format('[%d] R%dC%d  %.2f x %.2f cm  (%d x %d EMU)',
      [i, Img.Row, Img.Col, Img.WidthCM, Img.HeightCM,
       Img.WidthEMU, Img.HeightEMU]));
  end;

  Img := Sheet.Images.FindAt(3, 2);   // nil-check before use
  if Img <> nil then
    Img.Scale(80);
end;

Como as propriedades em unidade real são visões vivas, uma imagem importada de outra ferramenta em algum tamanho EMU já informa a geometria em centímetros na hora, sem nenhuma etapa de conversão da sua parte. Isso combina naturalmente com o modelo de desenho mais amplo; se você está posicionando gráficos e formas além de imagens raster, o guia complementar sobre gráficos, imagens e desenhos do Excel no HotXLS cobre o modelo de ancoragem que esses objetos compartilham

Margens métricas de configuração de página

A mesma tensão entre EMU e unidades reais aparece um nível acima, na página. OOXML e Excel armazenam margens de impressão em polegadas, o que é incômodo se seus modelos de relatório são especificados em milímetros, como a maior parte do mundo fora dos EUA. A v2.91.0 adiciona wrappers em centímetros sobre as margens em polegadas: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM e MarginFooterCM. Cada um é uma conveniência fina sobre a propriedade em polegadas correspondente, convertendo na razão exata de 1 polegada = 2,54 cm

Sheet.MarginLeftCM := 2.0;     // 2 cm  == 0.7874 inch
Sheet.MarginRightCM := 2.0;
Sheet.MarginTopCM := 2.5;
Sheet.MarginBottomCM := 2.5;
Sheet.MarginHeaderCM := 1.0;
Sheet.MarginFooterCM := 1.0;

As propriedades em polegadas (MarginLeft e companhia) continuam sendo o armazenamento canônico, então você pode misturar as duas, definir a margem superior em centímetros e lê-la de volta em polegadas, ou vice-versa, e o arquivo gravado em disco sai idêntico de qualquer forma. A conversão é uma multiplicação simples por 2,54, sem arredondamento para uma grade grosseira, então 2 cm continuam 2 cm em precisão total de double. É a mesma filosofia de conveniência métrica da geometria de imagem: o formato fala imperial por baixo, e a biblioteca deixa você escrever na unidade em que sua especificação foi redigida. Para o layout do relatório ao redor, títulos, blocos de metadados e totais, veja células mescladas e layout de modelo de relatório no HotXLS, que usa essas margens junto com regiões mescladas e uma área de impressão

O que a geometria garante, e o que fica fora dela

As propriedades de geometria controlam o tamanho declarado da imagem no arquivo, o tamanho em que um consumidor compatível a renderizará. Elas não reamostram os bytes da imagem; um PNG de 50×50 pixels dimensionado para 8 cm será ampliado e ficará quadrado, exatamente como ficaria no Excel. Dimensionamento é uma operação de layout, não de processamento de imagem, então forneça à imagem resolução de origem suficiente para o tamanho físico pretendido. A biblioteca também não recodifica formatos: os bytes que você passa para AddImage são armazenados e gravados como estão, com o TXLSXImageFormat que você declarar. Passe bytes JPEG mas marque como xlsxImagePng e você produzirá um arquivo que o Excel não abre, então deixe AddImageFromFile inferir o formato pela extensão quando puder

Nada disso é exótico quando você internaliza a única ideia por trás: em OOXML, tamanho físico é a grandeza real e pixels são uma sombra derivada e dependente de DPI. Escreva imagens e margens em centímetros, polegadas ou pontos, deixe o HotXLS mapeá-los para EMU exato, e suas notas fiscais e relatórios serão impressos no mesmo tamanho em qualquer máquina que os abrir

As APIs de geometria de imagem, escala e margens métricas descritas aqui fazem parte do componente de planilhas HotXLS para Delphi, que lê e grava XLS e XLSX em Delphi e C++Builder sem exigir instalação do Excel