Artículo técnico

Geometría de imágenes de HotXLS en Delphi: EMU, cm y escalado

Pones un logo de 600×400 píxeles en el encabezado de una factura generada, se ve bien en tu monitor de desarrollo de 96 DPI, y una semana después un cliente con una laptop de alta densidad informa que se imprime del tamaño de una estampilla. Los píxeles nunca cambiaron. Lo que cambió es la suposición de que un conteo de píxeles equivale a un tamaño físico, y en OOXML eso no ocurre. Una imagen de hoja de cálculo lleva sus dimensiones en EMU, y hasta que pienses en EMU, o en las unidades reales que se mapean limpiamente a él, el diseño queda a merced del DPI que el equipo de renderizado asuma

HotXLS es un componente nativo VCL de hojas de cálculo para Delphi y C++Builder que lee y escribe XLS y XLSX sin Excel ni dependencia de COM. Desde v2.91.0 el objeto de imagen XLSX deja de obligarte a hacer la aritmética de unidades a mano: junto con el EMU sin procesar expone ancho y alto en centímetros, pulgadas y puntos, además de un método Scale que redimensiona por porcentaje con bloqueo opcional de relación de aspecto. Este artículo trata sobre qué es realmente EMU, por qué DrawingML lo eligió y cómo usar la nueva superficie de geometría para colocar imágenes por tamaño físico en vez de por un conteo de píxeles en el que no puedes confiar

Qué es un EMU y por qué DrawingML usa uno

EMU significa English Metric Unit, y es la unidad base de longitud de DrawingML, la capa de dibujo compartida por toda la familia Office Open XML (ECMA-376, Part 1, §20). Un EMU se define de modo que haya exactamente 914400 EMU por pulgada y 360000 EMU por centímetro. Esas dos constantes son toda la razón de existir de la unidad. 914400 es divisible por 2, 3, 4, 5, 6, 8, 9, 10, 12 y muchos más; se factoriza como 26 × 32 × 52 × 127. Como 1 inch = 2.54 cm exactamente, elegir una unidad divisible tanto por 360000 como por una fracción limpia de 914400 permite que el formato exprese pulgadas, centímetros y puntos como enteros sin redondeo en el límite de la unidad. Donde un "1.27 cm" en punto flotante derivaría, EMU guarda 457200 y permanece exacto

La otra unidad que importa aquí es el punto. Un punto tipográfico es 1/72 inch, así que hay 12700 EMU por punto (914400 / 72). Los puntos son la forma en que Excel piensa internamente en la altura de fila, el tamaño de fuente y los márgenes, por eso exponer la geometría de imágenes en puntos es útil cuando quieres que una imagen se alinee con métricas de texto en lugar de con una regla impresa. HotXLS codifica las cuatro relaciones como constantes de unidad en la 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)

Esa última línea es la clave del error de la estampilla. Un píxel solo tiene tamaño físico cuando fijas un DPI, y 9525 EMU es el tamaño de un píxel específicamente a 96 DPI. El DPI de renderizado predeterminado de Excel es 96, así que una imagen de 100 píxeles llega a 100 × 9525 = 952500 EMU ≈ 2.54 cm en una configuración predeterminada, pero nada en el archivo garantiza que el consumidor use 96. Especifica en unidades reales y esa ambigüedad desaparece: 4 cm son 4 cm tanto si la pantalla es de 96 como de 220 DPI

La superficie de geometría de TXLSXImage

Una imagen incrustada en HotXLS es un TXLSXImage. Su almacenamiento canónico son dos campos enteros, WidthEMU y HeightEMU, anclados a un Row y Col basados en uno, que corresponden a la celda superior izquierda de la que cuelga la imagen. Las propiedades de unidades reales son vistas calculadas sobre esos campos EMU, no estado separado; leer WidthCM divide el EMU entre 360000, y escribirla multiplica y redondea de vuelta. Así que cada dimensión que defines es solo otra forma de escribir el mismo valor EMU subyacente:

  • WidthInch / HeightInch - EMU ÷ 914400
  • WidthCM / HeightCM - EMU ÷ 360000
  • WidthPt / HeightPt - EMU ÷ 12700
  • WidthEMU / HeightEMU - la fuente de verdad entera

Agregas una imagen con AddImage(ARow, ACol, AData, AFormat), pasando los bytes codificados sin procesar y un TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif o xlsxImageBmp); devuelve el índice base cero dentro de la colección Images de la hoja. También existe AddImageFromFile(ARow, ACol, AFileName), que infiere el formato a partir de la extensión del archivo. Nota la base del índice: AddImage devuelve base cero y Images[] también es base cero, un contraste deliberado con la cuadrícula Cells[Row, Col] basada en uno, así que no asumas que ambas coinciden

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;

Una imagen recién creada usa de forma predeterminada 100×100 píxeles, es decir 952500 EMU cuadrados, aproximadamente una caja de 2.54 cm a 96 DPI. Ese valor predeterminado existe para que la imagen sea visible aunque olvides dimensionarla, pero para cualquier diseño real debes fijar un tamaño físico explícito en lugar de confiar en el valor predeterminado derivado de píxeles

Escalado y la marca de relación de aspecto

Cuando quieras redimensionar con respecto a las dimensiones actuales en vez de a un destino absoluto, por ejemplo para reducir una imagen de gráfico al 60% de lo que importó, usa Scale:

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

APercent es un porcentaje donde 100 significa sin cambios, 150 agranda a la mitad y 50 reduce a la mitad. Con AKeepAspect en su valor predeterminado True, tanto el ancho como el alto se multiplican por el mismo factor, así que las proporciones se conservan y una imagen de 4×3 cm pasa a 6×4.5 cm después de Scale(150). Pasa False y solo se escala el ancho; el alto se deja exactamente como estaba. Esa asimetría es intencional: cuando quieres estirar un eje de forma independiente, la herramienta correcta son los setters explícitos WidthCM y HeightCM, y la rama sin relación de aspecto de Scale está ahí para el caso más estrecho de ajustar solo el ancho. Es fácil leer Scale(150, False) como "estira ambos libremente" y llevarse una sorpresa, así que usa los setters cuando realmente quieras dos dimensiones independientes

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

Un pequeño comportamiento que conviene conocer: Scale(100) se corta y devuelve sin tocar ninguno de los dos campos, así que es seguro llamarlo sin condición en un bucle donde el porcentaje pueda ser 100. Y como la geometría se almacena como EMU enteros, cada setter redondea. Hacer ida y vuelta mediante centímetros fraccionarios puede desviarse por una fracción de EMU, muy por debajo de cualquier cosa visible, pero útil de saber si alguna vez afirmas igualdad exacta en una prueba. Para control perfecto al píxel, define WidthEMU y HeightEMU directamente y omite por completo la conversión de unidades

Leer la geometría de vuelta

La colección de imágenes se puede consultar, lo cual importa cuando cargas un libro existente y necesitas inspeccionar o ajustar lo que ya está ahí en lugar de lo que acabas de agregar. Images.Count enumera cada imagen de la hoja, Images[i] las indexa base cero, y FindAt(ARow, ACol) devuelve la imagen anclada en una celda específica, o nil si no hay ninguna. También existe IndexOfCell para obtener el índice en lugar del objeto, y DeleteAt / DeleteInRange para eliminar

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 las propiedades de unidades reales son vistas vivas, una imagen importada con cierto tamaño EMU desde otra herramienta reporta su geometría en centímetros de inmediato, sin que tengas que convertirla. Esto encaja de forma natural con el modelo de dibujo más amplio; si colocas gráficos y formas además de imágenes raster, la guía complementaria sobre gráficos, imágenes y dibujos de Excel de HotXLS en Delphi cubre el modelo de anclaje que comparten esos objetos

Márgenes métricos de configuración de página

La misma tensión entre EMU y unidades reales aparece un nivel más afuera, en la página. OOXML y Excel almacenan los márgenes de impresión en pulgadas, lo cual resulta incómodo si tus plantillas de informe están especificadas en milímetros como en la mayor parte del mundo fuera de EE. UU. La v2.91.0 agrega envolturas en centímetros sobre los márgenes en pulgadas: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM y MarginFooterCM. Cada una es una ayuda delgada sobre la propiedad correspondiente en pulgadas, con conversión exacta a la relación de 1 inch = 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;

Las propiedades en pulgadas (MarginLeft y sus pares) siguen siendo el almacenamiento canónico, así que puedes mezclar ambas: fijar un margen superior en centímetros y leerlo de vuelta en pulgadas, o viceversa, y el archivo escrito en disco es idéntico en ambos casos. La conversión es una multiplicación simple por 2.54, sin redondeo a una rejilla gruesa, así que 2 cm siguen siendo 2 cm con precisión completa de doble. Esta es la misma filosofía de conveniencia métrica que la geometría de imágenes: el formato habla en sistema imperial por debajo, y la biblioteca te deja escribir en la unidad en la que esté redactada tu especificación. Para maquetar el informe circundante, títulos, bloques de metadatos y totales, consulta celdas combinadas y diseño de plantillas de informe en HotXLS, que usa estos márgenes junto con rangos combinados y un área de impresión

Una nota sobre lo que la geometría sí y no garantiza

Las propiedades de geometría controlan el tamaño declarado de la imagen en el archivo, es decir, el tamaño con el que un consumidor conforme la renderizará. No remuestrean los bytes de la imagen; un PNG de 50×50 píxeles dimensionado a 8 cm se ampliará y se verá pixelado, exactamente como en Excel. Dimensionar es una operación de maquetación, no de procesamiento de imagen, así que dale a la imagen suficiente resolución de origen para el tamaño físico que pretendes. La biblioteca tampoco recodifica formatos: los bytes que pasas a AddImage se almacenan y se escriben tal cual, con el TXLSXImageFormat que declares. Si pasas bytes JPEG pero los etiquetas como xlsxImagePng, producirás un archivo que Excel no puede abrir, así que deja que AddImageFromFile infiera el formato a partir de la extensión cuando puedas

Nada de esto es exótico una vez que interiorizas la única idea que lo sostiene: en OOXML, el tamaño físico es la cantidad real y los píxeles son una sombra derivada, dependiente del DPI. Redacta imágenes y márgenes en centímetros, pulgadas o puntos, deja que HotXLS los mapee a EMU exactos y tus facturas e informes se imprimirán del mismo tamaño en cualquier máquina que los abra

Las API de geometría de imágenes, escalado y márgenes métricos descritas aquí vienen con el componente de hoja de cálculo HotXLS para Delphi, que lee y escribe XLS y XLSX desde Delphi y C++Builder sin requerir una instalación de Excel