HotXLS lit la vraie géométrie en pixels d'une image depuis l'en-tête du fichier image lui-même avant qu'aucune géométrie de dessin soit écrite. GetImageSize dans lxImage.pas distribue sur l'extension de fichier vers GetJpegSize, GetGifSize, GetBmpSize, GetPngSize ou GetWmfEmfSize, convertit les pixels en points avec un facteur de 0.75, et retourne -1 quand il ne peut vraiment pas le dire
Cette dernière clause est la partie qui a mis le plus longtemps à être juste. Un lecteur de taille qui ne peut pas échouer est un lecteur de taille qui ment, et un mensonge sur la géométrie d'une image ne remonte pas au moment de l'insertion. Il remonte des semaines plus tard, quand quelqu'un ouvre le classeur et que le logo de la société est un rectangle écrasé
Pourquoi une image a-t-elle besoin de sa vraie géométrie avant d'aller dans le dessin ?
Parce que le format de feuille de calcul stocke l'étendue, pas l'image. Quand AddPicture ou Images.AddFromFile place une image, la partie dessin enregistre une largeur et une hauteur explicites pour la forme — en DrawingML, ce sont les attributs cx et cy de l'élément a:ext, en unités métriques anglaises, selon ECMA-376 Partie 1 §20.4. Rien dans cet enregistrement ne dit « utilise la taille naturelle de l'image », donc la taille naturelle doit venir de quelque part, et le seul endroit où elle existe, c'est les octets de l'image. Faites une erreur d'un facteur trois et l'image est étirée d'un facteur trois, dans chaque application qui ouvre le classeur, pour toujours. La conversion d'unités de ces dimensions en pixels jusqu'aux EMU est un souci séparé, couvert dans l'article sur les unités EMU et la mise à l'échelle ; ce morceau parle de l'étape d'avant, celle où les nombres de pixels sont obtenus tout court. Le repli quand un lecteur échoue est 100 par 100, et ce nombre est délibérément, évidemment faux. Ce n'est pas une estimation d'une taille plausible, c'est un marqueur qui veut dire « personne n'a pu lire ce fichier », et c'est bien mieux qu'un nombre faux à l'air plausible parce qu'un utilisateur le signale immédiatement
GIF : le descripteur qui n'a jamais été analysé
GetGifSize est nouveau, et avant qu'il n'existe, chaque GIF inséré dans une feuille de calcul ressortait à exactement 100 par 100 — il n'y avait aucune branche GIF du tout, donc l'extension retombait sur le bras par défaut de GetImageSize et retournait -1. C'est l'en-tête le moins cher de l'ensemble à analyser, ce qui rend l'omission légèrement embarrassante et très facile à corriger. La spécification GIF89a place le descripteur d'écran logique immédiatement après la signature de six octets, et §18 définit ses deux premiers champs comme la largeur et la hauteur de l'écran logique, chacune une valeur non signée de 16 bits stockée en little-endian. En termes de fichier, ce sont les octets 6 et 7 pour la largeur, les octets 8 et 9 pour la hauteur, donc une seule lecture de dix octets couvre la vérification de signature et les deux dimensions
function GetGifSize(const AFileName: WideString; var AWidth, AHeight: Double): Integer;
var
f: TWFileStream;
Header: array [0.. 9] of Byte;
wWidth, wHeight: Word;
begin
Result:= - 1;
AWidth:= 100;
AHeight:= 100;
f:= TWFileStream.Create(AFileName, fmOpenRead);
try
// Signature « GIF8 », puis le descripteur d'écran logique avec la largeur et
// la hauteur en mots little-endian aux offsets 6 et 8
if (f.Read(Header[0], SizeOf(Header))= SizeOf(Header))and
(Header[0]= Ord('G'))and (Header[1]= Ord('I'))and
(Header[2]= Ord('F'))and (Header[3]= Ord('8')) then
begin
wWidth:= Header[6]or (Header[7] shl 8);
wHeight:= Header[8]or (Header[9] shl 8);
if (wWidth> 0)and (wHeight> 0) then
begin
AWidth:= wWidth* 0.75;
AHeight:= wHeight* 0.75;
Result:= 1;
end;
end;
finally
f.Free;
end;
end;
Notez sa forme : les valeurs d'échec sont affectées d'abord, et le chemin de succès est la seule chose qui les écrase. Chaque lecteur dans lxImage.pas suit cet ordre, parce que cela fait de « je n'ai pas pu lire ceci » le défaut plutôt que quelque chose à se rappeler à chaque sortie anticipée. Ne vérifier que GIF8 plutôt que la chaîne complète GIF87a ou GIF89a est intentionnel — la disposition du descripteur est identique dans les deux révisions, et il n'y a aucun bénéfice à rejeter un fichier dont la géométrie est lisible
Que change réellement une taille d'en-tête BMP de 12 ?
GetBmpSize lit les quatre octets à l'offset 14 comme la taille de l'en-tête DIB et se branche sur la valeur, parce qu'un BMP avec une taille d'en-tête de 12 stocke ses dimensions dans une largeur de champ complètement différente de tous les autres BMP. [MS-WMF] §2.2.2 dispose la famille : BITMAPCOREHEADER fait 12 octets et porte une largeur et une hauteur de 16 bits, tandis que BITMAPINFOHEADER, BITMAPV4HEADER et BITMAPV5HEADER commencent tous par des biWidth et biHeight signés de 32 bits. Lire quatre octets là où le fichier en a deux n'échoue pas bruyamment ; cela attire le compte de plans et la profondeur de bits comme moitié haute d'un nombre et rapporte un bitmap de plusieurs centaines de milliers de pixels de large. Le second piège dans le même en-tête est le signe de biHeight : une valeur négative ne veut pas dire une hauteur négative, cela veut dire que les rangées DIB sont stockées du haut vers le bas plutôt que du bas vers le haut, une déclaration d'ordre de pixels qui n'a rien à voir avec la géométrie
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER stocke des dimensions de 16 bits
if (f.Read(wWidth, 2)= 2)and (f.Read(wHeight, 2)= 2) then
begin
lWidth:= wWidth;
lHeight:= wHeight;
Result:= 1;
end;
end
else if (f.Read(lWidth, 4)= 4)and (f.Read(lHeight, 4)= 4) then
Result:= 1;
end;
// ...
// Un biHeight négatif marque seulement un DIB du haut vers le bas ; la magnitude est la taille
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
La garde finale vaut la peine d'être lue de près. La hauteur est testée contre <> 0 plutôt que > 0, parce que le négatif est légal et que zéro ne l'est pas, et Abs n'est appliqué qu'au point de conversion. La largeur n'a pas de cas équivalent — un biWidth négatif n'est pas défini par le format, donc il est traité comme un fichier corrompu et dégradé en -1. Passer une valeur brute négative vers le haut produirait à la place une étendue négative dans l'enregistrement de dessin, qui est clampée, ignorée ou rendue comme une forme retournée selon l'application qui ouvre le classeur
JPEG : quels marqueurs sont vraiment des en-têtes de trame ?
GetJpegSize accepte l'ensemble de marqueurs [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF], et les trois trous dans cette plage sont tout le propos. ITU-T T.81 §B.2.2 définit l'en-tête de trame comme SOFn, et l'en-tête de trame est l'endroit où vivent le nombre de lignes et d'échantillons par ligne, dans chaque type de trame plutôt que seulement les courants. Les marqueurs qui tombent dans la même plage numérique mais ne sont pas des en-têtes de trame sont $C4 (DHT, définir les tables de Huffman), $C8 (JPG, réservé) et $CC (DAC, définir le conditionnement du codage arithmétique). Un lecteur qui ne reconnaît que la baseline, la séquentielle étendue et la progressive — $C0, $C1 et $C2 — marche sur presque tous les JPEG qu'un appareil photo ou un outil de capture produit, puis échoue tranquillement sur ceux qui comptent pour les flux documentaires : le JPEG sans perte ($C3), les variantes à codage arithmétique ($C9 à $CB) et les types de trames hiérarchiques ($CD à $CF) scannent tous au-delà de l'en-tête de trame, dépassent la fin du fichier et atterrissent dans le repli. L'imagerie médicale et les pipelines de documents scannés produisent exactement ces fichiers
// Tout marqueur SOFn porte les dimensions de trame ; $C4 (DHT), $C8 (JPG)
// et $CC (DAC) partagent la plage mais ne sont pas des en-têtes de trame
if Seg in [$C0.. $C3, $C5.. $C7, $C9.. $CB, $CD.. $CF] then
begin
ReadLen:= f.Read(Dummy[0], 3);
ReadLen:= f.Read(wHeight, 2);
wHeight:= swapByte(wHeight);
ReadLen:= f.Read(wWidth, 2);
wWidth:= swapByte(wWidth);
found:= True;
Break;
end
Deux détails dans ces sept lignes. Les trois octets sautés sont le mot de longueur de segment plus l'octet de précision d'échantillonnage, donc la lecture atterrit sur le compte de lignes ; JPEG stocke la hauteur avant la largeur, l'inverse de la plupart des formats et une transposition facile à commettre. Et les valeurs passent par swapByte parce que JPEG est big-endian de bout en bout, la même raison pour laquelle GetPngSize fait passer ses valeurs IHDR de 32 bits par lswapByte. La marche des segments a sa propre garde : quand un marqueur n'est pas sans paramètre, le lecteur prend la longueur de deux octets, l'échange d'octets, et refuse de seek si la valeur est en dessous de 2 — une longueur plus courte que son propre champ se traduirait par un seek en arrière, et un seek en arrière dans une boucle de scan est une boucle infinie sur un fichier contrôlé par quelqu'un d'autre
Retourner moins un bat retourner un nombre plausible
Le fil commun à travers les cinq lecteurs, c'est que GetImageSize distingue trois issues plutôt que deux. 1 veut dire que la géométrie a été lue depuis le fichier. -1 veut dire qu'elle ne l'a pas été, et l'appelant doit utiliser son défaut. 7 est retourné par GetWmfEmfSize et veut dire autre chose entièrement : il dit à AddPicture de prendre la taille depuis l'enregistrement BSE du métafichier à la place, parce qu'un WMF ou un EMF porte son étendue dans l'enregistrement d'image propre au classeur plutôt qu'à un offset fixe dans l'en-tête du fichier. La gestion des métafichiers en général est couverte dans l'article sur le décodeur EMF et WMF borné. GetJpegSize montre pourquoi cette distinction a besoin d'un drapeau explicite plutôt que d'une inférence : la boucle de scan peut se terminer de trois façons — elle a trouvé un en-tête de trame, elle a atteint la fin du fichier, ou elle a abandonné sur une longueur de segment corrompue — et seule la première affecte wWidth et wHeight. Sans le booléen found, les deux autres chemins convertiraient ce que ces locales contenaient par hasard et tendraient le résultat vers le haut comme un succès. Les locales non initialisées sont fréquemment zéro, et zéro est la pire réponse possible ici : elle survit à chaque vérification de validité en aval et produit une image invisible que personne ne peut sélectionner ni supprimer
var
W, H: Double;
begin
case GetImageSize(FileName, W, H) of
1: Sheet.AddPicture(FileName, Row, Col, W, H);
7: Sheet.AddPicture(FileName, Row, Col); // métafichier : taille depuis l'enregistrement BSE
else
// W et H valent 100 x 100 ; décidez une politique plutôt que d'accepter un placeholder
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
Où sont les limites honnêtes
Ces lecteurs analysent délibérément un en-tête puis s'arrêtent. Ils ne décodent pas les pixels, donc ils rapportent les dimensions stockées et rien de plus, et il y a des cas où les dimensions stockées ne sont pas toute l'histoire. Un GIF animé dont les trames sont plus petites que l'écran logique rapporte toujours l'écran logique, ce qui est correct pour la mise en page et peut ne pas correspondre à ce qu'un visualiseur montre. Un JPEG portant une balise d'orientation EXIF qui spécifie une rotation de 90 degrés rapporte les dimensions de trame non pivotées, donc une photo portrait prise sur un téléphone peut revenir en paysage. Un DIB avec un ratio d'aspect de pixels non carré rapporte des comptes de pixels bruts, et le facteur 0.75 suppose 96 DPI partout. Aucun de ces cas n'est un bug de la lecture d'en-tête et aucun n'est corrigeable sans décoder l'image, ce qui n'est pas le rôle de cette couche — ce sont des frontières documentées, donc si votre pipeline gère des photos de téléphone, lisez la balise d'orientation vous-même et échangez les valeurs avant d'appeler AddPicture. Tout le reste de la pose des images, de l'ancrage et du modèle d'objets de dessin est couvert dans l'article sur les graphiques, les images et les dessins
HotXLS lit et écrit XLS, XLSX, ODS et CSV nativement en Delphi et C++Builder sans aucune installation d'Excel requise, et l'insertion d'images passe par ce chemin d'en-tête dans chacun d'eux. Le support des formats et les détails de licence sont sur la page produit du HotXLS Delphi Component