Insertas un logotipo 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 un portátil de alta densidad de píxeles informa de que se imprime del tamaño de un sello postal. Los píxeles no cambiaron. Lo que cambió fue la suposición de que un conteo de píxeles equivale a un tamaño físico, y en OOXML no es así. Una imagen de hoja de cálculo guarda sus dimensiones en EMU, y hasta que pienses en EMU, o en las unidades reales que se traducen limpiamente a él, tu diseño quedará a merced del DPI que el equipo de renderizado decida asumir
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 ninguna dependencia COM. A partir de la versión v2.91.0, el objeto de imagen XLSX deja de obligarte a hacer a mano la aritmética de unidades: además del EMU bruto, expone ancho y alto en centímetros, pulgadas y puntos, y un método Scale que redimensiona por porcentaje con bloqueo opcional de la proporción de aspecto. Este artículo explica qué es realmente el 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 lugar 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, Parte 1, §20). Un EMU se define de forma que haya exactamente 914400 EMU por pulgada y 360000 EMU por centímetro. Esas dos constantes son la razón de ser de la unidad. 914400 es divisible por 2, 3, 4, 5, 6, 8, 9, 10, 12 y muchos más; se descompone como 26 × 32 × 52 × 127. Como 1 pulgada = 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 valor en coma flotante de "1,27 cm" se desviaría, EMU almacena 457200 y sigue siendo exacto
La otra unidad que importa aquí es el punto. Un punto tipográfico equivale a 1/72 de pulgada, 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 lo que exponer la geometría de la imagen en puntos resulta útil cuando quieres que una imagen se alinee con las métricas del texto y no 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 el núcleo del problema del sello postal. Un píxel solo tiene un 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 queda en 100 × 9525 = 952500 EMU ≈ 2,54 cm en una configuración predeterminada, pero nada en el archivo garantiza que el consumidor use 96. Si trabajas en unidades reales, esa ambigüedad desaparece: 4 cm son 4 cm tanto si la pantalla está a 96 como a 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 en una Row y una Col de base uno, es decir, la celda superior izquierda desde la que cuelga la imagen. Las propiedades de unidad real son vistas calculadas sobre esos campos EMU, no estado separado: leer WidthCM divide el EMU entre 360000, y escribirlo multiplica y redondea de vuelta. Así que cada dimensión que estableces no es más que una forma distinta de escribir el mismo valor EMU subyacente:
WidthInch/HeightInch- EMU ÷ 914400WidthCM/HeightCM- EMU ÷ 360000WidthPt/HeightPt- EMU ÷ 12700WidthEMU/HeightEMU- la fuente de verdad entera
Añades una imagen con AddImage(ARow, ACol, AData, AFormat), pasando los bytes codificados en bruto y un TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif o xlsxImageBmp); devuelve el índice de 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. Fíjate en la base del índice: AddImage devuelve base cero e Images[] también es base cero, en contraste deliberado con la cuadrícula Cells[Row, Col], que es base 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 creada desde cero arranca con un tamaño predeterminado de 100×100 píxeles, es decir, 952500 EMU cuadrados, aproximadamente un cuadro 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 conviene fijar un tamaño físico explícito en lugar de confiar en el valor predeterminado derivado de píxeles
Escalado y el indicador de proporción de aspecto
Cuando quieres redimensionar en relación con las dimensiones actuales y no hacia un objetivo absoluto, por ejemplo 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 en el que 100 significa sin cambios, 150 aumenta a la mitad y 50 divide entre dos. Con AKeepAspect en su valor predeterminado True, tanto el ancho como el alto se multiplican por el mismo factor, de modo que las proporciones se conservan y una imagen de 4×3 cm pasa a 6×4,5 cm después de Scale(150). Si pasas False, solo se escala el ancho; el alto queda exactamente como estaba. Esa asimetría es intencional: cuando quieres estirar un eje de forma independiente, la herramienta adecuada son los setters explícitos WidthCM/HeightCM, y la rama no proporcional de Scale está pensada para el caso más estrecho de ajustar solo el ancho. Es fácil leer Scale(150, False) como "estirar ambos libremente" y llevarse una sorpresa, así que recurre a los setters cuando de verdad 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) hace una salida rápida y vuelve sin tocar ninguno de los dos campos, así que es seguro llamarlo de forma incondicional en un bucle en el que el porcentaje pueda ser 100. Y como la geometría se guarda como EMU enteros, cada setter redondea. Por tanto, el ida y vuelta a través de centímetros fraccionarios puede desviarse en una fracción de EMU, muy por debajo de lo visible, pero conviene saberlo si alguna vez afirmas igualdad exacta en una prueba. Para un control perfecto de píxel, asigna WidthEMU y HeightEMU directamente y omite por completo la conversión de unidades
Lectura de la geometría
La colección de imágenes se puede consultar, y eso importa cuando cargas un libro existente y necesitas inspeccionar o ajustar lo que ya estaba ahí, en lugar de lo que acabas de añadir. Images.Count enumera cada imagen de la hoja, Images[i] las indexa desde cero, y FindAt(ARow, ACol) devuelve la imagen anclada en una celda concreta, 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 unidad real son vistas vivas, una imagen importada desde otra herramienta con cierto tamaño EMU informa de su geometría en centímetros de inmediato, sin que tengas que hacer ninguna conversión. Esto encaja de forma natural con el modelo de dibujo más amplio; si colocas gráficos y formas además de imágenes rasterizadas, la guía complementaria sobre gráficos, imágenes y dibujos de Excel 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 arriba, 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 ocurre en la mayor parte del mundo fuera de EE. UU. v2.91.0 añade envoltorios en centímetros sobre los márgenes en pulgadas: MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM y MarginFooterCM. Cada uno es una comodidad fina sobre la propiedad equivalente en pulgadas, y convierte con la relación exacta de 1 pulgada = 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 las demás) siguen siendo el almacenamiento canónico, así que puedes mezclar ambas, fijar un margen superior en centímetros y leerlo en pulgadas, o al revés, y el archivo escrito en disco será idéntico en ambos casos. La conversión es una simple multiplicación 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 comodidad métrica que la geometría de las imágenes: el formato habla en imperial por debajo, y la biblioteca te permite escribir en la unidad en la que esté redactada tu especificación. Para maquetar el informe que lo rodea, 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 garantiza y lo que no
Las propiedades de geometría controlan el tamaño declarado de la imagen en el archivo, es decir, el tamaño al que la renderizará un consumidor conforme. No remuestrean los bytes de la imagen; un PNG de 50×50 píxeles dimensionado a 8 cm se escalará hacia arriba y se verá cuadriculado, exactamente igual que en Excel. El dimensionado es una operación de diseño, 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 vuelve a codificar 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 interiorizas la idea que hay debajo: en OOXML, el tamaño físico es la cantidad real y los píxeles son una sombra derivada de él, dependiente del DPI. Escribe imágenes y márgenes en centímetros, pulgadas o puntos, deja que HotXLS los lleve a EMU exactos, y tus facturas e informes se imprimirán al mismo tamaño en cualquier máquina que los abra
Las API de geometría de imagen, escalado y márgenes métricos descritas aquí se incluyen con el componente de hojas de cálculo Delphi de HotXLS, que lee y escribe XLS y XLSX desde Delphi y C++Builder sin necesidad de instalar Excel