HotXLS liest die echten Pixelmaße eines Bildes aus dem Header der Bilddatei selbst, bevor irgendeine Zeichengeometrie geschrieben wird. GetImageSize in lxImage.pas dispatched anhand der Dateiendung an GetJpegSize, GetGifSize, GetBmpSize, GetPngSize oder GetWmfEmfSize, konvertiert Pixel mit einem Faktor von 0.75 in Punkte und gibt -1 zurück, wenn es wirklich nicht sagen kann
Genau diese letzte Klausel war der Teil, der am längsten brauchte, um richtig zu werden. Ein Größen-Reader, der nicht scheitern kann, ist ein Größen-Reader, der lügt, und eine Lüge über Bildgeometrie taucht nicht beim Einfügen auf. Sie taucht Wochen später auf, wenn jemand die Arbeitsmappe öffnet und das Firmenlogo ein gequetschtes Rechteck ist
Warum braucht ein Bild seine echte Geometrie, bevor es ins Drawing geht?
Weil das Tabellenformat die Ausdehnung speichert, nicht das Bild. Wenn AddPicture oder Images.AddFromFile ein Bild platziert, zeichnet der Drawing-Part eine explizite Breite und Höhe für die Form auf, in DrawingML die cx- und cy-Attribute des a:ext-Elements, in English Metric Units, nach ECMA-376 Part 1 §20.4. Nichts in diesem Record sagt »nimm die natürliche Größe des Bildes«, also muss die natürliche Größe von irgendwoher kommen, und der einzige Ort, wo sie existiert, sind die Bildbytes. Verfehlen Sie sie um einen Faktor drei, und das Bild ist um einen Faktor drei gestreckt, in jeder Anwendung, die die Arbeitsmappe öffnet, für immer. Die Einheitenkonvertierung von diesen Pixelsmaßen runter zu EMU ist ein separates Thema, behandelt in dem Artikel über EMU-Einheiten und Skalierung; dieses Stück ist über den Schritt davor, wo die Pixelzahlen überhaupt gewonnen werden. Der Fallback, wenn ein Reader scheitert, ist 100 mal 100, und diese Zahl ist bewusst, offensichtlich falsch. Sie ist keine Schätzung einer plausiblen Größe, sie ist ein Marker, der »niemand konnte diese Datei lesen« bedeutet, und sie ist weit besser als eine plausibel aussehende falsche Zahl, weil ein Benutzer sie sofort meldet
GIF: der Descriptor, der nie geparst wurde
GetGifSize ist neu, und bevor es existierte, kam jedes in ein Arbeitsblatt eingefügte GIF exakt mit 100 mal 100 heraus, es gab gar keinen GIF-Zweig, also fiel die Endung auf den Default-Arm von GetImageSize durch und gab -1 zurück. Das ist der billigste Header der Menge zu parsen, was die Lücke leicht peinlich und sehr einfach zu fixen macht. Die GIF89a-Spezifikation platziert den Logical Screen Descriptor direkt nach der Sechs-Byte-Signatur, und §18 definiert seine ersten zwei Felder als logische Bildschirmbreite und -höhe, je ein unsigned 16-Bit-Wert in Little-Endian. In Dateibegriffen sind das Byte 6 und 7 für die Breite, Byte 8 und 9 für die Höhe, also deckt ein einziger Zehn-Byte-Read den Signatur-Check und beide Dimensionen ab
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
// Signatur "GIF8", dann der Logical Screen Descriptor mit Breite und
// Höhe als Little-Endian-Wörter an Offset 6 und 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;
Beachten Sie die Form: Die Fehlerwerte werden zuerst zugewiesen, und nur der Erfolgspfad überschreibt sie. Jeder Reader in lxImage.pas folgt dieser Reihenfolge, denn sie macht »ich konnte nicht lesen« zum Default statt zu etwas, das man bei jedem frühen Exit mitdenken muss. Nur GIF8 statt des vollen GIF87a- oder GIF89a-Strings zu prüfen ist Absicht, das Descriptor-Layout ist in beiden Revisionen identisch, und es bringt nichts, eine Datei abzulehnen, deren Geometrie lesbar ist
Was ändert eine BMP-Header-Größe von 12 tatsächlich?
GetBmpSize liest die vier Bytes an Offset 14 als DIB-Header-Größe und verzweigt auf den Wert, denn ein BMP mit einer Header-Größe von 12 speichert seine Dimensionen in einer völlig anderen Feldbreite als jedes andere BMP. [MS-WMF] §2.2.2 legt die Familie aus: BITMAPCOREHEADER ist 12 Bytes und trägt 16-Bit-Breite und -Höhe, während BITMAPINFOHEADER, BITMAPV4HEADER und BITMAPV5HEADER alle mit 32-Bit-Signed-biWidth und biHeight beginnen. Vier Bytes zu lesen, wo die Datei zwei hat, scheitert nicht laut; es zieht die Plane-Count- und Bittiefe als obere Hälfte einer Zahl hinein und meldet eine Bitmap von mehreren hunderttausend Pixeln Breite. Die zweite Falle im selben Header ist das Vorzeichen von biHeight: Ein negativer Wert bedeutet keine negative Höhe, sondern dass die DIB-Reihen top-down statt bottom-up gespeichert sind, eine Pixelordnungs-Aussage, die mit Geometrie nichts zu tun hat
f.Seek(14, soFromBeginning);
if f.Read(HeaderSize, 4)= 4 then
begin
if HeaderSize= 12 then
begin
// BITMAPCOREHEADER speichert 16-Bit-Dimensionen
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;
// ...
// Ein negatives biHeight markiert nur ein Top-Down-DIB; der Betrag ist die Größe
if (Result= 1)and (lWidth> 0)and (lHeight<> 0) then
begin
AWidth:= lWidth* 0.75;
AHeight:= Abs(lHeight)* 0.75;
end
else
Result:= - 1;
Die letzte Wache lohnt genaues Lesen. Höhe wird gegen <> 0 statt > 0 geprüft, weil negativ legal ist und null nicht, und Abs wird nur am Punkt der Konvertierung angewendet. Breite hat keinen gleichwertigen Fall, ein negatives biWidth ist vom Format nicht definiert, also wird es als korrupte Datei behandelt und zu -1 herabgestuft. Einen rohen negativen Wert stattdessen nach oben zu reichen, würde eine negative Ausdehnung im Drawing-Record erzeugen, die je nach Anwendung, die die Arbeitsmappe öffnet, geklemmt, ignoriert oder als gespiegelte Form gerendert wird
JPEG: Welche Marker sind tatsächlich Frame-Header?
GetJpegSize akzeptiert den Markersatz [$C0..$C3, $C5..$C7, $C9..$CB, $CD..$CF], und die drei Lücken in diesem Bereich sind der ganze Punkt. ITU-T T.81 §B.2.2 definiert den Frame-Header als SOFn, und der Frame-Header ist der Ort, wo die Zeilenzahl und Samples pro Zeile liegen, in jedem Frame-Typ statt nur in den üblichen. Die Marker, die im selben numerischen Bereich liegen, aber keine Frame-Header sind, sind $C4 (DHT, definiert Huffman-Tabellen), $C8 (JPG, reserviert) und $CC (DAC, definiert arithmetische Kodierungs-Konditionierung). Ein Reader, der nur Baseline, Extended Sequential und Progressive erkennt, $C0, $C1 und $C2, funktioniert mit fast jedem JPEG, das eine Kamera oder ein Screenshot-Tool erzeugt, und scheitert dann still an genau denen, die für Dokument-Workflows zählen: Lossless-JPEG ($C3), die arithmetisch kodierten Varianten ($C9 bis $CB) und die hierarchischen Frame-Typen ($CD bis $CF) scannen alle am Frame-Header vorbei, laufen ans Dateiende und landen im Fallback. Medical Imaging und Scanned-Document-Pipelines erzeugen genau solche Dateien
// Jeder SOFn-Marker trägt die Frame-Dimensionen; $C4 (DHT), $C8 (JPG)
// und $CC (DAC) teilen den Bereich, sind aber keine Frame-Header
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
Zwei Details in diesen sieben Zeilen. Die drei übersprungenen Bytes sind das Segment-Längen-Wort plus das Sample-Präzisionsbyte, also landet der Read auf der Zeilenzahl; JPEG speichert Höhe vor Breite, die Umkehrung der meisten Formate und eine leichte Vertauschung. Und die Werte laufen durch swapByte, weil JPEG durchgehend Big-Endian ist, derselbe Grund, warum GetPngSize seine 32-Bit-IHDR-Werte durch lswapByte schickt. Der Segment-Walk hat seine eigene Wache: Wenn ein Marker nicht parameterlos ist, nimmt der Reader die Zwei-Byte-Länge, byte-swappt sie und verweigert den Seek, wenn der Wert unter 2 liegt, eine Länge kürzer als ihr eigenes Feld würde einen Rückwärts-Seek bedeuten, und ein Rückwärts-Seek in einer Scan-Schleife ist eine Endlosschleife auf einer Datei, die jemand anderes kontrolliert
Minus eins zurückzugeben schlägt eine plausible Zahl
Der gemeinsame Faden durch alle fünf Reader ist, dass GetImageSize drei Ergebnisse statt zwei unterscheidet. 1 bedeutet, die Geometrie wurde aus der Datei gelesen. -1 bedeutet, sie wurde es nicht, und der Aufrufer soll seinen Default verwenden. 7 wird von GetWmfEmfSize zurückgegeben und bedeutet etwas ganz anderes: Es sagt AddPicture, die Größe stattdessen aus dem Metafile-BSE-Record zu nehmen, denn ein WMF oder EMF trägt seine Ausdehnung im eigenen Bild-Record der Arbeitsmappe statt an einem festen Offset im Datei-Header. Metafile-Behandlung im Allgemeinen ist in dem Artikel über den gebundenen EMF- und WMF-Vektor-Decoder behandelt. GetJpegSize zeigt, warum diese Unterscheidung ein explizites Flag braucht statt einer Schlussfolgerung: Die Scan-Schleife kann auf drei Arten enden, sie fand einen Frame-Header, sie traf das Dateiende, oder sie brach bei einer korrupten Segmentlänge ab, und nur die erste weist wWidth und wHeight zu. Ohne das found-Boolean würden die anderen zwei Pfade konvertieren, was auch immer diese Lokalen gerade enthielten, und das Ergebnis als Erfolg nach oben reichen. Uninitialisierte Lokale sind oft null, und null ist hier die schlechteste mögliche Antwort: Sie überlebt jede Gültigkeitsprüfung flussabwärts und erzeugt ein unsichtbares Bild, das niemand auswählen oder löschen kann
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); // Metafile: Größe aus dem BSE-Record
else
// W und H sind 100 x 100; entscheiden Sie die Politik, statt einen Platzhalter zu akzeptieren
LogWarning('unreadable image geometry: ' + FileName);
end;
end;
Wo die ehrlichen Grenzen sind
Diese Reader parsen bewusst einen Header und stoppen. Sie dekodieren keine Pixel, also melden sie die gespeicherten Dimensionen und sonst nichts, und es gibt Fälle, wo die gespeicherten Dimensionen nicht die ganze Geschichte sind. Ein animiertes GIF, dessen Frames kleiner sind als der logische Screen, meldet trotzdem den logischen Screen, was für das Layout korrekt ist und vielleicht nicht dem entspricht, was ein Viewer zeigt. Ein JPEG mit einem EXIF-Orientierungs-Tag, das eine 90-Grad-Rotation spezifiziert, meldet die unrotierten Frame-Dimensionen, also kann ein auf dem Handy aufgenommenes Hochformat-Foto als Querformat zurückkommen. Ein DIB mit einem nicht-quadratischen Pixel-Aspect-Ratio meldet rohe Pixelzahlen, und der 0.75-Faktor nimmt durchgehend 96 DPI an. Keines davon ist ein Bug im Header-Lesen, und keines ist ohne Bilddekodierung fixbar, wofür diese Schicht nicht da ist, es sind dokumentierte Grenzen, also lesen Sie, wenn Ihre Pipeline Handyfotos verarbeitet, das Orientierungs-Tag selbst und tauschen Sie die Werte, bevor Sie AddPicture aufrufen. Alles andere über Bildplatzierung, Verankerung und das Drawing-Objektmodell ist in dem Artikel über Diagramme, Bilder und Drawings behandelt
HotXLS liest und schreibt XLS, XLSX, ODS und CSV nativ in Delphi und C++Builder ohne installiertes Excel, und die Bildeinfügung läuft in jedem davon durch diesen Header-Pfad. Format-Support- und Lizenzdetails stehen auf der Produktseite des HotXLS Delphi spreadsheet component