Vous insérez un logo de 600×400 pixels dans l’en-tête d’une facture générée, il s’affiche correctement sur votre moniteur de développement à 96 DPI, puis une semaine plus tard un client sur un ordinateur portable à écran haute résolution signale qu’il s’imprime à la taille d’un timbre-poste. Les pixels n’ont jamais changé. Ce qui a changé, c’est l’hypothèse selon laquelle un nombre de pixels correspond à une taille physique, et dans OOXML ce n’est pas le cas. Une image de feuille de calcul porte ses dimensions en EMU, et tant que vous ne raisonnez pas en EMU - ou dans les unités réelles qui s’y traduisent proprement - votre mise en page dépend du DPI que la machine de rendu suppose
HotXLS est un composant VCL natif de tableur pour Delphi et C++Builder qui lit et écrit les formats XLS et XLSX sans Excel ni aucune dépendance COM. Depuis la version v2.91.0, l’objet image XLSX ne vous oblige plus à faire l’arithmétique des unités à la main : à côté de l’EMU brut, il expose la largeur et la hauteur en centimètres, en pouces et en points, plus une Scale méthode qui redimensionne selon un pourcentage avec verrouillage optionnel du ratio d’aspect. Cet article explique ce qu’est réellement un EMU, pourquoi DrawingML l’a choisi, et comment utiliser la nouvelle surface géométrique pour placer des images selon leur taille physique plutôt que selon un nombre de pixels auquel vous ne pouvez pas vous fier
Ce qu’est un EMU et pourquoi DrawingML en utilise un
EMU signifie English Metric Unit, et c’est l’unité de longueur de base de DrawingML, la couche de dessin partagée par toute la famille Office Open XML (ECMA-376, Part 1, §20). Un EMU est défini de sorte qu’il y ait exactement 914400 EMU par pouce et 360000 EMU par centimètre. Ces deux constantes sont toute la raison d’être de l’unité. 914400 est divisible par 2, 3, 4, 5, 6, 8, 9, 10, 12 et bien d’autres encore ; sa factorisation est 26 × 32 × 52 × 127. Comme 1 pouce = 2,54 cm exactement, choisir une unité divisible à la fois par 360000 et par une fraction propre de 914400 permet au format d’exprimer les pouces, les centimètres et les points sous forme d’entiers sans aucun arrondi à la frontière de l’unité. Là où un flottant « 1,27 cm » dériverait, EMU stockerait 457200 et resterait exact
L’autre unité importante ici est le point. Un point typographique vaut 1/72 de pouce, donc il y a 12700 EMU par point (914400 / 72). C’est ainsi qu’Excel pense lui-même les hauteurs de ligne, les tailles de police et les marges en interne, ce qui explique pourquoi exposer la géométrie d’une image en points est utile lorsque vous voulez qu’une image s’aligne sur les métriques du texte plutôt que sur une règle imprimée. HotXLS encode les quatre relations sous forme de constantes d’unité dans la bibliothèque :
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)
Cette dernière ligne est le cœur du bug du timbre-poste. Un pixel n’a une taille physique qu’une fois le DPI fixé, et 9525 EMU correspond à la taille d’un pixel à 96 DPI précisément. Le DPI de rendu par défaut d’Excel est 96, donc une image de 100 pixels tombe à 100 × 9525 = 952500 EMU ≈ 2,54 cm dans une configuration par défaut - mais rien dans le fichier ne garantit que le consommateur utilise 96. Rédigez en unités réelles et cette ambiguïté disparaît : 4 cm restent 4 cm, que l’écran soit à 96 ou à 220 DPI
La surface géométrique de TXLSXImage
Une image intégrée dans HotXLS est un TXLSXImage. Son stockage canonique repose sur deux champs entiers, WidthEMU et HeightEMU, ancrés sur un Row et une Col à base 1 (la cellule en haut à gauche à laquelle l’image est rattachée). Les propriétés en unités réelles sont des vues calculées au-dessus de ces champs EMU, et non un état séparé - la lecture de WidthCM divise l’EMU par 360000, et son écriture le multiplie puis arrondit en sens inverse. Donc chaque dimension que vous définissez n’est qu’une autre manière d’écrire la même valeur EMU sous-jacente :
WidthInch/HeightInch- EMU ÷ 914400WidthCM/HeightCM- EMU ÷ 360000WidthPt/HeightPt- EMU ÷ 12700WidthEMU/HeightEMU- la source de vérité entière
Vous ajoutez une image avec AddImage(ARow, ACol, AData, AFormat), en passant les octets bruts encodés et une TXLSXImageFormat (xlsxImagePng, xlsxImageJpeg, xlsxImageGif ou xlsxImageBmp); elle renvoie l’index à base zéro dans la collection Images de la feuille de calcul. Il existe aussi AddImageFromFile(ARow, ACol, AFileName), qui déduit le format à partir de l’extension du fichier. Notez la base d’indexation : AddImage renvoie un index à base zéro et Images[] est à base zéro, ce qui contraste volontairement avec la Cells[Row, Col] à base 1, donc ne supposez pas que les deux coïncident
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;
Une image nouvellement créée adopte par défaut une taille de 100×100 pixels, soit un carré de 952500 EMU, environ une boîte de 2,54 cm à 96 DPI. Cette valeur par défaut existe pour que l’image reste visible même si vous oubliez de la dimensionner, mais pour toute mise en page réelle vous devriez définir une taille physique explicite plutôt que compter sur ce défaut dérivé des pixels
La mise à l’échelle et le drapeau de ratio d’aspect
Lorsque vous voulez redimensionner relativement aux dimensions actuelles plutôt qu’à une cible absolue - par exemple, réduire une image de graphique à 60 % de sa taille d’import - utilisez Scale:
procedure Scale(APercent: Double; AKeepAspect: Boolean = True);
APercent correspond à un pourcentage où 100 laisse inchangé, 150 agrandit de moitié, 50 divise par deux. Avec AKeepAspect à sa valeur par défaut True, la largeur et la hauteur sont toutes deux multipliées par le même facteur, donc les proportions sont conservées et une image de 4×3 cm devient 6×4,5 cm après Scale(150). Passez False et seule la largeur se met à l’échelle - la hauteur reste exactement telle quelle. Cette asymétrie est volontaire : lorsque vous voulez étirer un axe de façon indépendante, l’outil approprié est les WidthCM/HeightCM setters, et la branche sans conservation du ratio de Scale il est facile de lire Scale(150, False) comme « étirer librement les deux axes » et d’avoir une surprise, alors utilisez les setters lorsque vous voulez réellement deux dimensions indépendantes
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 petit comportement à connaître : Scale(100) s’interrompt et retourne sans toucher à l’un ou l’autre champ, donc il est sûr de l’appeler inconditionnellement dans une boucle où le pourcentage pourrait être 100. Et comme la géométrie est stockée en entier EMU, chaque setter arrondit. Ainsi, les allers-retours via des centimètres fractionnaires peuvent dériver d’une fraction d’EMU - bien en dessous de toute différence visible, mais bon à savoir si vous vérifiez un jour une égalité exacte dans un test. Pour un contrôle au pixel près, définissez WidthEMU et HeightEMU directement et sautez complètement la conversion d’unité
Lecture des géométries
La collection d’images est interrogeable, ce qui importe quand vous chargez un classeur existant et devez inspecter ou ajuster ce qui s’y trouve déjà plutôt que ce que vous venez d’ajouter. Images.Count répertorie chaque image sur la feuille,Images[i] indexe les éléments à base zéro, et FindAt(ARow, ACol) renvoie l’image ancrée à une cellule donnée - ou nil si aucune ne l’est. Il existe aussi IndexOfCell pour obtenir l’index plutôt que l’objet, et DeleteAt / DeleteInRange pour la suppression
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;
Parce que les propriétés en unités réelles sont des vues vivantes, une image importée à une certaine taille en EMU depuis un autre outil affiche immédiatement sa géométrie en centimètres - aucune conversion à faire de votre côté. Cela s’accorde naturellement avec le modèle de dessin plus large ; si vous placez des graphiques et des formes en plus des images matricielles, le guide compagnon sur HotXLS : graphiques, images et dessins Excel dans Delphi couvre le modèle d’ancrage que ces objets partagent
Marges métriques de mise en page
La même tension entre EMU et unités réelles réapparaît un cran plus loin, au niveau de la page. OOXML et Excel stockent les marges d’impression en pouces, ce qui est peu pratique si vos modèles de rapport sont spécifiés en millimètres comme dans la plupart des pays hors des États-Unis. La version v2.91.0 ajoute des enveloppes en centimètres autour des marges en pouces : MarginLeftCM, MarginRightCM, MarginTopCM, MarginBottomCM, MarginHeaderCM, et MarginFooterCM. Chacune est un simple raccourci au-dessus de la propriété en pouces correspondante, avec conversion au ratio exact de 1 pouce = 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;
Les propriétés en pouces (MarginLeft et leurs semblables) restent le stockage canonique, donc vous pouvez mélanger les deux - définir une marge supérieure en centimètres puis la relire en pouces, ou l’inverse - et le fichier écrit sur disque est identique dans les deux cas. La conversion se limite à une multiplication par 2,54, sans arrondi sur une grille grossière, donc 2 cm restent 2 cm avec toute la précision double. C’est la même philosophie de commodité métrique que pour la géométrie des images : le format parle impérial en interne, et la bibliothèque vous laisse rédiger dans l’unité utilisée par votre spécification. Pour la mise en page du rapport autour - titres, blocs de métadonnées, totaux - voir mise en page des cellules fusionnées et des modèles de rapport dans HotXLS, qui utilise ces marges avec des plages fusionnées et une zone d’impression
Une note sur ce que la géométrie garantit et ne garantit pas
Les propriétés de géométrie contrôlent la déclarée taille de l’image dans le fichier - la taille à laquelle un consommateur conforme la rendra. Elles ne rééchantillonnent pas les octets de l’image ; un PNG de 50×50 pixels dimensionné à 8 cm sera agrandi et apparaîtra grossier, exactement comme dans Excel. Le dimensionnement est une opération de mise en page, pas de traitement d’image, alors fournissez à l’image une résolution source suffisante pour la taille physique visée. La bibliothèque ne réencode pas non plus les formats : les octets que vous passez à AddImage sont stockés et réécrits tels quels, avec le TXLSXImageFormat que vous déclarez. Passez des octets JPEG mais étiquetez-les xlsxImagePng et vous produirez un fichier qu’Excel ne peut pas ouvrir, donc laissez AddImageFromFile déduire le format à partir de l’extension lorsque c’est possible
Rien de tout cela n’est exotique une fois que l’on internalise l’idée sous-jacente : dans OOXML, la taille physique est la vraie grandeur et les pixels en sont une ombre dérivée, dépendante du DPI. Spécifiez les images et les marges en centimètres, en pouces ou en points, laissez HotXLS les mapper sur des EMU exacts, et vos factures et rapports s’imprimeront à la même taille sur chaque machine qui les ouvre
Les API de géométrie d’image, de mise à l’échelle et de marges métriques décrites ici sont fournies avec le composant feuille de calcul Delphi HotXLS, qui lit et écrit les formats XLS et XLSX depuis Delphi et C++Builder sans installation d’Excel requise